Understanding Theme UI: 6 - The Hacks

Date published: 9-Feb-2021
4 min read / 1014 words
Author: Paul Scanlon

JavaScript
React
Theme UI

In this post i'm going to explain some of my Theme UI "hacks"... to be honest they're not really hacks but you won't necessarily find these in the docs since they mostly relate to standard CSS selectors but it wasn't immediately obvious to me that the sx prop can be used in this way.

If you're new to Theme UI i'd suggest having a read of the first five posts in this series to bring you up to speed

    1. Understanding Theme UI: 1 - Jsx Pragma
    1. Understanding Theme UI: 2 - Styled component
    1. Understanding Theme UI: 3 - Components
    1. Understanding Theme UI: 4 - Scales
    1. Understanding Theme UI: 5 - Media Queries

Variants - Buttons

There are some obvious uses for variants that are explained in the docs but one method I use a fair bit isn't covered by the docs so here's one way you can use variants inside the theme object.

Below is a method I use for styling button variants and as the docs mention the default variant is theme.buttons.primary. You'll see in the source below that if the <Button /> component is used without a variant prop it'll default to the styles in defined in theme.buttons.primary


Src 👇

<Button>Primary</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="ghost">Ghost</Button>

Output 👇

Theme 👇

// path-to-theme/index.js
export default {
colors: {
text: '#FFFFFF',
muted: '#8b87ea',
primary: '#f056c7',
secondary: '#c39eff',
background: '#131127',
},
buttons: {
primary: {
backgroundColor: 'primary',
borderRadius: 0,
color: 'text',
cursor: 'pointer',
minWidth: 120,
px: 3,
py: 2,
},
secondary: {
variant: 'buttons.primary',
color: 'background',
backgroundColor: 'secondary',
},
ghost: {
variant: 'buttons.primary',
color: 'muted',
backgroundColor: 'background',
},
},
}

To create alternative button variants I define them in the same buttons object and give each one a name. e.g "secondary" and "ghost".

Like in normal CSS you'll want the additional button variants to extend the default class, in this case it's called "primary" this is so you don't have to re-define or duplicate CSS properties for the padding, min-with etc and the way to do this in Theme UI is to use the variant "prop" inside the theme object.

Looking at the object for secondary you'll notice it has a variant of buttons.primary this means it'll extend all the CSS properties from the default button and will then apply / overwrite new CSS properties for it's color and backgroundColor. You can use this approach for as many button variants as you require, but do always "extend" using the variant before applying new styles.


Variants - typography

This one is a bit gnarly so strap in. The concept here is that the variant prop used on the component can be used to point to a specific set of styles in the theme, this can be any theme key, in the below example it's styles and as seen in the button example above you can also extend from a theme object using the variant key


Src 👇

<Heading as='h3' variant='styles.h3'>Heading h3</Heading>
<Heading as='h4' variant='styles.h4'>Heading h4</Heading>

Output 👇

Heading h3

Heading h4

Theme 👇

// path-to-theme/index.js
export default {
fonts: {
heading: 'Inconsolata, monospace',
},
fontSizes: [12, 16, 18],
text: {
heading: {
fontFamily: 'heading',
fontSize: 2,
},
},
styles: {
h3: {
variant: 'text.heading',
color: 'secondary',
},
h4: {
variant: 'text.heading',
color: 'text',
},
},
}

First thing to note with typography is that you'll most likely want to use the as prop to determine the HTML dom node, but... this doesn't automatically mean HTML dom nodes map to styles. As an aside it's quite possible you'll want to style h(n) tags differently on different pages and by de-coupling as from variant is quite handy albeit a bit complicated to grasp at first.

With that out of the way we can move on to variants.

You'll see above in the above <Heading as='h3' variant='styles.h3'/> I map the variant to styles.h3 and if you look at the theme object for styles.h3 it first extends the styles defined in text.heading and then applies a new CSS property for color.

These styles are from my theme: gatsby-theme-terminal and the typography treatments are quite simple but hopefully you can see from the above how to map from component usage to a theme key using the variant prop and from there you can extend from another theme key.

If you're coming to Theme UI from .scss it's a bit like @extend and if you're coming from css-modules it's a bit like composes

FYI The reason I point typography to styles is because the styles key is what Theme UI uses when styling HTML dom nodes found in markdown (.md) or MDX (.mdx)



CSS selectors

Every now and then you might run into an issue where you'll need to target a sibling or child of a Theme UI component.

I had this recently when I used the Reach UI - menu-button. For the purposes of an example i've removed a lot of the code but the TLDR is that you can style any sibling or child from the sx prop using normal CSS selectors

