react-systyle
v0.3.1
Published
A thin layer on top of your React components to quickly build design systems with `emotion`.
Downloads
42
Readme
React-Systyle
A thin layer on top of your React components to quickly build design systems with emotion
.
Installation
You will need react
and emotion
npm install react-systyle @emotion/core
Using the Styled component
This package exports a single React component that you can use to style your application. To do so, the simplest way is to use this component's props:
Every prop with a name matching a valid CSS property will be used to generate the corresponding CSS, the others will be forwarded normally
import Styled from 'react-systyle'
const App = props => {
// will render a div with id = main and a class that defines rules for the css props below
return <Styled id="main" width={100} height={100} background="blue" />
}
Note that if some of the props you pass have a valid CSS rule or selector as key, they will also be used to style your component:
const css = {
width: 100,
height: 100,
background: 'blue',
'& p': {
color: 'white'
},
':hover': {
background: 'yellow'
},
'@media (max-width: 720px)': {
'&': {
background: 'green'
}
'& p': {
color: 'yellow'
}
}
}
<Styled {...css} />
Changing the rendered component
In case you'd like to set which element will be rendered by a systyle component, you can define it as the as
prop.
// pass it directly as a prop
const Text = props => <Styled as="span">some text</Styled>
// or preset it
const Text = Styled.with({ as: 'span' })
// or use the shorthand
const Text = Styled.as('span')
Setting defaults
The Styled component comes with a static method .with()
that allows you to define a more specific version of the original component.
If you pass it an object, it will define the default props of your specialized component.
// this component and the one below render the same thing
const BlueSquare = props => (
<Styled width={100} height={100} background="blue" />
)
// but this one is a styled component with all the static methods that come with it
const BlueSquare = Styled.with({ width: 100, height: 100, background: 'blue' })
Every component generated with .with()
will also be a styled component so it'll also have access to this method. Defaults will stack in the order they were added, meaning values defined the latest will overwrite any previous one.
const Square = Styled.with({
width: 100,
height: 100,
background: 'transparent'
})
const BlueSquare = Square.with({ background: 'blue' })
And you can again use CSS selectors as props:
const HoverRed = Styled.with({
background: 'white',
':hover': {
background: 'red'
}
})
Every new kind of styled component gets a unique class name so you can use them as a key in the styling of other components:
const Text = Styled.as('span')
const Title = Text.as('h1')
const TextContainer = Styled.with({
[Text]: {
background: 'red',
color: 'white'
}
})
// Text below will be written in white on a red background
// Title was created from Text, but it doesn't have the same unique class
// => the Text styling won't apply to it.
;<TextContainer>
<Title>title</Title>
<Text>some text</Text>
</TextContainer>
Using a moulinette
A moulinette is a function that takes props as input and returns a modified version of those props. They are very useful if you want to control your component's final props depending on what was passed to it on render.
// take 'active' out of the props
function setActiveStyle({ active, ...props }) {
return {
// do not forget to pass down the other props
...props,
// add new props based on 'active'
background: active ? 'green' : 'red',
cursor: active ? 'pointer' : 'not-allowed'
}
}
const Toggle = Styled
.with({ width: 100, height: 100 })
.with(setActiveStyle)
// will render a red square with a not-allowed cursor
<Toggle />
// will render a green square with a pointer cursor
// `active` was removed from the props in setActiveStyle so it won't end up in the DOM
<Toggle active />
For convenience, you can pass many parameters to the .with()
method, it's equivalent to chaining the calls.
const Toggle = Styled.with({ width: 100, height: 100 }, setActiveStyle)
When you add many moulinettes to a component, the last added one will always be executed first, its result will be passed to the one added before and so on until everything is processed. This means that in a given moulinette, you'll only be able to access props generated by moulinettes added after this one.
function styleDisabled({ disabled, ...props }) {
return {
...props,
disabled,
background: disabled ? 'gray' : 'white'
cursor: disabled ? 'not-allowed' : 'pointer'
}
}
function loader({ loading, disabled, children, fallback = '...', ...props }) {
return {
...props,
// set disable here so that styleDisabled can use it to generate the right style after
disabled: loading || disabled,
// modify the rendered children while loading is true
children: loading ? fallback : children
}
}
const Button = Styled.as('button').with(styleDisabled)
const AsyncButton = Button.with(loader)
// renders a button with a white background and pointer cursor
<Button>Run</Button>
// renders a disabled button with a gray background and not-allowed cursor
<Button disabled>Run</Button>
// renders a button with a white background and pointer cursor
<AsyncButton>Load</AsyncButton>
// renders a disabled button with a gray background and not-allowed cursor
<AsyncButton disabled>Load</AsyncButton>
// renders a disabled button with a gray background and not-allowed cursor and 'Loading...' written inside
<AsyncButton loading fallback="Loading...">Load</AsyncButton>
Prop aliases
To ease the writing of frequently changed styles, some props that you can pass to a styled component are actually shortcuts for other props:
bg
=backgroundColor
font
=fontFamily
size
=fontSize
m
=margin
mx
=marginLeft
+marginRight
my
=marginTop
+marginBottom
mt
=marginTop
mr
=marginRight
mb
=marginBottom
ml
=marginLeft
p
=padding
px
=paddingLeft
+paddingRight
py
=paddingTop
+paddingBottom
pt
=paddingTop
pr
=paddingRight
pb
=paddingBottom
pl
=paddingLeft
b
=border
bx
=borderLeft
+borderRight
by
=borderTop
+borderBottom
bt
=borderTop
br
=borderRight
bb
=borderBottom
bl
=borderLeft
For example, your component's margin will vary quite often so it's better to define the value directly when rendering rather than defining one specific variation of a component for one specific margin.
const Box = Styled.with({ display: 'flex' })
const Text = Styled.as('span').with({ display: 'inline-flex' })
function App() {
return (
<Box flexDirection="column" alignItems="center">
<Text as="h1" size={32} mb={8}>
Title
</Text>
<Text as="p" size={18} mb={24}>
Subtitle
</Text>
<Box>
<Text mr={8}>Left</Text>
<Text>Right</Text>
</Box>
</Box>
)
}
Theme helpers
Thanks to emotion
, if a theme context is provided to your component tree, your styled components get access to some sugar.
import { ThemeContext } from '@emotion/core'
const theme = {
// multiplier for margin and padding numeric values
spacing: 8,
// aliases for color, borderColor, backgroundColor values
colors: {
primary: 'red'
},
// aliases for fontFamily values
fonts: {
header: 'serif',
body: 'sans-serif'
},
// aliases for fontSize values
sizes: {
M: 16,
L: 24
}
}
// creates a span with fontFamily = sans-serif and fontSize = 16px
const Text = Styled.as('span').with({ font: 'body', size: 'M' })
// creates a h1 with fontFamily = serif and fontSize = 24px and color = red
const Title = Styled.as('h1').with({
font: 'header',
size: 'L',
color: 'primary'
})
// creates a div with padding = '8px 16px', border = '1px solid black'
const Box = Styled.with({ px: 2, py: 1, b: '1px solid black' })
const App = props => (
<ThemeContext.Provider value={theme}>
<Box>
<Title>title</Title>
<Text>text</Text>
</Box>
</ThemeContext.Provider>
)
Responsive props
In your theme, you can define a set of breakpoints indexed by a unique name. You can specify 3 kinds of values for your breakpoints:
- full media query: will be used as is
- number: will generate
@media screen and (min-width: <number>px)
- string: will generate
@media screen and (min-width: <string>)
const theme = {
breakpoints: {
mobile: `@media screen and (max-width: 320px)`,
tablet: 600, // @media screen and (min-width: 600px)
desktop: '70em' // @media screen and (min-width: 70em)
}
}
With the above config injected in your styled components, you can now use those names as key in your props, they will ultimately be converted to the corresponding media query in your component's final CSS.
const Reponsive = Styled.with({
width: '100%',
background: '#000',
mobile: {
background: '#eee'
},
tablet: {
width: '66%'
},
desktop: {
width: '50%'
}
})
Using refs
Refs are automatically forwared to the element rendered by your styled component. This means that if your component generates a dom element, you'll have direct access to it.
class App extends React.Component {
video = React.createRef()
componentDidMount() {
this.video.current.play()
}
render() {
return <Styled as="video" ref={this.video} />
}
}
Recommendations
Most of the time, it's nicer to predefine your styled components instead of passing them styling props on render:
const Container = Styled.with({
display: 'flex',
flexDirection: 'column'
})
const Title = Styled.as('h1')
const Text = Styled.as('span')
const App = props => (
<Container>
<Title>Hi</Title>
<Text>Some text</Text>
</Container>
)
The only exception being for styling props that tend to vary often between different uses of a same component, like margins:
const Row = Container.with({ flexDirection: 'row' })
const App = props => (
<Container>
<Title mb={16}>Hi</Title>
<Row>
<Text>A</Text>
<Text mx={8}>B</Text>
<Text>C</Text>
</Row>
<Text mt={16}>D</Text>
</Container>
)
Styled components are defined as PureComponent so they will only rerender if their props change. Be sure to avoid passing them new objects or arrow functions on render if you want to avoid running the whole set of moulinette computation every time something changes.
const App = props => (
<Styled>
<Styled
as="button"
// the arrow function here is recreated on every render of App
// => this styled component will recompute itself everytime
onClick={() => alert('clicked!')}
>
CLICK ME
</Styled>
<Styled
width="100%"
// a new object is created here => rerender
dekstop={{ width: '50%' }}
/>
</Styled>
)