Storage
80% usedYou have used 8 of 10 GB. Old exports are deleted after 30 days.
Ordered by layer hierarchy: utilities → layout → atoms → molecules → organisms.
Practical decision rules for writing HTML and SCSS with SYX. Read this before creating any component.
.syx-* → Util
utilities/
atom-* → Atom
atoms/
mol-* → Molecule
molecules/
org-* → Organism
organisms/
pages/ or
bundle-*.scss
Quick reference
| Layer | Prefix | When to use | Anti-pattern |
|---|---|---|---|
| Util | .syx- |
Single CSS property: display, spacing, color, visibility | Never add visual design to a utility class |
| Atom | atom- |
Smallest reusable building block: btn, icon, label, pill, input | Don't compose atoms inside atoms |
| Mol | mol- |
Composition of 2+ atoms that always travel together | If sub-parts make sense independently → separate atoms |
| Org | org- |
Complete UI section; appears across multiple pages | Don't put page-unique content inside an organism |
| Page | — | Layout tweaks exclusive to one specific page | Don't duplicate component logic in page-level styles |
Rule of thumb: Utilities are adjectives
(.syx-d-flex → the display is flex). Components are nouns
(.mol-card → a card). If you're unsure between atom and molecule: would the
sub-parts
ever make sense independently? Yes → separate atoms. No → molecule.
Worked examples
atom-btn
atom-btn + atom-icon composed in HTML,
not a new mol-
mol-search (input + icon + btn always
together)
org-site-header
.syx-mx-auto or
.syx-text-center
Always start with the semantically correct element, then apply SYX classes. Never choose an element because of its default browser style.
✅ Correct
<nav aria-label="Main navigation" class="org-navbar">
<ul class="org-navbar__list" role="list">
<li><a href="/" class="atom-link">Home</a></li>
</ul>
</nav>
❌ Wrong — div soup
<div class="org-navbar">
<div class="org-navbar__list">
<div><span class="atom-link">Home</span></div>
</div>
</div>
atom-btn atom-btn--primary ✅ — atom-btn--primary alone ❌
__element__sub, those are two
independent elements.is-, not BEM modifiers. BEM modifiers are
permanent
variants; is-open, is-loading are JS-driven state.<!-- ✅ Correct -->
<button class="atom-btn atom-btn--primary"> <!-- block + modifier -->
<span class="atom-btn__icon" aria-hidden="true"> <!-- element -->
</button>
<button class="atom-btn is-loading"> <!-- dynamic state -->
Organisms and molecules are composed in HTML. Keep SCSS partials free of cross-layer selector references.
<!-- ✅ Composed in HTML — molecules inside organisms -->
<header class="org-header">
<div class="org-header__inner layout-grid">
<a href="/" class="atom-link org-header__logo">…</a>
<nav class="org-navbar">…</nav>
<div class="mol-btn-group">
<button class="atom-btn atom-btn--ghost">Log in</button>
<button class="atom-btn atom-btn--primary atom-btn--filled">Sign up</button>
</div>
</div>
</header>
<!-- ❌ Wrong — organism SCSS references a molecule selector -->
/* organisms/_header.scss */
.org-header .mol-btn-group { margin-left: auto; } ← coupling between layers
Valid exception: an organism CAN define layout-level
gap or grid-template-areas for its direct children. What it cannot do is
redefine the internal styles of a molecule.
| Element | Required attributes |
|---|---|
<img> |
alt="" (decorative) or alt="description" (informative) |
<button> icon-only |
aria-label="…" always |
<a> icon-only |
aria-label="…" always |
<nav> |
aria-label="…" to distinguish multiple navs |
<section> |
aria-labelledby="id-heading" pointing to the section's <h*> |
| Interactive state | aria-expanded, aria-current, aria-selected as appropriate
|
| Decorative icons | aria-hidden="true" on all decorative <span class="atom-icon">
|
<!-- ✅ Icon-only button done right -->
<button class="atom-btn atom-btn--ghost atom-btn--circle"
aria-label="Close menu">
<span class="atom-icon atom-icon--lc-x" aria-hidden="true"></span>
</button>
Use .layout-grid for all main content areas. Never
invent ad-hoc flex containers for page structure.
<!-- ✅ Standard responsive layout -->
<main class="layout-grid">
<div class="layout-grid__col-xs-12 layout-grid__col-md-8"><!-- main --></div>
<aside class="layout-grid__col-xs-12 layout-grid__col-md-4"><!-- sidebar --></aside>
</main>
<!-- ✅ Full-bleed section with constrained inner content -->
<section class="org-hero">
<div class="layout-grid">
<div class="layout-grid__col-xs-12 layout-grid__col-md-8 layout-grid__col-lg-6">
<h1 class="atom-title atom-title--h1">…</h1>
</div>
</div>
</section>
Every SCSS decision carries a cost in the compiled CSS. These rules keep the output lean.
✅ Efficient — max 3 levels
.org-header {
&__nav { // level 2
&--open { … } // level 3
}
}
// Output: .org-header__nav--open {}
❌ Expensive — 5 levels
.org-header {
&__nav {
&__list {
&__item {
&--active { … }
// Output: .org-header__nav__list__item--active
Every component MUST be wrapped in a @mixin with a
$theme parameter. This enables selective inclusion and multi-theme support.
// ✅ Correct
@mixin mol-card($theme: null) {
@layer syx.molecules {
.mol-card { /* base */ }
.mol-card--featured { /* modifier */ }
}
}
// Called from themes/example-01/_setup.scss:
@include mol-card("example-01");
// ❌ Wrong — top-level rules cannot be excluded from any bundle
.mol-card { … }
SYX mixins skip null values. Use this to avoid
emitting unnecessary properties.
// ✅ Only emits padding-top and padding-bottom — no left/right
@include padding(var(--semantic-space-inset-md) null);
// ❌ Raw CSS always emits all 4 properties (sets left/right to 0)
padding: var(--semantic-space-inset-md) 0;
// ✅ Horizontal centering without touching top/bottom
@include margin(null auto);
// → margin-right: auto; margin-left: auto;
| SCSS | CSS output | Note |
|---|---|---|
var(--component-btn-primary-bg) |
var(--component-btn-primary-bg) |
1 property, runtime theming ✅ |
var(--primitive-color-blue-500) |
var(--primitive-color-blue-500) |
❌ Skips semantic layer |
#3b82f6 |
#3b82f6 |
❌ Breaks theming |
@include size(100%, 48px) |
width:100%;height:48px |
null-safe ✅ |
@include flex-center() |
display:flex;align-items:center;justify-content:center |
3 props in 1 include ✅ |
@include transition(color 0.2s ease) |
transition:…; @media(prefers-reduced-motion){transition:none} |
Auto reduced-motion guard ✅ |
.syx-component {
// 1. Positioning
@include absolute($top: 0, $left: 0);
// 2. Display / Box model
@include flex-center();
// 3. Dimensions
@include size(100%, 48px);
// 4. Spacing
@include margin(null auto);
@include padding(var(--component-x-inset-y) var(--component-x-inset-x));
// 5. Typography
font-size: var(--component-x-font-size);
color: var(--component-x-color);
// 6. Visual
background-color: var(--component-x-bg);
@include border(all, 1px, solid, var(--component-x-border));
@include border-radius(var(--component-x-radius));
box-shadow: var(--component-x-shadow);
// 7. Transitions — ALWAYS before states
@include transition(color 0.2s ease, background-color 0.2s ease);
// 8. States
&:hover { … }
&:focus-visible { @include focus-ring(); }
&:disabled { … }
&--modifier { … }
// 9. Elements (__element)
&__icon { … }
&__label { … }
}
| Category | Mixin | Output / Note |
|---|---|---|
| Position | @include absolute($top:0, $right:0) |
position:absolute;top:0;right:0 — null-safe |
@include relative() |
position:relative — use even without coords |
|
@include fixed($bottom:0) |
position:fixed;bottom:0 |
|
@include sticky($top:0) |
position:sticky;top:0 |
|
| Spacing | @include padding(Yval null) |
Only top+bottom — left/right not emitted |
@include padding(Y X) |
Top+bottom = Y, Left+right = X | |
@include margin(null auto) |
Only margin-left+right: auto | |
| Flex | @include flex-center() |
display:flex;align-items:center;justify-content:center |
@include flex-between() |
display:flex;align-items:center;justify-content:space-between |
|
| Transition | @include transition(color 0.2s ease) |
Adds prefers-reduced-motion guard automatically ⭐ |
| Media | @include breakpoint(tablet) |
min-width: 50em — mobile-first |
@include breakpoint(desktop) |
min-width: 70em |
|
@include darkmode |
@media(prefers-color-scheme:dark) |
|
@include min-screen(48em) |
Custom value — always em, never px |
|
| A11y | @include sr-only() |
Visually hidden, accessible to screen readers |
@include focus-ring() |
WCAG focus outline — use only inside :focus-visible |
|
| Text | @include truncate(200px) |
Single-line ellipsis |
@include ellipsis(3) |
Multi-line clamp to 3 lines | |
| Border | @include border(top, 1px, solid, var(--color)) |
Or all for all 4 sides |
| Size | @include size(100%, 48px) |
width:100%;height:48px — null-safe |
// ✅ Mobile-first with breakpoints
.org-hero__title {
font-size: var(--semantic-font-size-xl); // mobile
@include breakpoint(tablet) {
font-size: var(--semantic-font-size-2xl); // tablet+
}
@include breakpoint(desktop) {
font-size: var(--semantic-font-size-display); // desktop+
}
}
// ✅ Focus ring only inside :focus-visible
&:focus-visible { @include focus-ring(); }
1 The "wrong layer" trap
Utilities are adjectives. If you're inventing a visual utility, you're in the wrong layer.
❌ <div class="syx-card-featured"> → this is a molecule, not a utility
✅ <div class="mol-card mol-card--featured">
2 Don't use @include for single-property CSS
Mixins add value when they include null-safety or extra logic. For simple properties, use raw CSS.
❌ @include color(var(--component-x-color)); → doesn't exist, adds no value
✅ color: var(--component-x-color);
✅ opacity: 0.5;
✅ cursor: pointer;
✅ z-index: var(--semantic-z-index-dropdown);
3 Let @layer resolve specificity
Never increase specificity to win a style conflict. If a component isn't winning, it's a layer assignment problem.
❌ .org-header .atom-btn--primary { background: red; } → forced specificity
✅ Use .syx-* utilities — they always win via @layer syx.utilities
4 Token without fallback → silent bug in other themes
If you define a token only in one theme's _theme.scss, the
other themes inherit an empty value.
// ❌ Only in themes/example-02/_theme.scss:
--component-header-bg: hsl(0, 0%, 5%); → themes 01,03,04,05 get no value
// ✅ Always define the fallback in abstracts/tokens/components/_header.scss:
--component-header-bg: var(--semantic-color-bg-primary); // default
// Then override only in the theme that needs it
5 $theme parameter flow
// _bundle-full.scss → passes the theme name to the mixin, once for all themes
@include org-site-header($theme);
// organisms/_site-header.scss → two methods based on type of difference
@mixin org-site-header($theme: null) {
// Method 1 — CSS token (automatic, all themes)
background: var(--component-header-bg);
// Method 2 — direct @if (1–2 themes only)
@if $theme == "example-02" { backdrop-filter: blur(8px); }
}
6 PurgeCSS: protect dynamic JS classes
// postcss.config.js — safelist
safelist: [/^atom-btn--/, /^is-/];
// Or include class names in an HTML comment (PurgeCSS scans content):
<!-- is-open is-active is-loading atom-btn--danger -->
Before writing any new code, ask yourself:
| Question | The answer determines… |
|---|---|
| Is this a reusable UI piece? | Yes → component (atom/mol/org). No → page/util |
| How many HTML elements does it need? | 1 → atom. 2–5 atoms → molecule. Full section → org |
| Does it need theme-aware colors/spacing? | Yes → use component tokens → var(--component-*) |
| Will it appear on multiple pages? | Yes → component layer. No → page layer |
| Is it a one-off CSS property tweak? | Yes → utility class .syx-* |
| Does it need JavaScript interaction? | Add a .js- hook class — never style .js- classes |
| Am I adding a raw value (oklch, px…)? | 🛑 Stop → use a token instead |
Am I writing transition: without @include? |
🛑 Stop → @include transition() |
Am I using position: absolute without @include? |
🛑 Stop → @include absolute() |
Am I using !important? |
🛑 Stop → check the @layer order instead |
A design system is agentic when an agent can ask it instead of reading it, write to it through a path that says no, and let it detect where a shipped app has drifted away. Those three verbs are the whole of this section — and section 7 is what happens when the consumer is not an app but Figma.
Why it isn't just "AI-friendly docs"
Documentation an agent has to read is documentation it has to fit in
context, resolve in its head, and trust. Answering “what colour is the primary button in Blueprint
dark?” used to mean loading 274 KB of tokens.json plus 317 KB of
token-contract.json and walking the cascade by hand. That is where invented token names come
from. Here it is one call, and the answer is the value the browser would paint.
Two front doors, one implementation
(scripts/lib/consulta.js). An agent speaking MCP and an application calling
require() must get the same answer to the same question — two copies of a criterion
always end up disagreeing.
Register it in any MCP client
{
"mcpServers": {
"syx": { "command": "npx", "args": ["-y", "-p", "syx-design-system", "syx-mcp"] }
}
}
Or from your own code
const syx = require('syx-design-system');
syx.getToken({
token: '--component-button-primary-filled-bg',
theme: 'syx-sketch', mode: 'dark'
}).value; // → oklch(0.740 0.133 267)
The twelve tools
| Tool | Answers | Instead of |
|---|---|---|
list_themes |
Which themes and modes exist | Guessing a theme name |
get_token |
A token's real value in a theme and mode, with the alias chain that produces it | Resolving the cascade by hand |
find_token_by_value |
Which token holds this colour or measure | Hardcoding a value found in the CSS |
list_components |
The inventory, with layer and base classes | Grepping the SCSS tree |
get_component |
Classes, modifiers, elements, states and tokens of one component | Reading four files and hoping |
validate_snippet |
R01–R04 and R09–R11 over SCSS before it is written, plus non-existent tokens | Writing first and validating later |
classify_change |
The trust tier of a change, and where a new token belongs | Assuming you may touch a file |
scan_for_drift |
Where an app has drifted from the system | Reading its CSS looking for smells |
list_mixins |
The 44 mixins with their signature and how often each is used | Reading 526 lines of README |
get_mixin |
One mixin's parameters and defaults, what it emits, who aliases it | Guessing the arguments |
get_figma_spec |
One component as Figma's Plugin API needs it: node property, converted value, variable name | Converting oklch() colours and rem sizes by hand |
find_capability |
Whether SYX already solves an idea, by concept: done, partial, rejected or open, with the file and line that prove it | Proposing what already exists because a guessed name was not found |
No dependencies: plain JSON-RPC over stdio. Everything about tokens and classes is arbitrated against the compiled CSS, so what comes out of it exists.
The mixins are the half that only exists in the SCSS
A token or a class can be checked against the compiled CSS, because that is
where they end up. A mixin leaves no recognisable trace once compiled — it dissolves into the
declarations it generates. So it is read from the source, and it has to be: R03 and R04 tell an agent
what it may not write, and until now nothing told it what to write instead.
validate_snippet now names the replacement — catch a raw position: and it
answers position(), plus the four aliases (absolute, fixed,
relative, sticky) that are how it is actually written here.
Reading is safe; writing is not. What an agent may change is
graded by how far the change travels — the boundary follows the direction of the
cascade this system already declares. The tiers live in contracts/trust.json, as data, so
they can be read without running anything.
| Tier | What | Why there |
|---|---|---|
| Automatic | Docs, changelog, generated artifacts | A mistake is visible in the diff and reaches nothing compiled |
| Via proposal | Component tokens, components, utilities, page styles | Scoped to one component — reviewable at a glance, but it ships |
| Human only | Primitives, semantics, themes, mixins, scripts/, rules.json,
trust.json |
One change lands in all 7 themes at once, or changes the criteria everything else is judged by |
The proposal path
# Where does this change sit?
npm run propose classify scss/atoms/_btn.scss CHANGELOG.md
# Propose a component token — nobody says which file it goes in
npm run propose token -- --name --component-feature-card-glow \
--value "var(--semantic-shadow-md)" --why "Optional lift for the featured card"
The destination is deduced from the token's family
— which file already declares --component-feature-card-* — never from a lookup
table that would quietly go stale. Then the tool writes it, recompiles the CSS, runs the validator
over the result, and only if that is green does it create a branch, a commit and an
evidence file in contracts/propuestas/. If it is not green, there is no branch. Review starts
with the proof in front of you instead of a claim that it works. It never pushes: it prints the command.
What it refuses, and says why
| You ask for | It answers |
|---|---|
| A primitive or semantic token | Human only — and offers to prepare the analysis instead |
| A literal colour as the value | Names the semantic token that already holds that colour |
A value pointing at --primitive-* |
That skips the semantic layer, one floor down |
| A token that already exists | Changing one isn't proposing a new one |
| A family nobody declares | Offers the declared families that look like it — usually the name was wrong |
| Anything, with a dirty working tree | A proposal has to be only the proposal |
A design system only stays a system if what ships still matches
it. syx-scan reads an app's HTML and CSS and reports where it has drifted — comparing
against the version of SYX that app has installed, which is the only comparison that means
anything.
npx syx-scan "src/**/*.html" src/app.css # from a consuming app
npm run scan -- docs.html --todo # from this repo
npx syx-scan app/ --json > drift.json # for CI
| What it finds | Why it matters |
|---|---|
var(--token, #6d28d9) where the token is now blue |
The fallback is a copy of a value that expired. Nobody notices, because the browser only uses it the day the token is missing |
var(--never-existed, …) |
The app paints the fallback always, believing it's an exception |
| A colour written by hand that already is a token | Names the semantic token that holds it |
.atom-icon--lc-users when the icon is --lc-user |
The modifier paints nothing at all — one letter, invisible in review |
!important and raw position in consumer CSS |
SYX governs the cascade with @layer; an !important outside voids it |
Two things it deliberately does not do
<pre>,
<code>, <script> and <textarea> is ignored
before it looks at anything: a documentation page teaches exactly what would otherwise be an error, and
a scanner that cries at every sample is a scanner people switch off. It also tells a dead class apart
from a JavaScript hook, and from a class the CSS reaches through
[class*=…] rather than by name.None of the above is worth much if it only runs when somebody
remembers. npm run check chains twelve checks and a build, and GitHub Actions runs them on every push across
Node 18, 20 and 22 — with the heavier ones (packing and installing the package for real, exercising
the proposal path) reserved for pull requests, so nobody is tempted to switch it off.
| Guard | Refuses to let through |
|---|---|
check:limpio |
A committed CSS that isn't what compiling produces — half the system measures itself against that file |
validate |
R01–R04 and R09–R11: primitives in components, !important, raw
transition/position, unknown mixins, exceptions, component token sources (also on the compiled CSS: check:compilado) |
check:themes |
A theme whose dark mode doesn't mirror its light mode |
check:version |
Ten version citations drifting apart — historical ones stay put on purpose |
check:tokens |
A resolved-token snapshot that no longer matches the CSS |
check:registry |
A component inventory that no longer matches the code — and it says what differs |
check:mcp |
A server that answers wrongly, or not at all, over the real protocol |
check:capacidades |
A capability ledger that still says done after the file proving it was renamed or deleted — and a reference question that no longer finds its entry |
check:escaner |
A scanner with false positives — six of its fifteen checks are things it must not report |
check:mixins |
A mixin reader that invents one, or misses one — checked both ways against the SCSS |
check:huerfanos |
A partial that no index imports — code that compiles into nothing and is read as if it shipped |
check:modos |
A mode whose Trust block promises what trust.json forbids, or an index
that lists a different set of modes than the folder holds |
check:encoding |
Text saved by reading UTF-8 as Windows-1252. Cosmetic until get_mixin started
serving those comments to agents |
check:package |
An exports path, a binary or a font the published package wouldn't actually
deliver |
The rule underneath all of it
One criterion, several consumers. The cascade resolver, the
contract rules and the queries live once, in scripts/lib/, and are shared by the validator,
the MCP server, the installed package, the proposal path and the scanner. Two copies of the same
criterion always end up saying different things — and then the system tells the agent one thing and
the application another. That failure repeated more than any other while this was being built, which is
why it is the one rule written down.
The argument is not that agents stop making mistakes. It is that the expensive mistakes become cheap to catch, and the cheap ones stop happening at all. Every figure below is measured in this repository, not estimated.
| What changes | Before | Now |
|---|---|---|
| A question costs a question, not a file dump | Load tokens.json + token-contract.json — 597 KB
— and walk the cascade by hand |
get_token answers in 352 bytes, with the alias chain, and it
is the value the browser paints |
| Wrong names never get written | Write, compile, notice later. The component registry once carried 81 of 111 token names that did not exist — the rules compared declarations, never consumption | validate_snippet runs the same R01–R04 before the file is
written, and flags every token that isn't real |
| Nobody has to say where things go | Tell the agent the file, or let it guess and review the guess | The destination is deduced from the token's family — from what the code already declares, not from a table someone maintains |
| A prohibition comes with its alternative | R03 and R04 said what not to write. The answer lived in 526 lines of README prose | get_mixin answers in 1 KB, and the rule itself now names the
replacement mixin and its aliases |
| Drift is measurable, so it can go down | “It looks fine” — a stale #6d28d9 sitting in a fallback for
months, invisible because the browser never uses it |
One pass over our own four pages: 64 findings → 19, none severe, each one verified against the compiled CSS |
| Speed is bounded by review, not by trust | Either the agent may write everything, or it may write nothing | Three tiers: docs move on their own, components arrive as a branch with the validator already green, primitives don't move at all |
| The agent and your app get the same answer | The contract lives in prose, and prose drifts from the code silently | One implementation in scripts/lib/, shared by the validator, the MCP server, the
installed package, the proposal path and the scanner |
What it does not buy you
var(--primitive-…) slipping past a rule that exempted the folder it
was headed for, three classes reported as dead that the CSS reached through
[class*=…], a repair pass that ate its own examples. The value isn't a system
that cannot be wrong — it's one where being wrong gets a name and a line number the same day.
Who it pays off for
The short version: a design system stops being documentation an agent has to believe, and becomes a service it can query and a gate it has to pass. Everything above follows from that one change.
Every block below is literal output, captured by running the commands against this repository at v4.25.0. Nothing is mocked up; where a transcript ran long it was cut at a line boundary and marked, never reworded.
The task: make the featured card react on hover. Five calls over MCP, not one file opened. Watch the third — the rule stops the agent and hands it the replacement, which is the half that used to be missing.
1 · What is it, and which tokens does it consume?
→ get_component { "name": "feature-card" }
{
"encontrado": true,
"name": "feature-card",
"layer": "molecule",
"classes": [
"mol-feature-card"
],
"file": "scss/molecules/_feature-card.scss",
"tokens": [
"--component-feature-card-bg",
"--component-feature-card-border-color",
…
2 · What colour does its border actually become on hover?
→ get_token { "token": "--component-feature-card-border-color-hover" }
{
"encontrado": true,
"value": "oklch(0.239 0.014 267)",
"theme": "syx-sketch",
"mode": "light",
"cadena": [
{
"token": "--component-feature-card-border-color-hover",
"declarado": "var(--semantic-color-border-strong)"
},
{
"token": "--semantic-color-border-strong",
"declarado": "var(--primitive-color-ink)"
},
…
3 · A snippet written from memory
→ validate_snippet { "code": ".mol-feature-card {\n transition: transform 0.2s ease;\n position: relative;\n}" }
{
"conforme": false,
"violaciones": {
"R03": {
"regla": "Sin `transition:` en crudo — usa el mixin transition()",
"casos": [
{
"file": "scss/molecules/_feature-card.scss",
"line": 2,
"content": "transition: transform 0.2s ease;"
}
],
"recambio": {
"mixin": "transition",
"signature": "transition($props...)",
"alias": [],
"ejemplo": " @include transition(opacity 0.2s ease, transform 0.3s ease);"
}
}
}
…
4 · So how is that mixin written?
→ get_mixin { "name": "transition" }
{
"encontrado": true,
"signature": "transition($props...)",
"params": [
{
"name": "$props",
"variadic": true
}
],
"emits": [
"transition"
],
…
5 · The same snippet, now using the system
→ validate_snippet { "code": ".mol-feature-card {\n @include transition(transform 0.2s ease);\n @include relative();\n}" }
{
"conforme": true,
"violaciones": {},
"tokensInexistentes": []
}
The keys come back in Spanish —
encontrado, cadena, declarado. That is the real payload, not a
translation: the contract layer was written in the language its author thinks in, and these transcripts
were not touched up to hide it.
The alias chain in call 2 is the point: the agent didn't resolve
the cascade, it was handed component → semantic → primitive → the literal value the browser
paints. And in call 5 the same snippet comes back conforme: true.
Nobody says which file the token belongs in.
token --component-feature-card-glow
valor var(--semantic-shadow-md)
destino scss/abstracts/tokens/components/_cards.scss (junto a --component-feature-card-bg)
deducido de familia «feature-card», 2 segmento(s) en común
nivel Vía propuesta — El alcance está acotado a un componente. Es reversible de un vistazo, pero toca CSS que se envía a producción.
escrito en scss/abstracts/tokens/components/_cards.scss:26
compilando y validando…
Switched to a new branch 'syx/token-component-feature-card-glow'
✅ Rama syx/token-component-feature-card-glow lista, un commit, validación en verde.
evidencia contracts/propuestas/component-feature-card-glow.md
volver git checkout principal
Para publicarla:
git push -u origin syx/token-component-feature-card-glow && gh pr create --fill --body-file contracts/propuestas/component-feature-card-glow.md
2bd75c1 feat(tokens): --component-feature-card-glow
And the evidence it leaves beside the commit, which is what a reviewer opens first:
# Propuesta — `--component-feature-card-glow`
**Generada por** `scripts/propose.js` · 2026-08-31T22:15:14.771Z · SYX v4.25.0
## Qué
| | |
|---|---|
| Token | `--component-feature-card-glow` |
| Valor | `var(--semantic-shadow-md)` |
| Fichero | `scss/abstracts/tokens/components/_cards.scss` |
| Nivel de confianza | Vía propuesta — El alcance está acotado a un componente. Es reversible de un vistazo, pero toca CSS que se envía a producción. |
**Por qué:** Realce opcional para la tarjeta destacada
## Dónde va, y por qué ahí
Nadie lo ha dicho: se dedujo de la familia `feature-card`, que ya vive en ese fichero (`--component-feature-card-bg`). Se insertó al final de su bloque para no partir la agrupación.
… (sigue el informe completo del validador, y qué revisar)
A system that stops an agent is worth more than one that lets it through. None of these four wrote a single byte.
A primitive
✋ --primitive-color-blue-999 es de la capa «primitive», que vive en scss/abstracts/tokens/primitives/
Nivel: Solo humano. Un cambio aquí se propaga a los siete temas de golpe, o cambia el criterio con el que se juzga a todo lo demás.
Lo que sí puede hacer un agente aquí es preparar el análisis: qué temas
se verían afectados y con qué valores. Escribirlo, no.
A colour written by hand
El valor es un color literal, y un token de componente debe apuntar a uno semántico.
Ese color ya es --semantic-color-primary, --semantic-color-border-focus, --semantic-color-link-active, --semantic-focus-ring-color, y 6 más.
Usa var(--semantic-color-primary) si es el papel que le corresponde.
A family nobody declares
Ningún token existente comparte familia con «--component-site-header-blur»: sería una familia nueva, y eso es decidir un fichero nuevo, no colocar un token.
Familias declaradas que se le parecen: header-bg, header-control, header-height, header-logo, header-padding, header-social, header-z, section-header
Puede que el nombre no siga la familia ya declarada. Si de verdad es nueva, crea el fichero a mano en scss/abstracts/tokens/components/.
A value that skips the semantic layer
El valor apunta a un primitivo, y un --component-* pasa por --semantic-*.
var(--primitive-color-blue-500)
Dar papel a un primitivo es trabajo de la capa semántica, que es solo humana.
This very site, today. It reported six a version ago; four were fixed in v4.25.0 — among them a progress bar that had gone months without painting, because it named a token that exists in no theme.
── DESVIACIÓN RESPECTO A SYX ───────────────────────────────────
comparado contra syx-sketch · light · SYX v4.25.0
ficheros 1
hallazgos 2 (2 baja)
Bases sin estilos, con modificadores que sí existen — 1
──────────────────────────────────────────────────────────────
· why-syx.html:346 .atom-txt no declara nada (38 usos)
Sus modificadores sí existen (.atom-txt--primary), así que la familia es real y solo falta la base.
→ O la base recibe los estilos que su nombre promete, o sobra en el marcado. Es una decisión de diseño.
Clases que solo usa el JavaScript — 1
──────────────────────────────────────────────────────────────
· why-syx.html:233 .syx--theme-syx-sketch no la declara ningún CSS
El script de la página sí la usa (syx--theme-…): parece un asidero de JavaScript, no una desviación.
→ Si es un asidero, mejor un data-* que una clase: así nadie espera que pinte.
El escáner no arregla nada a propósito: lo que encuentra entra por
scripts/propose.js o por las manos de alguien.
Why these are literal and not illustrative. An example written by hand proves the author's intention, not the system's behaviour — and it rots the first time the behaviour changes, silently. These were captured by running the commands. When any of them stops being true, the guards go red before this page does.
The first six sections are about an agent operating the system from the inside. This one is the same system pointed outward, and the direction is not negotiable: SYX is the source, Figma is a consumer — like an app is a consumer. Nothing here reads a design and writes SCSS.
Why the DTCG export wasn't enough
npm run export:tokens has spoken W3C DTCG since v4.12.0, and
Style Dictionary is happy with it. Figma is not: a COLOR variable stores numeric
{r,g,b,a}, and this system is written entirely in oklch(). A DTCG file
containing oklch(0.498 0.282 266.24) imports as text or doesn't import at all.
The real border is not a format — it's a conversion, which is why it lives in a
function (scripts/lib/figma.js) and not in a template. The same function answers
get_figma_spec and writes the export, so the variable you import and the value an agent
paints cannot diverge.
| In SYX | In Figma |
|---|---|
--component-button-border-radius: 0.25rem |
cornerRadius: 4 |
--semantic-color-primary: oklch(0.498 0.282 266.24) |
variable semantic/color/primary — {r:0.1175, g:0.2275, b:0.9999, a:1} |
.atom-btn--primary |
variant property primary on component atom/btn |
Ask, per component — while drawing
→ get_figma_spec { "component": "btn", "theme": "syx-sketch", "mode": "dark" }
{
"figmaName": "atom/btn",
"clases": { "base": ["atom-btn"], "modificadores": ["atom-btn--primary", …] },
"propiedades": [
{
"token": "--component-button-primary-filled-bg",
"variable": "component/button/primary/filled/bg",
"propiedad": "fills",
"tipo": "COLOR",
"valor": { "r": 0.5216, "g": 0.6555, "b": 0.9994, "a": 1 },
"hex": "#85a7ff",
"variante": "primary",
"estado": "default"
}, …40 in total
],
"sinTraducir": [
{ "token": "--component-button-font-size",
"motivo": "expresión CSS: solo un navegador la reduce" }, …
]
}
Or export the whole library
npm run export:figma # → contracts/figma/<theme>.figma.json (7 files)
npm run check:figma # fails if those files are stale
example-01 515 variables · 40 componentes · 274 omitidas
…
syx-sketch 543 variables · 40 componentes · 247 omitidas
3635 variables · 1879 omitidas con motivo
Two variable collections per theme —
SYX · Semantic and SYX · Component — each with a light and a
dark mode, plus the 40 components with the node property every token maps to. Three decisions
are worth knowing before you import:
| Decision | Because |
|---|---|
| Primitives don't go up — 233 per theme stay out | R01 forbids a component from reading a --primitive-* in CSS. Shipping them as
pickable Figma variables would open in design the shortcut the contract closes in code |
| One file per theme, not one with fourteen modes | Figma caps modes per collection by plan — four on Professional. A theme is a library; its two modes are light and dark |
| What can't be translated is listed, not hidden | 247 tokens in syx-sketch: 180 CSS expressions (the fluid clamp() type
is most of them), 19 relative units, 15 shadows, 11 durations, 8 embedded SVGs, 2 gradients.
Each one carries its reason, and the reason says where the thing does live in Figma |
Bind, don't paste
Node properties must be bound to the variables. A pasted
value is correct in light mode and wrong in dark, and nothing will tell you. The per-property
valores.light / valores.dark exist so a binding can be verified without opening
Figma. And inferencia marks the seam: classes and tokens come from the registry, arbitrated
against compiled CSS — the split by variant and state is deduced from token names.
What this repository cannot ship: Code Connect
Mapping a Figma component back to
<button class="atom-btn atom-btn--primary"> needs node IDs, which do
not exist until the components exist in a real file. What does ship is the input:
component-registry.json already holds the verified classes, and figmaName is
the pairing key. The step-by-step order is in
_agents/workflows/export-to-figma.md.
The section above is how the system answers and guards itself. This one is how you drive it. A mode is a lens: instead of a general-purpose answer, you get one tuned to a single discipline, with a declared permission ceiling and a declared reading list. There are ten, and they are deliberately siloed — a UX pass and a UI pass on the same problem produce a better result than one answer trying to be both.
Ordered by tier, which ranks how much context the mode spends. Use the lowest tier that does the job. Start at SKETCH or UX to check the idea is worth anything; escalate to TOKEN → UI → AUDIT once it is confirmed.
| Tier | Mode | Role | Writes | Produces |
|---|---|---|---|---|
| 1 | [SYX: SKETCH]: |
Rapid prototyper | nothing | Self-contained HTML + inline styles, diagrams, layout sketches |
| 2 | [SYX: UX]: |
UX consultant | nothing | Semantic HTML, component choice, accessibility, interaction states |
| 3 | [SYX: CREATIVE]: |
Creative director | nothing | Experimental HTML + CSS, advanced technique, awwwards-grade builds |
| 4 | [SYX: TOKEN]: |
Token architect | pr / recommends |
Token files, semantic mapping, registry entries |
| 5 | [SYX: THEME]: |
Theme designer | recommends | OKLCH scales, _theme.scss, surface token coverage, dark mode |
| 6 | [SYX: UI]: |
Senior SCSS developer | pr |
Component SCSS, component tokens, registration, R01–R04 compliance |
| 7 | [SYX: AUDIT]: |
QA reviewer | nothing | Contract violations with a verdict — R01–R04 and R09–R11 errors, R05, R06 and R08 warnings, R07 info — plus structure and naming checks |
| 8 | [SYX: MIGRATE]: |
Migration specialist | pr / recommends |
Legacy variable resolution, impact analysis, one variable at a time |
| 9 | [SYX: BRAND]: |
Brand identity architect | recommends | A two-round interview — register first, then the seven axes with IA always on the table — then the axes with their provenance, the identity contract, and the specification THEME builds from. Never the theme file: BRAND decides the direction, THEME writes it |
| 10 | [SYX: ROADMAP]: |
Systems strategist | recommends | A capability check of every idea against contracts/capabilities.json, what is already covered with its evidence, and only the proposals that prove their absence. Without a prefix, any request for improvements or a roadmap lands here |
The tier measures the system, not the cortex
Two separate axes, and adding them gives a wrong number. The tier counts what
it costs to interrogate SYX — tokens.json, the registry, the contracts. The mode's
Knowledge block counts what it costs to load the reading list. CREATIVE is tier 3 and still
pulls the whole motion domain when there is GSAP: cheap in system reads, expensive in corpus. SKETCH is
the one disciplined exception — its tier 1 is bought by reading nothing at all, so it has no
Always line.
BRAND is the second exception, in the opposite direction. It reads three files and still sits at tier 9, because the tier ranks the work and BRAND's work is the only one that has to come out internally consistent across seven axes at once. The expensive part is the coherence check, not the reads.
ROADMAP is tier 10 for the same bookkeeping reason BRAND sits after
MIGRATE: renumbering costs more than the order is worth. It reads little — one
find_capability per idea — but its failure is expensive in a different way: a
proposal for something already built costs the reader the time to refute it. It exists because an
external review proposed six improvements and five were already in the repository.
The prefix is the portable form. It is plain
text, so it survives into Claude Code, Codex, Cursor or anything else that ingests
AGENTS.md. It is a convention, though: nothing forces a client to honour it.
Portable — any agent
[SYX: UI]: implement the atom-tag
component with --primary and --neutral
[SYX: AUDIT]: review
scss/organisms/_site-header.scss
Claude Code — the slash command
/syx UI implement the atom-tag component
/syx UX → TOKEN → UI + AUDIT
a search field with autocomplete
.claude/commands/syx.md takes the same grammar and
resolves to the same ten files. It is a pointer, not a copy, so there is nothing to keep
in sync. What it adds is that the harness runs it: autocomplete, and the mode file gets read because the
command says so rather than because an agent remembered a convention.
Two decisions that look wrong and aren't
One command, not ten. Ten would be ten near-identical files restating what each mode is — the duplication this system spent a refactor removing.
A command, not a skill. A skill auto-invokes on its description, and here that is a defect: modes are lenses chosen on purpose, and choosing the lens chooses the tier, which is what the turn costs. Neither form picks a mode on its own.
Two operators. → is a pipeline:
each mode's output is the next one's input. + is evaluative: both modes work
the same artifact and both outputs come back together.
The grouping rule
+ groups before →. The
+ binds the modes that share an artifact; the → chains those groups. So
[SYX: UX → UI + AUDIT]: reads as UX → (UI + AUDIT): UX first, then UI
implements while AUDIT verifies that same output. For any other grouping, split into turns.
A middle step with no work does not stop the pipeline. If TOKEN finds every token it needed already exists, it hands off explicitly — “use these” — and the chain continues. Aborting is the user's call, not the mode's.
Valid
SKETCH → UX |
Check an idea before formalising it |
UX → TOKEN → UI |
The standard new-component flow |
UX → TOKEN → UI + AUDIT |
The same, verified |
TOKEN → THEME + AUDIT |
New theme with coverage check |
CREATIVE → TOKEN → UI |
An experiment into production |
AUDIT → MIGRATE |
Technical debt, if there are few variables |
BRAND → THEME |
A full identity: BRAND decides the seven axes, THEME builds and contrast-checks the scale |
BRAND + AUDIT |
Identity verified — every token it names exists, every block it hands over passes R01–R04 |
Invalid, and why
SKETCH + AUDIT |
SKETCH is exempt from the contracts AUDIT enforces |
CREATIVE + AUDIT |
Same. Use CREATIVE → TOKEN → UI + AUDIT |
UI → TOKEN |
Backwards. TOKEN defines, UI consumes |
THEME → UI |
THEME produces no component for UI to process |
MIGRATE + AUDIT |
Only AUDIT → MIGRATE makes sense |
UI → BRAND |
Backwards. An identity is decided before what wears it |
THEME → BRAND |
Backwards and more expensive: a palette with no identity to answer to |
This is the head of
_agents/modes/ui.md, unedited. Trust says what the mode may write;
Knowledge says what it reasons with.
> **Trust** — graded by `contracts/trust.json`, verified by `npm run check:modos`.
>
> · **Writes:** `scss/atoms/`, `scss/molecules/`, … — tier `pr`: prepare the change
> with `node scripts/propose.js`. A person merges.
> · **Recommends only:** `scss/abstracts/mixins/`, `scss/base/` — say so and stop there.
> · **Reads:** `contracts/rules.json`, `component-registry.json`, `mind-system/knowledges/`
> · **Ask, don't read:** `get_component` before duplicating one, `get_mixin` before writing
> a property R03 or R04 rejects, `validate_snippet` **before** writing the file.
> **Knowledge** — the cortex under `mind-system/knowledges/`, routed by `mind-system/routing.md`.
> It informs; it never executes.
>
> · **Always:** `syx/scss-pipeline.md` · `syx/component-patterns.md` · `syx/token-system.md`
> · **When relevant:** `ui/refactoring-ui.md` for spacing and composition calls · …
> · **On request:** `motion/08-ejecucion/gsap/03-patrones/` for a GSAP effect named in the brief
The two are not symmetric in force. A permission is a ceiling; a module is an argument. Knowledge never authorises anything and never wins against a rule — if a module recommends what R01 forbids, the module is what needs fixing.
A mode never grants a permission — it inherits one
npm run check:modos reads every Trust block and
compares it against contracts/trust.json. It fails if a mode lists a
human path under Writes, if it claims to only recommend somewhere it could actually
write, if it announces the proposal path without naming propose.js, if it mentions a file
none of its three lists covers, if it tells you to ask an MCP tool that does not exist, or if the three
indexes list a different set of modes than the folder holds. It exists because three modes once did
command exactly what the contract forbids, and nothing measured it: a mode is read, not executed.
The modes are the engine. Behind them sits
mind-system/, the cortex: colour theory, UX laws, WCAG, scale models, brand perception, a
motion language, and the editorial rules of a consuming product. It loads on demand, per the
Knowledge block, and it is not published to npm.
Precedence, highest first
| # | Authority | Decides | Checked? |
|---|---|---|---|
| 1 | contracts/trust.json |
Who may write what | yes — classify_change |
| 2 | contracts/rules.json |
R01–R11 | npm run validate |
| 3 | _agents/modes/*.md → Trust |
Each mode's ceiling | yes — npm run check:modos |
| 4 | mind-system/governance/ |
How editorial context composes with the modes | declared only |
| 5 | mind-system/atlas-rules/ |
Editorial decisions (guest domain) | declared only |
| 6 | mind-system/knowledges/ |
The reasoning | declared only |
A rung never beats one above it. That is the whole rule, and it is what keeps a markdown file from overruling a machine-checked contract. Rungs 1–3 are verified; 4–6 are written down and not yet guarded, which this page says out loud rather than implying otherwise.
Everything in the cortex passes the SYX filter
A knowledge module may reason however it likes, but the moment it shows
how to write something, it writes SYX. Its code blocks go through the same
scripts/lib/rules.js that validates the repository, and because R01 depends on the layer
— var(--primitive-*) is correct in a theme and forbidden in a component — each
block declares where it lives. A block that breaks a rule is only valid if it is marked as the
anti-pattern it is.
@layer syx.utilities — highest specificity. Always wins
over atoms and molecules.
.syx-type-h1Heading One.syx-type-h2Heading Two.syx-type-h3Heading Three.syx-type-body-largeBody large text..syx-type-bodyBody standard text..syx-type-body-smallBody small..syx-type-captionCaption — metadata..syx-type-labelLabel text..syx-type-overlineOverline — Section categoryText utilities
Social brand
Brand colours for icons and marks, not for running text: on a light page none of them reaches 4.5:1. The swatch shows the colour.
.syx-text-facebook
.syx-text-twitter
.syx-text-instagram
.syx-text-whatsapp
Semantic (_text.scss)
The state fills (error, success, warning, info) are swatches here: they lighten in dark mode, so no single text colour on them holds 4.5:1 in both. Text on a state colour goes through the component that owns it (atom-pill, mol-alert).
Theme-aware helpers (_backgrounds.scss)
.syx-bg-color-blue-facebook.syx-bg-color-blue-twitter.syx-bg-color-blue-instagram.syx-bg-color-green-whatsappMargin bottom scale (0–5)
Padding scale (1–5) · shorthands
.syx-d-flex + gaps + wrap
.syx-justify-between + .syx-items-center
.syx-d-grid + .syx-gap-3
.syx-img-fluid
.syx-embed--16by9
.syx-aspect-* + .syx-obj-cover — the same 4:3 image, ratio set on the element (no wrapper)
Object-fit · Background-size classes
Skip Link — focus to reveal (Tab key)
prefers-reduced-motion.
Responsive visibility
Responsive scale. Values map to --font-size-1…5
tokens.
Sizes 2–5 scale up at $breakpoint-md.
.syx-font-size-1Font size
1.syx-font-size-2Font size
2.syx-font-size-3Font size
3.syx-font-size-4Font size
4.syx-font-size-5Font size
5Square size tokens mapping to --primitive-size-1…5.
For
icons, avatars, thumbnails.
size-1
size-2
size-3
size-4
size-5
@layer syx.base — structural foundation. 12-column
responsive grid.
Column spans at xs
col-xs-12col-xs-6col-xs-6xs-4xs-4xs-4Responsive — xs-12 → sm-6 → md-4 (resize to see)
Modifiers: --no-gap · __nested · --is-edge2edge
--no-gap · xs-8xs-4@layer syx.atoms — smallest reusable components.
Outline (no --filled) · default size
Filled (--filled) · default size
Sizes — --size-sm · --size-md ·
--size-lg (outline)
Sizes — filled
Circle — --circle with atom-icon
·
sm · md · lg
atom-btn--has-icon — icon size + gap
auto-controlled by --size-*
States — normal · hover (hover me) · focus-visible (Tab) · disabled
Paragraph with an inline primary link — hover to see the fill effect. And another link.
Legacy system — --ui-* /
--arrow-*
/ --rrss-*
Lucide — Navigation --lc-*
Lucide — Actions
Lucide — Status · User
Sizes — --sm · --md ·
--lg · --xl
Color modifiers — --color-primary ·
--color-state-ok · --color-state-ko · --color-state-warning
H1 Title
H2 Title
H3 Title
H4 Title
H5 Title
H6 Title
Body paragraph with atom-txt--primary. Includes semantic
bottom margin for vertical rhythm.
Second paragraph to demonstrate consistent spacing between text blocks.
| Component | Layer | Status |
|---|---|---|
| atom-btn | syx.atoms | Stable |
| mol-card | syx.molecules | Stable |
| syx-d-flex | syx.utilities | Always wins |
.atom-code — block
.atom-btn { background-color: var(--component-btn-primary-bg); color: var(--component-btn-primary-color); /* token-driven — no hardcoded values */ }
.atom-code--inline
Use @layer syx.utilities to ensure
utilities always win.
@layer syx.molecules — compositions of atoms.
mol-btn-group
mol-label-group
Default (vertical stack)
--inline (horizontal wrap with flex: 1 1 auto)
@layer syx.organisms — compositions of molecules + atoms
forming page-level sections.
Live example — the header at the top of this page is this organism
Composes: atom-icon · atom-btn ·
mol-btn-group. Add more organisms to scss/organisms/ and forward from
organisms/index.scss.
Every component in component-registry.json — 40 components: 23 atoms, 15 molecules, 2 organisms. Generated by scripts/build-docs-componentes.js; the markup is the registry’s usage, rendered live with the theme picked above. Install and theme: README → Quick Start.
.atom-avataratomgenericscss/atoms/_avatar.scss
A person's photo or, without one, their initials, at a fixed size: --sm (32px), md by default (40px), --lg (56px); --square for accounts and organisations. The photo is an <img class="atom-avatar__img"> cropped with object-fit: cover. Initials sit on bg-tertiary in secondary text, measured at 4.5:1 in contracts/contrast.json.
Live demo
Markup
<span class="atom-avatar atom-avatar--lg"><img class="atom-avatar__img" src="img/avatar-demo.svg" alt="Ana López" width="56" height="56"></span><span class="atom-avatar" role="img" aria-label="Ana López">AL</span><span class="atom-avatar atom-avatar--sm atom-avatar--square" role="img" aria-label="Acme Inc.">AC</span>
| Classes | atom-avatar |
|---|---|
| Modifiers | atom-avatar--lg atom-avatar--sm atom-avatar--square |
| Elements | atom-avatar__img |
| States | — |
| Tokens | 11 component tokens--component-avatar-bg --component-avatar-border-radius --component-avatar-border-radius-square --component-avatar-color --component-avatar-font-size-lg --component-avatar-font-size-md --component-avatar-font-size-sm --component-avatar-font-weight --component-avatar-size-lg --component-avatar-size-md --component-avatar-size-sm |
| Accessibility | Photo: alt with the person's name, or alt="" when the name is written right next to it. Initials alone do not say who it is: role="img" with aria-label="Full name"; next to the visible name, aria-hidden="true" instead. Not focusable — if it links to a profile, the link carries the name. In forced colours a border keeps the shape. No JS. |
| JavaScript | None — CSS only. |
.atom-breadcrumbatomsystem-reusablescss/atoms/_breadcrumb.scss
Ancestor trail for the current page. Use it when the user arrived at a page nested two or more levels deep and needs to climb back. Not a substitute for primary navigation, and pointless on a flat site.
Live demo
Markup
<nav aria-label="Breadcrumb">
<ol class="atom-breadcrumb">
<li class="atom-breadcrumb__item"><a class="atom-breadcrumb__link" href="/">Home</a></li>
<li class="atom-breadcrumb__item" aria-current="page">Tokens</li>
</ol>
</nav>
| Classes | atom-breadcrumb |
|---|---|
| Modifiers | — |
| Elements | atom-breadcrumb__item atom-breadcrumb__link |
| States | :focus-visible :hover |
| Tokens | — |
| Accessibility | Wrap the list in <nav aria-label="Breadcrumb"> with an <ol>. The current page is plain text with aria-current="page", not a link. Separators are CSS and are not read. Links take the system focus ring. No JS. |
| JavaScript | None — CSS only. |
.atom-btnatomsystem-reusablescss/atoms/_btn.scss
The action trigger. --primary is the single dominant action of a decision context; --secondary/--tertiary/--quaternary step down from it; --danger marks a destructive action (delete, revoke) in the error state colour, never the brand; --filled makes any variant solid. Without a --size-* modifier a button is md, the same 48px height as a form field; --size-sm/--size-lg are 40/56px. Use --circle only for an icon-only control, and give it an aria-label. If the element navigates rather than acts, it is atom-link.
Live demo
Markup
<button class="atom-btn atom-btn--primary atom-btn--size-md" type="button">Get started</button> <button class="atom-btn atom-btn--danger atom-btn--filled" type="button">Delete project</button>
| Classes | atom-btn |
|---|---|
| Modifiers | atom-btn--circle atom-btn--danger atom-btn--filled atom-btn--has-icon atom-btn--primary atom-btn--quaternary atom-btn--secondary atom-btn--size-lg atom-btn--size-md atom-btn--size-sm atom-btn--tertiary |
| Elements | — |
| States | :active :disabled :focus-visible :hover |
| Composes | atom-icon |
| Tokens | 88 component tokens--component-button-active-offset-x --component-button-active-offset-y --component-button-border-radius --component-button-border-radius-circle --component-button-border-style --component-button-border-width --component-button-danger-bg-hover --component-button-danger-border --component-button-danger-border-hover --component-button-danger-color --component-button-danger-color-hover --component-button-danger-filled-bg --component-button-danger-filled-bg-hover --component-button-danger-filled-border --component-button-danger-filled-border-hover --component-button-danger-filled-color --component-button-danger-filled-color-hover --component-button-disabled-bg --component-button-disabled-border --component-button-disabled-color --component-button-disabled-filled-color --component-button-font-family --component-button-font-size --component-button-font-size-lg --component-button-font-size-md --component-button-font-size-sm --component-button-font-weight --component-button-height-lg --component-button-height-md --component-button-height-sm --component-button-line-height --component-button-padding-block --component-button-primary-bg-hover --component-button-primary-border --component-button-primary-border-hover --component-button-primary-color --component-button-primary-color-hover --component-button-primary-filled-bg --component-button-primary-filled-bg-hover --component-button-primary-filled-border --component-button-primary-filled-border-hover --component-button-primary-filled-color --component-button-primary-filled-color-hover --component-button-quaternary-bg-hover --component-button-quaternary-border --component-button-quaternary-border-hover --component-button-quaternary-color --component-button-quaternary-color-hover --component-button-quaternary-filled-bg --component-button-quaternary-filled-bg-hover --component-button-quaternary-filled-border --component-button-quaternary-filled-border-hover --component-button-quaternary-filled-color --component-button-quaternary-filled-color-hover --component-button-secondary-bg-hover --component-button-secondary-border --component-button-secondary-border-hover --component-button-secondary-color --component-button-secondary-color-hover --component-button-secondary-filled-bg --component-button-secondary-filled-bg-hover --component-button-secondary-filled-border --component-button-secondary-filled-border-hover --component-button-secondary-filled-color --component-button-secondary-filled-color-hover --component-button-shadow --component-button-shadow-active --component-button-shadow-hover --component-button-size-lg --component-button-size-md --component-button-size-sm --component-button-tertiary-bg-hover --component-button-tertiary-border --component-button-tertiary-border-hover --component-button-tertiary-color --component-button-tertiary-color-hover --component-button-tertiary-filled-bg --component-button-tertiary-filled-bg-hover --component-button-tertiary-filled-border --component-button-tertiary-filled-border-hover --component-button-tertiary-filled-color --component-button-tertiary-filled-color-hover --component-button-transition-duration --component-button-transition-duration-active --component-button-transition-easing --component-icon-size-lg --component-icon-size-md --component-icon-size-sm |
| Accessibility | Native <button> with an explicit type ("button", or "submit" inside a form); an <a href> only when it navigates. Enter/Space come from the element. Icon-only (--circle) needs aria-label. Disable with the disabled attribute, not a class. Focus: system ring. No JS. |
| JavaScript | None — CSS only. |
.atom-checkatomsystem-reusablescss/atoms/_check.scss
Checkbox with a visible label, for choices that are independent of each other. The native input stays in the DOM and carries the state; the styling hangs off it. For one on/off setting that applies immediately, atom-switch reads better; for mutually exclusive options, atom-radio.
Live demo
Markup
<label class="atom-check"><input class="atom-check__input" type="checkbox" name="opt"><span class="atom-check__label">Include drafts</span></label>
| Classes | atom-check |
|---|---|
| Modifiers | — |
| Elements | atom-check__input atom-check__label |
| States | :checked :disabled :focus-visible :hover |
| Tokens | 21 component tokens--component-check-bg --component-check-bg-checked --component-check-border-color --component-check-border-color-hover --component-check-border-radius --component-check-border-width --component-check-gap --component-check-mark-color --component-check-mark-height --component-check-mark-rotation --component-check-mark-thickness --component-check-mark-width --component-check-shadow-focus --component-check-size --component-check-status-disabled-opacity --component-check-transition-duration --component-check-transition-easing --component-form-field-color --component-form-field-font-size --component-form-field-line-height --component-form-field-transition-easing |
| Accessibility | Real <input type="checkbox"> inside the <label>, so the text is its name and the whole row toggles it. Space toggles. Related boxes go in a <fieldset> with a <legend>. The focus ring is drawn on the visible box. No JS. |
| JavaScript | None — CSS only. |
.atom-codeatomdomain-reusablescss/atoms/_code.scss
Code shown as content, on its own dark surface that ignores the page theme so a dark brand colour cannot make the syntax invisible. --inline for a fragment inside a sentence. The __tag/__attr/__val/__cls/__cmt elements are the syntax roles; nothing else colours code. Use in technical and documentation products, not as a generic panel.
Live demo
<button class="atom-btn">Markup
<pre class="atom-code" tabindex="0" role="group" aria-label="Button markup"><code><<span class="atom-code__tag">button</span> <span class="atom-code__attr">class</span>=<span class="atom-code__val">"atom-btn"</span>></code></pre>
| Classes | atom-code |
|---|---|
| Modifiers | atom-code--inline |
| Elements | atom-code__attr atom-code__cls atom-code__cmt atom-code__tag atom-code__val |
| States | :focus-visible |
| Tokens | 16 component tokens--component-code-bg --component-code-border-color --component-code-border-radius --component-code-border-width --component-code-color --component-code-font-size --component-code-inline-border-color --component-code-inline-border-radius --component-code-inline-border-width --component-code-inline-font-size --component-code-line-height --component-code-syntax-attr --component-code-syntax-comment --component-code-syntax-fn --component-code-syntax-tag --component-code-syntax-val |
| Accessibility | Static content in <pre><code>. A block that scrolls sideways needs tabindex="0" (and a label) so it can be scrolled from the keyboard. Colour is never the only cue. No JS. |
| JavaScript | None — CSS only. |
.atom-feature-iconatomdomain-reusablescss/atoms/_feature-icon.scss
Circular tinted container that gives an icon presence at the head of a card or list item. The shape is generic but the modifier set (--token, --mixin, --layer, --molecule, --organism) names SYX's own architecture: outside this domain only --primary carries meaning, so a new product needs its own modifiers rather than borrowing these. Always decorative — the meaning belongs to the text next to it.
Live demo
Markup
<span class="atom-feature-icon atom-feature-icon--token" aria-hidden="true"><span class="atom-icon atom-icon--lc-layers"></span></span>
| Classes | atom-feature-icon |
|---|---|
| Modifiers | atom-feature-icon--layer atom-feature-icon--mixin atom-feature-icon--molecule atom-feature-icon--organism atom-feature-icon--primary atom-feature-icon--token |
| Elements | — |
| States | — |
| Tokens | 18 component tokens--component-feature-icon-atom-bg --component-feature-icon-atom-color --component-feature-icon-border-color --component-feature-icon-border-radius --component-feature-icon-border-style --component-feature-icon-border-width --component-feature-icon-layer-bg --component-feature-icon-layer-color --component-feature-icon-mixin-bg --component-feature-icon-mixin-color --component-feature-icon-molecule-bg --component-feature-icon-molecule-color --component-feature-icon-organism-bg --component-feature-icon-organism-color --component-feature-icon-shadow --component-feature-icon-size --component-feature-icon-token-bg --component-feature-icon-token-color |
| Accessibility | Decorative: always aria-hidden="true"; the meaning lives in the text next to it. Never focusable. No JS. |
| JavaScript | None — CSS only. |
.atom-inputatomsystem-reusablescss/atoms/_form.scss
The five canonical form primitives: .atom-label, .atom-input, .atom-textarea, .atom-select and .atom-input-wrapper. .atom-textarea shares the input's box, border, focus and states but not its fixed height: rows sets it, with a floor of two touch heights and vertical-only resize (textarea.atom-input still works). They are the raw controls only — validation state, message and character count live in mol-form-field, so use these bare only in a layout that handles its own error reporting.
Live demo
Markup
<label class="atom-label" for="form-email">Email</label><span class="atom-input-wrapper"><input class="atom-input" id="form-email" type="email" autocomplete="email"></span><label class="atom-label" for="form-plan">Plan</label><span class="atom-input-wrapper"><select class="atom-select" id="form-plan"><option>Free</option><option>Pro</option></select></span><label class="atom-label" for="form-notes">Notes</label><textarea class="atom-textarea" id="form-notes" rows="3"></textarea>
| Classes | atom-input atom-input-wrapper atom-label atom-select atom-textarea |
|---|---|
| Modifiers | — |
| Elements | — |
| States | :disabled :focus-visible :hover |
| Composes | atom-label |
| Tokens | 21 component tokens--component-form-field-bg --component-form-field-bg-disabled --component-form-field-border --component-form-field-border-focus --component-form-field-border-radius --component-form-field-border-width --component-form-field-color --component-form-field-color-disabled --component-form-field-font-family --component-form-field-font-size --component-form-field-font-weight --component-form-field-label-margin-bottom --component-form-field-line-height --component-form-field-min-height --component-form-field-padding-x --component-form-field-padding-y --component-form-field-placeholder --component-form-field-transition-duration --component-form-field-transition-easing --component-form-textarea-line-height --component-form-textarea-min-height |
| Accessibility | Every control has a <label for> pointing at a unique id (ids repeat across a page more often than you think). Use the right type and autocomplete. .atom-select is a native <select>: keyboard and option list come from the browser. Inputs and selects are 48px tall at every width; a textarea is never shorter than two of those. Focus: system ring. No JS. |
| JavaScript | None — CSS only. |
.atom-iconatomsystem-reusablescss/atoms/_icon-lucide.scss
Extends .atom-icon with 63 Lucide glyphs as --lc-* modifiers. The SVGs are embedded as data URIs, not linked: they are painted with mask-image, and a cross-origin mask is blocked, which left the site iconless over file://. Regenerated by scripts/build-lucide-tokens.js from img/icons/lucide/ — do not hand-edit the modifier block.
Live demo
Markup
<span class="atom-icon atom-icon--lc-home atom-icon--color-primary" aria-hidden="true"></span>
| Classes | atom-icon |
|---|---|
| Modifiers | 61 modifiersatom-icon--lc-activity atom-icon--lc-arrow-left atom-icon--lc-arrow-right atom-icon--lc-arrow-up atom-icon--lc-atom atom-icon--lc-award atom-icon--lc-book-open atom-icon--lc-bot atom-icon--lc-box atom-icon--lc-briefcase atom-icon--lc-check atom-icon--lc-check-circle atom-icon--lc-chevron-down atom-icon--lc-chevron-left atom-icon--lc-chevron-right atom-icon--lc-chevron-up atom-icon--lc-chevrons-down atom-icon--lc-chevrons-left atom-icon--lc-chevrons-right atom-icon--lc-code atom-icon--lc-coins atom-icon--lc-component atom-icon--lc-copy atom-icon--lc-corner-down-right atom-icon--lc-database atom-icon--lc-download atom-icon--lc-error atom-icon--lc-external-link atom-icon--lc-file-text atom-icon--lc-filter atom-icon--lc-git-branch atom-icon--lc-git-compare atom-icon--lc-github atom-icon--lc-home atom-icon--lc-info atom-icon--lc-layers atom-icon--lc-layout atom-icon--lc-link atom-icon--lc-mail atom-icon--lc-menu atom-icon--lc-minus atom-icon--lc-moon atom-icon--lc-package atom-icon--lc-palette atom-icon--lc-pencil atom-icon--lc-pie-chart atom-icon--lc-plus atom-icon--lc-rocket atom-icon--lc-search atom-icon--lc-settings atom-icon--lc-shield-check atom-icon--lc-success atom-icon--lc-sun atom-icon--lc-terminal atom-icon--lc-trash atom-icon--lc-user atom-icon--lc-users atom-icon--lc-warning atom-icon--lc-x atom-icon--lc-x-circle atom-icon--lc-zap |
| Elements | — |
| States | — |
| Composes | atom-icon |
| Tokens | — |
| Accessibility | Decorative by default: aria-hidden="true". When it is the only content of a control, the control carries the aria-label, not the icon. No JS. |
| JavaScript | None — CSS only. |
.atom-iconatomsystem-reusablescss/atoms/_icon.scss
The icon primitive: a masked shape that inherits colour through --color-* modifiers and sizes through --sm/--md/--lg/--xl. This file holds the hand-drawn SYX set (arrows, bars, UI marks); the Lucide set extends the same class. An icon is decorative unless it is the only content of a control — mark it aria-hidden, or give the control an aria-label.
Live demo
Markup
<span class="atom-icon atom-icon--ui-tick atom-icon--color-state-ok atom-icon--md" aria-hidden="true"></span>
| Classes | atom-icon |
|---|---|
| Modifiers | 19 modifiersatom-icon--arrow-default atom-icon--arrow-double atom-icon--color-black atom-icon--color-primary atom-icon--color-secondary atom-icon--color-state-ko atom-icon--color-state-ok atom-icon--color-state-warning atom-icon--color-white atom-icon--content-bars atom-icon--lg atom-icon--md atom-icon--rrss-email atom-icon--sm atom-icon--ui-cancel atom-icon--ui-exclamation atom-icon--ui-home atom-icon--ui-tick atom-icon--xl |
| Elements | — |
| States | — |
| Tokens | 6 component tokens--component-icon-color --component-icon-size-lg --component-icon-size-md --component-icon-size-sm --component-icon-size-xl --component-icon-source |
| Accessibility | Decorative by default: aria-hidden="true". When it is the only content of a control, the control carries the aria-label, not the icon. No JS. |
| JavaScript | None — CSS only. |
.atom-labelatomsystem-reusablescss/atoms/_label.scss
Caution: two different things claim .atom-label. This file styles a small scope marker (--scope-1/-2/-3, --primary); scss/atoms/_form.scss declares the same class as a form field label. Check which one the compiled CSS gives you before relying on either, and prefer atom-pill for a standalone tag.
Live demo
Markup
<span class="atom-label atom-label--scope-1">Scope</span>
| Classes | atom-label |
|---|---|
| Modifiers | atom-label--primary atom-label--scope-1 atom-label--scope-2 atom-label--scope-3 |
| Elements | — |
| States | — |
| Tokens | 4 component tokens--component-label-border-radius --component-label-padding-lg --component-label-padding-md --component-label-padding-sm |
| Accessibility | Plain text, no role. If it marks a state, the words must say it. No JS. |
| JavaScript | None — CSS only. |
.atom-linkatomsystem-reusablescss/atoms/_link.scss
Inline navigational link. Use it when the element takes the user somewhere; if it performs an action, it is atom-btn regardless of how it looks. --primary is the styled variant with its own focus and hover treatment.
Live demo
Markup
<a class="atom-link atom-link--primary" href="/docs">Read the docs</a>
| Classes | atom-link |
|---|---|
| Modifiers | atom-link--primary |
| Elements | — |
| States | :active :focus-visible :hover |
| Tokens | 4 component tokens--component-link-decoration-thickness --component-link-primary-bg-hover --component-link-primary-color --component-link-primary-color-hover |
| Accessibility | Native <a href>; if it performs an action it is a <button>. The text must make sense out of context ("Read the docs", not "here"). Every variant takes the system focus ring. No JS. |
| JavaScript | None — CSS only. |
.atom-listatomsystem-reusablescss/atoms/_list.scss
Unstyled list plus two authored variants: --primary draws its own marker through ::before so the bullet colour follows the theme, --ordered numbers. Use it for real sequences and sets. Do not use it to lay out cards — that is a grid.
Live demo
Markup
<ul class="atom-list atom-list--primary">
<li class="atom-list__item">Primitives</li>
<li class="atom-list__item">Semantics</li>
</ul>
| Classes | atom-list |
|---|---|
| Modifiers | atom-list--ordered atom-list--primary |
| Elements | — |
| States | — |
| Tokens | 21 component tokens--component-list-icon-filter --component-list-icon-offset-y --component-list-icon-size --component-list-item-margin-top --component-list-lvl1-gap --component-list-lvl1-icon --component-list-lvl2-gap --component-list-lvl2-icon --component-list-lvl2-icon-offset-y --component-list-lvl2-margin-left --component-list-lvl3-gap --component-list-lvl3-margin-left --component-list-ordered-counter-bg --component-list-ordered-counter-color --component-list-ordered-counter-font-size --component-list-ordered-counter-line-height --component-list-ordered-counter-size --component-list-ordered-gap --component-list-ordered-item-margin-top --component-list-ordered-margin-bottom --component-list-ordered-margin-top |
| Accessibility | Native <ul>/<ol> so the item count is announced. The --primary marker is CSS; WebKit drops list semantics when list-style is none, so add role="list" if that matters. No JS. |
| JavaScript | None — CSS only. |
.atom-paginationatomsystem-reusablescss/atoms/_pagination.scss
Page-by-page navigation over a finite, countable result set. Prev/next arrows come from the Lucide token system via ::before — mark the item --is-prev/--is-next and give the trigger an aria-label instead of visible text (a word overflows the square trigger). If the total is unknown or the list is a feed, this is the wrong control.
Live demo
Markup
<nav aria-label="Pagination">
<ul class="atom-pagination">
<li class="atom-pagination__item atom-pagination__item--is-prev"><a class="atom-pagination__trigger" href="?p=1" aria-label="Previous page"></a></li>
<li class="atom-pagination__item"><a class="atom-pagination__trigger" href="?p=1">1</a></li>
<li class="atom-pagination__item atom-pagination__item--is-active"><a class="atom-pagination__trigger" href="?p=2" aria-current="page">2</a></li>
<li class="atom-pagination__item"><a class="atom-pagination__trigger" href="?p=3">3</a></li>
<li class="atom-pagination__item atom-pagination__item--is-next"><a class="atom-pagination__trigger" href="?p=3" aria-label="Next page"></a></li>
</ul>
</nav>
| Classes | atom-pagination |
|---|---|
| Modifiers | atom-pagination__item--is-active atom-pagination__item--is-disabled atom-pagination__item--is-next atom-pagination__item--is-prev |
| Elements | atom-pagination__item atom-pagination__trigger |
| States | :disabled :focus-visible :hover |
| Tokens | 14 component tokens--component-pagination-bg-active --component-pagination-border-color-hover --component-pagination-border-radius --component-pagination-border-width --component-pagination-color --component-pagination-color-active --component-pagination-color-disabled --component-pagination-color-hover --component-pagination-font-family --component-pagination-gap --component-pagination-icon-next --component-pagination-icon-prev --component-pagination-icon-size --component-pagination-item-size |
| Accessibility | Wrap it in <nav aria-label="Pagination">; the current page carries aria-current="page". Prev/next are icon-only: aria-label ("Previous page", "Next page"), no visible text. <a href> for page URLs, <button type="button"> when script loads the results. A dead end uses --is-disabled plus disabled (button) or aria-disabled="true" (link). No JS required. |
| JavaScript | None — CSS only. |
.atom-pillatomsystem-reusablescss/atoms/_pill.scss
Compact uppercase badge for a status, a category or a count. --success/--warning/--danger carry state; --neutral/--subtle/--dark are quiet; --primary/--secondary follow the brand. It is a label, not a button: if it is clickable, use atom-btn. Colour alone never conveys the state — the text has to say it too.
Live demo
Markup
<span class="atom-pill atom-pill--success">Stable</span>
| Classes | atom-pill |
|---|---|
| Modifiers | atom-pill--danger atom-pill--dark atom-pill--neutral atom-pill--primary atom-pill--secondary atom-pill--subtle atom-pill--success atom-pill--warning |
| Elements | — |
| States | — |
| Tokens | 26 component tokens--component-pill-border-radius --component-pill-border-style --component-pill-border-width --component-pill-danger-bg --component-pill-danger-color --component-pill-dark-bg --component-pill-dark-border-color --component-pill-dark-color --component-pill-gap --component-pill-line-height --component-pill-neutral-bg --component-pill-neutral-border-color --component-pill-neutral-color --component-pill-primary-bg --component-pill-primary-border-color --component-pill-primary-color --component-pill-secondary-bg --component-pill-secondary-border-color --component-pill-secondary-color --component-pill-subtle-bg --component-pill-subtle-border-color --component-pill-subtle-color --component-pill-success-bg --component-pill-success-color --component-pill-warning-bg --component-pill-warning-color |
| Accessibility | Static text, no role, not interactive (if it is clickable it is atom-btn). The state is in the words, never only in the colour. A count that updates needs an aria-live container. No JS. |
| JavaScript | None — CSS only. |
.atom-progressatomgenericscss/atoms/_progress.scss
A progress bar on the native <progress>: with value it is determinate, without value it is indeterminate (:indeterminate) and a segment sweeps the track. The fill is primary-strong, measured at 3:1 against the page and the track (WCAG 1.4.11). Under reduced motion the indeterminate bar stops moving and shows static stripes. For an unknown wait with no bar, use atom-spinner.
Live demo
Markup
<label class="atom-label" for="pg-upload">Uploading report.pdf</label><progress class="atom-progress" id="pg-upload" value="40" max="100">40%</progress><label class="atom-label" for="pg-sync">Syncing contacts</label><progress class="atom-progress" id="pg-sync">Syncing…</progress>
| Classes | atom-progress |
|---|---|
| Modifiers | — |
| Elements | — |
| States | — |
| Tokens | 7 component tokens--component-progress-border-radius --component-progress-duration --component-progress-fill --component-progress-height --component-progress-indeterminate-duration --component-progress-stripe-width --component-progress-track |
| Accessibility | Native <progress>: role progressbar, value and max come from the browser; remove value for indeterminate. It needs a name — <label for> on a unique id, or aria-label. The text inside <progress> is the fallback, not the name. Not focusable. Reduced motion: static stripes instead of the sweep. Forced colours: framed track, Highlight fill. No JS — the app updates value. |
| JavaScript | None — CSS only. |
.atom-radioatomsystem-reusablescss/atoms/_radio.scss
One choice out of a mutually exclusive set. Only meaningful in a group sharing a name, and the group needs a fieldset with a legend to be announced as a group. For independent choices use atom-check.
Live demo
Markup
<label class="atom-radio"><input class="atom-radio__input" type="radio" name="theme" value="dark"><span class="atom-radio__label">Dark</span></label>
| Classes | atom-radio |
|---|---|
| Modifiers | — |
| Elements | atom-radio__input atom-radio__label |
| States | :checked :disabled :focus-visible :hover |
| Tokens | 19 component tokens--component-form-field-color --component-form-field-font-size --component-form-field-line-height --component-form-field-shadow-focus --component-form-field-transition-easing --component-radio-bg --component-radio-bg-checked --component-radio-border-color --component-radio-border-color-hover --component-radio-border-radius --component-radio-border-width --component-radio-checked-size --component-radio-dot-border-radius --component-radio-dot-color --component-radio-gap --component-radio-size --component-radio-status-disabled-opacity --component-radio-transition-duration --component-radio-transition-easing |
| Accessibility | Real <input type="radio"> inside the <label>, one name per group, the group in <fieldset><legend>. Arrow keys move the choice (browser). No JS. |
| JavaScript | None — CSS only. |
.atom-skeletonatomgenericscss/atoms/_skeleton.scss
A placeholder with the shape of the content that is on its way: --text (a line; the last of several stops at 60%), --circle (avatar size) and --block. A shimmer sweeps it; under reduced motion it stays flat. It is decoration: hide it from assistive technology and say what is loading in text.
Live demo
Loading profile…
Markup
<p class="syx-sr-only" role="status">Loading profile…</p>
<div class="layout-cluster layout-cluster--md" aria-busy="true">
<span class="atom-skeleton atom-skeleton--circle" aria-hidden="true"></span>
<div class="layout-stack layout-stack--xs syx-flex-1" aria-hidden="true"><span class="atom-skeleton atom-skeleton--text"></span><span class="atom-skeleton atom-skeleton--text"></span></div>
</div>
| Classes | atom-skeleton |
|---|---|
| Modifiers | atom-skeleton--block atom-skeleton--circle atom-skeleton--text |
| Elements | — |
| States | — |
| Tokens | 7 component tokens--component-skeleton-bg --component-skeleton-block-height --component-skeleton-border-radius --component-skeleton-circle-size --component-skeleton-duration --component-skeleton-highlight --component-skeleton-text-height |
| Accessibility | aria-hidden="true" on every skeleton and aria-busy="true" on the container that will be filled; remove aria-busy when the content lands. Say what is loading with text in a role="status" outside the busy container (a busy region may hold its own announcements back). Never focusable. Reduced motion: no shimmer. Forced colours: a border keeps the shape. No JS. |
| JavaScript | None — CSS only. |
.atom-spinneratomgenericscss/atoms/_spinner.scss
A busy indicator for a wait of unknown length: a ring drawn on ::before, so the text does not spin. The text lives in __label, visually hidden by default and shown with --with-label. --sm, md by default, --lg. Under reduced motion it spins three times slower instead of stopping, because a still ring says nothing. If the wait has a known end, use atom-progress.
Live demo
Markup
<span class="atom-spinner" role="status"><span class="atom-spinner__label">Loading results…</span></span><span class="atom-spinner atom-spinner--lg atom-spinner--with-label" role="status"><span class="atom-spinner__label">Saving changes…</span></span>
| Classes | atom-spinner |
|---|---|
| Modifiers | atom-spinner--lg atom-spinner--sm atom-spinner--with-label |
| Elements | atom-spinner__label |
| States | — |
| Tokens | 10 component tokens--component-spinner-color --component-spinner-duration --component-spinner-duration-reduced --component-spinner-gap --component-spinner-label-color --component-spinner-size-lg --component-spinner-size-md --component-spinner-size-sm --component-spinner-thickness --component-spinner-track |
| Accessibility | Always a text in __label: the ring alone says nothing to a screen reader. role="status" announces it politely without moving focus; a live region inserted with its text already inside may not be read, so insert it empty and set the text after, or announce from a status region that already exists. Not focusable. Reduced motion: slower, not stopped. No JS. |
| JavaScript | None — CSS only. |
.atom-statatomsystem-reusablescss/atoms/_stat-counter.scss
One number plus the word that explains it, sized to be read across the page. Use it for a figure that supports an argument. It stops working the moment there are many: four is a row of evidence, twelve is a table.
Live demo
Markup
<div class="atom-stat"><span class="atom-stat__number">19</span><span class="atom-stat__label">Atoms</span></div>
| Classes | atom-stat |
|---|---|
| Modifiers | atom-stat--accent atom-stat--light |
| Elements | atom-stat__label atom-stat__number |
| States | — |
| Tokens | — |
| Accessibility | Static. The explaining word is visible text next to the number; if the number animates, the final value is in the DOM from the start. No JS. |
| JavaScript | None — CSS only. |
.atom-switchatomsystem-reusablescss/atoms/_switch.scss
Binary setting that takes effect on the spot, with no submit step. If the change only lands when a form is sent, the user expects a checkbox — use atom-check.
Live demo
Markup
<label class="atom-switch"><input class="atom-switch__input" type="checkbox" role="switch"><span class="atom-switch__slider" aria-hidden="true"></span><span class="atom-switch__label">Dark mode</span></label>
| Classes | atom-switch |
|---|---|
| Modifiers | — |
| Elements | atom-switch__input atom-switch__label atom-switch__slider |
| States | :checked :disabled :focus-visible :hover |
| Tokens | 19 component tokens--component-form-field-border-width --component-form-field-color --component-form-field-font-size --component-form-field-line-height --component-form-field-shadow-focus --component-form-field-transition-easing --component-switch-bg-off --component-switch-bg-on --component-switch-border-color --component-switch-border-radius --component-switch-height --component-switch-slider-bg --component-switch-slider-radius --component-switch-slider-shadow --component-switch-slider-size --component-switch-status-disabled-opacity --component-switch-transition-duration --component-switch-transition-easing --component-switch-width |
| Accessibility | <input type="checkbox" role="switch"> inside the <label>; the drawn track is aria-hidden. Space toggles and the on/off state is announced from the checkbox. It applies at once, so say what changed if it is not visible. No JS. |
| JavaScript | None — CSS only. |
.atom-tableatomsystem-reusablescss/atoms/_table.scss
Tabular data — rows that compare along shared columns. Wrap it in __container: the wrapper scrolls by itself (--resp only adds spacing and can stand in for it on a plain <div>), and inside it the table keeps a min-width floor so it scrolls instead of collapsing. A bare <table> has no floor, so it compresses instead of widening the page. Never put --resp on the <table>: overflow has no effect on a table box. --is-stick-thead pins the header. Never for layout: if the cells do not share a meaning down the column, it is not a table.
Live demo
| Token |
|---|
| --semantic-color-text-primary |
Markup
<div class="atom-table__container atom-table--resp" role="region" aria-label="Semantic tokens" tabindex="0">
<table class="atom-table atom-table--primary">
<thead>
<tr>
<th class="atom-table__th" scope="col">Token</th>
</tr>
</thead>
<tbody>
<tr>
<td class="atom-table__td">--semantic-color-text-primary</td>
</tr>
</tbody>
</table>
</div>
| Classes | atom-table |
|---|---|
| Modifiers | atom-table--is-stick-thead atom-table--primary atom-table--resp |
| Elements | atom-table__container atom-table__td atom-table__th |
| States | :focus-visible :hover |
| Tokens | 18 component tokens--component-table-bg-tfoot --component-table-bg-th --component-table-border-color --component-table-border-width --component-table-cell-padding-x --component-table-cell-padding-y --component-table-color-th --component-table-min-width --component-table-state-hover-bg --component-table-state-hover-border-color --component-table-state-hover-color --component-table-state-hover-shadow --component-table-sticky-active-bg --component-table-sticky-active-color --component-table-sticky-max-height --component-table-th-font-family --component-table-th-font-size --component-table-th-line-height |
| Accessibility | Native <table> with <th scope> headers; name it with <caption> or aria-label. The scrolling element is the wrapper <div>: give it role="region", an aria-label and tabindex="0" so it can be scrolled from the keyboard (it takes the system focus ring). No JS. |
| JavaScript | None — CSS only. |
.atom-titleatomsystem-reusablescss/atoms/_title.scss
Headline styling detached from document level: --h1 to --h6 set size only. Pick the heading tag from the document outline and the modifier from the visual hierarchy — they do not have to match, and forcing them to is how outlines get broken to win a size.
Live demo
Markup
<h2 class="atom-title atom-title--h3">Token architecture</h2>
| Classes | atom-title |
|---|---|
| Modifiers | atom-title--h1 atom-title--h2 atom-title--h3 atom-title--h4 atom-title--h5 atom-title--h6 |
| Elements | — |
| States | — |
| Tokens | 11 component tokens--component-title-color --component-title-font-family --component-title-font-weight --component-title-h1 --component-title-h2 --component-title-h3 --component-title-h4 --component-title-h5 --component-title-h6 --component-title-line-height --component-title-margin-bottom |
| Accessibility | Pick the heading level from the document outline; the modifier only sets the size. Never skip levels to get a size. No JS. |
| JavaScript | None — CSS only. |
.atom-txtatomsystem-reusablescss/atoms/_txt.scss
Body copy. --primary only sets the bottom margin of a paragraph in a stack; colour, size and measure come from the semantic type scale, so there is nothing to configure here. If a paragraph needs to look secondary, that is a token role, not a modifier of this atom.
Live demo
SYX is a token-driven, native SCSS design system.
Markup
<p class="atom-txt atom-txt--primary">SYX is a token-driven, native SCSS design system.</p>
| Classes | atom-txt |
|---|---|
| Modifiers | atom-txt--primary |
| Elements | — |
| States | — |
| Tokens | — |
| Accessibility | Static body text. No role, no JS. |
| JavaScript | None — CSS only. |
.mol-alertmoleculegenericscss/molecules/_alert.scss
A status message inside the page flow: --info, --success, --warning, --error. The state colours the accent rule and the title (state-*-text, measured at 4.5:1 on its background); the body stays in primary text. Use role="status" for information and role="alert" only for errors that need attention now; the role must be in the DOM before the text arrives.
Live demo
Your plan expires in 3 days
Renew it to keep your history.
Markup
<div class="mol-alert mol-alert--warning" role="status">
<p class="mol-alert__title">Your plan expires in 3 days</p>
<p class="mol-alert__body">Renew it to keep your history.</p>
</div>
| Classes | mol-alert |
|---|---|
| Modifiers | mol-alert--error mol-alert--info mol-alert--success mol-alert--warning |
| Elements | mol-alert__body mol-alert__title |
| States | — |
| Tokens | 16 component tokens--component-alert-accent-width --component-alert-bg --component-alert-border-radius --component-alert-color --component-alert-error-accent --component-alert-error-title-color --component-alert-gap --component-alert-info-accent --component-alert-info-title-color --component-alert-padding-block --component-alert-padding-inline --component-alert-success-accent --component-alert-success-title-color --component-alert-title-font-weight --component-alert-warning-accent --component-alert-warning-title-color |
| Accessibility | role="status" (polite) for information; role="alert" only for an error that needs attention now. The live region must be in the DOM before its text changes. Not focusable. No JS for a static message. |
| JavaScript | None — CSS only. |
.mol-btn-groupmoleculesystem-reusablescss/molecules/_btn-group.scss
Row of related actions with the standard gap between them, wrapping on narrow viewports. It arranges buttons; it does not rank them — the hierarchy is the buttons' own modifiers, and a group where every button is --primary has no primary action. .cont-set-btn is a deprecated alias.
Live demo
Markup
<div class="mol-btn-group"><button class="atom-btn atom-btn--primary atom-btn--size-md" type="button">Save</button><button class="atom-btn atom-btn--secondary atom-btn--size-md" type="button">Cancel</button></div>
| Classes | mol-btn-group |
|---|---|
| Modifiers | — |
| Elements | — |
| States | — |
| Tokens | 1 component tokens--component-btn-group-gap |
| Accessibility | Every button inside has an explicit type. As a plain row it needs no role; if it behaves as one control, it becomes role="toolbar" with an aria-label and arrow-key navigation in script. No JS otherwise. |
| JavaScript | None — CSS only. |
.mol-cardmoleculegenericscss/molecules/_card.scss
The general-purpose surface for product screens: optional __media, __header (with __title and maybe a pill or an action), __body and __footer, spaced by the card itself. Bordered by default; --elevated swaps the border for a shadow and --flat drops both on bg-secondary. For a whole-card link, put .mol-card__link on the title's <a>: its ::after covers the card, the focus ring surrounds the card, and any other control inside stays clickable above it. Body text is text-secondary, measured on both backgrounds.
Live demo
You have used 8 of 10 GB. Old exports are deleted after 30 days.
Invoices, payment method and tax details.
Markup
<article class="mol-card">
<header class="mol-card__header">
<h3 class="mol-card__title">Storage</h3>
<span class="atom-pill atom-pill--neutral">80% used</span>
</header>
<div class="mol-card__body">
<p>You have used 8 of 10 GB. Old exports are deleted after 30 days.</p>
</div>
<footer class="mol-card__footer"><button class="atom-btn atom-btn--secondary atom-btn--size-sm" type="button">Manage storage</button></footer>
</article>
<article class="mol-card mol-card--elevated">
<h3 class="mol-card__title"><a class="mol-card__link" href="#billing">Billing</a></h3>
<div class="mol-card__body">
<p>Invoices, payment method and tax details.</p>
</div>
</article>
| Classes | mol-card |
|---|---|
| Modifiers | mol-card--elevated mol-card--flat |
| Elements | mol-card__body mol-card__footer mol-card__header mol-card__link mol-card__media mol-card__title |
| States | :focus-visible :hover |
| Tokens | 17 component tokens--component-card-bg --component-card-body-color --component-card-border-color --component-card-border-color-hover --component-card-border-radius --component-card-border-width --component-card-color --component-card-elevated-shadow --component-card-elevated-shadow-hover --component-card-flat-bg --component-card-footer-border-color --component-card-gap --component-card-padding --component-card-shadow --component-card-title-font-size --component-card-title-font-weight --component-card-title-line-height |
| Accessibility | <article> when the card makes sense on its own (an item in a list), <div> otherwise; the page decides the heading level of __title. Whole-card link: the <a class="mol-card__link"> goes in the title so its name is the title, never an <a> wrapping the whole card (its name would be all the text). The ring is drawn on the link's ::after, around the card. Other controls inside are raised above the link area. Forced colours: every variant gets a visible frame. No JS. |
| JavaScript | None — CSS only. |
.mol-code-snippetmoleculedomain-reusablescss/molecules/_code-snippet.scss
A code block with a header that judges it: --ok for the correct way, --ko for the rejected one, --neutral for a plain sample. Built for teaching a rule by showing both sides, so a lone --neutral snippet is usually just atom-code. The label has to state the verdict in words — the colour is not the message.
Live demo
color: var(--semantic-color-text-primary);Markup
<div class="mol-code-snippet mol-code-snippet--ok">
<div class="mol-code-snippet__header"><span class="mol-code-snippet__label">Correct</span></div>
<pre class="mol-code-snippet__body atom-code" tabindex="0" role="group" aria-label="Correct example"><code>color: var(--semantic-color-text-primary);</code></pre>
</div>
| Classes | mol-code-snippet |
|---|---|
| Modifiers | mol-code-snippet--ko mol-code-snippet--neutral mol-code-snippet--ok |
| Elements | mol-code-snippet__body mol-code-snippet__filename mol-code-snippet__header mol-code-snippet__label |
| States | :focus-visible |
| Tokens | 15 component tokens--component-code-snippet-border-color --component-code-snippet-border-radius --component-code-snippet-border-style --component-code-snippet-border-width --component-code-snippet-header-bg --component-code-snippet-header-border-width --component-code-snippet-ko-border-color --component-code-snippet-ko-header-bg --component-code-snippet-ko-header-border-color --component-code-snippet-ko-label-color --component-code-snippet-ok-border-color --component-code-snippet-ok-header-bg --component-code-snippet-ok-header-border-color --component-code-snippet-ok-label-color --component-code-snippet-shadow |
| Accessibility | The header label states the verdict in words. A body that scrolls sideways needs tabindex="0". No JS. |
| JavaScript | None — CSS only. |
.mol-dialogmoleculegenericscss/molecules/_dialog.scss
A modal on the native <dialog>. The usage is the closed markup: open it from script with dialog.showModal() — never with the open attribute, which gives no top layer, no focus trap, no Escape and no inert page. Enters with @starting-style; no transition under reduced motion. For destructive confirmations use role="alertdialog", point aria-describedby at the body and autofocus the cancel button.
Live demo
Markup
<dialog class="mol-dialog" role="alertdialog" aria-labelledby="dlg-title" aria-describedby="dlg-desc">
<form method="dialog" class="mol-dialog__inner">
<h2 class="mol-dialog__title" id="dlg-title">Delete “Report”?</h2>
<p class="mol-dialog__body" id="dlg-desc">Files and history will be lost.</p>
<div class="mol-dialog__footer"><button class="atom-btn atom-btn--secondary atom-btn--size-md" value="cancel" autofocus>Cancel</button><button class="atom-btn atom-btn--danger atom-btn--filled atom-btn--size-md" value="ok">Delete</button></div>
</form>
</dialog>
| Classes | mol-dialog |
|---|---|
| Modifiers | — |
| Elements | mol-dialog__body mol-dialog__footer mol-dialog__inner mol-dialog__title |
| States | — |
| Tokens | 14 component tokens--component-dialog-backdrop --component-dialog-bg --component-dialog-border-color --component-dialog-border-radius --component-dialog-border-width --component-dialog-color --component-dialog-duration --component-dialog-easing --component-dialog-gap --component-dialog-max-width --component-dialog-padding --component-dialog-shadow --component-dialog-title-font-size --component-dialog-title-line-height |
| Accessibility | Native <dialog> opened with showModal() — the browser gives top layer, focus trap, Escape and an inert page; the open attribute gives none of it. aria-labelledby → title, aria-describedby → body. Destructive confirmation: role="alertdialog" and autofocus on Cancel. <form method="dialog"> buttons close it and return their value; focus goes back to the opener. JS: the showModal() call. |
| JavaScript | None — CSS only. |
.mol-disclosuremoleculegenericscss/molecules/_disclosure.scss
Show and hide on native <details>/<summary>: keyboard and expanded state come from the browser. Several with the same name="" form an exclusive accordion. Never replace <summary> with a <div>.
Live demo
Markup
<details class="mol-disclosure" name="faq">
<summary class="mol-disclosure__summary">Can I change plans?</summary>
<div class="mol-disclosure__content">Yes, whenever you want.</div>
</details>
| Classes | mol-disclosure |
|---|---|
| Modifiers | — |
| Elements | mol-disclosure__content mol-disclosure__summary |
| States | :focus-visible |
| Tokens | 9 component tokens--component-disclosure-border-color --component-disclosure-border-width --component-disclosure-content-color --component-disclosure-content-padding-block-end --component-disclosure-duration --component-disclosure-marker-size --component-disclosure-summary-color --component-disclosure-summary-font-weight --component-disclosure-summary-padding-block |
| Accessibility | Native <details>/<summary>: Enter/Space toggle and the expanded state is announced by the browser. No interactive content inside <summary>, and never a <div> in its place. No JS. |
| JavaScript | None — CSS only. |
.mol-empty-statemoleculegenericscss/molecules/_empty-state.scss
What shows where there is nothing yet — an empty list, a search with no results: a decorative __icon, a __title that says what is going on, a __text that says why or what to do, and __actions with the way out. Centred, because it takes the place of the missing content. --bordered frames it with a dashed border for a zone that waits for content (a drop target, an empty column). Title and text are measured at 4.5:1.
Live demo
Try another word, or clear the filters to see every project.
Markup
<div class="mol-empty-state mol-empty-state--bordered">
<span class="mol-empty-state__icon" aria-hidden="true"><span class="atom-icon atom-icon--lc-search atom-icon--lg"></span></span>
<h3 class="mol-empty-state__title">No projects match “q4”</h3>
<p class="mol-empty-state__text">Try another word, or clear the filters to see every project.</p>
<div class="mol-empty-state__actions"><button class="atom-btn atom-btn--secondary atom-btn--size-md" type="button">Clear filters</button><button class="atom-btn atom-btn--primary atom-btn--filled atom-btn--size-md" type="button">New project</button></div>
</div>
| Classes | mol-empty-state |
|---|---|
| Modifiers | mol-empty-state--bordered |
| Elements | mol-empty-state__actions mol-empty-state__icon mol-empty-state__text mol-empty-state__title |
| States | — |
| Tokens | 14 component tokens--component-empty-state-border-color --component-empty-state-border-radius --component-empty-state-border-width --component-empty-state-gap --component-empty-state-icon-bg --component-empty-state-icon-color --component-empty-state-icon-size --component-empty-state-padding-block --component-empty-state-padding-inline --component-empty-state-text-color --component-empty-state-text-max-width --component-empty-state-title-color --component-empty-state-title-font-size --component-empty-state-title-font-weight |
| Accessibility | The icon is decorative (aria-hidden="true"); the title says what happened, at the heading level the page needs. If the empty state appears after an action (filter, search), announce it: put it inside a role="status" container that already existed, or move focus to the title (tabindex="-1") when it replaces what had focus. Not focusable itself; its buttons are. No JS. |
| JavaScript | None — CSS only. |
.mol-feature-cardmoleculesystem-reusablescss/molecules/_feature-card.scss
One capability, stated once, inside a set whose members carry equal weight. It composes atom-feature-icon, a title and a body, and every card in the grid looks the same on purpose — the design asserts that no item outranks another. Wrong choice when the content has editorial hierarchy, when one item must dominate the others, or when the card carries article metadata (author, date, read time): use a layout that ranks, not a grid that levels.
Live demo
Four tiers, one direction.
Learn moreMarkup
<article class="mol-feature-card">
<span class="atom-feature-icon atom-feature-icon--token" aria-hidden="true"></span>
<h3 class="mol-feature-card__title">Token system</h3>
<p class="mol-feature-card__body">Four tiers, one direction.</p>
<a class="mol-feature-card__cta atom-link" href="/tokens">Learn more</a>
</article>
| Classes | mol-feature-card |
|---|---|
| Modifiers | — |
| Elements | mol-feature-card__body mol-feature-card__cta mol-feature-card__title |
| States | :hover |
| Tokens | 10 component tokens--component-feature-card-bg --component-feature-card-border-color --component-feature-card-border-color-hover --component-feature-card-border-radius --component-feature-card-border-style --component-feature-card-border-width --component-feature-card-hover-lift --component-feature-card-row-span --component-feature-card-shadow --component-feature-card-shadow-hover |
| Accessibility | <article> with a real heading. Only the CTA link is focusable (system ring); the card mirrors it with :focus-within. Icon aria-hidden. No JS. |
| JavaScript | None — CSS only. |
.mol-form-field-setmoleculesystem-reusablescss/molecules/_form-field-set.scss
Stacks related mol-form-field with the standard gap; --inline flows them in a row. Grouping for rhythm only — if the fields answer one question together, they also need a real fieldset and legend so the grouping is announced, not just seen.
Live demo
Markup
<div class="mol-form-field-set mol-form-field-set--inline">
<div class="mol-form-field"><label class="atom-label" for="ffs-given">First name</label><input class="atom-input" id="ffs-given" type="text" autocomplete="given-name"></div>
<div class="mol-form-field"><label class="atom-label" for="ffs-family">Last name</label><input class="atom-input" id="ffs-family" type="text" autocomplete="family-name"></div>
</div>
| Classes | mol-form-field-set |
|---|---|
| Modifiers | mol-form-field-set--inline |
| Elements | — |
| States | — |
| Composes | mol-form-field |
| Tokens | 2 component tokens--component-form-field-set-min-width --component-form-group-gap |
| Accessibility | Visual grouping only. If the fields answer one question together, wrap them in <fieldset><legend> so the group is announced. Each field keeps its own <label>. No JS. |
| JavaScript | None — CSS only. |
.mol-form-fieldmoleculesystem-reusablescss/molecules/_form-field.scss
The complete field: label, control, state and message. Use this rather than the bare atoms whenever the field can be wrong — --is-error/--is-success/--is-warning set the state and __msg carries the explanation. Wire the message with aria-describedby and mark the invalid control aria-invalid; the modifier paints, it does not announce. A textarea with maxlength can carry an optional __count: js/syx-char-count.js writes what is left (data-template, in the page's language), adds is-limit at the limit and announces it once typing pauses; without JS the markup text stays.
Live demo
Markup
<div class="mol-form-field mol-form-field--is-error"><label class="atom-label" for="ff-email">Email</label><input class="atom-input" id="ff-email" type="email" autocomplete="email" aria-invalid="true" aria-describedby="ff-email-msg"><span class="mol-form-field__msg" id="ff-email-msg">Enter a valid address.</span></div>
<div class="mol-form-field"><label class="atom-label" for="ff-bio">Short bio</label><textarea class="atom-textarea" id="ff-bio" rows="3" maxlength="160" aria-describedby="ff-bio-count"></textarea><span class="mol-form-field__count" id="ff-bio-count" data-template="{n} characters left">Up to 160 characters</span></div>
| Classes | mol-form-field |
|---|---|
| Modifiers | mol-form-field--has-select mol-form-field--is-error mol-form-field--is-success mol-form-field--is-warning |
| Elements | mol-form-field__count mol-form-field__live mol-form-field__msg |
| States | .is-limit |
| Composes | atom-input atom-label atom-select atom-textarea |
| Tokens | 18 component tokens--component-form-field-color --component-form-field-count-color --component-form-field-count-font-size --component-form-field-count-limit-color --component-form-field-error-border --component-form-field-error-color --component-form-field-error-shadow-focus --component-form-field-icon-position-right --component-form-field-icon-size --component-form-field-label-margin-bottom --component-form-field-margin-bottom --component-form-field-msg-font-size --component-form-field-msg-line-height --component-form-field-msg-margin-top --component-form-field-success-border --component-form-field-success-color --component-form-field-warning-border --component-form-field-warning-color |
| Accessibility | <label for> on a unique id. On error: aria-invalid="true" on the control and aria-describedby pointing at __msg — the modifier paints, the attributes announce. Character count: the textarea names __count in aria-describedby (heard on focus) and keeps maxlength; js/syx-char-count.js (no dependencies) adds a role="status" region that says what is left after a pause, never on every key. Validation itself is the app's. |
| JavaScript | js/syx-char-count.js |
.mol-label-groupmoleculesystem-reusablescss/molecules/_label-group.scss
Row of labels or pills sharing one gap, wrapping when it must. Use it for a tag list or a set of markers on one line. .cont-set-label is a deprecated alias.
Live demo
Markup
<div class="mol-label-group"><span class="atom-pill atom-pill--neutral">SCSS</span><span class="atom-pill atom-pill--neutral">Tokens</span></div>
| Classes | mol-label-group |
|---|---|
| Modifiers | — |
| Elements | — |
| States | — |
| Tokens | 1 component tokens--component-label-group-gap |
| Accessibility | Container only. If the labels are a list, use <ul role="list">. No JS. |
| JavaScript | None — CSS only. |
.mol-tabsmoleculegenericscss/molecules/_tabs.scss
Tabbed panels whose state lives in ARIA: the active tab has aria-selected="true", hidden panels carry hidden. Behaviour (click, arrows, Home/End, roving tabindex) comes from js/syx-tabs.js, dependency-free: import 'syx-design-system/js/syx-tabs.js' wires every tablist present on load, and initTabs(root) from the same module wires markup rendered later. Without it the selected panel shows and the tabs never switch.
Live demo
Markup
<div class="mol-tabs">
<div class="mol-tabs__list" role="tablist" aria-label="Settings"><button class="mol-tabs__tab" type="button" role="tab" id="tab-1" aria-selected="true" aria-controls="panel-1">Profile</button><button class="mol-tabs__tab" type="button" role="tab" id="tab-2" aria-selected="false" aria-controls="panel-2" tabindex="-1">Notifications</button></div>
<div class="mol-tabs__panel" role="tabpanel" id="panel-1" aria-labelledby="tab-1" tabindex="0">Profile settings</div>
<div class="mol-tabs__panel" role="tabpanel" id="panel-2" aria-labelledby="tab-2" tabindex="0" hidden>Notification settings</div>
</div>
| Classes | mol-tabs |
|---|---|
| Modifiers | — |
| Elements | mol-tabs__list mol-tabs__panel mol-tabs__tab |
| States | :focus-visible :hover |
| Tokens | 12 component tokens--component-tabs-border-color --component-tabs-border-width --component-tabs-gap --component-tabs-indicator-color --component-tabs-indicator-width --component-tabs-panel-padding-block --component-tabs-tab-color --component-tabs-tab-color-hover --component-tabs-tab-color-selected --component-tabs-tab-font-weight --component-tabs-tab-padding-block --component-tabs-tab-padding-inline |
| Accessibility | role tablist/tab/tabpanel, aria-selected, aria-controls and aria-labelledby; hidden on inactive panels; type="button" on every tab; panels focusable (tabindex="0"). Arrows, Home/End and roving tabindex come from js/syx-tabs.js (no dependencies); without it every panel stays reachable. |
| JavaScript | js/syx-tabs.js |
.mol-toastmoleculegenericscss/molecules/_toast.scss
Ephemeral notifications in a corner that confirm what just happened ("Saved", "Link copied") without interrupting or taking focus. The .mol-toast container is fixed and stacks __item elements (--info, --success, --warning, --error colour only the accent rule; text stays primary). js/syx-toast.js: showToast('Saved', { tone, duration }) creates one, announces only its text through the container's __live region (role="status"), removes it after 6 s by default and pauses the countdown while the pointer or focus is on it or the tab is hidden; duration: 0 keeps it until dismissed. A button with data-syx-toast="Text" shows one without writing JS. Never the only place for something that must be read or done: an error that needs attention is mol-alert with role="alert".
Live demo
3 files moved to Archive
Markup
<button class="atom-btn atom-btn--secondary atom-btn--size-md" type="button" data-syx-toast="Link copied to the clipboard" data-syx-toast-tone="success">Copy link</button>
<div class="mol-toast" data-dismiss-label="Dismiss notification">
<p class="mol-toast__live" role="status"></p>
<div class="mol-toast__item mol-toast__item--info">
<p class="mol-toast__text">3 files moved to Archive</p>
<button class="mol-toast__close" type="button" aria-label="Dismiss notification"><span class="atom-icon atom-icon--lc-x atom-icon--sm" aria-hidden="true"></span></button>
</div>
</div>
| Classes | mol-toast |
|---|---|
| Modifiers | mol-toast__item--error mol-toast__item--info mol-toast__item--success mol-toast__item--warning |
| Elements | mol-toast__close mol-toast__item mol-toast__live mol-toast__text |
| States | .is-leaving :focus-visible :hover |
| Tokens | 21 component tokens--component-toast-accent-width --component-toast-bg --component-toast-border-color --component-toast-border-radius --component-toast-border-width --component-toast-close-bg-hover --component-toast-close-color --component-toast-close-size --component-toast-color --component-toast-duration --component-toast-error-accent --component-toast-gap --component-toast-info-accent --component-toast-max-width --component-toast-offset --component-toast-padding-block --component-toast-padding-inline --component-toast-shadow --component-toast-success-accent --component-toast-warning-accent --component-toast-z-index |
| Accessibility | The container holds a __live element with role="status" that exists before any text arrives; js/syx-toast.js writes only the message there, so the close button is not read out. Focus never moves to a toast; its close button (aria-label, 32px target) is reachable with Tab, and closing it returns focus to where the user came from. The countdown pauses on hover, on focus and while the tab is hidden (WCAG 2.2.1); duration: 0 disables it. Reduced motion: no slide or fade. Without JS a static toast stays and its button does nothing. |
| JavaScript | js/syx-toast.js |
.mol-tooltipmoleculegenericscss/molecules/_tooltip.scss
A short sentence that describes a control, on the Popover API: the __bubble is popover="hint" (browsers that do not know hint treat it as manual), so it sits in the top layer above any overflow or z-index. js/syx-tooltip.js opens it on keyboard focus and on hover, keeps it while the pointer is over the control or the bubble, closes it on Escape, blur or leaving, and places it above the control — below if it does not fit — never over it. Position comes from script because CSS anchor positioning is not in the support floor. Colours are text-primary and bg-primary swapped, so contrast holds wherever primary text does. Plain text only.
Live demo
Markup
<span class="mol-tooltip"><button class="atom-btn atom-btn--secondary atom-btn--size-md" type="button" aria-describedby="tip-share">Share</button><span class="mol-tooltip__bubble" id="tip-share" role="tooltip" popover="hint">Copies a view-only link to the clipboard</span></span>
| Classes | mol-tooltip |
|---|---|
| Modifiers | — |
| Elements | mol-tooltip__bubble |
| States | — |
| Tokens | 11 component tokens--component-tooltip-bg --component-tooltip-border-radius --component-tooltip-color --component-tooltip-duration --component-tooltip-font-size --component-tooltip-line-height --component-tooltip-max-width --component-tooltip-padding-block --component-tooltip-padding-inline --component-tooltip-shadow --component-tooltip-z-index |
| Accessibility | aria-describedby on the control points at the bubble, which carries role="tooltip"; for an icon-only control the bubble is its name, so use aria-labelledby instead. WCAG 1.4.13: appears on focus and hover, hoverable (the pointer can move onto it), dismissed by Escape without moving focus or pointer, never covers the control. Nothing focusable inside. Never on a disabled control (no focus, no pointer events). JS: js/syx-tooltip.js (no dependencies); without it the bubble never opens, so do not put essential information only there. |
| JavaScript | js/syx-tooltip.js |
.org-app-shellorganismgenericscss/organisms/_app-shell.scss
The frame of an application screen (settings, a dashboard, a console): __header on top, a __sidebar with the section navigation and __main, laid out with named grid areas. Below 50em everything stacks and the sidebar's __nav becomes a horizontally scrolling row — no drawer, no JS; from 50em the sidebar is a column beside the content. --full fills the viewport and keeps the header and the sidebar in place while the content scrolls. The current section is painted from aria-current="page" (colour, background and an inline-start rule that survives forced colours). Inside __main, layout-stack and layout-cluster arrange sections and actions.
Live demo
Workspace name, members and notifications.
Markup
<div class="org-app-shell">
<header class="org-app-shell__header">
<span class="org-app-shell__brand">Acme Console</span>
<div class="org-app-shell__actions"><span class="atom-avatar atom-avatar--sm" role="img" aria-label="Signed in as Ana López">AL</span></div>
</header>
<nav class="org-app-shell__sidebar" aria-label="Sections">
<ul class="org-app-shell__nav">
<li><a class="org-app-shell__link" href="#overview">Overview</a></li>
<li><a class="org-app-shell__link" href="#settings" aria-current="page">Settings</a></li>
<li><a class="org-app-shell__link" href="#billing">Billing</a></li>
</ul>
</nav>
<main class="org-app-shell__main">
<div class="layout-stack">
<h2 class="atom-title atom-title--h4">Settings</h2>
<p class="atom-txt">Workspace name, members and notifications.</p>
<div class="layout-cluster layout-cluster--end"><button class="atom-btn atom-btn--secondary atom-btn--size-md" type="button">Cancel</button><button class="atom-btn atom-btn--primary atom-btn--filled atom-btn--size-md" type="button">Save</button></div>
</div>
</main>
</div>
| Classes | org-app-shell |
|---|---|
| Modifiers | org-app-shell--full |
| Elements | org-app-shell__actions org-app-shell__brand org-app-shell__header org-app-shell__link org-app-shell__main org-app-shell__nav org-app-shell__sidebar |
| States | :focus-visible :hover |
| Tokens | 23 component tokens--component-app-shell-bg --component-app-shell-border-color --component-app-shell-border-width --component-app-shell-brand-font-weight --component-app-shell-color --component-app-shell-header-bg --component-app-shell-header-height --component-app-shell-header-padding-inline --component-app-shell-link-bg-current --component-app-shell-link-bg-hover --component-app-shell-link-border-radius --component-app-shell-link-color --component-app-shell-link-color-current --component-app-shell-link-color-hover --component-app-shell-link-indicator-color --component-app-shell-link-indicator-width --component-app-shell-link-min-height --component-app-shell-link-padding-inline --component-app-shell-main-padding --component-app-shell-sidebar-bg --component-app-shell-sidebar-padding --component-app-shell-sidebar-width --component-app-shell-z-index |
| Accessibility | Three landmarks a screen-reader user jumps between: <header>, <nav aria-label> and <main> — one <main> per page (the docs demo nests it inside the docs page's own, which a real app never does). The current section carries aria-current="page"; the CSS paints it from that attribute. Sidebar links are 44px tall; the scrolling row on small screens is padded so the focus ring is never clipped. No JS. |
| JavaScript | None — CSS only. |
.org-site-headerorganismdomain-reusablescss/organisms/_site-header.scss
Mobile-first sticky header with a hamburger that opens a nav drawer over an overlay. The header, drawer and overlay are three sibling elements and all three must be present; the burger names the drawer with aria-controls. js/syx-site-nav.js (no dependencies) toggles [data-nav-open] on <body> and manages focus and inert. The __theme-select and __darkmode controls are documentation-site furniture — a product that ships no theme picker should drop them rather than hide them.
Live demo
Markup
<header class="org-site-header">
<a class="org-site-header__brand" href="/">SYX</a>
<nav class="org-site-header__nav" aria-label="Main navigation"><a href="/docs">Docs</a><a href="/themes">Themes</a></nav>
<div class="org-site-header__actions"><button class="org-site-header__burger" type="button" aria-controls="site-nav-drawer" aria-expanded="false" aria-label="Open navigation"><span class="org-site-header__burger-line" aria-hidden="true"></span><span class="org-site-header__burger-line" aria-hidden="true"></span><span class="org-site-header__burger-line" aria-hidden="true"></span></button></div>
</header>
<div class="org-site-nav-overlay" aria-hidden="true"></div>
<nav class="org-site-nav-drawer" id="site-nav-drawer" aria-label="Mobile navigation"><a class="org-site-nav-drawer__link" href="/docs">Docs</a><a class="org-site-nav-drawer__link" href="/themes">Themes</a></nav>
| Classes | org-site-header org-site-nav-drawer org-site-nav-overlay |
|---|---|
| Modifiers | — |
| Elements | org-site-header__actions org-site-header__brand org-site-header__burger org-site-header__burger-line org-site-header__darkmode org-site-header__nav org-site-header__theme-select org-site-nav-drawer__link org-site-nav-drawer__theme |
| States | .is-scrolled :focus-visible :hover |
| Composes | atom-btn atom-icon atom-select |
| Tokens | 7 component tokens--component-header-control-border-width --component-header-control-height --component-header-control-radius --component-header-overlay-z-index --component-header-z-index --component-section-padding-inline --component-sidenav-z-index |
| Accessibility | <header> with a labelled desktop <nav> and a separately labelled drawer <nav>. Burger: <button type="button"> with aria-controls (drawer id), aria-expanded and aria-label. js/syx-site-nav.js: closed drawer is inert (and visibility: hidden in CSS); opening moves focus to its first link and makes the rest of the page inert; Escape or the burger closes it and returns focus to the burger. The overlay is aria-hidden. |
| JavaScript | js/syx-site-nav.js |