(http://kennethormandy.com)",
5 | "main": "./lib/FitText.js",
6 | "repository": "https://github.com/kennethormandy/react-fittext",
7 | "license": "MIT",
8 | "dependencies": {
9 | "lodash.debounce": "^4.0.8",
10 | "prop-types": "^15.6.1"
11 | },
12 | "engines": {
13 | "node": ">=8.x",
14 | "npm": ">=5.x"
15 | },
16 | "devDependencies": {
17 | "@storybook/addon-info": "3.3.12",
18 | "@storybook/addon-notes": "3.3.12",
19 | "@storybook/addon-options": "3.4.2",
20 | "@storybook/react": "3.4.2",
21 | "ava": "0.15.2",
22 | "babel-cli": "6.26.0",
23 | "babel-core": "6.26.3",
24 | "babel-loader": "6.4.1",
25 | "babel-plugin-transform-class-properties": "6.24.1",
26 | "babel-plugin-transform-runtime": "6.23.0",
27 | "babel-preset-env": "1.7.0",
28 | "babel-preset-react": "6.24.1",
29 | "babel-register": "6.26.0",
30 | "babel-runtime": "6.26.0",
31 | "del-cli": "0.2.0",
32 | "fontfaceobserver": "2.0.13",
33 | "html-loader": "0.5.5",
34 | "jsdom": "9.5.0",
35 | "markdown-loader": "2.0.2",
36 | "prettier": "1.14.3",
37 | "react": "16.2.0",
38 | "react-dom": "16.2.0",
39 | "size-limit": "0.21.1",
40 | "webpack": "2.4.1",
41 | "webpack-bundle-analyzer": "^2.9.0",
42 | "webpack-dev-server": "2.8.2"
43 | },
44 | "peerDependencies": {
45 | "react": "^16.x",
46 | "react-dom": "^16.x"
47 | },
48 | "scripts": {
49 | "clean": "npx del ./dist/*",
50 | "build-js": "NODE_ENV=production webpack -p; babel src/ --out-dir ./lib/",
51 | "prebuild": "mkdir ./dist; npm run clean;",
52 | "build": "npm run build-js",
53 | "prepublishOnly": "npm run build",
54 | "start": "webpack-dev-server",
55 | "lint": "prettier --write './src/*.{js,jsx}'",
56 | "test": "ava; npx size-limit",
57 | "posttest": "npm run lint",
58 | "storybook": "start-storybook -p 8081 -c .storybook",
59 | "build-storybook": "build-storybook -c .storybook -o dist/storybook",
60 | "deploy-storybook": "npm run build-storybook; npx surge ./dist/storybook react-fittext.kennethormandy.com"
61 | },
62 | "prettier": {
63 | "semi": false,
64 | "trailingComma": "es5",
65 | "singleQuote": true,
66 | "bracketSpacing": true,
67 | "jsxBracketSameLine": true
68 | },
69 | "browserslist": "last 2 versions, safari >= 7",
70 | "babel": {
71 | "presets": [
72 | "env",
73 | "react"
74 | ]
75 | },
76 | "size-limit": [
77 | {
78 | "path": "./dist/FitText.js",
79 | "webpack": false,
80 | "limit": "3 KB"
81 | }
82 | ],
83 | "ava": {
84 | "failFast": true,
85 | "files": [
86 | "test/*.js"
87 | ],
88 | "require": [
89 | "babel-register",
90 | "./test/helpers/setup-browser-env.js"
91 | ],
92 | "babel": {
93 | "presets": [
94 | "env",
95 | "react"
96 | ]
97 | }
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/src/FitText.js:
--------------------------------------------------------------------------------
1 | /*
2 | * React FitText
3 | * https://github.com/kennethormandy/react-fittext
4 | * Kenneth Ormandy
5 | *
6 | * A rewrite of https://github.com/gianu/react-fittext (MIT)
7 | * …which is based on the FitText jQuery plugin
8 | * http://github.com/davatron5000/FitText.js
9 | *
10 | */
11 |
12 | import React from 'react'
13 | import PropTypes from 'prop-types'
14 | import _debounce from 'lodash.debounce'
15 |
16 | class FitText extends React.Component {
17 | constructor(props) {
18 | super(props)
19 |
20 | let defaultFontSize = props.defaultFontSize
21 |
22 | if (typeof props.defaultFontSize === 'number') {
23 | defaultFontSize = `${props.defaultFontSize}px`
24 | }
25 |
26 | this.state = {
27 | fontSize: defaultFontSize,
28 | }
29 |
30 | this._onBodyResize = this._onBodyResize.bind(this)
31 | this._parentNode = null
32 | }
33 |
34 | componentDidUpdate(prevProps) {
35 | // When a new parent ID is passed in, or the new parentNode
36 | // is available, run resize again
37 | if (this.props.parent !== prevProps.parent) {
38 | this._onBodyResize()
39 | }
40 | }
41 |
42 | componentDidMount() {
43 | if (0 >= this.props.compressor) {
44 | console.warn(`Warning: The compressor should be greater than 0.`)
45 | }
46 |
47 | if (this.props.parent) {
48 | this._parentNode =
49 | typeof this.props.parent === 'string'
50 | ? document.getElementById(this.props.parent)
51 | : this.props.parent
52 | }
53 |
54 | window.addEventListener(
55 | 'resize',
56 | _debounce(this._onBodyResize, this.props.debounce)
57 | )
58 | this._onBodyResize()
59 | }
60 |
61 | componentWillUnmount() {
62 | window.removeEventListener(
63 | 'resize',
64 | _debounce(this._onBodyResize, this.props.debounce)
65 | )
66 | }
67 |
68 | _getFontSize(value) {
69 | const props = this.props
70 |
71 | return Math.max(
72 | Math.min(value / (props.compressor * 10), props.maxFontSize),
73 | props.minFontSize
74 | )
75 | }
76 |
77 | _onBodyResize() {
78 | if (this.element && this.element.offsetWidth) {
79 | let value = this.element.offsetWidth
80 |
81 | if (this.props.vertical) {
82 | let parent = this._parentNode || this.element.parentNode
83 | value = parent.offsetHeight
84 | }
85 |
86 | let newFontSize = this._getFontSize(value)
87 |
88 | this.setState({
89 | fontSize: `${newFontSize}px`,
90 | })
91 | }
92 | }
93 |
94 | render() {
95 | return (
96 | (this.element = el)}
98 | style={{ fontSize: this.state.fontSize }}>
99 | {this.props.children}
100 |
101 | )
102 | }
103 | }
104 |
105 | FitText.defaultProps = {
106 | compressor: 1.0,
107 | debounce: 100,
108 | defaultFontSize: 'inherit',
109 | minFontSize: Number.NEGATIVE_INFINITY,
110 | maxFontSize: Number.POSITIVE_INFINITY,
111 | }
112 |
113 | FitText.propTypes = {
114 | children: PropTypes.oneOfType([PropTypes.element, PropTypes.string]),
115 | compressor: PropTypes.number,
116 | debounce: PropTypes.number,
117 | defaultFontSize: PropTypes.string,
118 | minFontSize: PropTypes.number,
119 | maxFontSize: PropTypes.number,
120 | parent: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
121 | }
122 |
123 | export default FitText
124 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React FitText
2 |
3 | [FitText.js](https://github.com/davatron5000/FitText.js) as a React v16+ component.
4 |
5 | If you want to make specific text fit within a container, and then maintain that ratio across screen sizes, this component is for you.
6 |
7 | FitText is a particularly useful approach for:
8 |
9 | - Predetermined content (ie. not user generated or dynamic)
10 | - Text that fits within a container until it hits a minimum or maximum font size, and then reflows normally from there
11 | - Multi-line text that fits
12 |
13 | ## Alternatives
14 |
15 | If you don’t have any of these requirements, another approach might suit you better. Some possible alternatives include:
16 |
17 | - Using a pre-made SVG without outlining the text, if you have predetermined content, and want truly the exact same layout across all viewports
18 | - Using SVG dynamically with [React FitterHappierText](https://github.com/jxnblk/react-fitter-happier-text) (the changes are all [open as Pull Requests](https://github.com/jxnblk/react-fitter-happier-text/pulls) on [Brent Jackson’s original](https://github.com/jxnblk/react-fitter-happier-text))
19 | - Using [BigIdeasText](http://github.com/kennethormandy/big-ideas-text) within React lifecycle hooks like `componentDidMount()`. I may open source a React-specific fork of [Zach Leatherman’s original](https://github.com/zachleat/BigText) in the future.
20 | - Using Mike Riethmuller’s clever [CSS-only fluid type technique](https://www.madebymike.com.au/writing/precise-control-responsive-typography/) and [other examples](https://www.madebymike.com.au/writing/fluid-type-calc-examples/), if you have some scaling constraints but aren’t concerned about reflow across all sizes
21 | - Plain viewport units, if the only relevant container is the width (or height) of the page:
22 |
23 | ```html
24 |
25 | Scale with the viewport
26 |
27 | ```
28 |
29 | ```css
30 | /* Minimum font size */
31 | .example {
32 | font-size: 24px;
33 | }
34 |
35 | /* Scale linearly after this breakpoint */
36 | @media (min-width: 480px) {
37 | .example {
38 | font-size: 5vw;
39 | }
40 | }
41 | ```
42 |
43 | If you’re curious why some sort of automatic scaling isn’t already possible using CSS alone, or why it might still be a challenge in the future, [read more in this CSS Working Group drafts issue](https://github.com/w3c/csswg-drafts/issues/2528).
44 |
45 | ## Differences from the existing React FitText
46 |
47 | This component is written specifically for React v16 and up, includes tests, and uses state to avoid DOM manipulation.
48 |
49 | The existing [React FitText component by @gianu](https://github.com/gianu/react-fittext) should still work with current versions of React, and is stateless, but manipulates the DOM directly to change font sizes.
50 |
51 | The approach I’m using feels more React-appropriate, at least to me. I use this component regularly enough that it made sense for me to maintain my own version regardless.
52 |
53 | ## Installation
54 |
55 | ```sh
56 | npm install --save @kennethormandy/react-fittext
57 | ```
58 |
59 | ## Example
60 |
61 | ```js
62 | import FitText from '@kennethormandy/react-fittext'
63 | ```
64 |
65 | ```jsx
66 | The quick brown fox jumps over the lazy dog.
67 | ```
68 |
69 | With multiple children:
70 |
71 | ```jsx
72 |
73 |
74 | Pangram
75 | The quick brown fox jumps over the lazy dog
76 |
77 |
78 | ```
79 |
80 | ## Props
81 |
82 | ### `compressor`
83 |
84 | From the original FitText.js documentation:
85 |
86 | > If your text is resizing poorly, you'll want to turn tweak up/down “The Compressor.” It works a little like a guitar amp. The default is `1`.
87 | > —[davatron5000](https://github.com/davatron5000/FitText.js)
88 |
89 | ```jsx
90 | The quick brown fox jumps over the lazy dog.
91 | ```
92 |
93 | ```jsx
94 | The quick brown fox jumps over the lazy dog.
95 | ```
96 |
97 | ```jsx
98 | The quick brown fox jumps over the lazy dog.
99 | ```
100 |
101 | ### `minFontSize` and `maxFontSize`
102 |
103 | ```jsx
104 |
105 | The quick brown fox jumps over the lazy dog.
106 |
107 | ```
108 |
109 | ### `debounce`
110 |
111 | Change the included debounce resize timeout. How long should React FitText wait before recalculating the `fontSize`?
112 |
113 | ```jsx
114 |
115 | The very slow brown fox
116 |
117 | ```
118 |
119 | The default is `100` milliseconds.
120 |
121 | ### `defaultFontSize`
122 |
123 | React FitText needs the viewport size to determine the size the type, but you might want to provide an explicit fallback when using server-side rendering with React.
124 |
125 | ```jsx
126 |
127 | The quick brown fox
128 |
129 | ```
130 |
131 | The default is `inherit`, so typically you will already have a resonable fallback without using this prop, using CSS only. For example:
132 |
133 | ```css
134 | .headline {
135 | font-size: 6.25rem;
136 | }
137 | ```
138 |
139 | ```jsx
140 |
141 | The quick brown fox
142 |
143 | ```
144 |
145 | ## `vertical`
146 |
147 | Add the `vertical` prop to scale vertically, rather than horizontally (the default).
148 |
149 | ```jsx
150 |
151 |
152 |
153 | - Waterfront
154 | - Vancouver City Centre
155 | - Yaletown–Roundhouse
156 | - Olympic Village
157 | - Broadway–City Hall
158 | - King Edward
159 | - Oakridge–41st Avenue
160 | - Langara–49th Avenue
161 | - Marine Drive
162 |
163 |
164 |
165 | ```
166 |
167 | ## `parent`
168 |
169 | Use a different parent, other than the immediate `parentNode`, to calculate the vertical height.
170 |
171 | ```jsx
172 |
173 |
174 |
175 | {dynamicChildren}
176 |
177 |
178 |
179 | ```
180 |
181 | ```jsx
182 |
183 |
(this.parentNode = el)}>
184 |
A contrived example!
185 |
186 |
187 | {dynamicChildren}
188 |
189 |
190 | ```
191 |
192 | ## Running locally
193 |
194 | ```sh
195 | git clone https://github.com/kennethormandy/react-fittext
196 | cd kennethormandy
197 |
198 | # Install dependencies
199 | npm install
200 |
201 | # Run the project
202 | npm start
203 | ```
204 |
205 | Now, you can open `http://localhost:8080` and modify `src/dev.js` while working on the project.
206 |
207 | To run the Storybook [stories](http://react-fittext.kennethormandy.com) instead:
208 |
209 | ```sh
210 | npm run storybook
211 | ```
212 |
213 | ## Samples
214 |
215 | I’ve used various versions of this project in the following [type specimen sites](https://kennethormandy.com/type-specimen-sites/):
216 |
217 | - [Regina Black](http://regina-black.losttype.com)
218 | - [DDC Hardware](https://kennethormandy.com/portfolio/ddc-hardware-type-specimen-site/)
219 | - [Google Fonts + Japanese collection](https://googlefonts.github.io/japanese)
220 | - [Boomville](http://boomville.losttype.com)
221 | - [Tofino v2](http://tofino.losttype.com)
222 | - [My website](https://kennethormandy.com)
223 | - [Protipo](https://protipo.type-together.com)
224 | - TBA
225 | - TBA
226 |
227 | Other projects:
228 |
229 | - [Cygnus Design Group](https://www.cygnus.group/) (added vertical support)
230 | - Your project? [Add it to the README](https://github.com/kennethormandy/react-fittext/edit/master/README.md)
231 |
232 | ## Credits
233 |
234 | - The original [FitText.js](https://github.com/davatron5000/FitText.js) by [@davatron5000](https://github.com/davatron5000/FitText.js)
235 | - [react-fittext](https://github.com/gianu/react-fittext) by [@gianu](https://github.com/gianu)
236 |
237 | ## License
238 |
239 | [The MIT License (MIT)](LICENSE.md)
240 |
241 | Copyright © 2014 [Sergio Rafael Gianazza](https://github.com/gianu/react-fittext/blob/master/LICENSE)
242 | Copyright © 2017–2019 [Kenneth Ormandy Inc.](https://kennethormandy.com)
243 |
--------------------------------------------------------------------------------
/stories/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { storiesOf } from '@storybook/react'
3 | import { action } from '@storybook/addon-actions'
4 | import { setDefaults, withInfo } from '@storybook/addon-info'
5 |
6 | import FitText from '../src/FitText'
7 | import CycleValue from './components/CycleValue'
8 | import MultiWidth from './components/MultiWidth'
9 | import './story-fonts.css'
10 |
11 | let padding = '5vw'
12 |
13 | setDefaults({
14 | header: true, // Toggles display of header with component name and description
15 | inline: true, // Displays info inline vs click button to view
16 | source: true, // Displays the source of story Component
17 | propTables: false,
18 | // https://github.com/storybooks/storybook/blob/master/addons/info/src/components/Story.js#L19
19 | styles: {
20 | infoBody: {
21 | fontWeight: 400,
22 | boxShadow: 'none',
23 | // marginTop: '100px',
24 | padding: `1rem ${padding}`,
25 | borderRadius: 0,
26 | borderWidth: '0',
27 | },
28 | infoStory: {
29 | background: '#000',
30 | color: '#FFF',
31 | padding: padding,
32 | overflow: 'hidden',
33 | },
34 | jsxInfoContent: { margin: 0 },
35 | header: {
36 | h1: {
37 | fontSize: '1rem',
38 | lineHeight: 1.5,
39 | display: 'inline-block',
40 | margin: 0,
41 | paddingBottom: '5px',
42 | paddingRight: '0.25em',
43 | fontWeight: 700,
44 | },
45 | h2: {
46 | fontSize: '1rem',
47 | lineHeight: 1.5,
48 | display: 'inline-block',
49 | margin: 0,
50 | paddingBottom: '5px',
51 | fontWeight: 400,
52 | },
53 | body: {
54 | margin: 0,
55 | paddingTop: 0,
56 | },
57 | },
58 | source: {
59 | h1: { fontSize: '1rem', fontWeight: 700, lineHeight: 1.5 },
60 | },
61 | },
62 | })
63 |
64 | let demoText = [
65 | 'Filles du Calvaire',
66 | 'Saint-Sébastien – Froissar',
67 | 'Chemin Vert',
68 | 'Bastille',
69 | 'Ledru-Rollin',
70 | 'Faidherbe – Chaligny',
71 | 'Reuilly – Diderot',
72 | 'Montgallet',
73 | 'Daumesnil',
74 | 'Michel Bizot',
75 | 'Porte Dorée',
76 | 'Porte de Charenton',
77 | 'Liberté',
78 | ]
79 |
80 | storiesOf('FitText', module)
81 | .add(
82 | 'Welcome',
83 | withInfo('')(() => (
84 |
90 | Saint
91 | Saint-Sébastien – Froissar
92 | Saint-Sébastien – Froissar
93 | Saint-Sébastien – Froissar
94 |
95 | ))
96 | )
97 | .add(
98 | 'with a text string',
99 | withInfo('More info')(() => (
100 | The Quick Brown Fox
101 | ))
102 | )
103 | .add(
104 | 'with scaling based on vertical height',
105 | withInfo(
106 | 'Scaling within a vertical space, rather than a horizontal space.'
107 | )(() => (
108 |
109 |
110 |
111 |
118 | {[
119 | 'Waterfront',
120 | 'Vancouver City Centre',
121 | 'Yaletown–Roundhouse',
122 | 'Olympic Village',
123 | 'Broadway–City Hall',
124 | 'King Edward',
125 | 'Oakridge–41st Avenue',
126 | 'Langara–49th Avenue',
127 | 'Marine Drive',
128 | ].map((item, index) => {
129 | return (
130 | -
131 | {item}{' '}
132 |
133 | Check times →
134 |
135 |
136 | )
137 | })}
138 |
139 |
140 |
141 |
142 | ))
143 | )
144 | .add(
145 | 'with scaling based on vertical of different parentNode',
146 | withInfo('')(() => (
147 |
150 |
151 |
152 |
153 |
160 | {[
161 | 'Waterfront',
162 | 'Vancouver City Centre',
163 | 'Yaletown–Roundhouse',
164 | 'Olympic Village',
165 | 'Broadway–City Hall',
166 | 'King Edward',
167 | 'Oakridge–41st Avenue',
168 | 'Langara–49th Avenue',
169 | 'Marine Drive',
170 | ].map((item, index) => {
171 | return (
172 | -
173 | {item}{' '}
174 |
175 | Check times →
176 |
177 |
178 | )
179 | })}
180 |
181 |
182 |
183 |
184 |
185 | ))
186 | )
187 | .add(
188 | 'with line breaks',
189 | withInfo('More info')(() => {
190 | let style = {
191 | textAlign: 'center',
192 | fontWeight: 200,
193 | marginBottom: padding,
194 | }
195 | return (
196 |
197 |
198 |
199 | ABCDEFGHIJKLMN
200 |
201 | OPQRSTUVWXYZ
202 |
203 |
204 |
205 | {`ABCDEFGHIJKLMN\nOPQRSTUVWXYZ`}
206 |
207 |
208 | )
209 | })
210 | )
211 | .add(
212 | 'with children in fixed sizes',
213 | withInfo('More info')(() => {
214 | return (
215 |
216 |
217 |
218 |
Baskerville’s Characteristicks
219 |
220 | Working from multiple masters allows type designers to provide
221 | graphic designers with a wider range of styles through separate
222 | fonts. What if the range between those extremes were available
223 | to manipulate at runtime on screens, allowing a typeface to
224 | respond to its context?
225 |
226 |
227 |
228 |
229 | )
230 | })
231 | )
232 | .add(
233 | 'with minFontSize',
234 | withInfo('More info')(() => {
235 | return (
236 |
237 |
238 |
239 | Minimum. Working from multiple masters allows type designers to
240 | provide graphic designers with a wider range of styles through
241 | separate fonts. What if the range between those extremes were
242 | available to manipulate at runtime on screens, allowing a typeface
243 | to respond to its context?
244 |
245 |
246 |
247 | )
248 | })
249 | )
250 | .add(
251 | 'with maxFontSize',
252 | withInfo('More info')(() => {
253 | return (
254 |
255 |
256 | Maximum
257 |
258 |
259 | )
260 | })
261 | )
262 | .add(
263 | 'with changing content',
264 | withInfo('More info')(() => {
265 | return (
266 |
267 |
268 |
269 |
270 |
271 | )
272 | })
273 | )
274 | .add(
275 | 'with custom debounce timeout',
276 | withInfo('More info')(() => {
277 | return (
278 |
279 | hello
280 |
281 | )
282 | })
283 | )
284 | .add(
285 | 'with border image slice',
286 | withInfo('More info')(() => {
287 | return (
288 |
297 | hello
298 |
299 | )
300 | })
301 | )
302 | .add(
303 | 'with missing word-break',
304 | withInfo('More info')(() => {
305 | return (
306 |
307 |
308 |
default
309 |
313 | Antidisestablishmentarianism
314 |
315 |
316 |
317 |
break-word
318 |
323 | Antidisestablishmentarianism
324 |
325 |
326 |
327 |
break-word, hyphens
328 |
334 | Antidisestablishmentarianism
335 |
336 |
337 |
338 |
break-all
339 |
344 | Antidisestablishmentarianism
345 |
346 |
347 |
348 |
break-none, nowrap
349 |
355 | Antidisestablishmentarianism
356 |
357 |
358 |
359 | )
360 | })
361 | )
362 |
--------------------------------------------------------------------------------