├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html └── manifest.json ├── src ├── App.css ├── App.js ├── Breakpoint │ └── index.js ├── DynamicComponent │ └── index.js ├── Layout │ └── index.js ├── Link │ └── index.js ├── Typography │ └── index.js ├── index.js ├── reset.css ├── serviceWorker.js └── theme.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## React typography system 2 | This is a collection of tools and conventions I've used to create a typography system in React. It uses [styled-components](https://www.styled-components.com/) and [styled-system](https://github.com/jxnblk/styled-system/) to allow you to import styled typeography elements and override both the styling and markup like so: 3 | 4 | ```jsx 5 | 6 | Canon 7 | 8 | ``` 9 | 10 | I've this approach to re-create [GEL](http://bbc.co.uk/gel/guidelines/typography), the excellent CSS based typography system by the BBC. 11 | Examples of this are on [CodeSandbox](https://codesandbox.io/s/kw89ro5y2r) and the full process is documented on [Medium](https://medium.com/@jezfx/building-a-react-typography-system-f9d1c8e16d55). 12 | 13 | ## Quick overview 14 | 15 | The DynamicComponent is what enables you to override the styling and markup 16 | 17 | ```jsx 18 | import React from "react"; 19 | import styled from "styled-components"; 20 | import tag from "clean-tag"; 21 | import { 22 | space, 23 | lineHeight, 24 | fontSize, 25 | fontStyle, 26 | size, 27 | color, 28 | colorStyle, 29 | textStyle, 30 | fontFamily, 31 | fontWeight, 32 | letterSpacing, 33 | borderRadius 34 | } from "styled-system"; 35 | 36 | const StyledDynamicComponent = styled(tag)` 37 | ${space} 38 | ${fontSize} 39 | ${fontStyle} 40 | ${color} 41 | ${size} 42 | ${colorStyle} 43 | ${textStyle} 44 | ${lineHeight} 45 | ${letterSpacing} 46 | ${fontFamily} 47 | ${fontWeight} 48 | ${borderRadius} 49 | `; 50 | 51 | const DynamicComponent = ({ tag = "div", children, ...props }) => { 52 | const WithComponent = StyledDynamicComponent.withComponent(tag); 53 | return {children}; 54 | }; 55 | 56 | DynamicComponent.defaultProps = { 57 | tag: "div" 58 | }; 59 | 60 | export default DynamicComponent; 61 | ``` 62 | [./DynamicComponent/index.js](https://github.com/Jezfx/react-typography-system/blob/master/src/DynamicComponent/index.js) 63 | 64 | Each of the Typography styles use the higher orer DynamicComponent to enable style and markup overrides. The default styles get spread in from the theme.js file which is also passed into the ThemeProvider. 65 | 66 | 67 | ```jsx 68 | export const Canon = props => ( 69 | 70 | {props.children} 71 | 72 | ); 73 | ``` 74 | [./Typography/index.js](https://github.com/Jezfx/react-typography-system/blob/master/src/Typography/index.js) 75 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typeography-system", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "react": "^16.6.3", 7 | "react-dom": "^16.6.3", 8 | "clean-tag": "^2.0.2", 9 | "react-scripts": "2.1.1", 10 | "styled-components": "^4.1.2", 11 | "styled-system": "^3.1.11", 12 | "webfontloader": "^1.6.28" 13 | }, 14 | "scripts": { 15 | "start": "react-scripts start", 16 | "build": "react-scripts build", 17 | "test": "react-scripts test", 18 | "eject": "react-scripts eject" 19 | }, 20 | "eslintConfig": { 21 | "extends": "react-app" 22 | }, 23 | "browserslist": [ 24 | ">0.2%", 25 | "not dead", 26 | "not ie <= 11", 27 | "not op_mini all" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jezfx/react-typography-system/ce48ed88a9ab9a592f08b939d90269af8e104555/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 22 | React App 23 | 24 | 25 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | animation: App-logo-spin infinite 20s linear; 7 | height: 40vmin; 8 | } 9 | 10 | .App-header { 11 | background-color: #282c34; 12 | min-height: 100vh; 13 | display: flex; 14 | flex-direction: column; 15 | align-items: center; 16 | justify-content: center; 17 | font-size: calc(10px + 2vmin); 18 | color: white; 19 | } 20 | 21 | .App-link { 22 | color: #61dafb; 23 | } 24 | 25 | @keyframes App-logo-spin { 26 | from { 27 | transform: rotate(0deg); 28 | } 29 | to { 30 | transform: rotate(360deg); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import styled, { ThemeProvider } from "styled-components"; 3 | import { Split, Row, Section } from "./Layout"; 4 | import Link from "./Link"; 5 | 6 | import { 7 | Canon, 8 | Trafalgar, 9 | Paragon, 10 | GreatPrimer, 11 | DoublePica, 12 | BodyCopy, 13 | Code 14 | } from "./Typography"; 15 | import Breakpoint from "./Breakpoint"; 16 | 17 | import theme from "./theme"; 18 | import "./App.css"; 19 | 20 | const Uppercase = styled(GreatPrimer)` 21 | text-transform: uppercase; 22 | `; 23 | 24 | class App extends Component { 25 | render() { 26 | return ( 27 | 28 | <> 29 |
30 | 31 |
32 | 33 |
34 | 35 | Canon 36 | Hero or blog post title 37 | 38 | 39 | 40 | Trafalgar 41 | Article title or section header 42 | 43 | 44 | 45 | Paragon 46 | Primary headline on indexes 47 | 48 | 49 | 50 | DoublePica 51 | Sub header 52 | 53 | 54 | 55 | GreatPrimer 56 | Headline title or subtitle 57 | 58 | 59 | 60 | BodyCopy 61 | Article body copy only 62 | 63 | 64 | 65 | Code 66 | For code wrapped snippets 67 | 68 |
69 | 70 |
71 | 72 | 73 | Canon 74 | 75 | {``} 78 | 79 | 80 | 81 | Trafalgar 82 | {`Trafalgar`} 85 | 86 | 87 | 88 | 89 | Paragon 90 | 91 | {``} 94 | 95 | 96 | 97 | 98 | Double Pica 99 | 100 | {`Double Pica`} 103 | 104 | 105 | 106 | 107 | Great Primer 108 | 109 | {`Great Primer`} 112 | 113 | 114 | 115 | Body Copy 116 |
117 | {`Body Copy`} 120 |
121 |
122 |
123 | 124 | View the full write up on{" "} 125 | Medium ✌️ 126 | 127 |
128 | 129 |
130 | ); 131 | } 132 | } 133 | 134 | export default App; 135 | -------------------------------------------------------------------------------- /src/Breakpoint/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | 4 | import theme from "../theme"; 5 | 6 | const Breakpoint = styled.span` 7 | padding: 2px 4px; 8 | background: ${theme.colours.citrineWhite}; 9 | 10 | &:after { 11 | content: "A"; 12 | 13 | @media screen and (min-width: ${theme.breakpoints[0]}) { 14 | content: "B"; 15 | } 16 | @media screen and (min-width: ${theme.breakpoints[1]}) { 17 | content: "C"; 18 | } 19 | } 20 | `; 21 | 22 | export default () => Breakpoint: ; 23 | -------------------------------------------------------------------------------- /src/DynamicComponent/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import tag from "clean-tag"; 4 | import { 5 | space, 6 | lineHeight, 7 | fontSize, 8 | fontStyle, 9 | size, 10 | color, 11 | colorStyle, 12 | textStyle, 13 | fontFamily, 14 | fontWeight, 15 | letterSpacing, 16 | borderRadius 17 | } from "styled-system"; 18 | 19 | const StyledDynamicComponent = styled(tag)` 20 | ${space} 21 | ${fontSize} 22 | ${fontStyle} 23 | ${color} 24 | ${size} 25 | ${colorStyle} 26 | ${textStyle} 27 | ${lineHeight} 28 | ${letterSpacing} 29 | ${fontFamily} 30 | ${fontWeight} 31 | ${borderRadius} 32 | `; 33 | 34 | const DynamicComponent = ({ tag = "div", children, ...props }) => { 35 | const WithComponent = StyledDynamicComponent.withComponent(tag); 36 | return {children}; 37 | }; 38 | 39 | DynamicComponent.defaultProps = { 40 | tag: "div" 41 | }; 42 | 43 | export default DynamicComponent; 44 | -------------------------------------------------------------------------------- /src/Layout/index.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import theme from "../theme"; 3 | 4 | const Article = styled.article` 5 | padding: 10px 0; 6 | margin-bottom: 40px; 7 | border-bottom: solid 1px #ccc; 8 | `; 9 | 10 | export const Section = styled.section` 11 | margin-bottom: 80px; 12 | `; 13 | 14 | export const Split = styled(Article)` 15 | display: flex; 16 | flex-direction: column; 17 | justify-content: space-between; 18 | align-items: baseline; 19 | 20 | @media screen and (min-width: ${theme.breakpoints[0]}) { 21 | flex-direction: row; 22 | } 23 | `; 24 | 25 | export const Row = styled(Article)` 26 | *:first-child { 27 | margin-bottom: 10px; 28 | } 29 | `; 30 | -------------------------------------------------------------------------------- /src/Link/index.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import theme from "../theme"; 3 | 4 | export default styled.a` 5 | color: ${theme.colours.bostonBlue}; 6 | text-decoration: underline; 7 | 8 | &:hover { 9 | color: ${theme.colours.stTropaz}; 10 | } 11 | `; 12 | -------------------------------------------------------------------------------- /src/Typography/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import DynamicComponent from "../DynamicComponent"; 3 | import theme from "../theme"; 4 | 5 | const { 6 | canon, 7 | trafalgar, 8 | paragon, 9 | greatPrimer, 10 | doublePica, 11 | bodyCopy, 12 | pica, 13 | code 14 | } = theme.textStyles; 15 | 16 | export const Canon = props => ( 17 | 18 | {props.children} 19 | 20 | ); 21 | 22 | export const Trafalgar = props => ( 23 | 24 | {props.children} 25 | 26 | ); 27 | 28 | export const Paragon = props => ( 29 | 30 | {props.children} 31 | 32 | ); 33 | 34 | export const GreatPrimer = props => ( 35 | 36 | {props.children} 37 | 38 | ); 39 | 40 | export const DoublePica = props => ( 41 | 42 | {props.children} 43 | 44 | ); 45 | 46 | export const BodyCopy = props => ( 47 | 48 | {props.children} 49 | 50 | ); 51 | 52 | export const Pica = props => ( 53 | 54 | {props.children} 55 | 56 | ); 57 | 58 | export const Code = props => ( 59 | 60 | {props.children} 61 | 62 | ); 63 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import WebFont from "webfontloader"; 4 | import { createGlobalStyle } from "styled-components"; 5 | 6 | import "./reset.css"; 7 | import App from "./App"; 8 | import * as serviceWorker from "./serviceWorker"; 9 | 10 | const GlobalStyle = createGlobalStyle` 11 | body { 12 | padding: 10px; 13 | max-width: 700px; 14 | } 15 | `; 16 | 17 | WebFont.load({ 18 | google: { 19 | families: ["Poppins: 400,500,700", "PT Serif: 400,700]", "Roboto Mono"] 20 | } 21 | }); 22 | 23 | const Root = ( 24 | <> 25 | 26 | 27 | 28 | ); 29 | 30 | ReactDOM.render(Root, document.getElementById("root")); 31 | 32 | // If you want your app to work offline and load faster, you can change 33 | // unregister() to register() below. Note this comes with some pitfalls. 34 | // Learn more about service workers: http://bit.ly/CRA-PWA 35 | serviceWorker.unregister(); 36 | -------------------------------------------------------------------------------- /src/reset.css: -------------------------------------------------------------------------------- 1 | /* http://meyerweb.com/eric/tools/css/reset/ 2 | v2.0 | 20110126 3 | License: none (public domain) 4 | */ 5 | 6 | html, body, div, span, applet, object, iframe, 7 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 8 | a, abbr, acronym, address, big, cite, code, 9 | del, dfn, em, img, ins, kbd, q, s, samp, 10 | small, strike, strong, sub, sup, tt, var, 11 | b, u, i, center, 12 | dl, dt, dd, ol, ul, li, 13 | fieldset, form, label, legend, 14 | table, caption, tbody, tfoot, thead, tr, th, td, 15 | article, aside, canvas, details, embed, 16 | figure, figcaption, footer, header, hgroup, 17 | menu, nav, output, ruby, section, summary, 18 | time, mark, audio, video { 19 | margin: 0; 20 | padding: 0; 21 | border: 0; 22 | font-size: 100%; 23 | vertical-align: baseline; 24 | } 25 | /* HTML5 display-role reset for older browsers */ 26 | article, aside, details, figcaption, figure, 27 | footer, header, hgroup, menu, nav, section { 28 | display: block; 29 | } 30 | body { 31 | line-height: 1; 32 | } 33 | ol, ul { 34 | list-style: none; 35 | } 36 | blockquote, q { 37 | quotes: none; 38 | } 39 | blockquote:before, blockquote:after, 40 | q:before, q:after { 41 | content: ''; 42 | content: none; 43 | } 44 | table { 45 | border-collapse: collapse; 46 | border-spacing: 0; 47 | } -------------------------------------------------------------------------------- /src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read http://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.1/8 is considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit http://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See http://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl) 104 | .then(response => { 105 | // Ensure service worker exists, and that we really are getting a JS file. 106 | const contentType = response.headers.get('content-type'); 107 | if ( 108 | response.status === 404 || 109 | (contentType != null && contentType.indexOf('javascript') === -1) 110 | ) { 111 | // No service worker found. Probably a different app. Reload the page. 112 | navigator.serviceWorker.ready.then(registration => { 113 | registration.unregister().then(() => { 114 | window.location.reload(); 115 | }); 116 | }); 117 | } else { 118 | // Service worker found. Proceed as normal. 119 | registerValidSW(swUrl, config); 120 | } 121 | }) 122 | .catch(() => { 123 | console.log( 124 | 'No internet connection found. App is running in offline mode.' 125 | ); 126 | }); 127 | } 128 | 129 | export function unregister() { 130 | if ('serviceWorker' in navigator) { 131 | navigator.serviceWorker.ready.then(registration => { 132 | registration.unregister(); 133 | }); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/theme.js: -------------------------------------------------------------------------------- 1 | // http://chir.ag/projects/name-that-color/ 2 | const colours = { 3 | bostonBlue: "#428bca", 4 | stTropaz: "#2a6496", 5 | maroonFlush: "#c7254e", 6 | softPeach: "#f9f2f4", 7 | mantis: "#6ECE5A", 8 | citrineWhite: "#FBF7DC" 9 | }; 10 | 11 | const fontFamilies = { 12 | heading: "Poppins, Helvetica, Arial, sans-serif", 13 | body: "PT Serif, Helvetica, Arial, sans-serif", 14 | code: "Roboto Mono, monospace" 15 | }; 16 | 17 | export default { 18 | textStyles: { 19 | canon: { 20 | tag: "h1", 21 | fontSize: [11, 12, 14], 22 | fontWeight: 700, 23 | lineHeight: ["32px", "36px", "56px"], 24 | fontFamily: fontFamilies.heading 25 | }, 26 | trafalgar: { 27 | tag: "h1", 28 | fontSize: [6, 9, 12], 29 | lineHeight: ["24px", "28px", "40px"], 30 | fontWeight: 500, 31 | fontFamily: fontFamilies.heading 32 | }, 33 | paragon: { 34 | tag: "h2", 35 | fontSize: [6, 8, 11], 36 | fontWeight: 500, 37 | fontFamily: fontFamilies.heading, 38 | lineHeight: ["20px", "26px", "32px"] 39 | }, 40 | doublePica: { 41 | fontSize: [6, 6, 10], 42 | fontWeight: 400, 43 | fontFamily: fontFamilies.heading, 44 | lineHeight: ["24px", "24px", "30px"] 45 | }, 46 | greatPrimer: { 47 | tag: "h3", 48 | fontWeight: 400, 49 | fontSize: [5, 5, 7], 50 | fontFamily: fontFamilies.heading, 51 | lineHeight: ["22px", "22px", "24px"] 52 | }, 53 | bodyCopy: { 54 | tag: "p", 55 | fontSize: [3, 4, 5], 56 | fontFamily: fontFamilies.body, 57 | lineHeight: ["20px", "22px", "24px"] 58 | }, 59 | code: { 60 | tag: "span", 61 | px: "4px", 62 | py: "2px", 63 | lineHeight: "22px", 64 | color: colours.maroonFlush, 65 | bg: colours.softPeach, 66 | borderRadius: 4, 67 | fontFamily: fontFamilies.code 68 | }, 69 | underline: { 70 | textDecoration: "underline" 71 | } 72 | }, 73 | fontSizes: [12, 13, 14, 15, 16, 18, 20, 21, 22, 24, 26, 28, 32, 36, 52], 74 | breakpoints: ["319px", "599px"], 75 | colours: { 76 | ...colours 77 | } 78 | }; 79 | --------------------------------------------------------------------------------