Skip to content

Mastering Astryx / lesson 5 of 13

Theme System & Design Tokens

Understand the design token scale, semantic colors, elevation shadows, and status variants — and add a light/dark toggle to the board.

Astryx utilizes a comprehensive CSS custom properties (tokens) engine for colors, elevation, radii, and spatial gaps. In this lesson, we break down the token categories, map them onto the board styles we wrote in lesson 4, and add a light/dark toggle button to the board header.

Files this lesson: src/app/components/KanbanBoard.tsx (rewritten). page.tsx stays untouched.

Token Categories

1. Spacing Scale

Astryx provides a 4px-based spacing scale via CSS variables:

The board uses these everywhere: padding: 'var(--spacing-4)' on the column strip, gap={3} between stacked content, padding={3} on cards.

2. Shadows & Elevation

Elevation shadows communicate hierarchy and interactive states:

Card also exposes an elevation prop ('none' | 'low' | 'med' | 'high') that maps to these same tokens — the idiomatic shortcut. We use xstyle with the raw --shadow-* variables to teach the override mechanism, but whenever a built-in prop exists, prefer it.

3. Container Radii

4. Background Colors

Light & Dark: It’s All One Theme

Astryx themes carry both a light and a dark token set — color tokens are [light, dark] tuples, and the <Theme> provider (which you installed in lesson 1) resolves whichever mode is active. mode="system" follows the OS, which is exactly why the @media (prefers-color-scheme: dark) hover conditionals from lesson 4 already activate on dark-mode machines: no code required.

Forcing a mode lets you test both sets without changing your OS setting. <Theme> accepts mode: 'system' | 'light' | 'dark' and manages data-theme / color-scheme on the DOM, which prefers-color-scheme follows — so your custom StyleX conditionals respond to the toggle with zero extra code.

Adding the Toggle to the Board

usePreferences() from lesson 3 already exposes mode and setMode. Wire a toggle button into the board header that cycles system → light → dark → system:

src/app/components/KanbanBoard.tsx
'use client';
import { Section } from '@astryxdesign/core/Section';
import {
Layout,
LayoutHeader,
LayoutContent,
HStack,
VStack,
} from '@astryxdesign/core/Layout';
import { Heading, Text } from '@astryxdesign/core/Text';
import { Card } from '@astryxdesign/core/Card';
import { Button } from '@astryxdesign/core/Button';
import { useTranslator } from '@astryxdesign/core/i18n';
import { usePreferences } from '../providers';
import { styles } from '../styles';
export default function KanbanBoard() {
const t = useTranslator();
const { locale, setLocale } = usePreferences();
const { mode, setMode, locale, setLocale } = usePreferences();
return (
<Section height="100dvh">
<Layout
height="fill"
header={
<LayoutHeader hasDivider padding={4}>
<HStack hAlign="between" vAlign="center">
<Heading level={3}>{t('@app.board.title')}</Heading>
<HStack gap={2} vAlign="center">
<Text type="supporting" color="secondary" hasTabularNumbers>
4 {t('@app.tasks')}
</Text>
{/* Language toggle (lesson 3) */}
<Button
label={t('@app.switchLanguage')}
variant="secondary"
onClick={() => setLocale(locale === 'en' ? 'fr' : 'en')}
/>
{/* Theme toggle: system → light → dark → system */}
<Button
label={
mode === 'system'
? 'Light mode'
: mode === 'light'
? 'Dark mode'
: 'System mode'
}
variant="secondary"
onClick={() =>
setMode(
mode === 'system'
? 'light'
: mode === 'light'
? 'dark'
: 'system',
)
}
/>
</HStack>
</HStack>
</LayoutHeader>
}
content={
<LayoutContent padding={0}>
<HStack gap={4} xstyle={styles.boardColumns}>
{[0, 1, 2, 3].map(i => (
<Card
key={i}
variant="muted"
padding={0}
xstyle={styles.columnShell}
>
<VStack gap={3} padding={3}>
<Heading level={4}>Column {i + 1}</Heading>
<Card padding={3} xstyle={styles.card}>
<Heading level={5}>Placeholder task</Heading>
<Text type="supporting" color="secondary" maxLines={2}>
A placeholder card inside the styled column shell.
</Text>
</Card>
</VStack>
</Card>
))}
</HStack>
</LayoutContent>
}
/>
</Section>
);
}

The button label always shows the next mode: from system it offers Light, from Light it offers Dark, from Dark it returns to System. Watch the whole board — column surfaces, text, borders, and the hover ring — re-theme instantly.

Semantic Variants

Astryx components like StatusDot and Badge accept semantic variant props. These map domain states to theme colors, and they are the vocabulary our board’s column/priority model uses from lesson 6 onward:

VariantPurposeKanban Board Use Case
neutralBaseline / default stateTo-do column, Task reference ID
accentIn-progress / primary actionIn-progress column status
warningNeeds attention / reviewIn-review column status, Medium priority
successCompleted / resolved stateDone column status
errorHigh urgency / blockerHigh priority tasks
tealLow urgency / infoLow priority tasks

Variant vocabularies overlap but differ: accent is a StatusDot variant. The accent-colored badge variant is named info (it uses the same --color-accent token), so a badge equivalent of the “In progress” dot is <Badge variant="info" />.

You’ll see these in action starting in lesson 6, when the board renders real data with priority badges.

See It in Action

Run npm run dev. Click the theme toggle: Light mode forces light even if your OS is dark; Dark mode forces dark on a light OS; System mode hands control back to the OS. Flip to dark and hover a task card — the light ring + deep shadow from styles.card appears, and the language toggle still works independently. Each setting is plain React state in providers.tsx; nothing about the components changes.

What You Built

In the next lesson, we will model the board’s data and render real columns and tasks.


Share this post on:

Previous
Styling Components & StyleX Integration
Next
Data Modeling with Astryx Icons & Semantic Variants