├── .babelrc ├── .editorconfig ├── .gitignore ├── .lvimrc ├── GUIDE.md ├── README.md ├── definition.js ├── definition.test.js ├── examples ├── aphrodite.js ├── emotion.js ├── glamor.js └── index.html ├── index.js ├── index.test.js ├── media.js ├── media.test.js ├── package-lock.json ├── package.json └── rollup.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "modules": false, 5 | "loose": true, 6 | "targets": { 7 | "browsers": ["last 1 version"] 8 | } 9 | }] 10 | ], 11 | "env": { 12 | "test": { 13 | "plugins": ["transform-es2015-modules-commonjs"] 14 | }, 15 | "development":{ 16 | "plugins": ["transform-react-jsx"] 17 | } 18 | }, 19 | "plugins": [ 20 | "transform-object-rest-spread" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | .cache 4 | -------------------------------------------------------------------------------- /.lvimrc: -------------------------------------------------------------------------------- 1 | let g:ale_fixers = { 2 | \ 'javascript': ['prettier_standard'], 3 | \} 4 | 5 | let g:ale_linters = {'javascript': ['']} 6 | -------------------------------------------------------------------------------- /GUIDE.md: -------------------------------------------------------------------------------- 1 | [combine-same-keys]: https://github.com/rafaelrinaldi/combine-same-keys 2 | [fcss]: https://github.com/chibicode/react-functional-css-protips 3 | [styled-system]: https://github.com/jxnblk/styled-system 4 | 5 | # Guide 6 | > Detailed guide on how to get the most out of `responsive-styles` 7 | 8 | ## Defining breakpoints 9 | 10 | In order for the library to work you must always specify a list of breakpoints you want to support. This can become very repetitive, so our suggestion is that you simply create a function that composes on `responsive-styles` so you can reference it instead: 11 | 12 | ```js 13 | // your-project/src/responsive.js 14 | import responsiveStyles from 'responsive-styles' 15 | 16 | // A list with the breakpoints you wish to support 17 | const breaks = [48, 64, 80] 18 | 19 | // Compose a new function on top of `responsiveStyles` passing down your breakpoints 20 | const responsive = (props, values) => responsiveStyles(props, values, breaks) 21 | 22 | export default responsive 23 | ``` 24 | 25 | For the sake of brevity, moving forward in the next code snippets we will assume you have a `responsive.js` file living within your own project and that you chose Aphrodite. 26 | 27 | ### Mobile first 28 | 29 | One of the assumptions is that you approach design using a mobile first mindset, so the first item you pass to the list would be applied by default to whatever property you're defining values for. That's the reason why the first value on the breakpoints list should not be zero, since it would be redundant. 30 | 31 | ### Use `null` to bypass definitions 32 | 33 | You can use `null` to bypass definitions for specific breakpoints. Example: 34 | 35 | ```js 36 | import responsive './responsive' 37 | 38 | responsive('color', [null, 'green', 'blue']) 39 | 40 | // Outputs: 41 | // { 42 | // '@media screen and (min-width: 48em)': { color: 'green' }, 43 | // '@media screen and (min-width: 64em)': { color: 'blue' }, 44 | // } 45 | ``` 46 | 47 | ### Functional CSS 48 | 49 | `responsive-styles` plays really well with the concept of [Functional CSS][fcss], which favors creating one CSS class per property definition. 50 | 51 | Creating classes is cheap, gives us more control, separate visual concerns and can potentially improve reusability. 52 | 53 | Even though the Functional CSS approach is encouraged, the API is flexible enough to allow for all sorts of different use cases: 54 | 55 | ### What’s encouraged 56 | 57 | ```js 58 | // Every rule has its own class name 59 | import { StyleSheet } from 'aphrodite' 60 | import responsive from './responsive' 61 | 62 | const styles = StyleSheet.create({ 63 | color: responsive('color', ['red', 'green', 'blue']), 64 | opacity: responsive('opacity', [0, 0.5, 1]), 65 | width: responsive('width', ['25vw', '50vw', '100vw']) 66 | }) 67 | ``` 68 | 69 | ### What’s possible 70 | 71 | You can combine multiple rules within the same media break by utilizing the [`combine-same-keys`][combine-same-keys] library: 72 | 73 | ```js 74 | import { StyleSheet } from 'aphrodite' 75 | import combineSameKeys from 'combine-same-keys' 76 | import responsive from './responsive' 77 | 78 | const styles = StyleSheet.create({ 79 | root: combineSameKeys( 80 | responsive('color', ['red', 'green', 'blue']), 81 | responsive('opacity', [0, 0.5, 1]), 82 | responsive('width', ['25vw', '50vw', '100vw']) 83 | ) 84 | }) 85 | ``` 86 | 87 | #### “It’s just JavaScript” 88 | 89 | We can benefit from having things isolated in small functions by exploring different possibilities when composing styles. For instance, you can use partial application for reducing boilerplate and making things look more concise: 90 | 91 | ```js 92 | import { StyleSheet } from 'aphrodite' 93 | import responsive from './responsive' 94 | 95 | const color = values => responsive('color', values) 96 | const opacity = values => responsive('opacity', values) 97 | 98 | const styles = StyleSheet.create({ 99 | color: color(['red', 'green', 'blue']), 100 | opacity: opacity([0, 0.5, 1]) 101 | }) 102 | ``` 103 | 104 | ### Passing down plain objects 105 | 106 | It's encouraged that you split up your styling into classes instead of a group of rules – in a more functional fashion – but you can also pass down plain objects for each breakpoint, which can be useful sometimes (specially for intereoperability with other libraries): 107 | 108 | ```js 109 | import { StyleSheet } from 'aphrodite' 110 | import responsive from './responsive' 111 | 112 | const textVariations = [ 113 | { fontSize: 16, letterSpacing: 0 }, 114 | { fontSize: 18, letterSpacing: 0.5 }, 115 | { fontSize: 22, letterSpacing: 1, fontWeight: 'bold' } 116 | ] 117 | 118 | const styles = StyleSheet.create({ 119 | root: { 120 | color: '#FFF', 121 | backgroundColor: '#FF0066', 122 | ...responsive(textVariations) 123 | } 124 | }) 125 | ``` 126 | 127 | ## Related projects 128 | 129 | - [styled-system][styled-system] 130 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [aphrodite]: https://github.com/Khan/aphrodite 2 | [brent]: http://jxnblk.com 3 | [combine-same-keys]: https://github.com/rafaelrinaldi/combine-same-keys 4 | [emotion]: https://github.com/emotion-js/emotion 5 | [glamor]: https://github.com/threepointone/glamor 6 | [react]: http://reactjs.org 7 | [rebass]: http://jxnblk.com/rebass 8 | [url]: https://rinaldi.io 9 | 10 | # Responsive Styles [![Build Status](https://semaphoreci.com/api/v1/rafaelrinaldi/responsive-styles/branches/master/badge.svg)](https://semaphoreci.com/rafaelrinaldi/responsive-styles) 11 | > Use arrays as values to specify mobile-first responsive styles for CSS-in-JS projects 12 | 13 | The main idea of this library is to provide a framework agnostic way to easily enable any property to become responsive. 14 | 15 | This is **100%** inspired by the awesome work done by [Brent Jackson][brent] on [Rebass][rebass]. 16 | 17 | ## Install 18 | 19 | ```sh 20 | npm i responsive-styles 21 | ``` 22 | 23 | ## Usage 24 | 25 | This library was tested against [Aphrodite][aphrodite], [glamor][glamor] and [emotion][emotion] so far and they all seem to work nicely. 26 | 27 |
28 | Aphrodite and React 29 | 30 | ```js 31 | import React from 'react' 32 | import { render } from 'react-dom' 33 | import { StyleSheet, css } from 'aphrodite/no-important' 34 | import combineSameKeys from 'combine-same-keys' 35 | import responsiveStyles from 'responsive-styles' 36 | 37 | const breaks = [48, 64, 80] 38 | const r = (props, values) => responsiveStyles(props, values, breaks) 39 | 40 | const styles = StyleSheet.create({ 41 | // A la functional CSS 42 | padding: r('padding', [8, 24, 48]), 43 | fontSize: r('fontSize', [16, 24, 36]), 44 | 45 | // Combine multiple definitions into a single class name 46 | colors: combineSameKeys( 47 | r('color', ['#FFF', '#005782', '#820005', '#16160B']), 48 | r('backgroundColor', ['#FF0066', '#27D88E', '#FFF5C3', '#E1E1E1']) 49 | ) 50 | }) 51 | 52 | const className = css(styles.padding, styles.fontSize, styles.colors) 53 | 54 | const App = () =>

Aphrodite

55 | 56 | render(, document.querySelector('[data-app]')) 57 | ``` 58 |
59 | 60 |
61 | glamor and React 62 | 63 | ```js 64 | import React from 'react' 65 | import { render } from 'react-dom' 66 | import { css } from 'glamor' // The API is exactly the same for emotion 67 | import combineSameKeys from 'combine-same-keys' 68 | import responsiveStyles from 'responsive-styles' 69 | 70 | const breaks = [48, 64, 80] 71 | const r = (props, values) => responsiveStyles(props, values, breaks) 72 | 73 | // A la functional CSS 74 | const padding = css({ 75 | ...r('padding', [8, 24, 48]), 76 | }) 77 | 78 | const fontSize = css({ 79 | ...r('fontSize', [16, 24, 36]), 80 | }) 81 | 82 | // Combine multiple definitions into a single class name 83 | const colors = css( 84 | combineSameKeys( 85 | r('color', ['#FFF', '#005782', '#820005', '#16160B']), 86 | r('backgroundColor', ['#FF0066', '#27D88E', '#FFF5C3', '#E1E1E1']) 87 | ) 88 | ) 89 | 90 | const className = `${padding} ${fontSize} ${colors}` 91 | 92 | const App = () =>

Glamor and Emotion

93 | 94 | render(, document.querySelector('[data-app]')) 95 | ``` 96 |
97 | 98 | ### Examples 99 | 100 | If you want to checkout working examples for all libraries, you can download the project, install its dependencies and run: 101 | 102 | ```sh 103 | npm start 104 | ``` 105 | 106 | ## API 107 | 108 | ### `responsive(propertyOrValues, [maybeValues], [breaks])` 109 | 110 | #### `propertyOrValues` 111 | 112 | Type: `String` or `Array` 113 | 114 | Property name or an array with all the values for each breakpoint. 115 | 116 | #### `maybeValues` 117 | 118 | Type: `Array` 119 | 120 | Array with all the values for each breakpoint. 121 | 122 | #### `breaks` 123 | 124 | Type: `Array` 125 | 126 | List of breakpoints available, from smallest to largest. You can pass straight up numbers which will default to `em` values, or you can simply pass down a list of strings with the units you want. 127 | 128 | ## More 129 | 130 | For more examples and details about how the project works, please [check our guide](/GUIDE.md). 131 | 132 | ## License 133 | 134 | MIT © [Rafael Rinaldi][url] 135 | 136 | --- 137 | 138 |

139 | Buy me a ☕ 140 |

141 | -------------------------------------------------------------------------------- /definition.js: -------------------------------------------------------------------------------- 1 | // Helper to render a definition given a value and a property name 2 | export default function definition (value, property) { 3 | return typeof value === 'object' ? value : { [property]: value } 4 | } 5 | -------------------------------------------------------------------------------- /definition.test.js: -------------------------------------------------------------------------------- 1 | import definition from './definition' 2 | 3 | test('Should be able to render a definition given a property and a value', () => { 4 | expect(definition('red', 'color')).toEqual({ color: 'red' }) 5 | }) 6 | 7 | test('Should be able to render a definition given a plain object as value', () => { 8 | const value = { 9 | fontSize: 16, 10 | letterSpacing: 0 11 | } 12 | 13 | expect(definition(value)).toEqual(value) 14 | }) 15 | -------------------------------------------------------------------------------- /examples/aphrodite.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render } from 'react-dom' 3 | import { StyleSheet, css } from 'aphrodite/no-important' 4 | import combine from 'combine-same-keys' 5 | import responsiveStyles from '../' 6 | 7 | const breaks = [48, 64, 80] 8 | const r = (props, values) => responsiveStyles(props, values, breaks) 9 | 10 | const styles = StyleSheet.create({ 11 | root: { 12 | width: '100%', 13 | fontFamily: 'SF Mono, monospace' 14 | }, 15 | 16 | fcss: { 17 | padding: 75, 18 | ':before': { 19 | ...r('content', ['"Small"', '"Medium"', '"Large"', '"Extra Large"']) 20 | } 21 | }, 22 | 23 | withCombine: { 24 | ...combine( 25 | r('color', ['#FFF', '#005782', '#820005', '#16160B']), 26 | r('backgroundColor', ['#FF0066', '#27D88E', '#FFF5C3', '#E1E1E1']) 27 | ) 28 | } 29 | }) 30 | 31 | const App = () => ( 32 |
33 |

Aphrodite

34 |
35 |
36 | ) 37 | 38 | render(, document.querySelector('[data-app-aphrodite]')) 39 | -------------------------------------------------------------------------------- /examples/emotion.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render } from 'react-dom' 3 | import { css } from 'emotion' 4 | import combine from 'combine-same-keys' 5 | import responsiveStyles from '../' 6 | 7 | const breaks = [48, 64, 80] 8 | const r = (props, values) => responsiveStyles(props, values, breaks) 9 | 10 | const root = css({ 11 | width: '100%', 12 | fontFamily: 'SF Mono, monospace' 13 | }) 14 | 15 | const fcss = css({ 16 | padding: 75, 17 | ':before': { 18 | ...r('content', ['"Small"', '"Medium"', '"Large"', '"Extra Large"']) 19 | } 20 | }) 21 | 22 | const withCombine = css({ 23 | ...combine( 24 | r('color', ['#FFF', '#005782', '#820005', '#16160B']), 25 | r('backgroundColor', ['#FF0066', '#27D88E', '#FFF5C3', '#E1E1E1']) 26 | ) 27 | }) 28 | 29 | const App = () => ( 30 |
31 |

Emotion

32 |
33 |
34 | ) 35 | 36 | render(, document.querySelector('[data-app-emotion]')) 37 | -------------------------------------------------------------------------------- /examples/glamor.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render } from 'react-dom' 3 | import { css } from 'glamor' 4 | import combine from 'combine-same-keys' 5 | import responsiveStyles from '../' 6 | 7 | const breaks = [48, 64, 80] 8 | const r = (props, values) => responsiveStyles(props, values, breaks) 9 | 10 | const root = css({ 11 | width: '100%', 12 | fontFamily: 'SF Mono, monospace' 13 | }) 14 | 15 | const fcss = css({ 16 | padding: 75, 17 | ':before': { 18 | ...r('content', ['"Small"', '"Medium"', '"Large"', '"Extra Large"']) 19 | } 20 | }) 21 | 22 | const withCombine = css({ 23 | ...combine( 24 | r('color', ['#FFF', '#005782', '#820005', '#16160B']), 25 | r('backgroundColor', ['#FF0066', '#27D88E', '#FFF5C3', '#E1E1E1']) 26 | ) 27 | }) 28 | 29 | const App = () => ( 30 |
31 |

Glamor

32 |
33 |
34 | ) 35 | 36 | render(, document.querySelector('[data-app-glamor]')) 37 | -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 13 | 14 | 15 |
16 |
17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import media from './media' 2 | import definition from './definition' 3 | 4 | export default function responsive (propertyOrValues, maybeValues, breaks) { 5 | const values = 6 | typeof propertyOrValues === 'string' ? maybeValues : propertyOrValues 7 | const initial = values[0] 8 | 9 | return values 10 | .slice(1) 11 | .map((value, index) => { 12 | return ( 13 | value !== null && { 14 | [media(breaks[index])]: definition(value, propertyOrValues) 15 | } 16 | ) 17 | }) 18 | .reduce( 19 | (accumulator, value) => ({ 20 | ...accumulator, 21 | ...value 22 | }), 23 | initial !== null ? definition(initial, propertyOrValues) : {} 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /index.test.js: -------------------------------------------------------------------------------- 1 | import responsive from './' 2 | 3 | const breaks = [48, 64, 80] 4 | const r = (props, values) => responsive(props, values, breaks) 5 | 6 | test('Should return different values per breakpoint', () => { 7 | const input = r('color', ['red', 'green', 'blue']) 8 | const output = { 9 | color: 'red', 10 | '@media screen and (min-width: 48em)': { color: 'green' }, 11 | '@media screen and (min-width: 64em)': { color: 'blue' } 12 | } 13 | 14 | expect(input).toEqual(output) 15 | }) 16 | 17 | test('Should work properly given custom breakpoint values', () => { 18 | const input = r('color', ['red', 'green', 'blue']) 19 | const output = { 20 | color: 'red', 21 | '@media screen and (min-width: 48em)': { color: 'green' }, 22 | '@media screen and (min-width: 64em)': { color: 'blue' } 23 | } 24 | 25 | expect(input).toEqual(output) 26 | }) 27 | 28 | test('Should bypass instances of `null`', () => { 29 | const input = r('color', ['red', null, 'blue']) 30 | const output = { 31 | color: 'red', 32 | '@media screen and (min-width: 64em)': { color: 'blue' } 33 | } 34 | 35 | expect(input).toEqual(output) 36 | }) 37 | 38 | test('Should also bypass instaces of `null` for initial breakpoints', () => { 39 | const input = r('color', [null, 'green', 'blue']) 40 | const output = { 41 | '@media screen and (min-width: 48em)': { color: 'green' }, 42 | '@media screen and (min-width: 64em)': { color: 'blue' } 43 | } 44 | 45 | expect(input).toEqual(output) 46 | }) 47 | 48 | test('Should handle cases where values are plain objects', () => { 49 | const values = [ 50 | { fontSize: 16, letterSpacing: 0 }, 51 | { fontSize: 18, letterSpacing: 0.5 }, 52 | { fontSize: 22, letterSpacing: 1, fontWeight: 'bold' } 53 | ] 54 | 55 | const input = r(values, null) 56 | const output = { 57 | ...values[0], 58 | '@media screen and (min-width: 48em)': { ...values[1] }, 59 | '@media screen and (min-width: 64em)': { ...values[2] } 60 | } 61 | 62 | expect(input).toEqual(output) 63 | }) 64 | 65 | test('Should consider 0 as a valid value', () => { 66 | const input = r('width', [1, 0, null, 2]) 67 | const output = { 68 | width: 1, 69 | '@media screen and (min-width: 48em)': { width: 0 }, 70 | '@media screen and (min-width: 80em)': { width: 2 } 71 | } 72 | 73 | expect(input).toEqual(output) 74 | }) 75 | 76 | test('Should consider negative values as valid', () => { 77 | const input = r('letterSpacing', [1, 0, -1.5]) 78 | const output = { 79 | letterSpacing: 1, 80 | '@media screen and (min-width: 48em)': { letterSpacing: 0 }, 81 | '@media screen and (min-width: 64em)': { letterSpacing: -1.5 } 82 | } 83 | 84 | expect(input).toEqual(output) 85 | }) 86 | -------------------------------------------------------------------------------- /media.js: -------------------------------------------------------------------------------- 1 | // Helper to write down a media query definition 2 | export default function media (value, feature = 'min-width', unit = 'em') { 3 | return `@media screen and (${feature}: ${value}${value > 0 ? unit : ''})` 4 | } 5 | -------------------------------------------------------------------------------- /media.test.js: -------------------------------------------------------------------------------- 1 | import media from './media' 2 | 3 | test('Defaults should just work', () => { 4 | expect(media(42)).toEqual('@media screen and (min-width: 42em)') 5 | }) 6 | 7 | test('If value is zero, there should not be a unit', () => { 8 | expect(media(0)).toEqual('@media screen and (min-width: 0)') 9 | }) 10 | 11 | test('If unit is specified it should be respected', () => { 12 | expect(media('1024px')).toEqual('@media screen and (min-width: 1024px)') 13 | }) 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "responsive-styles", 3 | "version": "0.2.0", 4 | "description": "Use arrays as values to specify mobile-first responsive styles for CSS-in-JS projects", 5 | "license": "MIT", 6 | "repository": "github:rafaelrinaldi/responsive-styles", 7 | "main": "dist/responsiveStyles.umd.js", 8 | "jsnext:main": "dist/responsiveStyles.es.js", 9 | "module": "dist/responsiveStyles.es.js", 10 | "keywords": [ 11 | "aphrodite", 12 | "css-in-js", 13 | "emotion", 14 | "glamor", 15 | "rebass", 16 | "styled-system" 17 | ], 18 | "scripts": { 19 | "build": "rollup -c", 20 | "test": "NODE_ENV=test prettier-standard index.js && jest --verbose", 21 | "start": "NODE_ENV=development parcel examples/index.html -p 8000" 22 | }, 23 | "files": [ 24 | "dist" 25 | ], 26 | "devDependencies": { 27 | "babel-core": "^6.26.0", 28 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 29 | "babel-preset-env": "^1.7.0", 30 | "jest-cli": "^23.5.0", 31 | "prettier-standard": "^8.0.1", 32 | "rollup": "^0.65.0", 33 | "rollup-plugin-babel": "^3.0.7" 34 | }, 35 | "optionalDependencies": { 36 | "emotion": "^9.0.2", 37 | "glamor": "^2.20.40", 38 | "combine-same-keys": "^0.1.0", 39 | "aphrodite": "^1.2.5", 40 | "react": "^16.2.0", 41 | "react-dom": "^16.2.0", 42 | "babel-plugin-transform-react-jsx": "^6.24.1", 43 | "parcel-bundler": "^1.6.2" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from 'rollup-plugin-babel' 2 | import pkg from './package.json' 3 | 4 | const name = 'responsiveStyles' 5 | 6 | const output = { 7 | umd: pkg.main, 8 | es: pkg.module 9 | } 10 | 11 | export default { 12 | input: 'index.js', 13 | output: [ 14 | { 15 | file: output.umd, 16 | format: 'umd', 17 | name 18 | }, 19 | { 20 | file: output.es, 21 | format: 'es' 22 | } 23 | ], 24 | plugins: [babel()] 25 | } 26 | --------------------------------------------------------------------------------