To target a child by id you can do this 👇


<Box
sx={{
'#menu--1': {
borderColor: 'primary',
borderStyle: 'solid',
borderWidth: '1px',
},
}}
>
<div id="menu--1">...</div>
</Box>


To target a child by class you can do this 👇


<Box
sx={{
'.menu': {
borderColor: 'primary',
borderStyle: 'solid',
borderWidth: '1px',
},
}}
>
<div className="menu">...</div>
</Box>


To target a child by data- attribute you can do this 👇


<Box
sx={{
'[data-reach-menu-list="menu"]': {
borderColor: 'primary',
borderStyle: 'solid',
borderWidth: '1px',
},
}}
>
<div data-reach-menu-list="menu">...</div>
</Box>

...

To target an adjacent sibling by class you can do this 👇


<Box
sx={{
'+ .menu': {
borderColor: 'primary',
borderStyle: 'solid',
borderWidth: '1px',
},
}}
/>
<div className="menu">...</div>


To target a general sibling by class you can do this 👇


<Box
sx={{
'~ .menu': {
borderColor: 'primary',
borderStyle: 'solid',
borderWidth: '1px',
},
}}
/>
<div className="menu">...</div>
<div className="menu menu-items">...</div>


I've written a post about how to use "style objects" for use with styled-components but the same approach works with Theme UI. You can read more about "style objects" in this post: styled-components Style Objects



Svg paths

One "hack" I used extensively in BumHub was to target the Svg's <path> tags via a className to set their fill to colors defined in the theme. This way if you change any color values for use around your site all your Svg's will update the same as any other HTML dom nodes.

the colors seen below are inherited from the theme used in this blog

import React, { FunctionComponent } from 'react'
import { Box } from 'theme-ui'
export const LogoIcon: FunctionComponent = () => {
return (
<Box
as="svg"
sx={{
'.logo-outline': {
fill: 'surface',
},
'.logo-solid': {
fill: 'primary',
},
'.logo-detail': {
fill: 'text',
},
}}
>
<g>
<path className="logo-solid" d="..." />
<g className="logo-detail">
<path d="..." />
<path d="..." />
<path d="..." />
<path d="..." />
<path d="..." />
</g>
<path className="logo-outline" d="..." />
</g>
</Box>
)
}




css keyframes

This is a lesser understood part of CSS and arguably even more so with Theme UI as it's not mentioned in the docs anywhere. However it is possible to animate using CSS keyframes using keyframes from @emotion/react

To help demonstrate how keyframes work here's a very simple loading component called <MrKeyframes /> and you can find the src here

import React from 'react'
import { Box, Grid } from 'theme-ui'
import { keyframes } from '@emotion/react'
export const MrKeyframes = () => {
const size = '8px'
const dots = new Array(10).fill(null)
return (
<Grid
sx={{
gap: 1,
p: 5,
textAlign: 'center',
justifyContent: 'center',
}}
>
Loading
<Grid
sx={{
gridAutoFlow: 'column',
gap: 2,
}}
>
{dots.map((dot, index) => (
<Box
key={index}
sx={{
animationDelay: `${index / 10}s`,
animationDuration: '1.2s',
animationTimingFunction: 'linear',
animationIterationCount: 'infinite',
animationName: keyframes({
'0%': {
opacity: 1,
},
'20%': {
opacity: 0,
},
'100%': {
opacity: 1,
},
}),
backgroundColor: 'primary',
borderRadius: `${size}`,
height: `${size}`,
width: `${size}`,
opacity: 0,
}}
/>
))}
</Grid>
</Grid>
)
}

Loading

functional values

I've talked a lot about how Theme UI maps CSS properties to specific theme objects, e.g color and background-color automatically map to colors... but if you need to access a value from your theme and map it to a different CSS property you can do so by using functional values

The idea here is you pass the theme object on via an inline function and by using template literals you can construct any CSS value you need.

Src 👇

<Box
sx={{
boxShadow: (theme) => `0 0 7px 3px ${theme.colors.secondary}`,
backgroundColor: 'surface',
color: 'secondary',
p: 3,
}}
>
I'm a Box
</Box>

Output 👇

I'm a Box

I'm sure i've implemented a number of other CSS methods using Theme UI on various projects but I can't think of any more right now. I'll endeavour to update this post as and when any new ones come to mind.

That just about wraps up this series on Theme UI, if you have any questions please feel free to find me on Twitter



If you've enjoyed this post I'd love to hear from you: @PaulieScanlon