Typographic systems with Theme UI

Create scalable type systems using Theme UI and Gatsby
Typography combines several factors: typeface, size, line height, line length, style, and weight. It is a multidimensional design component, whose readability relies on the correct combination of all of these parameters. With Theme UI, we can couple these typographic relationships into jsx components and use variations of them across our site. Let’s dig in.

Throughout my developer journey, I’ve longed for an easy way to create and use styles that were available outside of specific css selectors. I wanted styles to be:

  • semantically meaningful
  • decoupled from particular classes, attributes, or components
  • scoped, but accessible from any component

Over time, I’ve used a range of solutions such as SASS variables, CSS variables, and utility classes to accomplish this general purpose, but many lacked greater composition capabilities, particularly for typography.

Vignelli NYC Subway Guidelines

Page from NYCTA Graphics Standards Manual showcasing Massimo Vignelli’s fine attention to typographic detail.

Most recently,though, I’ve started to pair Theme UI with Gatsby. Theme UI is a library that allows developers to build scalable and maintainable design systems through the use of design tokens. Design tokens are javascript objects that house shareable styles for an entire site. They look something like this:

// design token pseudo-code
var styles = {
colors: {
primary: "#9114CC",
secondary: "#00994D",
muted: "#FBFAFF",
},
};

They can then be used in components like this:

// design token pseudo-code
<div style={{ backgroundColor: styles.colors.primary }}>Hello World</div>

The idea is that styles are associated with semantic naming conventions, like primary, and can be accessed by any component.

As I’ve continued to use Theme UI and dig into their documentation, I’ve found several helpful paradigms for code reusability. One of my favorites is text variants, and today we’ll be walking through how to leverage them to create typographic systems within a Gatsby site.

Here’s a quick glimpse of what we will building today:

Loading Video ...
Helpful Prerequisites:
  • A working knowledge of React
  • Familiarity with Gatsby is beneficial, though not essential
  • You can find the code for this tutorial here on Github

Kicking off a Gatsby Site

Gatsby is a framework built around React that enables developers to quickly deploy highly performant websites. If you haven’t used Gatsby before, and are interested in learning more, I highly recommend working through their starter tutorial. Alright, let’s get started!

First, create a new folder called typographic-systems-themeUI on your desktop. Then, in your terminal, change into this new directory:

cd ~/desktop/typographic-systems-themeUI

Next, let’s clone Gatsby’s hello world starter. In your terminal, run:

# note the "." at the end
git clone git@github.com:gatsbyjs/gatsby-starter-hello-world.git .

For our last step, let’s install our dependencies:

yarn install

Let’s see what we have and run Gatsby’s localhost dev environment:

yarn develop -o

Once we run this command, Gatsby will provide a URL, usually locahost:8000 for us to view our site. If we go to this URL, we should see a tiny “Hello world!”

Setting up Theme UI

Now, let’s wire up Theme UI with our Gatsby site. To do this, let's install the following dependencies:

yarn add theme-ui gatsby-plugin-theme-ui
As an aside, Gatsby has incredible documentation, and you can discover more about gatsby plugin theme ui here. Furthermore, Gatsby released an awesome feature called Recipes, that automagically takes care of a signficant portion of the plugin configuration for you.

Once we we install this plugin, we need to configure it in our gatsby-config.js file:

// src/gatsby-config.js
module.exports = {
/* Your site config here */
plugins: [`gatsby-plugin-theme-ui`],
};

When Gatsby uses Theme UI, it will look for the file path src/gatsby-plugin-theme-ui/index.js and expect a default export of an object. This object should contain a range of key, value pairs that includes the styles we want to use across our site.

So, in our src directory, let’s create a folder called gatsby-plugin-theme-ui. In this new folder, create a file called index.js.

Our first typographic values

The stage has been set. Now we can start to fiddle with our first version of a type ramp. We will start by creating our theme object in gatsby-plugin-theme-ui/index.js, and populating it with a few font sizes:

