Skip to main content
Component Gallery v4.28.0

SYX Design System

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

7 layers
1,088 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", "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 ten 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 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

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–R07: primitives in components, !important, raw transition/position
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: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 · 34 componentes · 274 omitidas
   …
   syx-sketch    543 variables · 34 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 26 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 nine, 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 nine 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 errors, R05–R07 warnings, R08 declared but not yet implemented — 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

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.

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 nine 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 nine. Nine would be nine 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/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–R08 R01–R07 — 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

.syx-text-facebook .syx-text-twitter .syx-text-instagram .syx-text-whatsapp

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

Semantic (_text.scss)

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
Facebook
Twitter
Instagram
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 · embed · object-fit

.syx-img-fluid

Responsive demo

.syx-embed--16by9

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