Understanding Theme UI: 6 - The Hacks
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
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>
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>
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](/posts/2020/08/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 from 'react';
import { Box } from 'theme-ui';
export const LogoIcon = () => {
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);
const animation = keyframes({
'0%': {
opacity: 1,
},
'20%': {
opacity: 0,
},
'100%': {
opacity: 1,
},
});
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: animation.toString(),
backgroundColor: 'primary',
borderRadius: `${size}`,
height: `${size}`,
width: `${size}`,
opacity: 0,
}}
/>
))}
</Grid>
</Grid>
);
};
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>
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