What is a VSpace?
VSpace is a utility component that I learned of and used quite a bit during my time at Bulb. Essentially it provides vertical space/rhythm to its direct descendants. You can read more about VSpace on the Bulb design system website.
It’s a useful component for controlling layout without writing lots of styling and it is versatile as it allows you to render it as any element of your choosing using an as
prop and provide different spacing tokens. The one thing it relies heavily on is lobotomised owls.
Lobotomised owls?
Lobotomised owls is a slightly odd looking combination of CSS selectors. The CSS selector is * + *
which according to the author (who also briefly worked for Bulb) thinks it looks like an owl that’s had a lobotomy.
A quick break down of this selector; The *
is the universal selector which means it will match against any element, followed by the +
which is the adjacent sibling combinator and finally another universal selector. All this together means, whatever the first element is, apply some styling to whatever sibling element follows after it and any element after that and so on and so on.
Vanilla Extract?
So what’s the deal with Vanilla Extract? It describes itself as:
Use TypeScript as your preprocessor. Write type‑safe, locally scoped classes, variables and themes, then generate static CSS files at build time.
It seems like a cool library but today is my first time using it. One thing that makes creating the lobotomised owl selector difficult with Vanilla Extract is the bit about “locally scoped classes, variables and themes”.
Because the selector relies on affecting child elements, that’s considered a no, no to Vanilla Extract, although they do provide some escape hatches that allow it to be done but figuring out how was quite an interesting bit of learning.
Implementation
The VSpace React component
/**
* VSpace.tsx
*/
import { createElement } from 'react'
import * as styles from './VSpace.css'
type Props = React.PropsWithChildren<{
spacing?: keyof typeof styles.spacings
as?: keyof React.ReactHTML
}>
export const VSpace: React.FC<Props> = ({
children,
spacing = 's_1',
as = 'div',
}) => createElement(as, { className: styles.spacer[spacing] }, children)
The component takes three props, children
which are any child components. as
is used to change the element type that will be rendered. Since this is used for layout I default to div
. Lastly spacing
which represents a design token for our spacings. In this example we default to s_1
which maps to 1rem
. I’ve used createElement
as I find it makes it easier to use as
with and I’ve also explicitly not allowed additional props to be passed in.
Styles
/**
* VSpace.css.ts
*/
import {
createVar,
globalStyle,
style,
styleVariants,
} from '@vanilla-extract/css'
export const spacings = {
s_0_25: '0.25rem',
s_0_5: '0.5rem',
s_1: '1rem',
s_2: '2rem',
// …
}
/**
* CSS custom property that will be passed to the lobotomised owl as margin-top.
*/
const spacing = createVar()
/**
* Base styling to compose with the variants.
*/
const base = style({})
/**
* The spacer gets the custom property and assigns the desired spacing to it.
*/
export const spacer = styleVariants(spacings, (spacingKey) => [
base,
{
vars: {
[spacing]: spacingKey,
},
},
])
/**
* Global styles are required due to affecting children and so break the
* component isolation enforced by vanilla extract.
*/
globalStyle(`${base} > * + *`, {
margin: `${spacing} 0 0`,
})