├── stories ├── components │ ├── .gitkeep │ ├── CycleValue.js │ └── MultiWidth.js ├── story-fonts.css └── index.js ├── .storybook ├── addons.js └── config.js ├── .npmignore ├── test ├── helpers │ └── setup-browser-env.js └── index.js ├── .gitignore ├── src ├── index.html ├── dev.js └── FitText.js ├── LICENSE.md ├── webpack.config.js ├── package.json └── README.md /stories/components/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.storybook/addons.js: -------------------------------------------------------------------------------- 1 | import '@storybook/addon-options/register' 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src/dev.js 2 | src/index.html 3 | dist/report.html 4 | dist/storybook 5 | 6 | .storybook 7 | stories 8 | 9 | !lib 10 | 11 | webpack.config.js 12 | -------------------------------------------------------------------------------- /test/helpers/setup-browser-env.js: -------------------------------------------------------------------------------- 1 | import jsdom from 'jsdom' 2 | 3 | global.document = jsdom.jsdom('') 4 | global.window = document.defaultView 5 | global.navigator = window.navigator 6 | -------------------------------------------------------------------------------- /stories/story-fonts.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Work+Sans:200,400,600,800'); 2 | 3 | body { 4 | /* `serif` fallback, so it’s obvious if it isn’t working */ 5 | font-family: "Work Sans", serif; 6 | font-weight: 600; 7 | margin: 0; 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OS 2 | .DS_Store 3 | 4 | # Logs 5 | logs 6 | *.log* 7 | 8 | # Compiled binary addons (http://nodejs.org/api/addons.html) 9 | build/Release 10 | 11 | # Dependency directory 12 | node_modules 13 | fonts 14 | *.woff 15 | *.woff2 16 | *.ttf 17 | *.otf 18 | 19 | # TODO 20 | stories/todo 21 | 22 | # Compile directories 23 | www/ 24 | build/ 25 | lib/ 26 | dist/ 27 | -------------------------------------------------------------------------------- /stories/components/CycleValue.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | class CycleValue extends React.Component { 4 | constructor() { 5 | super() 6 | this.state = { phraseListIndex: 0 } 7 | this.handleCycle = this.handleCycle.bind(this) 8 | } 9 | 10 | handleCycle() { 11 | let newIndex = this.state.phraseListIndex + 1 12 | let increase = this.props.phraseList.length - 1 >= newIndex 13 | 14 | this.setState({ phraseListIndex: increase ? newIndex : 0 }) 15 | 16 | this.timeout = window.setTimeout(this.handleCycle, this.props.timeout) 17 | } 18 | 19 | componentDidMount() { 20 | this.timeout = window.setTimeout(() => { 21 | this.handleCycle() 22 | }, this.props.timeout) 23 | } 24 | 25 | componentWillUnmount() { 26 | window.clearTimeout(this.timeout) 27 | } 28 | 29 | render() { 30 | return this.props.phraseList[this.state.phraseListIndex] 31 | } 32 | } 33 | 34 | CycleValue.defaultProps = { 35 | timeout: 3000, 36 | phraseList: [ 37 | 'FILLES DU CALVAIRE', 38 | 'SAINT-SÉBASTIEN – FROISSAR', 39 | 'CHEMIN VERT', 40 | ], 41 | } 42 | 43 | export default CycleValue 44 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | React FitText • Kenneth Ormandy 5 | 6 | 48 | 49 | 50 |
51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright © 2014 [Sergio Rafael Gianazza](https://github.com/gianu/react-fittext/blob/master/LICENSE)
4 | Copyright © 2017–2018 [Kenneth Ormandy Inc.](http://kennethormandy.com) 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 9 | 10 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 11 | -------------------------------------------------------------------------------- /stories/components/MultiWidth.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const MultiWidth = props => { 4 | return ( 5 | 6 | {props.widths.map((w, index) => { 7 | let width = `${w}px` 8 | let keyStr = `MultiWrapper_${index}` 9 | return ( 10 |
18 | 30 | {width} 31 | 32 |
{props.children}
33 |
34 | ) 35 | })} 36 |
37 | ) 38 | } 39 | 40 | MultiWidth.defaultProps = { 41 | mb: '5vw', 42 | widths: [250, 500, 750, 1000], 43 | } 44 | 45 | export default MultiWidth 46 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | // Testing config MIT via https://git.io/vXCGg 2 | 3 | import test from 'ava' 4 | import React from 'react' 5 | import { render, findDOMNode } from 'react-dom' 6 | import FitText from '../src/FitText' 7 | 8 | test('should exist', t => { 9 | const container = document.createElement('div') 10 | const component = render( 11 | 12 | The quick brown fox… 13 | , 14 | container 15 | ) 16 | const node = findDOMNode(component) 17 | 18 | t.deepEqual(node.childNodes.length, 1) 19 | t.deepEqual(node.textContent, 'The quick brown fox…') 20 | }) 21 | 22 | test('should have a default font size', t => { 23 | const container = document.createElement('div') 24 | const component = render(The quick brown fox…, container) 25 | const node = findDOMNode(component) 26 | 27 | t.deepEqual(node.childNodes.length, 1) 28 | t.deepEqual(node.style.fontSize, 'inherit') 29 | }) 30 | 31 | test('should have a default font size from prop', t => { 32 | const container = document.createElement('div') 33 | const component = render( 34 | The quick brown fox…, 35 | container 36 | ) 37 | const node = findDOMNode(component) 38 | 39 | t.deepEqual(node.childNodes.length, 1) 40 | t.deepEqual(node.style.fontSize, '100px') 41 | }) 42 | 43 | test('should assume defaultFontSize is in pixels when given a number', t => { 44 | const container = document.createElement('div') 45 | const component = render( 46 | The quick brown fox…, 47 | container 48 | ) 49 | const node = findDOMNode(component) 50 | 51 | t.deepEqual(node.childNodes.length, 1) 52 | t.deepEqual(node.style.fontSize, '100px') 53 | }) 54 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var pkg = require('./package.json') 2 | var path = require('path') 3 | var webpack = require('webpack') 4 | var BundleAnalyzerPlugin = require('webpack-bundle-analyzer') 5 | .BundleAnalyzerPlugin 6 | 7 | let config = { 8 | devServer: { 9 | contentBase: __dirname + '/src', 10 | }, 11 | context: __dirname + '/src', 12 | entry: { 13 | FitText: './FitText.js', 14 | }, 15 | output: { 16 | path: __dirname + '/dist', 17 | filename: 'FitText.js', 18 | }, 19 | module: { 20 | rules: [ 21 | { 22 | test: /\.js$/, 23 | exclude: [/node_modules/], 24 | use: [ 25 | { 26 | loader: 'babel-loader', 27 | options: pkg.babel, 28 | }, 29 | ], 30 | }, 31 | ], 32 | }, 33 | plugins: [ 34 | new BundleAnalyzerPlugin({ 35 | analyzerMode: 'static', 36 | analyzerPort: 8888, 37 | openAnalyzer: false, 38 | generateStatsFile: true, 39 | }), 40 | ], 41 | } 42 | 43 | if (process.env.NODE_ENV === 'production') { 44 | // This is for a stand-alone build, ex. if you are 45 | // going to use it in the browser with a script tag 46 | config.externals = { 47 | // This config is based on the draft-js Webpack config 48 | react: { 49 | root: 'React', 50 | commonjs2: 'react', 51 | commonjs: 'react', 52 | amd: 'react', 53 | }, 54 | 'react-dom': { 55 | root: 'ReactDOM', 56 | commonjs2: 'react-dom', 57 | commonjs: 'react-dom', 58 | amd: 'react-dom', 59 | }, 60 | } 61 | } else { 62 | config.module.rules.push({ 63 | test: /\.md$/, 64 | use: [ 65 | { 66 | loader: 'html-loader', 67 | }, 68 | { 69 | loader: 'markdown-loader', 70 | options: {}, 71 | }, 72 | ], 73 | }) 74 | 75 | config.entry['dev'] = './dev.js' 76 | config.output['filename'] = './[name].js' 77 | } 78 | 79 | module.exports = config 80 | -------------------------------------------------------------------------------- /.storybook/config.js: -------------------------------------------------------------------------------- 1 | import * as storybook from '@storybook/react' 2 | import { setOptions } from '@storybook/addon-options' 3 | 4 | // Option defaults: 5 | setOptions({ 6 | /** 7 | * name to display in the top left corner 8 | * @type {String} 9 | */ 10 | name: 'React FitText', 11 | /** 12 | * URL for name in top left corner to link to 13 | * @type {String} 14 | */ 15 | url: '/', 16 | /** 17 | * show story component as full screen 18 | * @type {Boolean} 19 | */ 20 | goFullScreen: false, 21 | /** 22 | * display panel that shows a list of stories 23 | * @type {Boolean} 24 | */ 25 | showStoriesPanel: true, 26 | /** 27 | * display panel that shows addon configurations 28 | * @type {Boolean} 29 | */ 30 | showAddonPanel: false, 31 | /** 32 | * display floating search box to search through stories 33 | * @type {Boolean} 34 | */ 35 | showSearchBox: false, 36 | /** 37 | * show addon panel as a vertical panel on the right 38 | * @type {Boolean} 39 | */ 40 | addonPanelInRight: false, 41 | /** 42 | * sorts stories 43 | * @type {Boolean} 44 | */ 45 | sortStoriesByKind: false, 46 | /** 47 | * regex for finding the hierarchy separator 48 | * @example: 49 | * null - turn off hierarchy 50 | * /\// - split by `/` 51 | * /\./ - split by `.` 52 | * /\/|\./ - split by `/` or `.` 53 | * @type {Regex} 54 | */ 55 | hierarchySeparator: null, 56 | /** 57 | * regex for finding the hierarchy root separator 58 | * @example: 59 | * null - turn off mulitple hierarchy roots 60 | * /\|/ - split by `|` 61 | * @type {Regex} 62 | */ 63 | hierarchyRootSeparator: null, 64 | /** 65 | * sidebar tree animations 66 | * @type {Boolean} 67 | */ 68 | sidebarAnimations: true, 69 | /** 70 | * id to select an addon panel 71 | * @type {String} 72 | */ 73 | selectedAddonPanel: undefined, // The order of addons in the "Addon panel" is the same as you import them in 'addons.js'. The first panel will be opened by default as you run Storybook 74 | /** 75 | * enable/disable shortcuts 76 | * @type {Boolean} 77 | */ 78 | enableShortcuts: false, // true by default 79 | }) 80 | 81 | storybook.configure(() => require('../stories'), module) 82 | -------------------------------------------------------------------------------- /src/dev.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | 4 | // Compiled version to confirm build worked as expected 5 | // import FitText from '../' 6 | 7 | // Live development version for working on the library 8 | import FitText from './FitText.js' 9 | import README from '../README.md' 10 | 11 | const Content = props => { 12 | return ( 13 |
').join('

Demo

'), 17 | }} 18 | /> 19 | ) 20 | } 21 | 22 | const DemoWrapper = props => { 23 | return ( 24 |
32 | ) 33 | } 34 | 35 | class SizeWrapper extends React.Component { 36 | constructor(props) { 37 | super(props) 38 | 39 | this.state = { 40 | wrapperWidth: 50, 41 | } 42 | 43 | this.handleUpdateWidth = this.handleUpdateWidth.bind(this) 44 | } 45 | 46 | handleUpdateWidth(e) { 47 | this.setState({ 48 | wrapperWidth: e.target.valueAsNumber, 49 | }) 50 | } 51 | 52 | render() { 53 | const state = this.state 54 | 55 | return ( 56 |
57 | 70 |
77 | {props.children} 78 |
79 |
80 | ) 81 | } 82 | } 83 | 84 | const FitTextExamples = props => { 85 | return ( 86 |
87 | 88 | The quick brown fox jumps over the lazy dog 89 | 90 | 91 |
92 | ) 93 | } 94 | 95 | ReactDOM.render(, document.getElementById('js-target')) 96 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@kennethormandy/react-fittext", 3 | "version": "0.6.0", 4 | "author": "Kenneth Ormandy (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 | --------------------------------------------------------------------------------