// src/gatsby-plugin-theme-ui/index.js
const theme = {
fontSizes: [10, 13, 18, 21, 26, 34, 48],
};
export default theme;

fontSizes is a predefined Theme Key that maps to the CSS property, font-size. If you would like to learn more, Theme-UI has a whole specification for Theme Keys that map to CSS values. The array of numbers represents different pixel font sizes.

We will go ahead and specify our font families as well:

// src/gatsby-plugin-theme-ui/index.js
const theme = {
fonts: {
body:
'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", sans-serif',
},
fontSizes: [10, 13, 18, 21, 26, 34, 48],
};
export default theme;

The fonts keyword maps to the font-family property within CSS, and body could be any name, but we have chosen body because it makes the most semantic sense.

Finally, let’s apply some basic styling to our site. To add base, top-level styles to the <body> element, we can use theme.styles.root:

// src/gatsby-plugin-theme-ui/index.js
var theme = {
fonts: {
body:
'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", sans-serif',
},
fontSizes: [10, 13, 18, 21, 26, 34, 48],
styles: {
root: {
backgroundColor: "white",
margin: "auto",
maxWidth: "1080px",
padding: "40px",
},
},
};
export default theme;

Applying styles with the sx prop

Now that we have our basic type ramp foundation, we can begin using this theme throughout our site. To do this we will need to use Theme UI’s sx prop, so we can style items inline, and a custom jsx pragma, so our inline styles get compiled correctly. This may sound a bit convoluted, but I promise the implementation is quite seamless.

To get started, switch over to your pages/index.js file, and at the top, add the custom /** @jsx jsx */ pragma comment to the top of your module and import the jsx function from theme-ui:

// pages/index.js
/** @jsx jsx */
import { jsx } from "theme-ui";
import React from "react";
export default () => <div>Hello world!</div>;

Now, let’s add some base typographic styling. Since sx is a prop that styles elements inline, we will add it directly to our div element, and pass it a javascript object of css styles:

/** @jsx jsx */
import { jsx } from "theme-ui";
import React from "react";
export default () => (
<div sx={{ fontFamily: "body", fontSize: "6" }}>Hello world!</div>
);

If we stop and restart our server, we should see the following at our localhost:

hello world image

Our sx styles are now styling our div. If you recall in our theme, body is the name of one of our fonts. Likewise, we defined the following fontSizes in our theme object:

// gatsby-plugin-theme-ui/index.js
const theme = {
// ... other theme styles
fontSizes: [10, 13, 18, 21, 26, 34, 48],
};

Using javascript’s way of counting items in an array, fontSize: "6" correlates with 48. So our div font size is 48px.

Now that we have a basic understanding of Theme UI, let’s accelerate our pace and start building our type ramp out.

Advancing our type styles

Currently, our type styles are quite flat: a font family and a size. As we discussed though, type is a multidimensional design component. Readability and visual impact are effected by various parameters. So, let’s add some of these in our theme.

Let’s kick it off with font weights, letter spacing, and line heights:

// src/gatsby-plugin-theme-ui/index.js
const theme = {
fonts: {
body:
'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", sans-serif',
},
fontSizes: [10, 13, 18, 21, 26, 34, 48],
fontWeights: {
light: 200,
regular: 400,
medium: 500,
bold: 600,
heavy: 700,
},
lineHeights: ["1em", "1.25em", "1.6em"],
letterSpacing: ["-0.025em", "0.025em", "0.125em"],
// ... more site styles
};
export default theme;

You may be wondering, why do we need various line heights and letter spacing styles? I don’t want to derail the article to much, but here are a few key typographic insights:

  • The larger and bolder your type is, particularly display sizes above 34px, the less letter spacing it needs.
  • The smaller your type is, particularly body sizes below 13px, the more letter spacing it needs.
  • The shorter the line length the less line height the type needs.
  • The longer the line length the more line height the type needs.

Typography is an intricate science with hundreds of years of research and adaptation behind it. If you’re wanting to learn more, I highly recommend Robert Bringhurst’s, “Elements of Typographic Style.”

