Skip to content

Design tokens

Abdulkader Safi edited this page Aug 18, 2026 · 1 revision

Design tokens

Colour, type, spacing and layout widths, defined once and emitted as CSS custom properties into the head of both the public page and the editor preview, from the same layout.

That last part is the point. If the editor and the front end read different sources for colour and spacing, the preview lies, and a preview that lies is worth less than no preview at all.

The shipped set

Defaults live in Safi\Atelier\Tokens:

Group Keys Default
color primary #171717
on-primary #ffffff
text #171717
muted #737373
surface #ffffff
border #e5e5e5
font sans ui-sans-serif, system-ui, sans-serif
arabic "Noto Sans Arabic", "Segoe UI", Tahoma, sans-serif
space section 6rem
gutter 1.5rem
width container 80rem
prose 48rem

Deliberately small. A token nothing renders is a token nobody maintains, so the set grows when a block needs one rather than in anticipation.

Overriding them

config/atelier.php:

'tokens' => [
    'color' => [
        'primary' => '#0f766e',
        'on-primary' => '#ffffff',
    ],
    'font' => [
        'arabic' => '"IBM Plex Sans Arabic", sans-serif',
    ],
],

Overrides merge group key by group key, so changing one colour does not mean restating the other five. Anything you leave out keeps its shipped value, which also means an install that predates this config key gets the full palette without republishing anything.

What lands in the page

<style>
  :root{--atelier-color-primary:#0f766e;--atelier-color-text:#171717; ...}
  [dir="rtl"]{--atelier-font-sans:var(--atelier-font-arabic)}
  body{font-family:var(--atelier-font-sans)}
</style>

Inline rather than a stylesheet, because the whole block is under a kilobyte and a separate request would cost more than it saves. It also cannot go stale, which a published file can.

The Arabic swap rides on [dir="rtl"] rather than a locale code, so adding a third RTL language gets it for free.

Using them in a block

Two ways, and the second is the one that matters.

Directly in CSS, when the block always uses that value:

<section {{ $shared->class(['px-6']) }} style="padding-block: var(--atelier-space-section)">

As a stored reference, when the client chooses:

Select::make('background')
    ->options(Tokens::options('color'))
    ->formatStateUsing(fn (mixed $state) => is_array($state) ? ($state['token'] ?? null) : $state)
    ->dehydrateStateUsing(fn (?string $state) => $state ? ['token' => $state] : null)

The tree then holds a reference, not a literal:

"background": { "token": "color.primary" }

and the renderer resolves it to var(--atelier-color-primary) before your view runs. Your view just echoes $attributes['background'] into a style attribute and never learns that tokens exist.

This is why the shared background control stores a reference: change color.primary in config and every page that picked it restyles, with no data migration.

The API

use Safi\Atelier\Tokens;

Tokens::all();                       // the merged set, defaults plus your overrides
Tokens::css();                       // the :root block, as a string
Tokens::value('color.primary');      // 'var(--atelier-color-primary)', or null if unknown
Tokens::options('color');            // ['color.primary' => 'Primary', ...] for a Select
Tokens::resolve(['token' => '...']); // reference to CSS value, anything else passes through

Tokens::value() returns null for a key nobody defined, rather than emitting a variable that resolves to nothing.

If you use your own layout

Tokens ship in atelier::partials.tokens, and it has to come after your stylesheet so the custom properties win:

@vite(['resources/css/app.css'])

@include('atelier::partials.tokens')

Leave it out and nothing errors. Every var(--atelier-*) resolves to nothing, so the background and spacing controls silently stop doing anything and Arabic loses its font stack. See Layouts.

What tokens are not

They are not a theme system, and they are not exposed to the client. Nobody picks a font size in the panel. The developer sets the palette, the client picks from it where a block offers a choice, and the site stays coherent because there was never a control that could break it.

Clone this wiki locally