Skip to main content
Component Gallery v5.1.0

SYX Design System

Ordered by layer hierarchy: utilities → layout → atoms → molecules → organisms.

7 layers
1,127 tokens
7 themes
WCAG AA
Zero dependencies

Authoring Guide

Practical decision rules for writing HTML and SCSS with SYX. Read this before creating any component.

Where does my code go? — Decision flowchart

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 "I need a button" → atom-btn
atom "Button with icon" → atom-btn + atom-icon composed in HTML, not a new mol-
mol "Search field" → mol-search (input + icon + btn always together)
"Site header" → org-site-header
util "Center a div on this page" → .syx-mx-auto or .syx-text-center

HTML Authoring

2.1 — Semantics first

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>

2.2 — BEM in HTML

  • Block + modifier always together. Never write a modifier without the base block class: atom-btn atom-btn--primary ✅ — atom-btn--primary alone ❌
  • Max 2 BEM depth levels. If you need __element__sub, those are two independent elements.
  • Dynamic states → 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 -->

2.3 — Compose in HTML, not in SCSS

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.

2.4 — Accessibility (required)

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>

2.5 — Layout Grid

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>

SCSS for efficient CSS output

Every SCSS decision carries a cost in the compiled CSS. These rules keep the output lean.

3.1 — Max 3 nesting levels

✅ 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

3.2 — The @mixin wrapper pattern (mandatory)

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 { … }

3.3 — Null-safe shorthand

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;

3.4 — Token → CSS output

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 ✅

3.5 — Property order inside a rule

.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 { … }
}

Mixin Deep-Dive

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(); }

Tips, Patterns & Gotchas

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 -->

Quick decision checklist

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

Agentic Design System

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.

1 — Ask: the MCP server and the Node API

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.

2 — Write: graded trust

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
  • Anything unmatched falls to human-only. The default errs toward the side that only costs a wait.
  • The rules and the guards are human-only on purpose. An agent that could rewrite the rule it is judged by, or the guard that judges it, would not have permissions — it would have a suggestion.

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

3 — Detect: drift in a consuming app

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

  • It never shouts at an example. Everything inside <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.
  • It fixes nothing. A scanner that also repairs is a scanner you must trust before you've read it. What it finds goes through the proposal path above, or through someone's hands.

4 — What runs on its own

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.

5 — What this buys you

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

  • Agents that don't get it wrong. Several of the bugs in this very layer were found by its own guards: a 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.
  • Taste. Whether a colour is the right colour, whether a component should exist, what the hierarchy of a page should be. The tiers say so out loud: primitives, semantics and themes are human-only, and anything unclassified defaults there too.

Who it pays off for

  • Whoever reviews. A proposal arrives compiled, validated and with the evidence attached, instead of a paragraph claiming it works.
  • Whoever consumes. An app can ask the version it has installed — the only comparison that means anything — and find out it drifted before a user does.
  • Whoever maintains. Eleven guards run on every push. The registry, the token snapshot and the component inventory are generated from the code, so they cannot quietly disagree with it.

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.

6 — Four worked examples

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.

6.1 — An agent asks before it writes

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.

6.2 — A proposal, end to end

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)

6.3 — What it refuses, and what it says instead

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.

6.4 — Auditing a page nobody was watching

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.

7 — Out to Figma

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 Mode System

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.

1 — The ten modes

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.

2 — Two doors, one room

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.

3 — Composing modes

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

4 — Every mode opens with two blocks

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.

5 — The engine and the cortex

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.

Utilities

@layer syx.utilities — highest specificity. Always wins over atoms and molecules.

Typography Scale — .syx-type-*

.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 category

Text utilities

syx-text-center
syx-text-uppercase
syx-font-bold
syx-font-medium
syx-text-underline
syx-text-strikethrough
syx-max-w-50ch

Text Colors — .syx-text-*

.syx-text-primary .syx-text-secondary .syx-text-gray .syx-text-muted .syx-text-error .syx-text-success .syx-text-warning .syx-text-white .syx-text-inverse

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

Backgrounds — .syx-bg-* · .syx-bg-color-*

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).

bg-white
bg-gray-50
bg-gray-100
bg-primary
bg-primary-light
bg-dark
bg-error
bg-success
bg-warning
bg-info

Theme-aware helpers (_backgrounds.scss)

.syx-bg-color-primary
.syx-bg-color-secondary
.syx-bg-color-black
.syx-bg-color-blue-facebook
.syx-bg-color-blue-twitter
.syx-bg-color-blue-instagram
.syx-bg-color-green-whatsapp