Updating our page layout

Now that we have more styles in place, let’s update our home page and add some more layers and hierarchy of information. Go ahead and update the pages/index component to the following:

// src/pages/index.js
/** @jsx jsx */
import { jsx } from "theme-ui";
import React from "react";
export default () => (
<div>
<h1
sx={{
fontFamily: "body",
fontSize: "6",
fontWeight: "heavy",
letterSpacing: "0",
}}
>
Hello world!
</h1>
<h4
sx={{
fontFamily: "body",
fontSize: "1",
fontWeight: "bold",
letterSpacing: "2",
textTransform: "uppercase",
}}
>
Authored by John Smith
</h4>
<p
sx={{
fontFamily: "body",
fontSize: "2",
fontWeight: "regular",
lineHeight: "2",
letterSpacing: "1",
}}
>
I'm baby craft beer schlitz seitan put a bird on it yr aesthetic.
Hexagon street art truffaut chicharrones. Ugh offal cray typewriter
lyft paleo. Shoreditch viral tousled ugh shaman. Vinyl pinterest
shaman butcher marfa coloring book scenester humblebrag truffaut +1
unicorn kitsch. Paleo meggings yr, portland keytar four loko viral
post-ironic literally biodiesel la croix waistcoat. Ramps portland
neutra synth
</p>
</div>
);

A few things to note:

  • The sx object has been updated to include more typographic styles.
  • The value of these styles are derived from our theme object.
  • The wonderful copy writing is from hipster ipsum.

If we stop and restart our server, we should see the following:

home page screenshot

Alright, we have some good initial styling set up. But there’s a problem: none of these sets of typographic styles are reusable. If we want to standardize type styles, we’ll need to either create classes or resort to copy and paste every time we want to apply a paragraph or header style. Thankfully, Theme-UI has provided a more scalable way to build type styles through the use of Text Variants.

Text variants and type ramps

To get started with text variants, we will leverage the type styles we created in our home page. Let’s first define our body, or paragraph, styles. Switch over to the theme object, and add the following text object to theme:

// src/gatsby-plugin-theme-ui/index.js
const theme = {
// ... theme type styles
text: {
body: {
fontFamily: "body",
fontSize: "2",
fontWeight: "regular",
lineHeight: "2",
letterSpacing: "1",
},
},
// ... theme body styles
};

We now have our first text variant! And, we referenced the type styles we already defined in the theme object to build this out. Let’s add some more variants:

// src/gatsby-plugin-theme-ui/index.js
const theme = {
// ... theme type styles
text: {
body: {
fontFamily: "body",
fontSize: "2",
fontWeight: "regular",
lineHeight: "2",
letterSpacing: "1",
},
headline: {
fontFamily: "body",
fontSize: "6",
fontWeight: "heavy",
lineHeight: "0",
letterSpacing: "0",
},
caps: {
fontFamily: "body",
fontSize: "1",
fontWeight: "bold",
lineHeight: "1",
letterSpacing: "2",
textTransform: "uppercase",
},
caption: {
fontFamily: "body",
fontSize: "1",
fontWeight: "regular",
fontStyle: "italic",
lineHeight: "1",
letterSpacing: "1",
},
sectionHeader: {
fontFamily: "body",
fontSize: "4",
fontWeight: "bold",
lineHeight: "0",
letterSpacing: "0",
},
},
// ... theme body styles
};

We now have a range of type styles for different pieces of information. Let’s use this in our home page.

Switch over to src/pages/index.js and at the top, import the Text component from theme-ui:

/** @jsx jsx */
import { jsx, Text } from "theme-ui";
import React from "react";
// ... more jsx

The Text component contains access to all of the text variants we defined in our theme object. To test this, let’s replace our h1 tag with our Text component.

/** @jsx jsx */
import { jsx, Text } from "theme-ui";
import React from "react";
export default () => (
<div>
<Text
sx={{
fontFamily: "body",
fontSize: "6",
fontWeight: "heavy",
letterSpacing: "0",
}}
>
Hello world!
</Text>
// ... more jsx
</div>
);

