Skip to content

Lesson 1 — Design tokens with @theme

What are design tokens?

Design tokens are named values that represent design decisions — colours, spacing, typography, border radii. Rather than hardcoding #2563eb throughout your codebase, you name it primary and reference that name everywhere. The value lives in one place; everything else references the name.

Tailwind v4 and @theme

In Tailwind v4, design tokens are defined directly in CSS using the @theme directive. Every variable defined inside @theme automatically becomes a Tailwind utility class.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
/* app/assets/tailwind/application.css */
@import "tailwindcss";

@theme {
    /* Primary action colour */
    --color-primary:         #2563eb;
    --color-primary-hover:   #1d4ed8;
    --color-primary-ring:    #3b82f6;

    /* Secondary action colour */
    --color-secondary:       #16a34a;
    --color-secondary-hover: #15803d;
    --color-secondary-ring:  #22c55e;

    /* Danger / destructive actions */
    --color-danger:          #dc2626;
    --color-danger-hover:    #b91c1c;
    --color-danger-ring:     #f87171;

    /* Outline actions */
    --color-outline-text:    #374151;
    --color-outline-border:  #d1d5db;
    --color-outline-hover:   #f9fafb;


  /* Surfaces */
  --color-surface:         #ffffff;
  --color-surface-alt:     #f9fafb;
  --color-surface-raised:  #ffffff;

  /* Borders */
  --color-border:          #e5e7eb;
  --color-border-strong:   #d1d5db;

  /* Text */
  --color-text:            #111827;
  --color-text-muted:      #6b7280;
  --color-text-subtle:     #9ca3af;

  /* Status colours */
  --color-success:         #16a34a;
  --color-success-bg:      #f0fdf4;
  --color-warning:         #d97706;
  --color-warning-bg:      #fffbeb;
  --color-info:            #2563eb;
  --color-info-bg:         #eff6ff;
}

Once defined, Tailwind generates utility classes for every token:

--color-primary       → bg-primary, text-primary, border-primary
--color-primary-hover → bg-primary-hover, text-primary-hover
--color-surface       → bg-surface, text-surface

You use these exactly like standard Tailwind classes:

1
2
div(class: "bg-surface border border-border rounded-lg")
a(class: "text-primary hover:text-primary-hover")

Why semantic names matter

bg-blue-600 is a palette value — it describes what the colour looks like. bg-primary is a semantic value — it describes what the colour means. This distinction becomes critical when:

  • You want dark mode: bg-blue-600 in dark mode looks wrong; bg-primary can be redefined to something appropriate for dark backgrounds
  • You want multiple themes: swap the token values and everything repaints
  • You want to rebrand: change --color-primary once and every button, link, and badge updates

Typography tokens

Add font and type scale tokens alongside the colour tokens:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
@theme {
  /* ... colour tokens above ... */

  /* Typography */
  --font-sans:  "Inter", ui-sans-serif, system-ui, sans-serif;
  --font-mono:  "JetBrains Mono", ui-monospace, monospace;

  /* Border radius */
  --radius-sm:  0.25rem;
  --radius-md:  0.375rem;
  --radius-lg:  0.5rem;
  --radius-xl:  0.75rem;
}

Loading Inter

Add the Inter font via a Google Fonts import at the top of application.css, before the @import "tailwindcss" line:

1
2
3
4
5
6
7
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap");
@import "tailwindcss";

@theme {
  --font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
  /* ... */
}

Exercise

Add the full token set above to app/assets/tailwind/application.css and run bin/dev.

Note on how Tailwind v4 generates utilities: Unlike Tailwind v3 which generated all possible utility classes upfront, Tailwind v4 scans your source files and only generates classes that are actually used. This means bg-primary and text-text-muted won’t appear in the compiled CSS until a view or component references them.

To verify your tokens are working, add a temporary test element to any view:

1
2
div(class: "bg-primary text-white p-4") { "Token test — primary background" }
div(class: "text-text-muted text-sm mt-2") { "Token test — muted text" }

Run bin/dev, visit the page, and confirm the elements render with the correct colours. Then check app/assets/builds/tailwind.css and search for .bg-primary — it should appear with background-color: #2563eb.

Once verified, remove the test elements. The tokens are working correctly even if they don’t appear in the compiled CSS without a corresponding class reference.