Chapter 01 · Tokens & primitives

Foundations. The grammar.

Every component in this library is built from the tokens on this page. Learn these first — the rest is just combinations.

1.1 Colour

Eighteen named colours, four font families, one hero accent. The default surface is warm cream — never default to dark. Use semantic tokens (--fg, --bg, --accent) in component CSS; reserve the raw palette names for the token file.

Ink & paper

Ink
--ink
#191E32
Primary foreground; deep slate blue — never pure black.
Paper
--paper
#FFFFFF
Pure white. Reserve for paper-like surfaces.
Slate
--slate
#466099
Soft foreground for secondary copy.
Lavender
--lavender
#6E90CC
Muted accent; hints at sky without stealing attention.

Warm neutrals (default surface family)

Warm 1
--warm-1
#FBFAF7
Lightest — cards lifted over the page (warm-1 over warm-3).
Warm 2
--warm-2
#F6F4ED
Sidebar & chrome — subtle lift from the page. Reads as "panel."
Warm 3
--warm-3
#F4ECE4
Base canvas — where the user lives. Default page background.
Warm 5
--warm-5
#E4DCD0
Insets & wells — recessed regions, table headers, expanded code.
Warm 7
--warm-7
#C9B89F
Deep emphasis — active charts, progress fills, focused tabs.

Block quad — pink is hero

Pink 300
--pink-300
#FE84A9
Soft tint — highlights, borders-on-fill.
Pink 500
--pink-500
#FF5B8D
Secondary hue.
Pink 700
--pink-700
#FF3F7A
Hero accent — the one colour that does the heavy lifting.
Yellow 300
--yellow-300
#FFD878
Yellow 500
--yellow-500
#F9C33E
Yellow 700
--yellow-700
#F9AD03
Warning / highlight surface.
Green 300
--green-300
#7DF4D0
Green 500
--green-500
#47DDB2
Green 700
--green-700
#37BC9B
Success / confirmed.
Blue 300
--blue-300
#5BD9FC
Blue 500
--blue-500
#30C4F2
Blue 700
--blue-700
#1FAAE8
Info / system.

Semantic — resolved tokens

Accent
--accent
#FF3F7A
Resolves to --pink-700 in light mode.
Success
--success
#37BC9B
Warning
--warning
#F9AD03
Info
--info
#1FAAE8
Error
--error
#D64545
Only this red appears anywhere in the system.

Token reference

CSS custom properties

Every component CSS selector in this library reads from tokens below. Flipping [data-theme="dark"] on <body> remaps the semantic layer in place.

/* Always reference tokens, never literal hex values,
   so theme-switching flips automatically. */

color:            var(--fg);         /* body text */
background:       var(--bg);         /* page surface */
background:       var(--bg-paper);   /* cards / panels */
background:       var(--bg-sunk);    /* code blocks, chips */

border-color:     var(--hair);       /* 12% ink — default hairline */
border-color:     var(--hair-soft);  /* 6% ink — subliminal */

color:            var(--fg-soft);    /* secondary copy */
color:            var(--fg-dim);     /* 55% ink — metadata */
color:            var(--fg-faint);   /* 35% ink — whispers */

background:       var(--accent-soft); /* 12% pink — bg for pill/alert */
// Tokens flow into React via the Tailwind preset shipped with
// @magicblocksai/ui. Import the CSS once at the app root, then
// reference tokens by their Tailwind class names — never hex.
import "@magicblocksai/css";

export default function Card() {
  return (
    <div className="bg-paper text-fg border border-hair rounded-md p-s-5">
      <p className="text-fg-soft">Secondary copy</p>
      <span className="bg-accent-soft text-accent-text px-s-2 rounded-pill">
        Pink pill
      </span>
    </div>
  );
}

1.2 Spacing scale

A 4-step base multiplied up to 128px. Use tokens, not magic numbers. Most components live between --s-3 and --s-7; heroes and chapter heads reach for --s-10 upward.

The ruler

--s-1 → --s-13

Lowest values for compact inline gaps; highest values for page rhythm. Skipping a step is usually a sign you need a different token, not a fractional one.