Then, remove the sx prop and object, and add the following:

/** @jsx jsx */
import { jsx, Text } from "theme-ui";
import React from "react";
export default () => (
<div>
<Text variant="headline">Hello world!</Text>
// ...more jsx
</div>
);

We are passing the Text component a prop called variant with a value of headline which correlates to the headline text variant we defined in our theme object.

If we stop and restart our server, we should see the following in our localhost:

homescreen with text variants

The lack of visual change indicates that our headline is working correctly. If we inspect this element in our web dev tools though, we will discover that this is no longer an h1 tag, but a div. By default, Theme UI uses divs for our text variants. If we want more semantic html, we can specify which html tag a text variant should use by passing Text the as property:

/** @jsx jsx */
import { jsx, Text } from "theme-ui";
import React from "react";
export default () => (
<div>
<Text variant="headline" as="h1">
Hello world!
</Text>
// ... more jsx
</div>
);

With our new found knowledge, let’s replace all of our html elements and sx props with their equivalent Text variants:

/** @jsx jsx */
import { jsx, Text } from "theme-ui";
import React from "react";
export default () => (
<div>
<Text variant="headline" as="h1">
Hello world!
</Text>
<Text variant="caps" as="h4">
Authored by John Smith
</Text>
<Text variant="body" as="p">
I'm baby craft beer schlitz seitan put a bird on it yr aesthetic.
Hexagon street art truffaut chicharrones. Ugh offal cray typewriter
lyft paleo. Shoreditch viral tousled ugh shaman. Vinyl pinterest
shaman butcher marfa coloring book scenester humblebrag truffaut +1
unicorn kitsch. Paleo meggings yr, portland keytar four loko viral
post-ironic literally biodiesel la croix waistcoat. Ramps portland
neutra synth
</Text>
</div>
);

In our localhost, we should see the following:

homepage with text variants

It appears, all of our padding and margin has disappeared. We can easily fix this by adding a space object to our theme object and then leveraging that spacing in our Text variants.

Spacing in Theme UI

Let’s switch back over to our theme object and add the space object to it. This object will hold different spacing increments that can be used across our site:

// src/gatsby-plugin-theme-ui/index.js
const theme = {
// ... theme and root styles
space: {
xxs: 5,
xs: 10,
s: 15,
m: 20,
l: 30,
xl: 45,
xxl: 60,
xxxl: 90,
},
};

Now, let’s update the spacing on our home page. To do this, we will add the sx prop to our Text variants, and leverage our space increments for styling the padding and margin.

Let’s start with our headline:

// src/pages/index.js
/** @jsx jsx */
import { jsx, Text } from "theme-ui";
import React from "react";
export default () => (
<div>
<Text variant="headline" as="h1" sx={{ marginBottom: "m" }}>
Hello world!
</Text>
// ... more jsx
</div>
);

Now this Text variant will use 20px for its bottom margin. Let’s continue to update our other Text variants with some spacing:

// src/pages/index.js
/** @jsx jsx */
import { jsx, Text } from "theme-ui";
import React from "react";
export default () => (
<div>
<Text variant="headline" as="h1" sx={{ marginBottom: "m" }}>
Hello world!
</Text>
<Text variant="caps" as="h4" sx={{ marginBottom: "xl" }}>
Authored by John Smith
</Text>
<Text variant="body" as="p" sx={{ marginBottom: "xs" }}>
I'm baby craft beer schlitz seitan put a bird on it yr aesthetic.
Hexagon street art truffaut chicharrones. Ugh offal cray typewriter
lyft paleo. Shoreditch viral tousled ugh shaman. Vinyl pinterest
shaman butcher marfa coloring book scenester humblebrag truffaut +1
unicorn kitsch. Paleo meggings yr, portland keytar four loko viral
post-ironic literally biodiesel la croix waistcoat. Ramps portland
neutra synth
</Text>
</div>
);

We should see the following in our localhost now:

