Creating a custom style hook

In this tutorial we'll go through the process of creating a style hook from scratch

import {css} from '@emotion/core'
import {useStyles} from '@style-hooks/core'
const
boxConfig = {
name: 'box',
styles: {width: value => css`width: ${value}`}
},
useBox = props => useStyles(boxConfig, props)

1. Understanding useStyles(config, props)

The useStyles() hook takes two arguments, config and props.

config is an object that contains 2 properties: name and styles.

  • name <string> required

    This is the key name you'll use for this hook when configuring it in your theme. As such, it should be unique. Names are important because they allow your hook to use powerful features like the kind prop anddefaultProps in your theme. They also provide organization to your theme allowing you to provide a name argument to useTheme('name').

  • styles <object> required

    This is the object that defines your component's style props. The key names of this object become prop names in your component and the value handles the creation of style definitions. See creating style props below to learn the numerous ways you can generate styles.

props allude to, as you'd guess, the props object provided to an element when it is created with React.createElement(). But it can be any plain object you want to derive styles from. useStyles() is an immutable function. That is, the input props are cloned and the return value is a new object containing a css prop with your generated styles. It will also remove any keys from the input props that generated styles. For example, let's say your component has a boolean style prop for hidden which provides a display: none; style:

// Given the input props
{hidden: true, foo: 'bar'}
// Expect the output props
{
foo: 'bar',
css: [{
name: "1ecy55y",
styles: "display: none;"
}]
}

2. Creating style props

There are three ways to create style props in a useStyles() hook

  1. Creating a boolean style prop

    This is the most basic type of style prop you can create. To use it, create a key in your styles object that has an Emotion css object as its value.

    import {css} from '@emotion/core'
    // style definitions
    const styles = {
    // this creates a `boolean` prop for `hidden`
    hidden: css`display: none;`
    }
    // ... add your hook MyComponent ... //
    <MyComponent hidden/>
    // Here, the prop `hidden` becomes
    // {css: [{name: '...', styles: 'display: none;'}]}
  2. Creating an enum style prop

    To construct an enum style prop just use the same pattern we used for a boolean prop, but as part of a nested object.

    import {css} from '@emotion/core'
    // style definitions
    const styles = {
    // this creates an `enum` prop for `display`
    // with the choices 'none', 'block', and 'flex'
    display: {
    none: css`display: none;`,
    block: css`display: block;`,
    flex: css`display: flex;`,
    }
    }
    // ... add your hook MyComponent ... //
    <MyComponent display='block'/>
    // Here, the prop `display` becomes
    // {css: [{name: '...', styles: 'display: block;'}]}
  3. Creating a functional style prop

    The true magic begins when you create functional style props. Here we can customize our styles based on the component theme and props passed to the useStyles() hook. Return an Emotion css object to add styles, or null/undefined to skip adding styles.

    Here is the function signature

    (value: <any>, theme: <object>, props: <object>) => <css|void>

    • value <any>

      The value provided to the style prop in React.createElement().

    • theme <object>

      The theme as defined in ThemeProvider.

    • props <object>

      Other props that were passed to our useStyles() hook.

    import {css} from '@emotion/core'
    // fake default theme
    const theme = {
    box: {
    sizes: {
    sm: 16,
    md: 32,
    lg: 64
    }
    }
    }
    // style definitions
    const styles = {
    // this creates a `functional` prop for `size`
    size: (value, theme, props) => {
    const px = theme.box.sizes[value]
    // in reality you'd throw an error, but for this example
    // we just skip the prop
    if (px === undefined)
    return
    return css`
    width: ${px}px;
    height: ${px}px;
    `
    }
    }
    // ... add your hook MyComponent ... //
    <MyComponent size='md'/>
    // Here, the prop `size` becomes
    // {css: [{name: '...', styles: 'width: 32px; height: 32px;'}]}

Another important thing to remember when constructing your style props is thatuseStyles will handle all of the breakpoint props on its own. You will never receive the breakpoint as part of your value. You will only receive the portion in front of the delimiter.

See here for a plethora of examples in the Curls <Box> component

3. Understanding the relationship between themes and hooks

