Our Tailwind v4 design system and how we handle brand tokens
Tailwind v4 moved configuration to CSS. Here's how we set up brand tokens, handled WCAG contrast, and solved the white-logo-on-light-background problem.

Tailwind v4 changed how configuration works. No more `tailwind.config.js`. Everything lives in CSS now, inside the `@theme` directive.
This is a good change. Your design tokens sit next to the styles that use them. One file, one source of truth.
The theme
Here is our full `globals.css` theme block:
@import "tailwindcss";
@theme {
--font-sans: 'DM Sans', sans-serif;
--font-mono: 'IBM Plex Mono', monospace;
--color-brand-blue: #3d33ff;
--color-brand-blue-light: #7b74ff;
--color-brand-yellow: #f9dc5c;
--color-brand-yellow-dark: #8a7400;
--color-brand-red: #e94f37;
--color-brand-red-dark: #c7392a;
--color-brand-slate: #393246;
--color-brand-dark: #0c0c0c;
--color-brand-mid: #1a1a1a;
--color-brand-gray: #6b7280;
--color-brand-light: #f7f6f3;
--color-brand-white: #ffffff;
--color-border-subtle: rgba(0, 0, 0, 0.08);
--color-border-dark: rgba(255, 255, 255, 0.08);
--color-surface-muted: rgba(0, 0, 0, 0.04);
--color-text-muted-light: rgba(255, 255, 255, 0.5);
--color-text-muted-dark: rgba(0, 0, 0, 0.45);
--color-status-success-bg: #f0fdf0;
--color-status-success-border: #bbf7d0;
--color-status-success-text: #166534;
--color-status-error-bg: #fff1f0;
--color-status-error-border: #fecaca;
--color-status-error-text: #991b1b;
}Every colour, font, and semantic token defined once. Used everywhere via Tailwind classes like `bg-brand-dark`, `text-brand-blue`, `border-border-subtle`.
Why we have dark variants
Our brand yellow is `#f9dc5c`. Looks great on dark backgrounds. On light backgrounds? 1.3:1 contrast ratio against white. That is not just bad, it is invisible to many users.
WCAG AA requires 4.5:1 for normal text. We needed a dark variant.
`#8a7400` hits 4.6:1 against white. Not beautiful, but legible. We use `brand-yellow` on dark sections and `brand-yellow-dark` on light ones. Same logic for red: `#e94f37` works on dark, `#c7392a` on light.
This is not optional. If your accent colours fail contrast checks, your site fails accessibility. Run every colour pair through a contrast checker before committing to a palette.
Semantic tokens
Beyond brand colours, we define semantic tokens for borders, surfaces, and text states:
--color-border-subtle: rgba(0, 0, 0, 0.08);
--color-surface-muted: rgba(0, 0, 0, 0.04);
--color-text-muted-dark: rgba(0, 0, 0, 0.45);These use rgba so they adapt to whatever background they sit on. `border-subtle` is a barely-there line on any light surface. `text-muted-dark` is subdued text that still passes contrast.
We also have status colours for the contact form: success green and error red, each with background, border, and text variants. Consistent feedback states without ad-hoc hex values scattered through components.
The white logo problem
During the migration, we scraped partner logos from the old staging site. That site had a dark background. Every logo was white on transparent.
On our light-background logo grid, they vanished.
The CSS trick `brightness-0` turns any image fully black. Works for single-colour logos. But it destroys multi-colour logos entirely. There is no pure CSS solution that works for both.
Our approach: treat it per component. The logo grid on light backgrounds applies `brightness-0` because all our partner logos are single-colour marks. Components on dark backgrounds render logos as-is. It is not clever but it works for our specific set of logos.
The real fix, which we should have done from the start, is to store two variants per logo in Sanity: one for dark backgrounds, one for light. We will likely do this eventually.
Prose styles
Blog content uses a `.prose` class rather than Tailwind's typography plugin. Custom line heights, heading sizes, blockquote styling, code blocks. All defined in `globals.css`, all using our theme variables.
.prose blockquote {
border-left: 3px solid var(--color-brand-blue);
padding-left: 1.5em;
font-style: italic;
color: var(--color-brand-gray);
}
.prose pre {
background-color: var(--color-brand-dark);
color: rgba(255, 255, 255, 0.85);
padding: 1.5em;
font-family: var(--font-mono);
}This keeps blog styling predictable and tied to the brand, regardless of what Sanity's Portable Text renderer outputs.
Tailwind v4's CSS-first approach made all of this cleaner. One file to grep when something looks wrong.
Next in the series: [From force-dynamic to on-demand ISR with Sanity webhooks](/digital-insights/from-force-dynamic-to-on-demand-isr-with-sanity-webhooks)

Chris Ryan
Managing Director
17+ years in full-stack web development, most of it leading teams agency-side across e-commerce, CMS platforms, and bespoke applications. Specialises in infrastructure, system integration, and data privacy, with hands-on experience as a Data Protection Officer. Founded Innatus Digital in 2020 to offer the kind of honest, technically-led partnership that he felt was missing from the agency world.