Spacing — .syx-m/p-*

Margin bottom scale (0–5)

mb-0
mb-1
mb-2
mb-3
mb-4
mb-5

Padding scale (1–5) · shorthands

p-1
p-2
p-3
p-4
p-5
px-3 py-1
mx-auto

Display · Flex · Gap · Position

.syx-d-flex + gaps + wrap

Pill

.syx-justify-between + .syx-items-center

Section title

.syx-d-grid + .syx-gap-3

Col 1
Col 2
Col 3
Col 4

Media — img-fluid · aspect · embed · object-fit

.syx-img-fluid

Responsive demo

.syx-embed--16by9

.syx-aspect-* + .syx-obj-cover — the same 4:3 image, ratio set on the element (no wrapper)

.syx-aspect-16by9 · 16:9
.syx-aspect-4by3 · 4:3
.syx-aspect-1by1 · 1:1

Object-fit · Background-size classes

.syx-obj-cover .syx-obj-contain .syx-obj-fill .syx-bg-cover .syx-bg-contain

Accessibility — .syx-sr-only · skip-link · motion-safe

Skip Link — focus to reveal (Tab key)

.syx-sr-only — Visually hidden but accessible to screen readers.Hidden text for assistive technology.
.syx-sr-only-focusable — Becomes visible on keyboard focus.
.syx-motion-safe — Removes animations for prefers-reduced-motion.

Responsive visibility

.syx-d-sm-only .syx-d-sm-up .syx-d-md-up .syx-d-lg-up

Font Sizes — .syx-font-size-*

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 5

Dimensions — .syx-size-*

Square size tokens mapping to --primitive-size-1…5. For icons, avatars, thumbnails.

size-1
size-2
size-3
size-4
size-5

Layout Grid

@layer syx.base — structural foundation. 12-column responsive grid.

layout-grid — xs / sm / md breakpoints

Column spans at xs

col-xs-12
col-xs-6
col-xs-6
xs-4
xs-4
xs-4

Responsive — xs-12 → sm-6 → md-4 (resize to see)

A
B
C

Modifiers: --no-gap · __nested · --is-edge2edge

--no-gap · xs-8
xs-4

Atoms

@layer syx.atoms — smallest reusable components.

Breadcrumb

Buttons — .atom-btn

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

Pills · Labels

Neutral Primary Secondary Success Warning Danger Dark

Icons — .atom-icon

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

Title · Txt

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.

Form — label · input · input-wrapper

Check · Switch

Demo Checkboxes
Demo Switches

Lists

  • First item
  • Second item
    • Nested A
    • Nested B
  • Third item
  1. First ordered
  2. Second ordered
  3. Third ordered

Table

Component Layer Status
atom-btn syx.atoms Stable
mol-card syx.molecules Stable
syx-d-flex syx.utilities Always wins

Pagination

Code — syntax highlight

.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.

Radio

Demo Radio Group

Molecules

@layer syx.molecules — compositions of atoms.

Form Field — states

Validation error.
Looks good!
Consider improving.

Btn Group · Label Group

mol-btn-group

mol-label-group

SYX Design System v5.1.0 Beta features Production ready

Form Field Set — group wrapper

Default (vertical stack)

--inline (horizontal wrap with flex: 1 1 auto)

Organisms

@layer syx.organisms — compositions of molecules + atoms forming page-level sections.

Site Header — brand + nav + actions

Live example — the header at the top of this page is this organism

Brand Name

Composes: atom-icon · atom-btn · mol-btn-group. Add more organisms to scss/organisms/ and forward from organisms/index.scss.

Component reference

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.

Atoms · 23

Avatar .atom-avatar

atomgenericscss/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

Ana LópezALAC

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>
Classesatom-avatar
Modifiersatom-avatar--lg atom-avatar--sm atom-avatar--square
Elementsatom-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
AccessibilityPhoto: 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.
JavaScriptNone — CSS only.

Breadcrumb .atom-breadcrumb

atomsystem-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>
Classesatom-breadcrumb
Modifiers—
Elementsatom-breadcrumb__item atom-breadcrumb__link
States:focus-visible :hover
Tokens—
AccessibilityWrap 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.
JavaScriptNone — CSS only.

Btn .atom-btn

atomsystem-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>
Classesatom-btn
Modifiersatom-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
Composesatom-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
AccessibilityNative <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.
JavaScriptNone — CSS only.