Effectively, you can put anything you want to in your theme. The values in your theme are provided to your functional style props and accessible via useTheme(). There are, however, two special keys that Style Hooks uses in the useStyles() hook for a given hook name:

const theme = {
// assumes you've made a useText() hook w/ the
// name: 'text'
text: {
// default props prepended to the props object
// received by useStyles()
defaultProps: {
size: 'sm'
},
// groups of other default props applied when
// using kind='' props
kinds: {
h1: {
as: 'h1',
size: 'lg',
}
}
}
}
  • defaultProps <object>

    These style props are provided to every component of this type by default. As the name implies, they work just like React default props and can be overwritten by the user.

  • kinds <object>

    These are basically an extension to defaultProps, but in the form of variants. See the kind prop section.

4. Composing style hooks

Another cool feature of style hooks is that they are composable. For example, say you want all of your components to inherit style props from a useBox() hook providing access to margins, padding, and other props from the box model. You could do the following:

import {css, useStyles, createElement} from '@style-hooks/core'
const
config = {
name: 'myComponent',
styles: {foo: css`bar: baz;`}
},
useMyOtherHook = props => useStyles(config, props)
const MyComponent = React.forwardRef((props, ref) => {
// Here is the composition
props = useBox(useMyOtherHook(props))
props.ref = ref
return createElement('div', props)
})
export {MyComponent, useMyComponent}
// Now you can use <MyComponent/> with the
// Box model in addition to your custom styles
<MyComponent foo d='block'/>
// {
// css: [
// {name: '...', styles: 'bar: baz;'},
// {name: '...', styles: 'display: block;'}
// ]
// }

5. Adding default styles to a hook

Sometimes you may want to add styles to a hook by default. Below is an example of how you may approach the task:

import {css} from '@emotion/core'
import {useStyles} from '@style-hooks/core'
const
rowConfig = {
name: 'row',
styles: {}
},
useRow = (props) => {
props = useStyles(rowConfig, props)
props.css = [
css`
display: flex;
flex-direction: row;
`,
...props.css || []
]
return props
}

Next let's build a real style hook from start to finish

