├── .gitignore ├── .babelrc ├── .travis.yml ├── .flowconfig ├── example ├── index.js ├── index.html └── src │ ├── colors.js │ ├── Box.js │ ├── Button.js │ └── App.js ├── __snapshots__ └── test.js.snap ├── rollup.config.js ├── index.js.flow ├── index.js ├── LICENSE ├── package.json ├── test.js ├── README.md ├── flow-typed └── npm │ ├── styled-components_v2.x.x.js │ └── jest_v20.x.x.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | example/bundle.js 4 | dist 5 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [["es2015", { "loose": true }], "react"], 3 | "plugins": ["transform-class-properties"] 4 | } 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '8' 4 | cache: yarn 5 | script: yarn test -- --runInBand --coverage && yarn flow 6 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | /node_modules/styled-components/.* 3 | 4 | [include] 5 | 6 | [libs] 7 | 8 | [options] 9 | 10 | [lints] 11 | -------------------------------------------------------------------------------- /example/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import {render} from 'react-dom'; 4 | import App from './src/App'; 5 | 6 | render(, document.getElementById('root')); 7 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | styled-theming example 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /example/src/colors.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export default { 3 | white: '#fff', 4 | 5 | grayLighter: '#eee', 6 | grayLight: '#ccc', 7 | grayDark: '#444', 8 | grayDarker: '#222', 9 | 10 | blueLight: '#2196F3', 11 | blueDark: '#104977', 12 | 13 | greenLight: '#8bc34a', 14 | greenDark: '#3b5221', 15 | 16 | yellowLight: '#ffc107', 17 | yellowDark: '#715605', 18 | 19 | redLight: '#e91e63', 20 | redDark: '#670a2a', 21 | }; 22 | -------------------------------------------------------------------------------- /__snapshots__/test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`basic 1`] = ` 4 |
7 | `; 8 | 9 | exports[`basic 2`] = ` 10 |
13 | `; 14 | 15 | exports[`variants 1`] = ` 16 |

19 | `; 20 | 21 | exports[`variants 2`] = ` 22 |

25 | `; 26 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import uglify from 'rollup-plugin-uglify'; 2 | import commonjs from 'rollup-plugin-commonjs'; 3 | 4 | let dest; 5 | let plugins = [commonjs()]; 6 | 7 | if (process.env.NODE_ENV === 'production') { 8 | dest = 'dist/styled-theming.min.js'; 9 | plugins.push(uglify()); 10 | } else { 11 | dest = 'dist/styled-theming.js'; 12 | } 13 | 14 | export default { 15 | entry: 'index.js', 16 | format: 'umd', 17 | moduleName: 'theme', 18 | plugins, 19 | dest, 20 | }; 21 | -------------------------------------------------------------------------------- /index.js.flow: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | export type ThemeValue = string | (props: Object) => string; 4 | export type ThemeMap = { [key: string]: ThemeValue }; 5 | export type VariantMap = { [key: string]: ThemeMap }; 6 | 7 | export type ThemeSet = (props: Object) => string; 8 | export type VariantSet = (props: Object) => string; 9 | 10 | declare export default { 11 | (name: string, values: ThemeMap): ThemeSet; 12 | variants(name: string, prop: string, values: VariantMap): VariantSet; 13 | provider(theme: Object): Object; 14 | }; 15 | -------------------------------------------------------------------------------- /example/src/Box.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import styled from 'styled-components'; 3 | import theme from '../..'; 4 | import colors from './colors'; 5 | 6 | const fontSize = theme('size', { 7 | normal: '1em', 8 | large: '1.2em', 9 | }); 10 | 11 | const boxBackgroundColor = theme('mode', { 12 | light: colors.white, 13 | dark: colors.grayDarker, 14 | }); 15 | 16 | const boxColor = theme('mode', { 17 | light: colors.grayDarker, 18 | dark: colors.grayLighter, 19 | }); 20 | 21 | const Box = styled.div` 22 | position: relative; 23 | width: 100%; 24 | height: 100%; 25 | padding: 4em; 26 | font-size: ${fontSize}; 27 | background-color: ${boxBackgroundColor}; 28 | color: ${boxColor}; 29 | `; 30 | 31 | export default Box; 32 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function getThemeValue(name, props, values) { 4 | var value = ( 5 | props.theme && 6 | props.theme[name] 7 | ); 8 | 9 | var themeValue; 10 | 11 | if (typeof value === 'function') { 12 | themeValue = value(values); 13 | } else { 14 | themeValue = values[value]; 15 | } 16 | 17 | if (typeof themeValue === 'function') { 18 | return themeValue(props); 19 | } else { 20 | return themeValue; 21 | } 22 | } 23 | 24 | function theme(name, values) { 25 | return function(props) { 26 | return getThemeValue(name, props, values); 27 | }; 28 | } 29 | 30 | theme.variants = function(name, prop, values) { 31 | return function(props) { 32 | var variant = props[prop] && values[props[prop]]; 33 | return variant && getThemeValue(name, props, variant); 34 | }; 35 | }; 36 | 37 | module.exports = theme; 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017-present James Kyle 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "styled-theming", 3 | "description": "Create themes for your app using styled-components", 4 | "version": "2.2.0", 5 | "main": "index.js", 6 | "repository": "git@github.com:styled-components/styled-theming.git", 7 | "author": "James Kyle ", 8 | "license": "MIT", 9 | "files": [ 10 | "index.js", 11 | "index.js.flow", 12 | "dist/styled-theming.js", 13 | "dist/styled-theming.min.js" 14 | ], 15 | "scripts": { 16 | "test": "jest", 17 | "example": "browserify -t babelify example/index.js -o example/bundle.js --debug", 18 | "build": "rm -rf dist && rollup -c && NODE_ENV=production rollup -c", 19 | "prepublish": "yarn build" 20 | }, 21 | "devDependencies": { 22 | "babel-plugin-transform-class-properties": "^6.24.1", 23 | "babel-preset-es2015": "^6.24.1", 24 | "babel-preset-react": "^6.24.1", 25 | "babelify": "^7.3.0", 26 | "browserify": "^14.4.0", 27 | "flow-bin": "^0.51.0", 28 | "jest": "^20.0.4", 29 | "prop-types": "^15.5.10", 30 | "react": "^15.6.1", 31 | "react-dom": "^15.6.1", 32 | "react-test-renderer": "^15.6.1", 33 | "rollup": "^0.45.2", 34 | "rollup-plugin-commonjs": "^8.0.2", 35 | "rollup-plugin-uglify": "^2.0.1", 36 | "styled-components": "^2.1.1" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /example/src/Button.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import styled from 'styled-components'; 3 | import theme from '../..'; 4 | import colors from './colors'; 5 | 6 | const buttonBackgroundColor = theme.variants('mode', 'kind', { 7 | default: { light: colors.grayLight, dark: colors.grayDark }, 8 | primary: { light: colors.blueLight, dark: colors.blueDark }, 9 | success: { light: colors.greenLight, dark: colors.greenDark, }, 10 | warning: { light: colors.yellowLight, dark: colors.yellowDark, }, 11 | danger: { light: colors.redLight, dark: colors.redDark, }, 12 | }); 13 | 14 | const buttonColor = theme.variants('mode', 'kind', { 15 | default: { light: colors.grayDarker, dark: colors.grayLighter }, 16 | primary: { light: colors.blueDark, dark: colors.blueLight }, 17 | success: { light: colors.greenDark, dark: colors.greenLight }, 18 | warning: { light: colors.yellowDark, dark: colors.yellowLight }, 19 | danger: { light: colors.redDark, dark: colors.redLight }, 20 | }); 21 | 22 | const Button = styled.button` 23 | font: inherit; 24 | padding: 0.5em 1em; 25 | border: none; 26 | background-color: ${buttonBackgroundColor}; 27 | color: ${buttonColor}; 28 | border-radius: 0.25em; 29 | margin-right: 0.5em; 30 | cursor: pointer; 31 | `; 32 | 33 | Button.defaultProps = { 34 | kind: 'default', 35 | }; 36 | 37 | export default Button; 38 | -------------------------------------------------------------------------------- /example/src/App.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import styled, {ThemeProvider, injectGlobal} from 'styled-components'; 4 | import Box from './Box'; 5 | import Button from './Button'; 6 | 7 | injectGlobal` 8 | * { 9 | box-sizing: border-box; 10 | } 11 | 12 | html, 13 | body, 14 | #root { 15 | position: relative; 16 | width: 100%; 17 | height: 100%; 18 | } 19 | 20 | body { 21 | margin: 0; 22 | font: normal 1em/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; 23 | } 24 | `; 25 | 26 | const Code = styled.pre` 27 | font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace; 28 | border: 3px solid; 29 | padding: 1em; 30 | border-radius: 0.25em; 31 | `; 32 | 33 | export default class App extends React.Component { 34 | state = { 35 | mode: 'light', 36 | size: 'normal', 37 | }; 38 | 39 | handleToggleMode = () => { 40 | this.setState({ mode: this.state.mode === 'light' ? 'dark' : 'light' }); 41 | }; 42 | 43 | handleToggleSize = () => { 44 | this.setState({ size: this.state.size === 'normal' ? 'large' : 'normal' }); 45 | }; 46 | 47 | render() { 48 | return ( 49 | 50 | 51 |

styled-theming

52 | 53 | 54 | 55 | 56 | 57 |
58 |
59 | ); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import PropTypes from 'prop-types'; 4 | import theme from './'; 5 | import styled, {ThemeProvider} from 'styled-components'; 6 | import ReactTestRenderer from 'react-test-renderer'; 7 | 8 | function render(jsx) { 9 | return ReactTestRenderer.create(jsx).toJSON(); 10 | } 11 | 12 | test('basic', () => { 13 | const backgroundColor = theme('mode', { 14 | light: '#fff', 15 | dark: '#000' 16 | }); 17 | 18 | const borderColor = theme('mode', { 19 | light: props => props.theme.accent.light, 20 | dark: props => props.theme.accent.dark, 21 | }); 22 | 23 | const Page = styled.div` 24 | background-color: ${backgroundColor}; 25 | border-color: ${borderColor}; 26 | `; 27 | 28 | const accent = { 29 | light: 'skyblue', 30 | dark: 'royalblue' 31 | }; 32 | 33 | expect(render( 34 | 35 | 36 | 37 | )).toMatchSnapshot(); 38 | 39 | expect(render( 40 | 41 | 42 | 43 | )).toMatchSnapshot(); 44 | }); 45 | 46 | test('variants', () => { 47 | const headingColor = theme.variants('mode', 'variant', { 48 | default: { light: '#000', dark: '#fff' }, 49 | fancy: { 50 | light: props => props.theme.accent.light, 51 | dark: props => props.theme.accent.dark, 52 | }, 53 | }); 54 | 55 | const Heading = styled.h1` 56 | color: ${headingColor}; 57 | `; 58 | 59 | Heading.propTypes = { variant: PropTypes.oneOf(['default', 'fancy']) }; 60 | Heading.defaultProps = { variant: 'default' }; 61 | 62 | const accent = { 63 | light: 'skyblue', 64 | dark: 'royalblue' 65 | }; 66 | 67 | expect(render( 68 | 69 | 70 | 71 | )).toMatchSnapshot(); 72 | 73 | expect(render( 74 | 75 | 76 | 77 | )).toMatchSnapshot(); 78 | }); 79 | 80 | describe('theme()', () => { 81 | const fn = theme('mode', { light: '#fff', dark: '#000' }); 82 | 83 | it('should create a function that returns a matching prop when passed a string', () => { 84 | expect(fn({ theme: { mode: 'light' } })).toBe('#fff'); 85 | expect(fn({ theme: { mode: 'dark' } })).toBe('#000'); 86 | }); 87 | 88 | it('should create a function that returns calls a passed function', () => { 89 | expect(fn({ theme: { mode: themes => themes.light } })).toBe('#fff'); 90 | expect(fn({ theme: { mode: themes => themes.dark } })).toBe('#000'); 91 | }); 92 | }); 93 | 94 | describe('theme.variants()', () => { 95 | const fn = theme.variants('mode', 'kind', { 96 | default: { light: '#fff', dark: '#000' }, 97 | fancy: { light: '#f0f', dark: '#0f0' }, 98 | }); 99 | 100 | it('should create a function that returns a matching prop when passed a string', () => { 101 | expect(fn({ kind: 'default', theme: { mode: 'light' } })).toBe('#fff'); 102 | expect(fn({ kind: 'default', theme: { mode: 'dark' } })).toBe('#000'); 103 | expect(fn({ kind: 'fancy', theme: { mode: 'light' } })).toBe('#f0f'); 104 | expect(fn({ kind: 'fancy', theme: { mode: 'dark' } })).toBe('#0f0'); 105 | }); 106 | 107 | it('should create a function that returns calls a passed function', () => { 108 | expect(fn({ kind: 'default', theme: { mode: themes => themes.light } })).toBe('#fff'); 109 | expect(fn({ kind: 'default', theme: { mode: themes => themes.dark } })).toBe('#000'); 110 | expect(fn({ kind: 'fancy', theme: { mode: themes => themes.light } })).toBe('#f0f'); 111 | expect(fn({ kind: 'fancy', theme: { mode: themes => themes.dark } })).toBe('#0f0'); 112 | }); 113 | }); 114 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # styled-theming 2 | 3 | > Create themes for your app using [styled-components](https://www.styled-components.com/) 4 | 5 | Read the [introductory blog post](http://thejameskyle.com/styled-theming.html) 6 | 7 | ## Installation 8 | 9 | ```sh 10 | yarn add styled-components styled-theming 11 | ``` 12 | 13 | ## Example 14 | 15 | ```js 16 | import React from 'react'; 17 | import styled, {ThemeProvider} from 'styled-components'; 18 | import theme from 'styled-theming'; 19 | 20 | const boxBackgroundColor = theme('mode', { 21 | light: '#fff', 22 | dark: '#000', 23 | }); 24 | 25 | const Box = styled.div` 26 | background-color: ${boxBackgroundColor}; 27 | `; 28 | 29 | export default function App() { 30 | return ( 31 | 32 | 33 | Hello World 34 | 35 | 36 | ); 37 | } 38 | ``` 39 | 40 | ## API 41 | 42 | ### `` 43 | 44 | See [styled-components docs](https://www.styled-components.com/docs/advanced#theming) 45 | 46 | `` is part of styled-components, but is required for styled-theming. 47 | 48 | ```js 49 | import {ThemeProvider} from 'styled-components'; 50 | ``` 51 | 52 | `` accepts a single prop `theme` which you should pass an object 53 | with either strings or getter functions. For example: 54 | 55 | ```js 56 | 57 | modes.dark, size: sizes => sizes.large }}> 58 | ``` 59 | 60 | You should generally set up a `` at the root of your app: 61 | 62 | ```js 63 | function App() { 64 | return ( 65 | 66 | {/* rest of your app */} 67 | 68 | ); 69 | } 70 | ``` 71 | 72 | ### `theme(name, values)` 73 | 74 | Most of your theming will be done with this function. 75 | 76 | `name` should match one of the keys in your `` theme. 77 | 78 | ```js 79 | 80 | 81 | theme('whatever', {...}); 82 | ``` 83 | 84 | `values` should be an object where one of the keys will be selected by the 85 | value provided to `` theme. 86 | 87 | ```js 88 | 89 | 90 | 91 | theme('mode', { 92 | light: '...', 93 | dark: '...', 94 | }); 95 | ``` 96 | 97 | The values of this object can be any CSS value. 98 | 99 | ```js 100 | theme('mode', { 101 | light: '#fff', 102 | dark: '#000', 103 | }); 104 | 105 | theme('font', { 106 | sansSerif: '"Helvetica Neue", Helvetica, Arial, sans-serif', 107 | serif: 'Georgia, Times, "Times New Roman", serif', 108 | monoSpaced: 'Consolas, monaco, monospace', 109 | }); 110 | ``` 111 | 112 | These values can also be functions that return CSS values. 113 | 114 | ```js 115 | theme('mode', { 116 | light: props => props.theme.userProfileAccentColor.light, 117 | dark: props => props.theme.userProfileAccentColor.dark, 118 | }); 119 | ``` 120 | 121 | `theme` will create a function that you can use as a value in 122 | styled-component's `styled` function. 123 | 124 | ```js 125 | import styled from 'styled-components'; 126 | import theme from 'styled-theming'; 127 | 128 | const backgroundColor = theme('mode', { 129 | light: '#fff', 130 | dark: '#000', 131 | }); 132 | 133 | const Box = styled.div` 134 | background-color: ${backgroundColor} 135 | `; 136 | ``` 137 | 138 | The values will be passed through like any other interpolation 139 | in styled-components. You can use the `css` helper to add entire 140 | blocks of styles, including their own interpolations. 141 | 142 | ```js 143 | import styled, {css} from 'styled-components'; 144 | import theme from 'styled-theming'; 145 | 146 | const white = "#fff"; 147 | const black = "#000"; 148 | 149 | const boxStyles = theme('mode', { 150 | light: css` 151 | background: ${white}; 152 | color: ${black}; 153 | `, 154 | dark: css` 155 | background: ${black}; 156 | color: ${white}; 157 | `, 158 | }); 159 | 160 | const Box = styled.div` 161 | ${boxStyles} 162 | `; 163 | ``` 164 | 165 | ### `theme.variants(name, prop, themes)` 166 | 167 | It's often useful to create variants of the same component that are selected 168 | via an additional prop. 169 | 170 | To make this easier with theming, styled-theming provides a `theme.variants` 171 | function. 172 | 173 | ```js 174 | import styled from 'styled-components'; 175 | import theme from 'styled-theming'; 176 | 177 | const backgroundColor = theme.variants('mode', 'variant', { 178 | default: { light: 'gray', dark: 'darkgray' }, 179 | primary: { light: 'blue', dark: 'darkblue' }, 180 | success: { light: 'green', dark: 'darkgreen' }, 181 | warning: { light: 'orange', dark: 'darkorange' }, 182 | }); 183 | 184 | const Button = styled.button` 185 | background-color: ${backgroundColor}; 186 | `; 187 | 188 | Button.propTypes = { 189 | variant: PropTypes.oneOf(['default', 'primary', 'success', 'warning']) 190 | }; 191 | 192 | Button.defaultProps = { 193 | variant: 'default', 194 | }; 195 | 196 |