`1px solid ${t.colors.primary}`,
130 | }}
131 | />
132 |
133 | )
134 | expect(json).toHaveStyleRule('border', '1px solid #609')
135 | })
136 |
137 | test('picks up fallback theme values for non-standard properties', () => {
138 | const json = renderJSON(
139 |
146 |
151 |
152 | )
153 | expect(json).toHaveStyleRule(
154 | 'background-image',
155 | 'linear-gradient(cyan,magenta)'
156 | )
157 | })
158 |
--------------------------------------------------------------------------------
/docs/packages.md:
--------------------------------------------------------------------------------
1 | # Styled System Packages
2 |
3 | The main `styled-system` package is composed of several other packages that can be installed and used on their own for special use-cases.
4 |
5 | | Package Name | Exports |
6 | | ------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ |
7 | | [`@styled-system/core`](https://github.com/styled-system/styled-system/tree/master/packages/core) | `system`, `createParser`, `createStyleFunction`, `get` |
8 | | [`@styled-system/space`](https://github.com/styled-system/styled-system/tree/master/packages/space) | `space`, `margin`, `padding` |
9 | | [`@styled-system/color`](https://github.com/styled-system/styled-system/tree/master/packages/color) | `color` |
10 | | [`@styled-system/layout`](https://github.com/styled-system/styled-system/tree/master/packages/layout) | `layout` |
11 | | [`@styled-system/typography`](https://github.com/styled-system/styled-system/tree/master/packages/typography) | `typography` |
12 | | [`@styled-system/flexbox`](https://github.com/styled-system/styled-system/tree/master/packages/flexbox) | `flexbox` |
13 | | [`@styled-system/border`](https://github.com/styled-system/styled-system/tree/master/packages/border) | `border` |
14 | | [`@styled-system/background`](https://github.com/styled-system/styled-system/tree/master/packages/background) | `background` |
15 | | [`@styled-system/position`](https://github.com/styled-system/styled-system/tree/master/packages/position) | `position` |
16 | | [`@styled-system/grid`](https://github.com/styled-system/styled-system/tree/master/packages/grid) | `grid` |
17 | | [`@styled-system/shadow`](https://github.com/styled-system/styled-system/tree/master/packages/shadow) | `shadow` |
18 | | [`@styled-system/variant`](https://github.com/styled-system/styled-system/tree/master/packages/variant) | `variant`, `textStyle` `buttonStyle`, `colorStyle` |
19 |
20 | The Styled System ecosystem also includes these optional packages
21 |
22 | | Package Name | Description |
23 | | ------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
24 | | [`@styled-system/css`](https://github.com/styled-system/styled-system/tree/master/packages/css) | Styled System for the `css` prop |
25 | | [`@styled-system/jsx`](https://github.com/styled-system/styled-system/tree/master/packages/jsx) | Custom JSX pragma for using Styled System with the `css` prop |
26 | | [`@styled-system/should-forward-prop`](https://github.com/styled-system/styled-system/tree/master/packages/should-forward-prop) | Utility for filtering Styled System props with Emotion's shouldForwardProp option |
27 | | [`@styled-system/prop-types`](https://github.com/styled-system/styled-system/tree/master/packages/prop-types) | Prop type definitions for components built with Styled System |
28 | | [`@styled-system/theme-get`](https://github.com/styled-system/styled-system/tree/master/packages/theme-get) | Utility to safely access values from a theme |
29 | | [`@styled-system/edit`](https://github.com/styled-system/styled-system/tree/master/packages/edit) | Debugging components for live editing Styled System theme objects |
30 |
--------------------------------------------------------------------------------
/docs/guides/color-modes.md:
--------------------------------------------------------------------------------
1 | # Color Modes
2 |
3 | While there are many different ways to handle theming in a web application,
4 | there are just as many ways to handle color schemes.
5 | A common feature of modern web applications is including an optional dark mode.
6 | Usually a dark mode feature includes changes to the colors of a site without changing other typographic or layout styles.
7 | This guide will walk through one approach (the one used on this site) that includes multiple color modes that can be changed by the end user.
8 |
9 | Since the only styles that change between these color modes are the colors themselves, the different color palettes are stored in the `theme.colors` object.
10 | If you look at this site's [`src/theme.js`][] file, you'll see that it includes a nested `colors.modes` object for the different color schemes.
11 | Each color mode object matches the same shape as the base default colors and uses a simple naming abstraction for setting colors for the text, background, links, and other styles.
12 | This site's colors object looks something like the following:
13 |
14 | ```js
15 | const colors = {
16 | text: '#000',
17 | background: '#fff',
18 | primary: '#00f',
19 | secondary: '#00a',
20 | gray: '#eee',
21 | lightgray: '#fafafa',
22 | modes: {
23 | dark: {
24 | text: '#fff',
25 | background: '#000',
26 | primary: '#0cf',
27 | secondary: '#f0e',
28 | gray: '#222',
29 | lightgray: '#111',
30 | },
31 | // other color modes...
32 | },
33 | }
34 | ```
35 |
36 | ## Using the theme colors
37 |
38 | By default the base colors are picked up by other components using Styled System. For example, the root layout component uses Emotion's `Global` component to set text and background colors.
39 |
40 | ```jsx
41 | // example
42 |
50 | ```
51 |
52 | ## Adding color mode state
53 |
54 | The root layout component also uses React state to cycle through the different color modes and creates a new `theme` object based on state.
55 | There are several different ways to store this state persistently, but this is outside of the scope of this guide.
56 | The following is an example of one way to set up the color mode state in your app.
57 |
58 | ```jsx
59 | import React, { useState } from 'react'
60 | import merge from 'lodash.merge'
61 | import get from 'lodash.get'
62 | // the full theme object
63 | import baseTheme from './theme'
64 |
65 | // options for different color modes
66 | const modes = [
67 | 'light',
68 | 'dark',
69 | // more than two modes can follow...
70 | ]
71 |
72 | // merge the color mode with the base theme
73 | // to create a new theme object
74 | const getTheme = mode =>
75 | merge({}, baseTheme, {
76 | colors: get(baseTheme.colors.modes, mode, baseTheme.colors),
77 | })
78 |
79 | export default props => {
80 | // state for changing modes dynamically
81 | const [mode, setMode] = useState(modes[0])
82 | const theme = getTheme(mode)
83 |
84 | return (
85 |
{/* application elements */}
86 | )
87 | }
88 | ```
89 |
90 | Next you'll want to add the UI controls for changing between color modes.
91 | With this basic approach, you should be able to add as many different color modes to your site as you wish.
92 | Be sure that _all_ components within your application are using color values from the theme (not hard-coded values) in order for this to work as expected.
93 |
94 | There are other ways to achieve a similar effect – this just demonstrates one approach.
95 | For a different approach to persisting data, you may want to look into the [`prefers-color-scheme`][] media query, but it only handles binary `light` or `dark` modes.
96 | You might also want to look into [CSS Custom Properties][],
97 | which can be defined as inline styles, but be aware that they are not supported in IE11.
98 |
99 | [`src/theme.js`]: https://github.com/styled-system/styled-system/blob/master/docs/src/theme.js
100 | [`prefers-color-scheme`]: https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme
101 | [css custom properties]: https://developer.mozilla.org/en-US/docs/Web/CSS/--*
102 |
103 |
107 |
--------------------------------------------------------------------------------
/packages/core/src/index.js:
--------------------------------------------------------------------------------
1 | import assign from 'object-assign'
2 |
3 | export const merge = (a, b) => {
4 | let result = assign({}, a, b)
5 | for (const key in a) {
6 | if (!a[key] || typeof b[key] !== 'object') continue
7 | assign(result, {
8 | [key]: assign(a[key], b[key]),
9 | })
10 | }
11 | return result
12 | }
13 |
14 | const defaults = {
15 | breakpoints: [40, 52, 64].map(n => n + 'em'),
16 | }
17 | const createMediaQuery = n => `@media screen and (min-width: ${n})`
18 | const getValue = (n, scale) => get(scale, n, n)
19 |
20 | export const get = (obj, key, def, p, undef) => {
21 | key = key && key.split ? key.split('.') : [key]
22 | for (p = 0; p < key.length; p++) {
23 | obj = obj ? obj[key[p]] : undef
24 | }
25 | return obj === undef ? def : obj
26 | }
27 |
28 | export const createParser = config => {
29 | const cache = {}
30 | const parse = props => {
31 | let styles = {}
32 | for (const key in props) {
33 | if (!config[key]) continue
34 | const sx = config[key]
35 | const raw = props[key]
36 | const scale = get(props.theme, sx.scale, sx.defaults)
37 |
38 | if (typeof raw === 'object') {
39 | cache.breakpoints =
40 | cache.breakpoints ||
41 | get(props.theme, 'breakpoints', defaults.breakpoints)
42 | if (Array.isArray(raw)) {
43 | cache.media = cache.media || [
44 | null,
45 | ...cache.breakpoints.map(createMediaQuery),
46 | ]
47 | styles = merge(
48 | styles,
49 | parseResponsiveStyle(cache.media, sx, scale, raw)
50 | )
51 | continue
52 | }
53 | if (raw !== null) {
54 | styles = merge(
55 | styles,
56 | parseResponsiveObject(cache.breakpoints, sx, scale, raw)
57 | )
58 | }
59 | continue
60 | }
61 |
62 | assign(styles, sx(raw, scale))
63 | }
64 |
65 | return styles
66 | }
67 | parse.config = config
68 | parse.propNames = Object.keys(config)
69 | parse.cache = cache
70 | return parse
71 | }
72 |
73 | const parseResponsiveStyle = (mediaQueries, sx, scale, raw) => {
74 | let styles = {}
75 | raw.slice(0, mediaQueries.length).forEach((value, i) => {
76 | const media = mediaQueries[i]
77 | const style = sx(value, scale)
78 | if (style === undefined) return
79 | if (!media) {
80 | assign(styles, style)
81 | } else {
82 | assign(styles, {
83 | [media]: assign({}, styles[media], style),
84 | })
85 | }
86 | })
87 | return styles
88 | }
89 |
90 | const parseResponsiveObject = (breakpoints, sx, scale, raw) => {
91 | let styles = {}
92 | for (let key in raw) {
93 | const breakpoint = breakpoints[key]
94 | const value = raw[key]
95 | const style = sx(value, scale)
96 | if (!breakpoint) {
97 | assign(styles, style)
98 | } else {
99 | const media = createMediaQuery(breakpoint)
100 | assign(styles, {
101 | [media]: assign({}, styles[media], style),
102 | })
103 | }
104 | }
105 | return styles
106 | }
107 |
108 | export const createStyleFunction = ({
109 | properties,
110 | property,
111 | scale,
112 | transform = getValue,
113 | defaultScale,
114 | }) => {
115 | properties = properties || [property]
116 | const sx = (value, scale) => {
117 | const result = {}
118 | const n = transform(value, scale)
119 | if (n === null) return
120 | properties.forEach(prop => {
121 | result[prop] = n
122 | })
123 | return result
124 | }
125 | sx.scale = scale
126 | sx.defaults = defaultScale
127 | return sx
128 | }
129 |
130 | // new v5 API
131 | export const system = (args = {}) => {
132 | const config = {}
133 | Object.keys(args).forEach(key => {
134 | const conf = args[key]
135 | if (conf === true) {
136 | // shortcut definition
137 | config[key] = createStyleFunction({
138 | property: key,
139 | scale: key,
140 | })
141 | return
142 | }
143 | config[key] = createStyleFunction(conf)
144 | })
145 |
146 | const parser = createParser(config)
147 | return parser
148 | }
149 |
150 | export const compose = (...parsers) => {
151 | let config = {}
152 | parsers.forEach(parser => {
153 | if (!parser || !parser.config) return
154 | assign(config, parser.config)
155 | })
156 | const parser = createParser(config)
157 |
158 | return parser
159 | }
160 |
--------------------------------------------------------------------------------
/packages/core/test/system.js:
--------------------------------------------------------------------------------
1 | import { system } from '../src'
2 |
3 | test('returns a style parser', () => {
4 | const parser = system({
5 | color: true,
6 | backgroundColor: {
7 | property: 'backgroundColor',
8 | scale: 'colors',
9 | },
10 | mx: {
11 | scale: 'space',
12 | properties: ['marginLeft', 'marginRight'],
13 | },
14 | })
15 | expect(typeof parser).toBe('function')
16 | const styles = parser({
17 | theme: {
18 | space: [0, 4, 8, 16, 32],
19 | colors: {
20 | primary: 'rebeccapurple',
21 | },
22 | },
23 | color: 'tomato',
24 | backgroundColor: 'primary',
25 | mx: [2, 3, 4],
26 | })
27 | expect(styles).toEqual({
28 | color: 'tomato',
29 | backgroundColor: 'rebeccapurple',
30 | marginLeft: 8,
31 | marginRight: 8,
32 | '@media screen and (min-width: 40em)': {
33 | marginLeft: 16,
34 | marginRight: 16,
35 | },
36 | '@media screen and (min-width: 52em)': {
37 | marginLeft: 32,
38 | marginRight: 32,
39 | },
40 | })
41 | })
42 |
43 | test('merges multiple responsive styles', () => {
44 | const parser = system({
45 | margin: true,
46 | padding: true,
47 | width: true,
48 | })
49 | const styles = parser({
50 | margin: [0, 4, 8],
51 | padding: [16, 32, 64],
52 | width: ['100%', '50%'],
53 | })
54 | expect(styles).toEqual({
55 | margin: 0,
56 | padding: 16,
57 | width: '100%',
58 | '@media screen and (min-width: 40em)': {
59 | margin: 4,
60 | padding: 32,
61 | width: '50%',
62 | },
63 | '@media screen and (min-width: 52em)': {
64 | margin: 8,
65 | padding: 64,
66 | },
67 | })
68 | })
69 |
70 | test('merges multiple responsive object styles', () => {
71 | const parser = system({
72 | margin: true,
73 | padding: true,
74 | width: true,
75 | })
76 | const styles = parser({
77 | margin: { _: 0, 0: 4, 1: 8 },
78 | padding: { _: 16, 0: 32, 1: 64 },
79 | width: { _: '100%', 0: '50%' },
80 | })
81 | expect(styles).toEqual({
82 | margin: 0,
83 | padding: 16,
84 | width: '100%',
85 | '@media screen and (min-width: 40em)': {
86 | margin: 4,
87 | padding: 32,
88 | width: '50%',
89 | },
90 | '@media screen and (min-width: 52em)': {
91 | margin: 8,
92 | padding: 64,
93 | },
94 | })
95 | })
96 |
97 | test('gets values from theme', () => {
98 | const parser = system({
99 | mx: {
100 | properties: ['marginLeft', 'marginRight'],
101 | scale: 'space',
102 | },
103 | color: {
104 | property: 'color',
105 | scale: 'colors',
106 | },
107 | })
108 | const style = parser({
109 | theme: {
110 | colors: {
111 | primary: 'tomato',
112 | },
113 | space: [0, 6, 12, 24, 48, 96],
114 | },
115 | mx: [0, 1, 2, 3],
116 | color: ['primary', 'black'],
117 | })
118 | expect(style).toEqual({
119 | color: 'tomato',
120 | marginLeft: 0,
121 | marginRight: 0,
122 | '@media screen and (min-width: 40em)': {
123 | color: 'black',
124 | marginLeft: 6,
125 | marginRight: 6,
126 | },
127 | '@media screen and (min-width: 52em)': {
128 | marginLeft: 12,
129 | marginRight: 12,
130 | },
131 | '@media screen and (min-width: 64em)': {
132 | marginLeft: 24,
133 | marginRight: 24,
134 | },
135 | })
136 | })
137 |
138 | test('gets 0 index values from theme', () => {
139 | const parser = system({
140 | width: {
141 | property: 'width',
142 | scale: 'sizes',
143 | }
144 | })
145 | const style = parser({
146 | theme: {
147 | sizes: [ 24, 48 ],
148 | },
149 | width: 0,
150 | })
151 | expect(style).toEqual({ width: 24 })
152 | })
153 |
154 | test('ignores null values', () => {
155 | const parser = system({
156 | color: true,
157 | })
158 | const style = parser({ color: null })
159 | expect(style).toEqual({})
160 | })
161 |
162 | test('returns a noop function with no arguments', () => {
163 | const parser = system()
164 | expect(typeof parser).toBe('function')
165 | })
166 |
167 | test('skips null values in arrays', () => {
168 | const parser = system({
169 | fontSize: true,
170 | })
171 | const style = parser({
172 | fontSize: [ 16, null, null, 18 ],
173 | })
174 | expect(style).toEqual({
175 | fontSize: 16,
176 | '@media screen and (min-width: 64em)': {
177 | fontSize: 18,
178 | }
179 | })
180 | })
181 |
--------------------------------------------------------------------------------
/docs/src/layout.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled from '@emotion/styled'
3 | import { flexDirection } from 'styled-system'
4 | import { useAppContext } from './index'
5 | import NavLink from './nav-link'
6 | import { Box, css, useColorMode } from 'theme-ui'
7 | import Burger from './burger'
8 | import Sidebar from './sidebar'
9 | import Pagination from './pagination'
10 | import EditLink from './edit-link'
11 |
12 | const HeaderRoot = styled(Box)(
13 | css({
14 | width: '100%',
15 | height: 64,
16 | display: 'flex',
17 | alignItems: 'center',
18 | position: 'relative',
19 | zIndex: 2,
20 | bg: 'background',
21 | '@media screen and (max-width: 40em)': {
22 | position: 'fixed',
23 | top: 0,
24 | left: 0,
25 | right: 0,
26 | },
27 | })
28 | )
29 | const HeaderSpacer = styled.div(
30 | css({
31 | display: 'none',
32 | '@media screen and (max-width: 40em)': {
33 | display: 'block',
34 | height: 64,
35 | },
36 | })
37 | )
38 |
39 | const modes = ['light', 'dark', 'gray', 'cyan', 'book']
40 |
41 | export const Header = ({ sidebar = true, ...props }) => {
42 | const [mode, setMode] = useColorMode()
43 | const state = useAppContext()
44 | const cycleMode = () => {
45 | const i = (modes.indexOf(mode) + 1) % modes.length
46 | setMode(modes[i])
47 | }
48 |
49 | return (
50 | <>
51 |
52 |
60 | Styled System
61 |
62 |
63 |
85 | {sidebar && (
86 |
106 | )}
107 |
108 |
109 | >
110 | )
111 | }
112 |
113 | const Root = styled(Box)(
114 | css({
115 | display: 'flex',
116 | flexDirection: 'column',
117 | })
118 | )
119 |
120 | const Main = styled(Box)(
121 | {
122 | display: 'flex',
123 | },
124 | flexDirection
125 | )
126 |
127 | const Overlay = props => (
128 |
138 | )
139 |
140 | export const Container = styled(Box)(
141 | css({
142 | width: '100%',
143 | maxWidth: 1024,
144 | lineHeight: 'body',
145 | mx: 'auto',
146 | p: 4,
147 | })
148 | )
149 | Container.defaultProps = {
150 | as: 'main',
151 | }
152 |
153 | export default ({ banner, ...props }) => {
154 | const state = useAppContext()
155 |
156 | return (
157 |
158 |
159 | {state.open && state.setOpen(false)} />}
160 | {banner}
161 |
162 | state.setOpen(false)}
165 | width={[1, 256, 320]}
166 | />
167 |
172 | {props.children}
173 |
174 |
175 |
176 |
177 |
178 | )
179 | }
180 |
--------------------------------------------------------------------------------
/packages/styled-system/test/styles.js:
--------------------------------------------------------------------------------
1 | import {
2 | color,
3 | width,
4 | fontSize,
5 | size,
6 | gridGap,
7 | gridRowGap,
8 | gridColumnGap,
9 | borders,
10 | shadow,
11 | } from '../src'
12 |
13 | const theme = {
14 | colors: {
15 | blue: '#07c',
16 | black: '#111',
17 | },
18 | }
19 |
20 | test('returns color values from theme', () => {
21 | const a = color({ theme, color: 'blue', bg: 'black' })
22 | expect(a).toEqual({ color: '#07c', backgroundColor: '#111' })
23 | })
24 |
25 | test('returns raw color values', () => {
26 | const a = color({
27 | theme,
28 | color: 'inherit',
29 | bg: 'tomato',
30 | })
31 | expect(a).toEqual({ color: 'inherit', backgroundColor: 'tomato' })
32 | })
33 |
34 | test.skip('backgroundColor prop overrides bg prop', () => {
35 | const a = color({
36 | backgroundColor: 'tomato',
37 | bg: 'blue',
38 | })
39 | expect(a).toEqual({ backgroundColor: 'tomato' })
40 | })
41 |
42 | test('returns a pixel font-size', () => {
43 | const a = fontSize({ fontSize: 48 })
44 | expect(a).toEqual({ fontSize: 48 })
45 | })
46 |
47 | test('uses a default font-size scale', () => {
48 | const a = fontSize({ fontSize: 2 })
49 | expect(a).toEqual({ fontSize: 16 })
50 | })
51 |
52 | test('returns a string font-size', () => {
53 | const a = fontSize({ fontSize: '2em' })
54 | expect(a).toEqual({ fontSize: '2em' })
55 | })
56 |
57 | test('returns a percentage based width', () => {
58 | const a = width({ width: 1 / 2 })
59 | expect(a).toEqual({ width: '50%' })
60 | })
61 |
62 | test('returns a pixel based width', () => {
63 | const a = width({ width: 256 })
64 | expect(a).toEqual({ width: 256 })
65 | })
66 |
67 | test('returns a string width', () => {
68 | const a = width({ width: 'auto' })
69 | expect(a).toEqual({ width: 'auto' })
70 | })
71 |
72 | test('returns a width based on theme.sizes', () => {
73 | const a = width({
74 | theme: {
75 | sizes: [24, 48],
76 | },
77 | width: 1,
78 | })
79 | expect(a).toEqual({ width: 48 })
80 | })
81 |
82 | test('returns fractional responsive widths', () => {
83 | const a = width({
84 | width: [1, 1 / 2, 1 / 4],
85 | })
86 | expect(a).toEqual({
87 | width: '100%',
88 | '@media screen and (min-width: 40em)': {
89 | width: '50%',
90 | },
91 | '@media screen and (min-width: 52em)': {
92 | width: '25%',
93 | },
94 | })
95 | })
96 |
97 | test('size returns width and height', () => {
98 | const styles = size({
99 | size: 4,
100 | })
101 | expect(styles).toEqual({ width: 4, height: 4 })
102 | })
103 |
104 | // grid
105 | test('gridGap returns a scalar style', () => {
106 | const a = gridGap({
107 | theme: {
108 | space: [0, 2, 4, 8],
109 | },
110 | gridGap: 3,
111 | })
112 | expect(a).toEqual({ gridGap: 8 })
113 | })
114 |
115 | test('gridGap uses the default scale', () => {
116 | const a = gridGap({
117 | theme: {},
118 | gridGap: 2,
119 | })
120 | expect(a).toEqual({ gridGap: 8 })
121 | })
122 |
123 | test('gridRowGap returns a scalar style', () => {
124 | const a = gridRowGap({
125 | theme: {
126 | space: [0, 2, 4, 8],
127 | },
128 | gridRowGap: 3,
129 | })
130 | expect(a).toEqual({ gridRowGap: 8 })
131 | })
132 |
133 | test('gridRowGap uses the default scale', () => {
134 | const a = gridRowGap({
135 | theme: {},
136 | gridRowGap: 2,
137 | })
138 | expect(a).toEqual({ gridRowGap: 8 })
139 | })
140 |
141 | test('gridColumnGap returns a scalar style', () => {
142 | const a = gridColumnGap({
143 | theme: {
144 | space: [0, 2, 4, 8],
145 | },
146 | gridColumnGap: 3,
147 | })
148 | expect(a).toEqual({ gridColumnGap: 8 })
149 | })
150 |
151 | test('gridColumnGap uses the default scale', () => {
152 | const a = gridColumnGap({
153 | theme: {},
154 | gridColumnGap: 2,
155 | })
156 | expect(a).toEqual({ gridColumnGap: 8 })
157 | })
158 |
159 | test('borders prop returns correct sequence', () => {
160 | const a = borders({
161 | borderBottom: '1px solid',
162 | borderWidth: '2px',
163 | borderStyle: 'dashed',
164 | borderColor: 'red',
165 | })
166 | expect(a).toEqual({
167 | borderBottom: '1px solid',
168 | borderWidth: '2px',
169 | borderStyle: 'dashed',
170 | borderColor: 'red',
171 | })
172 | })
173 |
174 | test('shadow handles boxShadow and textShadow props', () => {
175 | const a = shadow({
176 | textShadow: '0 -1px rgba(255, 255, 255, .25)',
177 | boxShadow: 'none',
178 | })
179 | expect(a).toEqual({
180 | textShadow: '0 -1px rgba(255, 255, 255, .25)',
181 | boxShadow: 'none',
182 | })
183 | })
184 |
--------------------------------------------------------------------------------
/packages/css/src/index.js:
--------------------------------------------------------------------------------
1 | // based on https://github.com/developit/dlv
2 | export const get = (obj, key = '', def, p, undef) => {
3 | key = key.split ? key.split('.') : [key]
4 | for (p = 0; p < key.length; p++) {
5 | obj = obj ? obj[key[p]] : undef
6 | }
7 | return obj === undef ? def : obj
8 | }
9 |
10 | const defaultBreakpoints = [40, 52, 64].map(n => n + 'em')
11 |
12 | const defaultTheme = {
13 | space: [0, 4, 8, 16, 32, 64, 128, 256, 512],
14 | fontSizes: [12, 14, 16, 20, 24, 32, 48, 64, 72],
15 | }
16 |
17 | const aliases = {
18 | bg: 'backgroundColor',
19 | m: 'margin',
20 | mt: 'marginTop',
21 | mr: 'marginRight',
22 | mb: 'marginBottom',
23 | ml: 'marginLeft',
24 | mx: 'marginX',
25 | my: 'marginY',
26 | p: 'padding',
27 | pt: 'paddingTop',
28 | pr: 'paddingRight',
29 | pb: 'paddingBottom',
30 | pl: 'paddingLeft',
31 | px: 'paddingX',
32 | py: 'paddingY',
33 | }
34 |
35 | const directions = {
36 | marginX: ['marginLeft', 'marginRight'],
37 | marginY: ['marginTop', 'marginBottom'],
38 | paddingX: ['paddingLeft', 'paddingRight'],
39 | paddingY: ['paddingTop', 'paddingBottom'],
40 | }
41 |
42 | const scales = {
43 | color: 'colors',
44 | backgroundColor: 'colors',
45 | borderColor: 'colors',
46 | margin: 'space',
47 | marginTop: 'space',
48 | marginRight: 'space',
49 | marginBottom: 'space',
50 | marginLeft: 'space',
51 | marginX: 'space',
52 | marginY: 'space',
53 | padding: 'space',
54 | paddingTop: 'space',
55 | paddingRight: 'space',
56 | paddingBottom: 'space',
57 | paddingLeft: 'space',
58 | paddingX: 'space',
59 | paddingY: 'space',
60 | fontFamily: 'fonts',
61 | fontSize: 'fontSizes',
62 | fontWeight: 'fontWeights',
63 | lineHeight: 'lineHeights',
64 | letterSpacing: 'letterSpacings',
65 | border: 'borders',
66 | borderTop: 'borders',
67 | borderRight: 'borders',
68 | borderBottom: 'borders',
69 | borderLeft: 'borders',
70 | borderWidth: 'borderWidths',
71 | borderStyle: 'borderStyles',
72 | borderRadius: 'radii',
73 | borderTopRightRadius: 'radii',
74 | borderTopLeftRadius: 'radii',
75 | borderBottomRightRadius: 'radii',
76 | borderBottomLeftRadius: 'radii',
77 | boxShadow: 'shadows',
78 | zIndex: 'zIndices',
79 | width: 'sizes',
80 | minWidth: 'sizes',
81 | maxWidth: 'sizes',
82 | height: 'sizes',
83 | minHeight: 'sizes',
84 | maxHeight: 'sizes',
85 | }
86 |
87 | const getMargin = (scale, value) => {
88 | if (typeof value !== 'number' || value >= 0) {
89 | return get(scale, value, value)
90 | }
91 | const absolute = Math.abs(value)
92 | const n = get(scale, absolute, absolute)
93 | if (typeof n === 'string') return '-' + n
94 | return n * -1
95 | }
96 |
97 | const transforms = {
98 | margin: getMargin,
99 | marginTop: getMargin,
100 | marginRight: getMargin,
101 | marginBottom: getMargin,
102 | marginLeft: getMargin,
103 | marginX: getMargin,
104 | marginY: getMargin,
105 | }
106 |
107 | export const responsive = styles => theme => {
108 | const next = {}
109 | const breakpoints = get(theme, 'breakpoints', defaultBreakpoints)
110 | const mediaQueries = [ null, ...breakpoints.map(n => `@media screen and (min-width: ${n})`) ]
111 |
112 | for (const key in styles) {
113 | const value = styles[key]
114 | if (!Array.isArray(value)) {
115 | next[key] = value
116 | continue
117 | }
118 | for (let i = 0; i < value.length; i++) {
119 | const media = mediaQueries[i]
120 | if (!media) {
121 | next[key] = value[i]
122 | continue
123 | }
124 | next[media] = next[media] || {}
125 | next[media][key] = value[i]
126 | }
127 | }
128 |
129 | return next
130 | }
131 |
132 | export const css = args => (props = {}) => {
133 | const theme = { ...defaultTheme, ...(props.theme || props) }
134 | let result = {}
135 | const obj = typeof args === 'function' ? args(theme) : args
136 | const styles = responsive(obj)(theme)
137 |
138 | for (const key in styles) {
139 | const prop = get(aliases, key, key)
140 | const scaleName = get(scales, prop)
141 | const scale = get(theme, scaleName, get(theme, prop, {}))
142 | const x = styles[key]
143 | const val = typeof x === 'function' ? x(theme) : x
144 | if (key === 'variant') {
145 | const variant = css(get(theme, val))(theme)
146 | result = { ...result, ...variant }
147 | continue
148 | }
149 | if (val && typeof val === 'object') {
150 | result[prop] = css(val)(theme)
151 | continue
152 | }
153 | const transform = get(transforms, prop, get)
154 | const value = transform(scale, val, val)
155 | if (directions[prop]) {
156 | const dirs = directions[prop]
157 | for (let i = 0; i < dirs.length; i++) {
158 | result[dirs[i]] = value
159 | }
160 | } else {
161 | result[prop] = value
162 | }
163 | }
164 |
165 | return result
166 | }
167 |
168 | export default css
169 |
--------------------------------------------------------------------------------
/docs/guides/spacing.md:
--------------------------------------------------------------------------------
1 | # Spacing
2 |
3 | There are many ways to approach spacing (margin and padding) in a web application,
4 | and Styled System is intended to work at a low enough level to support several of these
5 | depending on your team's preferences.
6 |
7 | If you're not familiar with Styled System's `space` utility,
8 | you might want to read the [Getting Started](/getting-started/#margin--padding) documentation first.
9 |
10 | ## Where does space belong?
11 |
12 | Generally speaking, it's a good idea to avoid adding default margins to reusable components in React.
13 | Some people prefer to use a declarative approach by adding a "spacer" component in between other components.
14 | This is the _[spacer.gif][]_ of React, and it's a completely acceptable practice, despite what some people may say.
15 |
16 | [spacer.gif]: https://en.wikipedia.org/wiki/Spacer_GIF
17 |
18 | Others prefer creating wrapper layout components that apply spacing to child components.
19 | This is also an acceptable approach, but requires an additional layer of abstraction and creates a larger API surface area
20 | for your component library. This approach will also be less familiar to people new to React.
21 |
22 | The third common way, and the way that Styled System's API encourages you to approach this is through spacing props.
23 |
24 | All three approaches are valid, and all three can be used together with Styled System helping ensure that the values used for spacing
25 | are consistent across your entire application.
26 |
27 | ## Spacer Component
28 |
29 | One of the simpler approaches comes from the early days of the web, and is a really great way to add space to UI.
30 |
31 | To create a spacer component with Styled System, you can either reuse a [Box component](/guides/build-a-box) or create a specialized component
32 | for this purpose.
33 |
34 | ```js
35 | // example Spacer component
36 | import styled from 'styled-components'
37 | import { space } from 'styled-system'
38 |
39 | const Spacer = styled.div(space)
40 | ```
41 |
42 | To use the Spacer component, render it without any children and use the margin props.
43 |
44 | ```js
45 | // example Spacer usage
46 |
47 |
48 |
49 | ```
50 |
51 | The Spacer component is also really great for flexbox layouts, where `margin: auto` will fill any remaining space.
52 |
53 | ```js
54 | // example Spacer in flexbox
55 |
56 |
57 |
58 | Beep
59 | Boop
60 |
61 | ```
62 |
63 | ## Wrapper Component
64 |
65 | Some people use a wrapping parent component to control spacing on child components.
66 | This is exactly what [Rebass Space][] does and can be useful for tiled layouts or grid systems.
67 |
68 | To target child elements you can either use the `React.Children` API to map over child elements, or use child CSS selectors.
69 | The use of child CSS selectors can lead to styling bugs and isn't generally recommended.
70 |
71 | ```js
72 | // example using child CSS selectors
73 | import styled from 'styled-components'
74 | import { space } from 'styled-system'
75 |
76 | const SpaceChildren = styled.div`
77 | & > * {
78 | ${space}
79 | }
80 | `
81 | ```
82 |
83 | The following example comes from [Rebass Space][].
84 | This approach does not create a wrapping element and does not rely on child CSS selectors.
85 |
86 | ```jsx
87 | import React from 'react'
88 | import styled from 'styled-components'
89 | import { space } from 'styled-system'
90 |
91 | const classnames = (...args) => args.join(' ')
92 | const getClassName = el => (el.props && el.props.className) || ''
93 |
94 | export const StyledChildren = ({ className, children, ...props }) => {
95 | const styledChildren = React.Children.toArray(children).map(child =>
96 | React.cloneElement(child, {
97 | className: classnames(getClassName(child), className),
98 | })
99 | )
100 | return <>{styledChildren}>
101 | }
102 |
103 | const SpaceChildren = styled(StyledChildren)(space)
104 |
105 | SpaceChildren.propTypes = space.propTypes
106 |
107 | export default SpaceChildren
108 | ```
109 |
110 | ## Space Props
111 |
112 | While the above approaches work perfectly fine, it can be useful to control margin and padding on a per-element basis.
113 | This is where Styled System's `space` utility really shines.
114 | It lets you add margin and padding props to any component, whether it's a Box layout component, a heading, or a button.
115 | By including the `space` props in your components, you can quickly make one-off adjustments and adapt to changing requirements.
116 |
117 | The `space` utility can be added to any component that accepts the `className` prop.
118 |
119 | ```js
120 | // example
121 | import styled from 'styled-component'
122 | import { space } from 'styled-system'
123 |
124 | const Heading = styled.h2(space)
125 | ```
126 |
127 | When using the component, you can adjust margin and padding in any direction needed.
128 |
129 | ```jsx
130 | // example usage
131 |
132 | Hello
133 |
134 | ```
135 |
136 | [rebass space]: https://github.com/rebassjs/space
137 |
--------------------------------------------------------------------------------
/docs/src/theme.js:
--------------------------------------------------------------------------------
1 | const colors = {
2 | text: '#000',
3 | background: '#fff',
4 | primary: '#00f',
5 | secondary: '#00a',
6 | highlight: '#c0f',
7 | gray: '#eee',
8 | lightgray: '#fafafa',
9 | midgray: '#777',
10 | modes: {
11 | dark: {
12 | text: '#fff',
13 | background: '#000',
14 | primary: '#0cf',
15 | secondary: '#f0e',
16 | gray: '#222',
17 | lightgray: '#111',
18 | },
19 | cyan: {
20 | text: '#023',
21 | background: '#0ff',
22 | primary: '#03c',
23 | secondary: '#01a',
24 | gray: '#0cc',
25 | lightgray: '#0ee',
26 | },
27 | gray: {
28 | text: '#eef',
29 | background: '#333336',
30 | primary: '#09f',
31 | secondary: '#0bf',
32 | gray: '#55555a',
33 | lightgray: '#444448',
34 | },
35 | book: {
36 | text: '#322',
37 | background: '#fff9f9',
38 | primary: '#c30',
39 | secondary: '#400',
40 | gray: '#e9e6e6',
41 | lightgray: '#f9f6f6',
42 | },
43 | magenta: {
44 | text: '#203',
45 | background: '#f3f',
46 | primary: '#208',
47 | secondary: '#106',
48 | gray: '#c0c',
49 | lightgray: '#e0e',
50 | },
51 | },
52 | }
53 |
54 | const prism = {
55 | [['.comment', '.prolog', '.doctype', '.cdata']]: {
56 | color: 'midgray',
57 | },
58 | '.punctuation': {
59 | color: 'midgray',
60 | },
61 | [['.property', '.tag', '.constant', '.symbol', '.deleted']]: {
62 | color: 'primary',
63 | },
64 | [['.boolean', '.number']]: {
65 | color: 'secondary',
66 | },
67 | [['.selector', '.attr-name', '.string', '.char', '.builtin', '.inserted']]: {
68 | color: 'highlight',
69 | },
70 | [['.operator', '.entity', '.url', '.string', '.variable']]: {
71 | color: 'highlight',
72 | },
73 | [['.atrule', '.attr-value', '.function']]: {
74 | color: 'primary',
75 | },
76 | '.keyword': {
77 | color: 'primary',
78 | },
79 | '.regex': {},
80 | '.important': {},
81 | }
82 |
83 | export default {
84 | initialColorMode: 'light',
85 | colors,
86 | fonts: {
87 | body: 'system-ui, sans-serif',
88 | monospace: 'Menlo, monospace',
89 | },
90 | fontSizes: [12, 14, 16, 18, 24, 32, 48, 64, 72],
91 | lineHeights: {
92 | body: 1.75,
93 | heading: 1.25,
94 | },
95 | styles: {
96 | a: {
97 | color: 'primary',
98 | '&:hover': {
99 | color: 'secondary',
100 | },
101 | },
102 | h1: {
103 | fontSize: [5, 6],
104 | lineHeight: 'heading',
105 | a: {
106 | color: 'inherit',
107 | textDecoration: 'none',
108 | },
109 | },
110 | h2: {
111 | fontSize: [4, 5],
112 | lineHeight: 'heading',
113 | a: {
114 | color: 'inherit',
115 | textDecoration: 'none',
116 | },
117 | },
118 | h3: {
119 | fontSize: 3,
120 | lineHeight: 'heading',
121 | a: {
122 | color: 'inherit',
123 | textDecoration: 'none',
124 | },
125 | },
126 | h4: {
127 | fontSize: 2,
128 | lineHeight: 'heading',
129 | a: {
130 | color: 'inherit',
131 | textDecoration: 'none',
132 | },
133 | },
134 | h5: {
135 | fontSize: 1,
136 | lineHeight: 'heading',
137 | a: {
138 | color: 'inherit',
139 | textDecoration: 'none',
140 | },
141 | },
142 | h6: {
143 | fontSize: 0,
144 | lineHeight: 'heading',
145 | a: {
146 | color: 'inherit',
147 | textDecoration: 'none',
148 | },
149 | },
150 | pre: {
151 | fontFamily: 'monospace',
152 | fontSize: 1,
153 | p: 3,
154 | my: 3,
155 | bg: 'lightgray',
156 | overflowX: 'auto',
157 | ...prism,
158 | },
159 | code: {
160 | fontFamily: 'monospace',
161 | // fontSize: '87.5%',
162 | },
163 | inlineCode: {
164 | fontFamily: 'monospace',
165 | color: 'secondary',
166 | fontSize: '87.5%',
167 | },
168 | ul: {
169 | pl: 3,
170 | ul: {
171 | // pl: 2
172 | // textIndent: '1em',
173 | },
174 | },
175 | table: {
176 | width: '100%',
177 | my: 4,
178 | // borderColor: colors.gray,
179 | borderCollapse: 'separate',
180 | borderSpacing: 0,
181 | },
182 | th: {
183 | textAlign: 'left',
184 | verticalAlign: 'bottom',
185 | paddingTop: '4px',
186 | paddingBottom: '4px',
187 | paddingRight: '4px',
188 | paddingLeft: 0,
189 | borderColor: 'inherit',
190 | borderBottomWidth: '2px',
191 | borderBottomStyle: 'solid',
192 | },
193 | td: {
194 | textAlign: 'left',
195 | verticalAlign: 'top',
196 | paddingTop: '4px',
197 | paddingBottom: '4px',
198 | paddingRight: '4px',
199 | paddingLeft: 0,
200 | borderColor: 'inherit',
201 | borderBottomWidth: '1px',
202 | borderBottomStyle: 'solid',
203 | },
204 | hr: {
205 | border: 0,
206 | borderBottom: '1px solid',
207 | borderColor: 'lightgray',
208 | },
209 | },
210 | }
211 |
--------------------------------------------------------------------------------