Check .atom-check

atomsystem-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>
Classesatom-check
Modifiers—
Elementsatom-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
AccessibilityReal <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.
JavaScriptNone — CSS only.

Code .atom-code

atomdomain-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>&lt;<span class="atom-code__tag">button</span> <span class="atom-code__attr">class</span>=<span class="atom-code__val">"atom-btn"</span>&gt;</code></pre>
Classesatom-code
Modifiersatom-code--inline
Elementsatom-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
AccessibilityStatic 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.
JavaScriptNone — CSS only.

Feature icon .atom-feature-icon

atomdomain-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>
Classesatom-feature-icon
Modifiersatom-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
AccessibilityDecorative: always aria-hidden="true"; the meaning lives in the text next to it. Never focusable. No JS.
JavaScriptNone — CSS only.

Form .atom-input

atomsystem-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>
Classesatom-input atom-input-wrapper atom-label atom-select atom-textarea
Modifiers—
Elements—
States:disabled :focus-visible :hover
Composesatom-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
AccessibilityEvery 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.
JavaScriptNone — CSS only.

Icon lucide .atom-icon

atomsystem-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>
Classesatom-icon
Modifiers
61 modifiers
atom-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—
Composesatom-icon
Tokens—
AccessibilityDecorative 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.
JavaScriptNone — CSS only.

Icon .atom-icon

atomsystem-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>
Classesatom-icon
Modifiers
19 modifiers
atom-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
AccessibilityDecorative 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.
JavaScriptNone — CSS only.

Label .atom-label

atomsystem-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

Scope

Markup

<span class="atom-label atom-label--scope-1">Scope</span>
Classesatom-label
Modifiersatom-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
AccessibilityPlain text, no role. If it marks a state, the words must say it. No JS.
JavaScriptNone — CSS only.

List .atom-list

atomsystem-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

  • Primitives
  • Semantics

Markup

<ul class="atom-list atom-list--primary">
  <li class="atom-list__item">Primitives</li>
  <li class="atom-list__item">Semantics</li>
</ul>
Classesatom-list
Modifiersatom-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
AccessibilityNative <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.
JavaScriptNone — CSS only.

Pagination .atom-pagination

atomsystem-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>
Classesatom-pagination
Modifiersatom-pagination__item--is-active atom-pagination__item--is-disabled atom-pagination__item--is-next atom-pagination__item--is-prev
Elementsatom-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
AccessibilityWrap 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.
JavaScriptNone — CSS only.

Pill .atom-pill

atomsystem-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

Stable

Markup

<span class="atom-pill atom-pill--success">Stable</span>
Classesatom-pill
Modifiersatom-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
AccessibilityStatic 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.
JavaScriptNone — CSS only.

Progress .atom-progress

atomgenericscss/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

40%Syncing…

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>
Classesatom-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
AccessibilityNative <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.
JavaScriptNone — CSS only.

Radio .atom-radio

atomsystem-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>
Classesatom-radio
Modifiers—
Elementsatom-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
AccessibilityReal <input type="radio"> inside the <label>, one name per group, the group in <fieldset><legend>. Arrow keys move the choice (browser). No JS.
JavaScriptNone — CSS only.

Skeleton .atom-skeleton

atomgenericscss/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>
Classesatom-skeleton
Modifiersatom-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
Accessibilityaria-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.
JavaScriptNone — CSS only.

Spinner .atom-spinner

atomgenericscss/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

Loading results…Saving changes…

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>
Classesatom-spinner
Modifiersatom-spinner--lg atom-spinner--sm atom-spinner--with-label
Elementsatom-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
AccessibilityAlways 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.
JavaScriptNone — CSS only.

Stat counter .atom-stat

atomsystem-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

19Atoms

Markup

<div class="atom-stat"><span class="atom-stat__number">19</span><span class="atom-stat__label">Atoms</span></div>
Classesatom-stat
Modifiersatom-stat--accent atom-stat--light
Elementsatom-stat__label atom-stat__number
States—
Tokens—
AccessibilityStatic. 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.
JavaScriptNone — CSS only.

Switch .atom-switch

atomsystem-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>
Classesatom-switch
Modifiers—
Elementsatom-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.
JavaScriptNone — CSS only.

Table .atom-table

atomsystem-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>
Classesatom-table
Modifiersatom-table--is-stick-thead atom-table--primary atom-table--resp
Elementsatom-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
AccessibilityNative <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.
JavaScriptNone — CSS only.

