├── .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 |
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 |
--------------------------------------------------------------------------------