--s-1
4px
Inline gap between icon + text; tight chips
--s-2
8px
Compact pad; icon buttons; tag gaps
--s-3
12px
Form field internal gap; inline paddings
--s-4
16px
Default gap; button padding vertical
--s-5
20px
Card internal gap; button padding horizontal
--s-6
24px
Section padding; form field margin
--s-7
32px
Card padding; page gutter
--s-8
40px
Hero padding; section head margin
--s-9
48px
Section rhythm; chapter head bottom
--s-10
64px
Hero rhythm; big section breaks
--s-11
80px
Top of chapter; generous breathing room
--s-12
96px
Chapter-level vertical rhythm
--s-13
128px
Page-level bottom; landing hero
/* Use spacing tokens everywhere — never hard-coded px. */
.card        { padding: var(--s-7); gap: var(--s-5); }
.section     { padding: var(--s-11) 0; }
.button      { padding: var(--s-3) var(--s-5); }
.stack  > * + * { margin-top: var(--s-4); }
// Tailwind preset (shipped with @magicblocksai/ui) maps every
// --s-N token to a corresponding utility — p-s-7, gap-s-5, etc.
export default function Card() {
  return (
    <article className="p-s-7 gap-s-5 flex flex-col bg-paper rounded-md">
      <header className="mb-s-4">Card title</header>
      <p className="text-fg-soft">Body copy.</p>
    </article>
  );
}

// Or read the token directly via inline style for dynamic spacing:
<div style={{ padding: "var(--s-5)", gap: "var(--s-3)", display: "flex", flexDirection: "column" }}>
  <div className="card">Top</div>
  <div className="card">Bottom</div>
</div>

1.3 Radii

Seven steps from hairline chip to full pill. Friendly but restrained — the whole system leans warm, so radii should too. Avoid mixing non-token radii on the same surface.

Radius tokens

--r-xs → --r-pill

--r-xs
4px
Hairline chips, inline badges
--r-sm
6px
Inputs; tight buttons
--r-md
10px
Default button; menu items
--r-lg
14px
Cards; demo panels; tooltips
--r-xl
20px
Feature cards; marketing hero
--r-2xl
28px
Hero shelves; large panels
--r-pill
999px
Pills, avatars, segmented toggles
.chip    { border-radius: var(--r-xs); }
.input   { border-radius: var(--r-sm); }
.button  { border-radius: var(--r-md); }
.card    { border-radius: var(--r-lg); }
.hero    { border-radius: var(--r-2xl); }
.avatar, .pill { border-radius: var(--r-pill); }
// Tailwind preset maps each radius token to a utility:
// rounded-xs · rounded-sm · rounded-md · rounded-lg · rounded-xl · rounded-2xl · rounded-pill
export default function Surfaces() {
  return (
    <>
      <span className="rounded-xs">chip</span>
      <input className="rounded-sm" />
      <button className="rounded-md">Click</button>
      <article className="rounded-lg">Card</article>
      <section className="rounded-2xl">Hero</section>
      <img className="rounded-pill" src="/avatar.png" />
    </>
  );
}

1.4 Shadow & elevation

Four elevation steps, one pink emphasis, one focus ring. Shadows bias warm — they're all based on ink at low opacity, never pure black. Don't stack shadows; pick one token.

Elevation tokens

--sh-0 → --sh-pink

--sh-0
Flush — no elevation
--sh-1
Hairline lift — buttons, toggle pills
--sh-2
Cards at rest
--sh-3
Hover / raised / menus
--sh-4
Modals, dialogs
--sh-pink
Hero CTA — use once per screen
--sh-focus
Keyboard focus ring (not elevation)
.card          { box-shadow: var(--sh-2); }
.card:hover    { box-shadow: var(--sh-3); }
.modal         { box-shadow: var(--sh-4); }
.button.cta    { box-shadow: var(--sh-pink); }
:focus-visible { outline: 0; box-shadow: var(--sh-focus); }
// Shadows are CSS variables — reach for them directly via inline style
// or the kit's component classes (`.card`, `.button.cta`) imported from
// @magicblocksai/css.
export default function Card({ children }: { children: React.ReactNode }) {
  return (
    <article
      className="rounded-lg p-s-7 transition-shadow"
      style={{ boxShadow: "var(--sh-2)" }}
      onMouseEnter={(e) => (e.currentTarget.style.boxShadow = "var(--sh-3)")}
      onMouseLeave={(e) => (e.currentTarget.style.boxShadow = "var(--sh-2)")}
    >
      {children}
    </article>
  );
}