Title .atom-title

atomsystem-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

Token architecture

Markup

<h2 class="atom-title atom-title--h3">Token architecture</h2>
Classesatom-title
Modifiersatom-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
AccessibilityPick the heading level from the document outline; the modifier only sets the size. Never skip levels to get a size. No JS.
JavaScriptNone — CSS only.

Txt .atom-txt

atomsystem-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>
Classesatom-txt
Modifiersatom-txt--primary
Elements—
States—
Tokens—
AccessibilityStatic body text. No role, no JS.
JavaScriptNone — CSS only.

Molecules · 15

Alert .mol-alert

moleculegenericscss/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>
Classesmol-alert
Modifiersmol-alert--error mol-alert--info mol-alert--success mol-alert--warning
Elementsmol-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
Accessibilityrole="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.
JavaScriptNone — CSS only.

Btn group .mol-btn-group

moleculesystem-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>
Classesmol-btn-group
Modifiers—
Elements—
States—
Tokens
1 component tokens
--component-btn-group-gap
AccessibilityEvery 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.
JavaScriptNone — CSS only.

Card .mol-card

moleculegenericscss/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

Storage

80% used

You have used 8 of 10 GB. Old exports are deleted after 30 days.

Billing

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>
Classesmol-card
Modifiersmol-card--elevated mol-card--flat
Elementsmol-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.
JavaScriptNone — CSS only.

Code snippet .mol-code-snippet

moleculedomain-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

Correct
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>
Classesmol-code-snippet
Modifiersmol-code-snippet--ko mol-code-snippet--neutral mol-code-snippet--ok
Elementsmol-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
AccessibilityThe header label states the verdict in words. A body that scrolls sideways needs tabindex="0". No JS.
JavaScriptNone — CSS only.

Dialog .mol-dialog

moleculegenericscss/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

Delete “Report”?

Files and history will be lost.

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>
Classesmol-dialog
Modifiers—
Elementsmol-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
AccessibilityNative <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.
JavaScriptNone — CSS only.

Disclosure .mol-disclosure

moleculegenericscss/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

Can I change plans?
Yes, whenever you want.

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>
Classesmol-disclosure
Modifiers—
Elementsmol-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
AccessibilityNative <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.
JavaScriptNone — CSS only.

Empty state .mol-empty-state

moleculegenericscss/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

No projects match “q4”

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>
Classesmol-empty-state
Modifiersmol-empty-state--bordered
Elementsmol-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
AccessibilityThe 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.
JavaScriptNone — CSS only.

Feature card .mol-feature-card

moleculesystem-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

Markup

<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>
Classesmol-feature-card
Modifiers—
Elementsmol-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.
JavaScriptNone — CSS only.

Form field set .mol-form-field-set

moleculesystem-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>
Classesmol-form-field-set
Modifiersmol-form-field-set--inline
Elements—
States—
Composesmol-form-field
Tokens
2 component tokens
--component-form-field-set-min-width --component-form-group-gap
AccessibilityVisual 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.
JavaScriptNone — CSS only.

Form field .mol-form-field

moleculesystem-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

Enter a valid address.
Up to 160 characters

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>
Classesmol-form-field
Modifiersmol-form-field--has-select mol-form-field--is-error mol-form-field--is-success mol-form-field--is-warning
Elementsmol-form-field__count mol-form-field__live mol-form-field__msg
States.is-limit
Composesatom-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.
JavaScriptjs/syx-char-count.js
import 'syx-design-system/js/syx-char-count.js';
// markup rendered later:
import { initCharCount } from 'syx-design-system/js/syx-char-count.js';
initCharCount(container);

Label group .mol-label-group

moleculesystem-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

SCSSTokens

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>
Classesmol-label-group
Modifiers—
Elements—
States—
Tokens
1 component tokens
--component-label-group-gap
AccessibilityContainer only. If the labels are a list, use <ul role="list">. No JS.
JavaScriptNone — CSS only.

Menu .mol-menu

moleculegenericscss/molecules/_menu.scss