In this example, we are going to write our own useBox() hook

  1. Name your component so its configurable in the theme.

    import React from 'react'
    import {css} from '@emotion/core'
    import {useStyles} from '@style-hooks/core'
    const
    boxConfig = {
    // This is the name in our theme
    name: 'box'
    }
  2. Next we add our style props. We only want our box to handle overflow, width, height, background color, and a display: block; boolean

    import React from 'react'
    import {css} from '@emotion/core'
    import {useStyles} from '@style-hooks/core'
    const
    boxConfig = {
    // This is the name in our theme
    name: 'box',
    // These are our style prop definitions
    styles: {
    // Adds a boolean prop
    block: css`display: block;`,
    // Adds an enum prop
    overflow: {
    hidden: css`overflow: hidden;`,
    hiddenX: css`overflow-x: hidden;`,
    hiddenY: css`overflow-y: hidden;`,
    auto: css`overflow: auto;`,
    autoY: css`overflow-y: auto;`,
    autoX: css`overflow-x: auto;`
    },
    // Adds functional props
    bg: value =>
    css`background-color: ${value};`,
    width: (value, theme) =>
    css`width: ${value + theme.sizeUnit)};`,
    height: (value, theme) =>
    css`height: ${value + theme.sizeUnit)};`,
    }
    }
  3. Cool, we've got our config! Let's create a style hook called useBox()

    import React from 'react'
    import {css} from '@emotion/core'
    import {useStyles} from '@style-hooks/core'
    const
    boxConfig = {
    // This is the name in our theme
    name: 'box',
    // These are our style prop definitions
    styles: {
    // Adds a boolean prop
    block: css`display: block;`,
    // Adds an enum prop
    overflow: {
    hidden: css`overflow: hidden;`,
    hiddenX: css`overflow-x: hidden;`,
    hiddenY: css`overflow-y: hidden;`,
    auto: css`overflow: auto;`,
    autoY: css`overflow-y: auto;`,
    autoX: css`overflow-x: auto;`
    },
    // Adds functional props
    bg: value =>
    css`background-color: ${value};`,
    width: (value, theme) =>
    css`width: ${value + theme.sizeUnit)};`,
    height: (value, theme) =>
    css`height: ${value + theme.sizeUnit)};`,
    }
    },
    // Woohoo! Here is our style hook!
    useBox = props => useStyles(boxConfig, props)
  4. Create a <Box> using our useBox() hook

    /* @jsx jsx */
    import React from 'react'
    import {jsx, css} from '@emotion/core'
    import {useStyles, createElement} from '@style-hooks/core'
    const
    boxConfig = {
    // This is the name in our theme
    name: 'box',
    // These are our style prop definitions
    styles: {
    // Adds a boolean prop
    block: css`display: block;`,
    // Adds an enum prop
    overflow: {
    hidden: css`overflow: hidden;`,
    hiddenX: css`overflow-x: hidden;`,
    hiddenY: css`overflow-y: hidden;`,
    auto: css`overflow: auto;`,
    autoY: css`overflow-y: auto;`,
    autoX: css`overflow-x: auto;`
    },
    // Adds functional props
    bg: value =>
    css`background-color: ${value};`,
    width: (value, theme) =>
    css`width: ${value + theme.sizeUnit)};`,
    height: (value, theme) =>
    css`height: ${value + theme.sizeUnit)};`,
    }
    },
    // Woohoo! Here is our style hook!
    useBox = props => useStyles(boxConfig, props)
    const Box = React.forwardRef((props, ref) => {
    // The style hook will generate a css prop and
    // remove prop keys that generated styles in
    // useBox()
    props = useBox(props)
    // props are mutable here because the props returned
    // by useBox() are always cloned from the input
    // props
    //
    // using 'ref' here with React.forwardRef allows
    // our <Box> component to accept a 'ref' prop
    props.ref = ref
    // Here we're telling createElement we want the default element
    // type to be a 'div'. This can be overwritten using
    // an 'as' prop
    return createElement('div', props)
    })
  5. Finally, since we referenced a sizeUnit in our functional style props for width and height above, let's put a size unit in our theme. While we're add it, we'll add some defaultProps and kind props.

    /* @jsx jsx */
    import React from 'react'
    import {jsx, css} from '@emotion/core'
    import {useStyles, createElement} from '@style-hooks/core'
    const
    boxConfig = {
    // This is the name in our theme
    name: 'box',
    // These are our style prop definitions
    styles: {
    // Adds a boolean prop
    block: css`display: block;`,
    // Adds an enum prop
    overflow: {
    hidden: css`overflow: hidden;`,
    hiddenX: css`overflow-x: hidden;`,
    hiddenY: css`overflow-y: hidden;`,
    auto: css`overflow: auto;`,
    autoY: css`overflow-y: auto;`,
    autoX: css`overflow-x: auto;`
    },
    // Adds functional props
    bg: value =>
    css`background-color: ${value};`,
    width: (value, theme) =>
    css`width: ${value + theme.sizeUnit)};`,
    height: (value, theme) =>
    css`height: ${value + theme.sizeUnit)};`,
    }
    },
    // Woohoo! Here is our style hook!
    useBox = props => useStyles(boxConfig, props)
    const Box = React.forwardRef((props, ref) => {
    // The style hook will generate a css prop and
    // remove prop keys that generated styles in
    // useBox()
    props = useBox(props)
    // props are mutable here because the props returned
    // by useBox() are always cloned from the input
    // props
    //
    // using 'ref' here with React.forwardRef allows
    // our <Box> component to accept a 'ref' prop
    props.ref = ref
    // Here we're telling createElement we want the default element
    // type to be a 'div'. This can be overwritten using
    // an 'as' prop
    return createElement('div', props)
    })
    // Here's our theme where we specify a
    // size unit, default props, and kinds for
    // this box component
    const theme = {
    sizeUnit: 'px',
    box: {
    defaultProps: {
    width: 50,
    height: 50
    },
    kinds: {
    small: {
    width: '100:phone 250:desktop',
    height: '100:phone 250:desktop'
    },
    big: {
    width: '300:phone 500:desktop',
    height: '300:phone 500:desktop'
    }
    }
    }
    }
  6. Now the fun part, let's use our useBox() hook and <Box> component

That's all I've got for this tutorial! Let me know what you think and feel free to ask me any questions to have on Twitter @jaredlunde.

Continue to Server side rendering