1.5 Motion

Four durations. One ease. Motion should be confident, not chatty — it confirms what happened, not what's about to happen. Respect prefers-reduced-motion; the tokens below do.

Durations + easing

--dur-1 → --dur-4 · --ease

--ease is a spring-like out-curve (0.2, 0.8, 0.2, 1) that feels alive without being bouncy. Use it for nearly every transition.

--dur-1 · 100ms
Keyboard feedback, checkbox toggles
--dur-2 · 160ms
Hover, focus, colour shifts
--dur-3 · 240ms
Card hover, modal enter
--dur-4 · 400ms
Layout shifts, hero reveals

Hover the squares → each transitions at its own duration.

:root {
  --dur-1: 100ms;
  --dur-2: 160ms;
  --dur-3: 240ms;
  --dur-4: 400ms;
  --ease:  cubic-bezier(0.2, 0.8, 0.2, 1);
}

.button { transition: background var(--dur-2) var(--ease); }
.card   { transition: box-shadow var(--dur-3) var(--ease),
                       transform  var(--dur-3) var(--ease); }
.modal  { animation: pop var(--dur-4) var(--ease); }

@media (prefers-reduced-motion: reduce) {
  * { transition-duration: 0.01ms !important;
      animation-duration:  0.01ms !important; }
}
// Tailwind preset maps duration tokens to: duration-1/2/3/4 utilities.
// All components in @magicblocksai/ui already use these tokens internally,
// so prefers-reduced-motion is honoured automatically.
export default function InteractiveButton() {
  return (
    <button
      className="transition-colors duration-2 ease-out
                  bg-accent text-paper rounded-md px-s-5 py-s-3"
    >
      Hover me
    </button>
  );
}

1.6 Borders

Borders are almost always 1px, almost always var(--hair). A 2px border reads as a shout — reserve it for the one element that needs a shout.

Border styles

hair / hair-soft / accent

1px solid var(--hair)
Hairline — dividers, cards
1px solid var(--hair-soft)
Whisper — chips, nested surfaces
2px solid var(--fg)
Loud — sparingly, never default
1px dashed var(--hair)
Empty states, drop zones
1px solid var(--accent)
Selected state — with bg accent-soft
inset box-shadow 1px var(--hair)
Border that doesn't affect layout
.card   { border: 1px solid var(--hair); }
.nested { border: 1px solid var(--hair-soft); }
.dropzone { border: 1px dashed var(--hair); }
.selected { border: 1px solid var(--accent);
            background: var(--accent-soft); }

/* Inset border — no layout shift on hover */
.chip   { box-shadow: inset 0 0 0 1px var(--hair); }
// Tailwind preset exposes hair / hair-soft as border colours.
export default function Surfaces() {
  return (
    <>
      <article className="border border-hair rounded-lg">Card</article>
      <div className="border border-hair-soft">Nested</div>
      <div className="border border-dashed border-hair p-s-7">
        Drop files here
      </div>
      <div className="border border-accent bg-accent-soft">Selected</div>
      <span className="shadow-[inset_0_0_0_1px_var(--hair)]">Chip</span>
    </>
  );
}

1.7 Opacity & emphasis

Text hierarchy is built with named foreground tokens, not raw opacity. The only time opacity-alone is appropriate is for disabled affordances.

Emphasis levels

fg / fg-soft / fg-dim / fg-faint

Each step drops the reader's attention by a clear notch. Don't use more than three of these in the same block of text.

Aa
--fg
100% · primary
Aa
--fg-soft
slate · secondary
Aa
--fg-dim
55% · metadata
Aa
--fg-faint
35% · whispers
Aa
opacity: .4
disabled states
/* Text emphasis by token, not by raw opacity. */
.title      { color: var(--fg); }          /* primary */
.body       { color: var(--fg-soft); }     /* secondary */
.meta       { color: var(--fg-dim); }      /* metadata */
.caption    { color: var(--fg-faint); }    /* hints */