A button that opens a list of actions ("More", "Actions", a table row's menu). The __list is popover="auto": top layer, light dismiss and Escape come from the browser, and popovertarget opens it even without script. js/syx-menu.js adds the WAI-ARIA menu button keyboard, keeps aria-expanded true to the real state and places the list under the button (above if it does not fit), aligned to its start. __item--danger for destructive actions, aria-disabled="true" for an action that exists but cannot run now, __separator between groups. To navigate to other pages this is not a menu: use a list of links or mol-disclosure.

Live demo

Markup

<div class="mol-menu">
  <button class="atom-btn atom-btn--secondary atom-btn--size-md mol-menu__trigger" type="button" id="menu-actions-btn" aria-haspopup="menu" aria-expanded="false" aria-controls="menu-actions" popovertarget="menu-actions">Actions <span class="atom-icon atom-icon--lc-chevron-down atom-icon--sm" aria-hidden="true"></span></button>
  <div class="mol-menu__list" id="menu-actions" role="menu" aria-labelledby="menu-actions-btn" popover="auto">
    <button class="mol-menu__item" type="button" role="menuitem">Rename</button><button class="mol-menu__item" type="button" role="menuitem">Duplicate</button><button class="mol-menu__item" type="button" role="menuitem" aria-disabled="true">Move to folder</button>
    <div class="mol-menu__separator" role="separator"></div>
    <button class="mol-menu__item mol-menu__item--danger" type="button" role="menuitem">Delete</button>
  </div>
</div>
Classesmol-menu
Modifiersmol-menu__item--danger
Elementsmol-menu__item mol-menu__list mol-menu__separator mol-menu__trigger
States:focus-visible :hover
Tokens
17 component tokens
--component-menu-bg --component-menu-border-color --component-menu-border-radius --component-menu-border-width --component-menu-color --component-menu-item-bg-hover --component-menu-item-border-radius --component-menu-item-danger-color --component-menu-item-disabled-color --component-menu-item-gap --component-menu-item-min-height --component-menu-item-padding-inline --component-menu-max-height --component-menu-min-width --component-menu-separator-color --component-menu-shadow --component-menu-z-index
AccessibilityButton: aria-haspopup="menu", aria-expanded, aria-controls and popovertarget pointing at the list. List: role="menu" with aria-labelledby → button; each option role="menuitem" (a <button>, or an <a> for a link). Keyboard from js/syx-menu.js (no dependencies): ↓/Enter/Space open on the first option, ↑ on the last; ↑↓ move with wrap, Home/End, a letter jumps to the next option starting with it, Escape and Tab close and return focus to the button; activating an option closes the menu. aria-disabled options stay focusable but do nothing. Without JS, popovertarget still opens and closes it and Tab reaches the options.
JavaScriptjs/syx-menu.js
import 'syx-design-system/js/syx-menu.js';
// markup rendered later:
import { initMenus } from 'syx-design-system/js/syx-menu.js';
initMenus(container);

Tabs .mol-tabs

moleculegenericscss/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

Profile settings

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>
Classesmol-tabs
Modifiers—
Elementsmol-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
Accessibilityrole 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.
JavaScriptjs/syx-tabs.js
import 'syx-design-system/js/syx-tabs.js';
// markup rendered later:
import { initTabs } from 'syx-design-system/js/syx-tabs.js';
initTabs(container);

Toast .mol-toast

moleculegenericscss/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>
Classesmol-toast
Modifiersmol-toast__item--error mol-toast__item--info mol-toast__item--success mol-toast__item--warning
Elementsmol-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
AccessibilityThe 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.
JavaScriptjs/syx-toast.js
import 'syx-design-system/js/syx-toast.js';
// markup rendered later:
import { initToasts, showToast } from 'syx-design-system/js/syx-toast.js';
initToasts(container);

Tooltip .mol-tooltip

moleculegenericscss/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

Copies a view-only link to the clipboard

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>
Classesmol-tooltip
Modifiers—
Elementsmol-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
Accessibilityaria-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.
JavaScriptjs/syx-tooltip.js
import 'syx-design-system/js/syx-tooltip.js';
// markup rendered later:
import { initTooltips } from 'syx-design-system/js/syx-tooltip.js';
initTooltips(container);

Organisms · 2

App shell .org-app-shell

organismgenericscss/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

Acme Console
AL

Settings

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>
Classesorg-app-shell
Modifiersorg-app-shell--full
Elementsorg-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
AccessibilityThree 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.
JavaScriptNone — CSS only.

Site header .org-site-header

organismdomain-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>
Classesorg-site-header org-site-nav-drawer org-site-nav-overlay
Modifiers—
Elementsorg-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
Composesatom-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.
JavaScriptjs/syx-site-nav.js
import 'syx-design-system/js/syx-site-nav.js';
// markup rendered later:
import { initSiteNav } from 'syx-design-system/js/syx-site-nav.js';
initSiteNav(container);