├── .flowconfig ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── README.md ├── example ├── components │ └── App.js ├── index.html └── index.js ├── package.json └── src ├── _.test.js ├── core.js ├── index.js └── index.test.js /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | /dist -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | .idea 3 | node_modules 4 | dist 5 | yarn.lock 6 | package-lock.json 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "stable" -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | # Change log 3 | 4 | ### 2.1.0 5 | 6 | - added the `module` field 7 | - added badges to README 8 | 9 | ### 2.0.2 10 | 11 | - fixed use of multiple expressions [#11](https://github.com/jameslnewell/styled-components-breakpoint/issues/11) 12 | 13 | ### 2.0.1 14 | 15 | - added missing `./dist` files 😬 16 | 17 | #### 2.0.0 18 | 19 | - **breaking change:** removed support for non-numeric breakpoint values (so we can perform numerical operations on the breakpoint values) 20 | - **breaking change:** simplified the use of custom breakpoints with `breakpoint()` and `map()` e.g. `${({theme}) => breakpoint('xs', theme.breakpoints)``}` is now `${(breakpoint('xs')``}` 21 | - added the `lt` param in `breakpoint(gte, lt)` 22 | - added the `createStatic()` function 23 | - added a handful of tests 24 | 25 | #### 1.0.3 26 | 27 | - changed how the package is built 28 | - added a demo page 29 | 30 | #### 1.0.2 31 | 32 | - updated `peerDependency` for `styled-components` to support v3 - 👏 Thanks [@ApacheEx](https://github.com/ApacheEx) ([#10](https://github.com/jameslnewell/styled-components-breakpoint/pull/10)) 33 | - fixed a bug in the `map()` fn 34 | 35 | #### 1.0.1 36 | 37 | Updated the docs. 38 | 39 | #### 1.0.0 40 | 41 | New features: 42 | 43 | - You're now able to specify breakpoints in any type of units if you use a string. Breakpoints that are numbers will still be considered to be `px` and will be converted to `ems`. 44 | 45 | Breaking changes: 46 | 47 | - `map(value, mapValueToCSS, [breakpoints])` will now call `mapValueToCSS` with `undefined` so you can set any necessary styles for all breakpoints when: 48 | - `value` is `undefined` 49 | - `value` is an `object` 50 | 51 | before: 52 | 53 | ```js 54 | const Grid = styled.div` 55 | ${({wrap}) => map(wrap, value => `flex-wrap: ${value && 'wrap' || 'nowrap'};`)} 56 | `; 57 | 58 | Grid.defaultProps = { 59 | wrap: true 60 | }; 61 | 62 | //works 63 | //works 64 | //works 65 | //works 66 | 67 | /* 68 | This breaks because no value is set for the `mobile` breakpoint and CSS defaults to `nowrap`. This is easily fixed 69 | by manually setting `flex-wrap: wrap;` outside of the `map()` for all breakpoints... but for complex fns this may require 70 | additional interpolation. 71 | */ 72 | 73 | 74 | ``` 75 | 76 | after: 77 | 78 | ```js 79 | const Grid = styled.div` 80 | ${({wrap}) => map(wrap, (value = true) => `flex-wrap: ${value && 'wrap' || 'nowrap'};`)} 81 | `; 82 | 83 | //works 84 | //works 85 | //works 86 | //works 87 | 88 | //works 89 | 90 | ``` 91 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # styled-components-breakpoint 2 | 3 | ![npm](https://img.shields.io/npm/v/styled-components-breakpoint.svg) ![npm bundle size (minified + gzip)](https://img.shields.io/bundlephobia/minzip/styled-components-breakpoint.svg) ![npm](https://img.shields.io/npm/dm/styled-components-breakpoint.svg) [![Build Status](https://travis-ci.org/jameslnewell/styled-components-breakpoint.svg?branch=master)](https://travis-ci.org/jameslnewell/styled-components-breakpoint) 4 | 5 | Utility functions for creating breakpoints in `styled-components` 💅. 6 | 7 | > [Change log](https://github.com/jameslnewell/styled-components-breakpoint/blob/master/CHANGELOG.md) 8 | 9 | > Have a look 👀 at [`styled-components-spacing`](https://github.com/jameslnewell/styled-components-spacing) and [`styled-components-grid`](https://github.com/jameslnewell/styled-components-grid) which both work well with this package. 10 | 11 | ## Installation 12 | 13 | ```bash 14 | yarn add styled-components styled-components-breakpoint 15 | ``` 16 | 17 | ## Usage 18 | 19 | > [Examples](https://jameslnewell.github.io/styled-components-breakpoint/) 20 | 21 | ### Using the default breakpoints 22 | 23 | `./Heading.jsx` 24 | 25 | ```js 26 | import styled from 'styled-components'; 27 | import breakpoint from 'styled-components-breakpoint'; 28 | 29 | const Heading = styled.h1` 30 | 31 | color: #444; 32 | font-family: sans-serif; 33 | 34 | font-size: 12px; 35 | 36 | ${breakpoint('tablet')` 37 | font-size: 16px; 38 | `} 39 | 40 | ${breakpoint('desktop')` 41 | font-size: 24px; 42 | `} 43 | 44 | `; 45 | 46 | export default Heading; 47 | 48 | ``` 49 | 50 | `./index.jsx` 51 | 52 | ```js 53 | import React from 'react'; 54 | import Heading from './Heading'; 55 | 56 | Hello World! 57 | 58 | ``` 59 | 60 | ### Using custom breakpoints 61 | 62 | Breakpoints may be customised using `ThemeProvider`. For example, to use the same breakpoints as [Bootstrap](https://getbootstrap.com/docs/4.0/layout/overview/#responsive-breakpoints), you can do so like this: 63 | 64 | `./Heading.jsx` 65 | ```js 66 | import styled from 'styled-components'; 67 | import breakpoint from 'styled-components-breakpoint'; 68 | 69 | const Heading = styled.h1` 70 | 71 | color: #444; 72 | font-family: sans-serif; 73 | 74 | ${breakpoint('sm')` 75 | font-size: 12px; 76 | `} 77 | 78 | ${breakpoint('md')` 79 | font-size: 16px; 80 | `} 81 | 82 | ${breakpoint('lg')` 83 | font-size: 24px; 84 | `} 85 | 86 | `; 87 | 88 | export default Heading; 89 | 90 | ``` 91 | 92 | `./index.jsx` 93 | 94 | ```js 95 | import React from 'react'; 96 | import {ThemeProvider} from 'styled-components'; 97 | 98 | const theme = { 99 | breakpoints: { 100 | xs: 0, 101 | sm: 576, 102 | md: 768, 103 | lg: 992, 104 | xl: 1200 105 | } 106 | }; 107 | 108 | 109 | Hello World! 110 | 111 | 112 | ``` 113 | 114 | ## API 115 | 116 | ### `breakpoint(gte)` 117 | ### `breakpoint(gte, lt)` 118 | 119 | Wraps styles in a `@media` block. 120 | 121 | **Properties:** 122 | - `gte` - Required. A `string`. The name of the breakpoint from which the styles will apply. 123 | - `lt` - Optional. A `string`. The name of the breakpoint at which the styles will no longer apply. 124 | 125 | **Returns:** 126 | 127 | The `@media` block. 128 | 129 | ##### Example: 130 | ```js 131 | import styled from 'styled-components'; 132 | import breakpoint from 'styled-components-breakpoint'; 133 | 134 | const Thing = styled.div` 135 | 136 | font-size: 12px; 137 | 138 | ${breakpoint('tablet')` 139 | font-size: 16px; 140 | `}; 141 | 142 | ${breakpoint('desktop')` 143 | font-size: 24px; 144 | `}; 145 | 146 | `; 147 | 148 | 149 | 150 | ``` 151 | 152 | ##### Output: 153 | ```css 154 | .cESAFm { 155 | font-size: 12px; 156 | } 157 | 158 | @media (min-width: 46.0625em) { 159 | .cESAFm { 160 | font-size: 16px; 161 | } 162 | } 163 | 164 | @media (min-width: 64.0625em) { 165 | .cESAFm { 166 | font-size: 24px; 167 | } 168 | } 169 | ``` 170 | 171 | 172 | ### `map(value, mapValueToCSS)` 173 | 174 | Maps values to styles in `@media` blocks. 175 | 176 | **Properties:** 177 | - `value` - Required. `{[string]: T}` or `T`. A map of values to breakpoint names. 178 | - `mapValueToCSS` - Required. `T => string`. A function to map a value to styles at the specified breakpoint. 179 | 180 | **Returns:** 181 | 182 | The `@media` blocks. 183 | 184 | ##### Example: 185 | 186 | ```js 187 | import styled from 'styled-components'; 188 | import {map} from 'styled-components-breakpoint'; 189 | 190 | const Thing = styled.div` 191 | ${({size}) => map(size, val => `width: ${Math.round(val * 100)}%;`)} 192 | `; 193 | 194 | 195 | 196 | ``` 197 | 198 | ##### Output: 199 | 200 | ```css 201 | .cESAFm { 202 | width: 100%; 203 | } 204 | 205 | @media (min-width: 46.0625em) { 206 | .cESAFm { 207 | width: 50%; 208 | } 209 | } 210 | 211 | @media (min-width: 64.0625em) { 212 | .cESAFm { 213 | width: 25%; 214 | } 215 | } 216 | ``` 217 | 218 | ### `createStatic()` 219 | ### `createStatic(breakpoints)` 220 | 221 | Creates a static set of breakpoints which aren't themable. 222 | 223 | **Properties:** 224 | - `breakpoints` - Optional. `{[string]: number}`. A map of breakpoint names and sizes. 225 | 226 | **Returns:** 227 | 228 | - an `object` containing the breakpoints, the `breakpoint` and `map` functions 229 | 230 | ##### Example: 231 | 232 | ```js 233 | import styled from 'styled-components'; 234 | import {createStatic} from 'styled-components-breakpoint'; 235 | 236 | const breakpoints = createStatic(); 237 | 238 | const Thing = styled.div` 239 | 240 | font-size: 12px; 241 | 242 | ${breakpoints.tablet` 243 | font-size: 16px; 244 | `}; 245 | 246 | ${breakpoints.desktop` 247 | font-size: 24px; 248 | `}; 249 | 250 | `; 251 | 252 | 253 | 254 | ``` 255 | 256 | ##### Output: 257 | 258 | ```css 259 | .cESAFm { 260 | font-size: 12px; 261 | } 262 | 263 | @media (min-width: 46.0625em) { 264 | .cESAFm { 265 | font-size: 16px; 266 | } 267 | } 268 | 269 | @media (min-width: 64.0625em) { 270 | .cESAFm { 271 | font-size: 24px; 272 | } 273 | } 274 | ``` 275 | 276 | ## Default breakpoints 277 | 278 | The default breakpoints are: 279 | 280 | ```js 281 | { 282 | mobile: 0, // targeting all devices 283 | tablet: 737, // targeting devices that are larger than the iPhone 6 Plus (which is 736px in landscape mode) 284 | desktop: 1025 // targeting devices that are larger than the iPad (which is 1024px in landscape mode) 285 | } 286 | ``` 287 | -------------------------------------------------------------------------------- /example/components/App.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import createComponentFromTagProp from 'react-create-component-from-tag-prop'; 4 | import styled, { injectGlobal, ThemeProvider } from 'styled-components'; 5 | import breakpoint, { map, createStatic } from '../../src'; 6 | 7 | const DEFAULT_BREAKPOINTS = { 8 | mobile: 0, 9 | tablet: 737, 10 | desktop: 1025 11 | }; 12 | 13 | const CUSTOM_BREAKPOINTS = { 14 | xs: 0, 15 | sm: 576, 16 | md: 768, 17 | lg: 992, 18 | xl: 1200 19 | }; 20 | 21 | const BREAKPOINT_TITLES = { 22 | mobile: 'Mobile', 23 | tablet: 'Tablet', 24 | desktop: 'Desktop', 25 | xs: 'XS', 26 | sm: 'SM', 27 | md: 'MD', 28 | lg: 'LG', 29 | xl: 'XL' 30 | }; 31 | 32 | const BREAKPOINT_COLORS = { 33 | mobile: '#D7F2BA', 34 | tablet: '#BDE4A8', 35 | desktop: '#9CC69B', 36 | xs: '#D7F2BA', 37 | sm: '#BDE4A8', 38 | md: '#9CC69B', 39 | lg: '#79B4A9', 40 | xl: '#556C70' 41 | }; 42 | 43 | const STATIC_BREAKPOINTS = createStatic(); 44 | 45 | 46 | /* eslint-disable no-unused-expressions */ 47 | injectGlobal` 48 | 49 | body { 50 | 51 | margin: auto; 52 | padding: 0 1em; 53 | min-width: 500px; 54 | max-width: 800px; 55 | 56 | color: #444; 57 | font-size: 0.9em; 58 | font-family: -apple-system,system-ui,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif; 59 | 60 | ${STATIC_BREAKPOINTS.tablet` 61 | font-size: 1em; 62 | `} 63 | 64 | ${STATIC_BREAKPOINTS.desktop` 65 | font-size: 1.1em; 66 | `} 67 | 68 | } 69 | 70 | `; 71 | 72 | /* eslint-enable no-unused-expressions */ 73 | 74 | const whitelistedDiv = createComponentFromTagProp({ 75 | tag: 'div', 76 | propsToOmit: ['name', 'color', 'operator', 'visible'] 77 | }); 78 | 79 | const Main = styled.main` 80 | padding-bottom: 1em; 81 | `; 82 | 83 | const H1 = styled.h1` 84 | margin: 1em 0; 85 | `; 86 | 87 | const H2 = styled.h2` 88 | margin: 1em 0; 89 | `; 90 | 91 | const P = styled.p` 92 | margin: 1em 0; 93 | `; 94 | 95 | const Grid = styled.div` 96 | display: flex; 97 | align-items: center; 98 | `; 99 | 100 | const Col1 = styled.div` 101 | flex-grow: 1; 102 | `; 103 | 104 | const Col2 = styled.div` 105 | `; 106 | 107 | const Button = styled.button` 108 | padding: 0.5em; 109 | font-size: 1em; 110 | border-radius: 3px; 111 | `; 112 | 113 | const Instruction = styled.blockquote` 114 | margin: 0; 115 | font-size: 0.9em; 116 | font-weight: bold; 117 | text-align: right; 118 | `; 119 | 120 | const Breakpoint = styled(whitelistedDiv) ` 121 | display: flex; 122 | align-items: center; 123 | padding: 1em; 124 | :after { 125 | content: '❌'; 126 | text-align: right; 127 | } 128 | ${({ gte, lt }) => breakpoint(gte, lt) ` 129 | background-color: ${({ color }) => color}; 130 | :after { 131 | content: '✅'; 132 | } 133 | `} 134 | `; 135 | 136 | const BreakpointMap = styled(whitelistedDiv) ` 137 | display: flex; 138 | align-items: center; 139 | padding: 1em; 140 | :after { 141 | content: '✅'; 142 | text-align: right; 143 | } 144 | ${({ color }) => map(color, (val = 'transparent') => `background-color: ${val || ''};`)} 145 | `; 146 | 147 | const BreakpointTitle = styled.div` 148 | flex-shrink: 0; 149 | width: 5em; 150 | font-weight: bold; 151 | ${ 152 | ({ text = {} }) => map(text, (val = '') => { 153 | return `:before { 154 | content: '${val || ''}'; 155 | }`; 156 | }) 157 | } 158 | `; 159 | 160 | const BreakpointCode = styled.pre` 161 | margin-right: 1em; 162 | flex-grow: 1; 163 | color: #666; 164 | text-overflow: ellipsis; 165 | white-space: nowrap; 166 | overflow: hidden; 167 | `; 168 | 169 | export type AppProps = { 170 | 171 | }; 172 | 173 | export type AppState = { 174 | breakpoints: { [name: string]: number }; 175 | }; 176 | 177 | export default class App extends React.Component { 178 | 179 | state = { 180 | breakpoints: DEFAULT_BREAKPOINTS 181 | }; 182 | 183 | handleToggleBreakpoints = () => { 184 | this.setState(state => ({ 185 | breakpoints: state.breakpoints !== DEFAULT_BREAKPOINTS ? DEFAULT_BREAKPOINTS : CUSTOM_BREAKPOINTS 186 | })); 187 | } 188 | 189 | render() { 190 | const { breakpoints } = this.state; 191 | const colors = Object.keys(breakpoints).reduce((accum, name) => { 192 | accum[name] = BREAKPOINT_COLORS[name]; 193 | return accum; 194 | }, {}); 195 | const titles = Object.keys(breakpoints).reduce((accum, name) => { 196 | accum[name] = BREAKPOINT_TITLES[name]; 197 | return accum; 198 | }, {}); 199 | return ( 200 | 201 |
202 |