/* Disabled uses real opacity. */
.button[aria-disabled="true"] { opacity: .4; pointer-events: none; }
// Tailwind preset: text-fg · text-fg-soft · text-fg-dim · text-fg-faint.
export default function Article() {
  return (
    <article>
      <h2 className="text-fg">Primary title</h2>
      <p className="text-fg-soft">Body copy uses fg-soft.</p>
      <span className="text-fg-dim text-sm">Updated 2h ago</span>
      <span className="text-fg-faint text-xs">Optional hint</span>
      <button aria-disabled="true" className="opacity-40 pointer-events-none">
        Disabled
      </button>
    </article>
  );
}

1.8 Z-index

Five named layers, big gaps between them. Never write a naked z-index — if something needs to sit between two layers, the layer tokens are wrong, not the component.

Stacking tokens

--z-base → --z-toast

--z-base · 1
Default flow. Everything starts here.
--z-sticky · 10
Top nav, side TOC, sticky shelf.
--z-overlay · 100
Dropdowns, popovers, tooltips.
--z-modal · 200
Modal dialog + its backdrop.
--z-toast · 300
Toasts + transient system messages.
:root {
  --z-base:    1;
  --z-sticky:  10;
  --z-overlay: 100;
  --z-modal:   200;
  --z-toast:   300;
}

.topnav  { position: sticky;  z-index: var(--z-sticky); }
.tooltip { position: absolute; z-index: var(--z-overlay); }
.modal   { position: fixed;   z-index: var(--z-modal); }
.toast   { position: fixed;   z-index: var(--z-toast); }
// Reach for the z-index tokens directly via inline style. The kit's overlay,
// modal, and toast components from @magicblocksai/ui already use them.
export default function StickyTopnav() {
  return (
    <nav
      className="sticky top-0 bg-paper border-b border-hair"
      style={{ zIndex: "var(--z-sticky)" }}
    ></nav>
  );
}

1.9 How a token flows into a component

Every component in this library is assembled from the same short list of tokens. Here's the canonical example — the hero CTA button, broken out into the four aspects (colour, radius, spacing, motion) that every component in the library uses.

1
Colourbackground: var(--accent); color: var(--paper);
2
Radiusborder-radius: var(--r-md);
3
Spacingpadding: var(--s-3) var(--s-5); gap: var(--s-2);
4
Motiontransition: transform var(--dur-2) var(--ease);

1.10 Brand mark

The MagicBlocks glyph and lockup, shipped from the kit (no consumer-side /public/brand assets needed). Self-coloured — the glyph renders the canonical four-colour spark (pink · yellow · green · blue) regardless of theme; the wordmark is the real magicblocks logotype and flips for ink backgrounds via variant="dark". Available since v1.61.0; glyph + wordmark corrected to the full brand artwork in v3.2.0.

Logomark + Logo

.logomark / .logo

Glyph alone at three sizes, then the lockup on paper and ink backgrounds.

<!-- Glyph alone -->
<svg class="logomark" viewBox="61.6 495.46 184 184" width="28" height="28">
  <!-- twelve polygons: the four-quadrant spark (pink/yellow/green/blue) -->
</svg>

<!-- Lockup -->
<span class="logo logo-md">
  <svg class="logomark" .../>
  <span class="logo-wordmark"><svg viewBox="260 500 1030 180" fill="currentColor"><!-- magicblocks logotype paths --></svg></span>
</span>

<!-- Lockup on ink background -->
<span class="logo logo-md logo-dark"> ... </span>
/* Ships in _shared.css — .logo / .logo-{sm,md,lg} / .logo-dark / .logo-wordmark / .logomark.
 * The glyph fills are inline on the SVG (canonical brand palette); the
 * wordmark colour flips with .logo-dark. */
// No JS — pure markup. The glyph is self-coloured; no theme-toggle wiring needed.
import { Logomark, Logo } from "@magicblocksai/ui";

<Logomark size={28} />

<Logo size="md" />
<Logo variant="dark" size="md" />  {/* on --ink */}