SYX Design System
Ordered by layer hierarchy: utilities → layout → atoms → molecules → organisms.
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
.syx-* → Util
utilities/
atom-* → Atom
atoms/
mol-* → Molecule
molecules/
org-* → Organism
organisms/
pages/ or
bundle-*.scss
Quick reference
| Layer | Prefix | When to use | Anti-pattern |
|---|---|---|---|
| Util | .syx- |
Single CSS property: display, spacing, color, visibility | Never add visual design to a utility class |
| Atom | atom- |
Smallest reusable building block: btn, icon, label, pill, input | Don't compose atoms inside atoms |
| Mol | mol- |
Composition of 2+ atoms that always travel together | If sub-parts make sense independently → separate atoms |
| Org | org- |
Complete UI section; appears across multiple pages | Don't put page-unique content inside an organism |
| Page | — | Layout tweaks exclusive to one specific page | Don't duplicate component logic in page-level styles |
Rule of thumb: Utilities are adjectives
(.syx-d-flex → the display is flex). Components are nouns
(.mol-card → a card). If you're unsure between atom and molecule: would the
sub-parts
ever make sense independently? Yes → separate atoms. No → molecule.
Worked examples
atom-btn
atom-btn + atom-icon composed in HTML,
not a new mol-
mol-search (input + icon + btn always
together)
org-site-header
.syx-mx-auto or
.syx-text-center
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--primaryalone ❌ - 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-loadingare 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 categoryText utilities
Text Colors — .syx-text-*
Social brand
Backgrounds — .syx-bg-* · .syx-bg-color-*
Semantic (_text.scss)
Theme-aware helpers (_backgrounds.scss)
Spacing — .syx-m/p-*
Margin bottom scale (0–5)
Padding scale (1–5) · shorthands
Display · Flex · Gap · Position
.syx-d-flex + gaps + wrap
.syx-justify-between + .syx-items-center
.syx-d-grid + .syx-gap-3
Media — img-fluid · embed · object-fit
.syx-img-fluid
.syx-embed--16by9
Object-fit · Background-size classes
Accessibility — .syx-sr-only · skip-link · motion-safe
Skip Link — focus to reveal (Tab key)
prefers-reduced-motion.
Responsive visibility
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
5Dimensions — .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-12col-xs-6col-xs-6xs-4xs-4xs-4Responsive — xs-12 → sm-6 → md-4 (resize to see)
Modifiers: --no-gap · __nested · --is-edge2edge
--no-gap · xs-8xs-4Atoms
@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
Link
Paragraph with an inline primary link — hover to see the fill effect. And another link.
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
Lists
- First item
- Second item
- Nested A
- Nested B
- Third item
- First ordered
- Second ordered
- 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
Molecules
@layer syx.molecules — compositions of atoms.
Form Field — states
Btn Group · Label Group
mol-btn-group
mol-label-group
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
Composes: atom-icon · atom-btn ·
mol-btn-group. Add more organisms to scss/organisms/ and forward from
organisms/index.scss.