styled-components-breakpoint

203 | 204 | 205 | 206 | 209 | 210 | 211 | Try resizing the window. 👉 212 | 213 | 214 | 215 |

gte

216 |

Greater than or equal to.

217 | {Object.keys(breakpoints).map(name => ( 218 | 219 | {BREAKPOINT_TITLES[name]} 220 | {`$\{ breakpoint('${name}') \`/* styles go here */\`}`} 221 | 222 | ))} 223 | 224 |

gte and lt

225 |

Greater than or equal to X but less than Y.

226 | { 227 | Object.keys(breakpoints).map((name, index) => { 228 | const nextName = Object.keys(breakpoints)[index + 1]; 229 | return ( 230 | 231 | {BREAKPOINT_TITLES[name]} 232 | {`$\{breakpoint('${name}'${nextName ? `, '${nextName}'` : ''}) \`/* styles go here */\`}`} 233 | 234 | ); 235 | }) 236 | } 237 | 238 |

map

239 |

Map a value to styles for each breakpoint where a value is specified.

240 | 241 | 242 | {`$\{map(${JSON.stringify(colors, null, 2)}, (color) => \`/* styles go here */\`}`} 243 | 244 | 245 |
246 |
247 | ); 248 | }; 249 | } -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | styled-components-breakpoint: Example 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /example/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import ReactDOM from 'react-dom'; 4 | import App from './components/App'; 5 | 6 | const element = document.getElementById('app'); 7 | if (element) { 8 | ReactDOM.render(, element); 9 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "styled-components-breakpoint", 3 | "repository": "jameslnewell/styled-components-breakpoint", 4 | "version": "2.1.1", 5 | "description": "Utility functions for creating breakpoints in `styled-components` 💅.", 6 | "keywords": [ 7 | "react", 8 | "styled-components", 9 | "breakpoint", 10 | "breakpoints", 11 | "media", 12 | "style", 13 | "tablet", 14 | "desktop" 15 | ], 16 | "main": "dist/cjs/index.js", 17 | "module": "dist/esm/index.js", 18 | "jsnext:main": "dist/esm/index.js", 19 | "files": [ 20 | "dist/cjs", 21 | "dist/esm" 22 | ], 23 | "peerDependencies": { 24 | "styled-components": ">= 1" 25 | }, 26 | "devDependencies": { 27 | "@tradie/react-component-scripts": "^1.0.0-alpha.a573b72a", 28 | "enzyme": "^3.3.0", 29 | "enzyme-adapter-react-16": "^1.1.1", 30 | "enzyme-to-json": "^3.3.1", 31 | "gh-pages": "^1.1.0", 32 | "husky": "^0.14.3", 33 | "jest-styled-components": "^4.11.0-1", 34 | "react": "^16.2.0", 35 | "react-create-component-from-tag-prop": "^1.3.1", 36 | "react-dom": "^16.2.0", 37 | "styled-components": "^3.1.6" 38 | }, 39 | "scripts": { 40 | "clean": "tradie clean", 41 | "lint": "tradie lint", 42 | "build": "tradie build", 43 | "watch": "tradie build --watch", 44 | "dev": "tradie dev", 45 | "test": "tradie test", 46 | "deploy": "gh-pages -d \"./dist/example\"", 47 | "prepublishOnly": "yarn run clean && yarn run build && yarn run test", 48 | "postpublish": "yarn run deploy", 49 | "precommit": "yarn run test" 50 | }, 51 | "license": "MIT" 52 | } 53 | -------------------------------------------------------------------------------- /src/_.test.js: -------------------------------------------------------------------------------- 1 | import Enzyme from 'enzyme'; 2 | import Adapter from 'enzyme-adapter-react-16'; 3 | 4 | Enzyme.configure({ adapter: new Adapter() }); -------------------------------------------------------------------------------- /src/core.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | /* global process */ 3 | import { css } from 'styled-components'; 4 | 5 | export type StyledComponentsInterpolation = 6 | | ((executionContext: Object) => StyledComponentsInterpolation) 7 | | string 8 | | number 9 | | StyledComponentsInterpolation[] 10 | ; 11 | export type StyledComponentsTemplateLiteral = (strings: string[], ...interpolations: StyledComponentsInterpolation[]) => StyledComponentsInterpolation[]; 12 | 13 | export type BreakpointMap = { [name: string]: number }; 14 | export type BreakpointValueMap = T | { [name: string]: T }; 15 | export type BreakpointMapValueToCSSFunction = (value: ?T) => StyledComponentsInterpolation; // eslint-disable-line no-undef 16 | 17 | function convertPxToEm(pixels: number): number { 18 | // @media is always calculated off 16px regardless of whether the root font size is the default or not 19 | return pixels / 16; 20 | } 21 | 22 | function getValueFromName(breakpoints: BreakpointMap, name: string): number { 23 | const value = breakpoints[name]; 24 | if (process.env.NODE_ENV !== 'production' && typeof value === 'undefined') { 25 | console.error(`A breakpoint named "${name}" does not exist.`); // eslint-disable-line no-console 26 | return 0; 27 | } 28 | return value; 29 | } 30 | 31 | function withSingleCriteria(breakpoints: BreakpointMap, name: string, operator: 'min-width' | 'max-width', offset: number = 0) { 32 | const value = getValueFromName(breakpoints, name); 33 | 34 | // special case for 0 to avoid wrapping styles in an unnecessary @media block 35 | // FIXME: typings 36 | // if (operator === 'max-width' && value === 0) { 37 | // return () => ''; 38 | // } 39 | 40 | // special case for 0 to avoid wrapping styles in an unnecessary @media block 41 | if (operator === 'min-width' && value === 0) { 42 | return function (strings: string[], ...interpolations: StyledComponentsInterpolation[]) { 43 | return css(strings, ...interpolations); 44 | } 45 | } 46 | 47 | 48 | return function (strings: string[], ...interpolations: StyledComponentsInterpolation[]) { 49 | return css`@media (${operator}: ${convertPxToEm(value + offset)}em) { 50 | ${css(strings, ...interpolations)} 51 | }`; 52 | }; 53 | } 54 | 55 | export function _gt(breakpoints: BreakpointMap, name: string) { 56 | return withSingleCriteria(breakpoints, name, 'min-width', +1); 57 | } 58 | 59 | export function _gte(breakpoints: BreakpointMap, name: string) { 60 | return withSingleCriteria(breakpoints, name, 'min-width'); 61 | } 62 | 63 | export function _lt(breakpoints: BreakpointMap, name: string) { 64 | return withSingleCriteria(breakpoints, name, 'max-width', -1); 65 | } 66 | 67 | export function _lte(breakpoints: BreakpointMap, name: string) { 68 | return withSingleCriteria(breakpoints, name, 'max-width'); 69 | } 70 | 71 | export function _between(breakpoints: BreakpointMap, gte: string, lt: string) { 72 | const gteValue = getValueFromName(breakpoints, gte); 73 | const ltValue = getValueFromName(breakpoints, lt); 74 | return function (strings: string[], ...interpolations: StyledComponentsInterpolation[]) { 75 | return css`@media (min-width: ${convertPxToEm(gteValue)}em) and (max-width: ${convertPxToEm(ltValue - 1)}em) { 76 | ${css(strings, ...interpolations)} 77 | }`; 78 | }; 79 | } 80 | 81 | export function _breakpoint(breakpoints: BreakpointMap, gte: string, lt?: string) { 82 | if (typeof lt === 'undefined') { 83 | return _gte(breakpoints, gte); 84 | } else { 85 | return _between(breakpoints, gte, lt); 86 | } 87 | }; 88 | 89 | // TODO: allow the operator to be customised 90 | export function _map(breakpoints: BreakpointMap, value: BreakpointValueMap, mapValueToCSS: BreakpointMapValueToCSSFunction) { 91 | const values = value; 92 | 93 | if (values === null || typeof values !== 'object') { 94 | return mapValueToCSS(values); 95 | } 96 | 97 | return [ 98 | // eslint-disable-next-line no-undefined 99 | mapValueToCSS(undefined), // set the default value 100 | ...Object.keys(values).map((name: string) => { 101 | const tag = _gte(breakpoints, name); 102 | const val = values[name]; 103 | const styles = tag([], [].concat(mapValueToCSS(val))); 104 | return styles; 105 | }) 106 | ]; 107 | 108 | }; 109 | 110 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { 3 | type StyledComponentsInterpolation, 4 | type StyledComponentsTemplateLiteral, 5 | type BreakpointMap, 6 | type BreakpointValueMap, 7 | type BreakpointMapValueToCSSFunction, 8 | _breakpoint, 9 | _map 10 | } from './core'; 11 | 12 | const defaultBreakpoints = { 13 | mobile: 0, // targeting all devices 14 | tablet: 737, // targeting devices that are LARGER than the iPhone 6 Plus (which is 736px in landscape mode) 15 | desktop: 1025 // targeting devices that are LARGER than the iPad (which is 1024px in landscape mode) 16 | }; 17 | 18 | type ComponentProps = { 19 | theme: { 20 | breakpoints?: BreakpointMap 21 | } 22 | }; 23 | 24 | function breakpoint(gte: string, lt?: string) { 25 | return function (strings: string[], ...interpolations: StyledComponentsInterpolation[]) { 26 | return function ({ theme = {} }: ComponentProps) { 27 | return _breakpoint(theme.breakpoints || defaultBreakpoints, gte, lt)(strings, ...interpolations); 28 | }; 29 | }; 30 | } 31 | 32 | export function map(value: BreakpointValueMap, mapValueToCSS: BreakpointMapValueToCSSFunction) { 33 | return function ({ theme = {} }: ComponentProps) { 34 | return _map(theme.breakpoints || defaultBreakpoints, value, mapValueToCSS); 35 | }; 36 | } 37 | 38 | export type StaticBreakpoints = { 39 | [name: string]: StyledComponentsTemplateLiteral; 40 | breakpoint: (gte: string, lt?: string) => StyledComponentsTemplateLiteral; 41 | map: (value: BreakpointValueMap < T >, mapValueToCSS: BreakpointMapValueToCSSFunction) => StyledComponentsInterpolation; // eslint-disable-line no-undef 42 | }; 43 | 44 | export function createStatic(breakpoints: BreakpointMap = defaultBreakpoints): StaticBreakpoints { 45 | return Object.keys(breakpoints).reduce((accum, name) => { 46 | accum[name] = _breakpoint(breakpoints, name); 47 | return accum; 48 | }, { 49 | breakpoint: (gte: string, lt?: string) => _breakpoint(breakpoints, gte, lt), 50 | map: (value: BreakpointValueMap, mapValueToCSS: BreakpointMapValueToCSSFunction) => _map(breakpoints, value, mapValueToCSS), 51 | }); 52 | } 53 | 54 | export default breakpoint; 55 | -------------------------------------------------------------------------------- /src/index.test.js: -------------------------------------------------------------------------------- 1 | import 'jest-styled-components'; 2 | import React from 'react'; 3 | import styled from 'styled-components'; 4 | import toJson from 'enzyme-to-json'; 5 | import { shallow } from 'enzyme'; 6 | import breakpoint, { map } from './index'; 7 | 8 | const customBreakpoints = { 9 | xs: 320, 10 | md: 960, 11 | xl: 2880 12 | } 13 | 14 | describe('breakpoint()', () => { 15 | 16 | const DefaultThing = styled.h1` 17 | 18 | ${breakpoint('mobile') ` 19 | font-size: 12px; 20 | `} 21 | 22 | ${breakpoint('tablet') ` 23 | font-size: 16px; 24 | `} 25 | 26 | ${breakpoint('desktop') ` 27 | font-size: 24px; 28 | `} 29 | 30 | `; 31 | 32 | const CustomThing = styled.h1` 33 | 34 | ${breakpoint('xs') ` 35 | font-size: 12px; 36 | `} 37 | 38 | ${breakpoint('md') ` 39 | font-size: 16px; 40 | `} 41 | 42 | ${breakpoint('xl') ` 43 | font-size: 24px; 44 | `} 45 | 46 | `; 47 | 48 | it('should have styles defined for each default breakpoint', () => { 49 | const element = shallow(); 50 | expect(element).toHaveStyleRule('font-size', '12px'); 51 | expect(element).toHaveStyleRule('font-size', '16px', { media: '(min-width:46.0625em)' }); 52 | expect(element).toHaveStyleRule('font-size', '24px', { media: '(min-width:64.0625em)' }); 53 | }); 54 | 55 | it('should have styles defined for each custom breakpoint', () => { 56 | const element = shallow( 57 | 58 | ); 59 | expect(element).toHaveStyleRule('font-size', '12px', { media: '(min-width:20em)' }); 60 | expect(element).toHaveStyleRule('font-size', '16px', { media: '(min-width:60em)' }); 61 | expect(element).toHaveStyleRule('font-size', '24px', { media: '(min-width:180em)' }); 62 | }); 63 | 64 | // https://github.com/jameslnewell/styled-components-breakpoint/issues/11 65 | it('should render multiple expressions correctly #11', () => { 66 | const Thing = styled.div` 67 | ${breakpoint('tablet') ` 68 | background: ${'grey'}; 69 | color: ${'white'}; 70 | `} 71 | `; 72 | const element = shallow( 73 | 74 | ); 75 | expect(element).toHaveStyleRule('background', 'grey', { media: '(min-width:46.0625em)' }); 76 | expect(element).toHaveStyleRule('color', 'white', { media: '(min-width:46.0625em)' }); 77 | }); 78 | 79 | }); 80 | 81 | describe('map()', () => { 82 | 83 | const Thing = styled.h1` 84 | ${({ size }) => map(size, val => `width: ${val * 100}%;`)} 85 | `; 86 | 87 | it('should have styles defined for each default breakpoint', () => { 88 | const element = shallow(); 89 | expect(element).toHaveStyleRule('width', '100%'); 90 | expect(element).toHaveStyleRule('width', '50%', { media: '(min-width:46.0625em)' }); 91 | expect(element).toHaveStyleRule('width', '25%', { media: '(min-width:64.0625em)' }); 92 | }); 93 | 94 | it('should have styles defined for each custom breakpoint', () => { 95 | const element = shallow( 96 | 100 | ); 101 | expect(element).toHaveStyleRule('width', '100%', { media: '(min-width:20em)' }); 102 | expect(element).toHaveStyleRule('width', '50%', { media: '(min-width:60em)' }); 103 | expect(element).toHaveStyleRule('width', '25%', { media: '(min-width:180em)' }); 104 | }); 105 | 106 | }); 107 | --------------------------------------------------------------------------------