Increasingly I’ve been including 11ty in my WordPress builds as part of the theme. Great for static assets, such as offline.html, or using inbuilt features such as addPassthroughCopy to move assets around the theme. But part of the problem I’ve had is how do I get my static files looking the same as the WordPress theme, it’s easy enough to link to the main stylesheet I am generating. But WordPress generates a bunch more stuff on the fly that we’ll need if we want full parity.
So I got to thinking, theme.json is JSON right, and 11ty 🥰 JSON.
Theme.json
In modern WordPress, ‘Global Settings and Styles‘ (a sort of jsony Design System) are set via theme.json, now being JSON, we can simply treat this as global data in Eleventy.
// Read and parse the WP theme.json file
const themeJSON = JSON.parse(await readFile(new URL('./theme.json', import.meta.url)));
// Add theme settings to Eleventy global data
eleventyConfig.addGlobalData('theme', themeJSON);
We can now get things out of my theme.json file via a nunjucks file, wp.njk, via an output tag such as {{ theme.settings.appearanceTools }}. We’ll also need to then output to a CSS or SCSS file that we can import back into our build or use directly.
---
permalink: '../src/assets/scss/wp.scss'
---
The plot thickens…
Palette
Colors! We all need some color in our life. So let’s start with a relatively straightforward one to get all of our colors.
{%- for color in theme.settings.color.palette %}
--wp--preset--color--{{ color.slug }}: {{ color.color }};
{%- endfor %}
And in our wp.scss this generates
--wp--preset--color--primary: #FA008A;
--wp--preset--color--secondary: #00E5E5;
--wp--preset--color--accent-magenta: #C3006A;
--wp--preset--color--accent-peach: #FFC3B9;
--wp--preset--color--accent-purple: #9A00FA;
--wp--preset--color--dark: #0d0f05;
--wp--preset--color--light: #ffffff;
--wp--preset--color--alt: #f1f2f0;
--wp--preset--color--focus: #FFDD00;
Woohoo. We are away.
Fluid Typography
This is a little more complex. I am increasingly setting all my typography in WordPress’ theme.json file. It means the admin and front-end is the same and we have a bunch of CSS vars we can use everywhere.
WordPress enables fluid typography out of the box via the Fluid property, for example.
{
"name": "Medium",
"size": "1.5625rem",
"fluid": {
"min": "1.275rem",
"max": "1.5625rem"
},
"slug": "md"
}
But we need to get this into our CSS and convert the min and max to clamp(), WordPress does this via it’s own methods, so we’ll need to bake our own. Using the calculations behind Utopia we can create a custom Eleventy shortcode.
eleventyConfig.addShortcode( 'calculateClamp' , (
minSize,
maxSize,
minWidth,
maxWidth,
usePx = false,
relativeTo = 'viewport'
) => {
// Helpers
const roundValue = (n) => Math.round((n + Number.EPSILON) * 10000) / 10000;
// Clamp
const isNegative = minSize > maxSize;
const min = isNegative ? maxSize : minSize;
const max = isNegative ? minSize : maxSize;
const divider = usePx ? 1 : 16;
const unit = usePx ? 'px' : 'rem';
const relativeUnits = {
viewport: 'vi',
'viewport-width': 'vw',
container: 'cqi'
};
const relativeUnit = relativeUnits[relativeTo] || relativeUnits.viewport;
const slope = ((maxSize / divider) - (minSize / divider)) / ((maxWidth / divider) - (minWidth / divider));
const intersection = (-1 * (minWidth / divider)) * slope + (minSize / divider);
return `clamp(${roundValue(min / divider)}${unit}, ${roundValue(intersection)}${unit} + ${roundValue(slope * 100)}${relativeUnit}, ${roundValue(max / divider)}${unit})`;
});
And now in our nunchucks file we can make our way through the Typography Font Sizes array. I also had issues with values like 2-xl. Even though they are defined in the theme.json as 2xl, they come out as 2-xl. So I created a little utility filter called hyphenate that transforms anything with %xs or %xl to %-xs, or %-xl.
{% set minWidth = 320 %}
{% set maxWidth = 1500 %}
{%- for size in theme.settings.typography.fontSizes %}
{%- if size.fluid %}
{%- set minSize = size.fluid.min | remToPx -%}
{%- set maxSize = size.fluid.max | remToPx -%}
--wp--preset--font-size--{{ size.slug | hyphenate }}: {% calculateClamp minSize, maxSize, minWidth, maxWidth %};
{% else %}
--wp--preset--font-size--{{ size.slug | hyphenate }}: {{ size.size }};
{% endif %}
{%- endfor -%}
And drumroll 🥁… we get
--wp--preset--font-size--body: clamp(1.0625rem, 1.0117rem + 0.2542vi, 1.25rem);
--wp--preset--font-size--md: clamp(1.275rem, 1.197rem + 0.3898vi, 1.5625rem);
--wp--preset--font-size--lg: clamp(1.53rem, 1.4153rem + 0.5737vi, 1.9531rem);
--wp--preset--font-size--xl: clamp(1.836rem, 1.6718rem + 0.8209vi, 2.4414rem);
--wp--preset--font-size--2-xl: clamp(2.2032rem, 1.9731rem + 1.1506vi, 3.0518rem);
--wp--preset--font-size--3-xl: clamp(2.6438rem, 2.3263rem + 1.5877vi, 3.8147rem);
It’s not exactly the same as the WP version (see below) as they are likely using a different method for responsive typography as can be seen below. But I’ll take the Utopia version.
--wp--preset--font-size--body: clamp(1.0625rem, 1.063rem + ((1vw - 0.2rem) * 0.234), 1.25rem);
--wp--preset--font-size--md: clamp(1.275rem, 1.275rem + ((1vw - 0.2rem) * 0.36), 1.5625rem);
--wp--preset--font-size--lg: clamp(1.53rem, 1.53rem + ((1vw - 0.2rem) * 0.529), 1.9531rem);
--wp--preset--font-size--xl: clamp(1.836rem, 1.836rem + ((1vw - 0.2rem) * 0.756), 2.4414rem);
--wp--preset--font-size--2-xl: clamp(2.2032rem, 2.203rem + ((1vw - 0.2rem) * 1.061), 3.0518rem);
--wp--preset--font-size--3-xl: clamp(2.6438rem, 2.644rem + ((1vw - 0.2rem) * 1.464), 3.8147rem);
Typography
Now for the rest of the typgraphy. We’ll need font-families, line-heights, letter-spacing. One little gotcha I got was for nested values, take our line height. We have some nested values for the heading values. So we’ll need to traverse our way over those as well, via {%- if value is mapping %} and then {% for subkey, subvalue in value %}.
"lineHeight": {
"body": "1.5",
"heading": {
"sm": 1.25,
"lg": 1.05
}
}
Here is the full typography section for font-family, font-sizes, letter-spacing and line height:
{%- if theme.settings.typography -%}
{%- for family in theme.settings.typography.fontFamilies %}
--wp--preset--font-family--{{ family.slug }}: {{ family.fontFamily | safe }};
{%- endfor -%}
{%- for size in theme.settings.typography.fontSizes %}
{%- if size.fluid %}
{%- set minSize = size.fluid.min | remToPx -%}
{%- set maxSize = size.fluid.max | remToPx -%}
--wp--preset--font-size--{{ size.slug | hyphenate }}: {% calculateClamp minSize, maxSize, minWidth, maxWidth %};
{% else %}
--wp--preset--font-size--{{ size.slug | hyphenate }}: {{ size.size }};
{% endif %}
{%- endfor -%}
{%- for key, value in theme.settings.custom.typography.letterSpacing %}
--wp--custom--typography--letter-spacing--{{ key }}: {{ value }};
{%- endfor -%}
{%- for key, value in theme.settings.custom.typography.lineHeight -%}
{%- if value is mapping %}
{% for subkey, subvalue in value %}
--wp--custom--typography--line-height--{{ key }}--{{ subkey }}: {{ subvalue }};
{%- endfor %}
{%- else %}
--wp--custom--typography--line-height--{{ key }}: {{ value }};
{%- endif %}
{%- endfor -%}
{%- endif -%}
Spacing
Default and Custom. I like to define the default WordPress spacing values so I can use them in the admin. I also map the default ones to my own custom spacing scale which are also responsive (generated via Utopia). Now this seems to change with every bloody version of WordPress but this setup in theme.json seems to allow us to override the default spacing sizes in the WordPress admin (note this is a subset for brevity)
"spacing": {
"defaultFontSizes": false,
"defaultSpacingSizes": false,
"spacingScale": { "steps": 0 },
"spacingSizes": [
{
"name": "Small",
"size": "var(--wp--custom--spacing--sm)",
"slug": "small"
},
{
"name": "Medium",
"size": "var(--wp--custom--spacing--md)",
"slug": "medium"
},
{
"name": "Large",
"size": "var(--wp--custom--spacing--lg)",
"slug": "large"
}
],
}
And then my custom values:
"custom": {
"spacing": {
"sm": "clamp(1.0625rem, 1.0117rem + 0.2542vi, 1.25rem)",
"md": "clamp(1.625rem, 1.5572rem + 0.339vi, 1.875rem)",
"lg": "clamp(2.125rem, 2.0233rem + 0.5085vi, 2.5rem)",
}
}
Oof..deep breath. Now we can put it all together (and lest we forget we need to hyphenate as well).
{%- for size in theme.settings.spacing.spacingSizes %}
--wp--preset--spacing--{{ size.slug | hyphenate }}: {{ size.size }};
{%- endfor -%}
{%- for key, value in theme.settings.custom.spacing -%}
{%- if value is mapping -%}
{%- for subkey, subvalue in value %}
--wp--custom--spacing--{{ key | hyphenate }}--{{ subkey }}: {{ subvalue }};
{%- endfor %}
{%- else %}
--wp--custom--spacing--{{ key | hyphenate }}: {{ value }};
{%- endif -%}
{%- endfor -%}
Core styles
Obvs we want to pipe through some document defaults for things like <body>, H1, H2, H3, H4, H4, H5, H6. I drew the line at custom elements, because JFC.
body {
color: {{ theme.styles.color.text }};
background-color: {{ theme.styles.color.background }};
font-family: {{ theme.styles.typography.fontFamily }};
font-size: {{ theme.styles.typography.fontSize }};
font-weight: {{ theme.styles.typography.fontWeight }};
letter-spacing: {{ theme.styles.typography.letterSpacing }};
line-height: {{ theme.styles.typography.lineHeight }};
}
{%- for key, value in theme.styles.elements %}
{{ key }} {
{%- if key in ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'] %}
font-family: {{ value.typography.fontFamily }};
font-size: {{ value.typography.fontSize }};
font-weight: {{ value.typography.fontWeight }};
line-height: {{ value.typography.lineHeight }};
letter-spacing: {{ value.typography.letterSpacing }};
{%- endif %}
}
{%- endfor %}
And finally a little wash up for some WordPress global CSS which seems to incanted from some weird place but if affects stuff like WordPress global component spacing. A lot of this can be a callback to “blockGap”: 2rem and useRootPaddingAwareAlignments. This is still somewhat verbose, and opinionated and I honestly get a little lost. But if we want parity we’ll need at least some of these:
:root {
--wp--style--block-gap: var(--buffer, var(--wp--custom--spacing--gap))
}
:root :where(.is-layout-flow) > :first-child {
margin-block-start: 0
}
:root :where(.is-layout-flow) > :last-child {
margin-block-end: 0
}
:root :where(.is-layout-flow) > * {
margin-block-start: var(--buffer, var(--wp--custom--spacing--gap));
margin-block-end: 0
}
And well that’s about it. Probably didn’t really need to do this, but it’s quite useful to understanding more about what’s going on in WordPress theme.json and its global styling system. Further we have one1 source of truth for our Design System.
- (-ish, let’s face it, there is never a 100% single source of truth) ↩︎