updated home page

You may be wondering, why didn’t we just define the margin and padding on our text variants in our theme object? The reason we define these in the sx prop instead is so our text variants work in as many situations as possible. In other words, the spacing around our text variants will vary according to their context. This approach incorporates the best practices of reusable code. We purposefully leave margin and padding out of the definition of our text variants, so we can leverage them for a variety of different situations.

Injecting some life into our site

We have now learned how to create Text variants, add css styles through the sx prop, and specialize our theme object. Equipped with this knowledge, we can create lively blog pages with simplicity, composing our Text variants in a range of ways.

Here’s one of the variations I built with my Text variants and images from Pexels:

updated home page

And, here’s the code I updated in my src/pages/index.js file:

// src/pages/index.js
/** @jsx jsx */
import { jsx, Text } from "theme-ui";
import React from "react";
export default () => (
<div>
<Text
variant="caps"
as="ul"
sx={{
display: "flex",
flexFlow: "row wrap",
listStyleType: "none",
marginBottom: "xxl",
padding: 0,
li: {
marginRight: "m",
},
}}
>
<li>home</li>
<li>blog</li>
<li>shop</li>
<li>about</li>
<li>contact</li>
</Text>
<Text variant="headline" as="h1" sx={{ marginBottom: "m" }}>
Rediscovering music
</Text>
<Text variant="caps" as="h4" sx={{ marginBottom: "xl" }}>
Authored by John Smith
</Text>
<Text variant="body" as="p" sx={{ marginBottom: "xs" }}>
I'm baby craft beer schlitz seitan put a bird on it yr aesthetic.
Hexagon street art truffaut chicharrones. Ugh offal cray typewriter
lyft paleo. Shoreditch viral tousled ugh shaman. Vinyl pinterest
shaman butcher marfa coloring book scenester humblebrag truffaut +1
unicorn kitsch. Paleo meggings yr, portland keytar four loko viral
post-ironic literally biodiesel la croix waistcoat. Ramps portland
neutra synth
</Text>
<Text
variant="sectionHeader"
sx={{ marginTop: "l", marginBottom: "s" }}
>
Authentic Sounds and Vinyls
</Text>
<Text variant="body" as="p" sx={{ marginBottom: "xs" }}>
I'm baby craft beer schlitz seitan put a bird on it yr aesthetic.
Hexagon street art truffaut chicharrones.Vinyl pinterest shaman
butcher marfa coloring book scenester humblebrag truffaut +1 unicorn
kitsch.
</Text>
<img
src="https://images.pexels.com/photos/1173651/pexels-photo-1173651.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940"
sx={{ marginTop: "m", marginBottom: "xs", maxWidth: "100%" }}
alt="photo by Alina Vilchenko"
/>
<Text variant="caption" as="h6" sx={{ marginBottom: "m" }}>
Photo by Alina Vilchenko on Pexels. Vinyl pinterest shaman butcher
marfa coloring book truffaut +1 unicorn.
</Text>
<Text variant="body" as="p" sx={{ marginBottom: "xs" }}>
I'm baby craft beer schlitz seitan put a bird on it yr aesthetic.
Hexagon street art truffaut chicharrones.Vinyl pinterest shaman
butcher marfa coloring book scenester humblebrag truffaut +1 unicorn
kitsch.
</Text>
</div>
);

And there we have it! We can easily use these text variants across our entire site, allowing us to maintain a consistent type system. I encourage you to introduce some new variants, like quote, and maybe even a second typeface for headlines or for the body.

Before we go, I’d like to recap what we’ve learned:

  • Theme UI allows us to build maintainable, consistent design systems for our site.
  • Text Variants are powerful type components that allow us to create a strong typographic hierarchy.
  • It is best to only include type related parameters, e.g. font size or line height, in text variants.
  • We can extend, or specialize, text variants with the sx prop and add further styling like padding, margin, or color.

Thanks for watching, I hope you have enjoyed this article. If you come up with any layouts you would like to share, please get at me on Twitter.