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
// setup.scss → passes the theme name to the mixin
@include org-header("example-02");
// organisms/_header.scss → three methods based on type of difference
@mixin org-header($theme: null) {
// Method 1 — CSS token (automatic, all themes)
background: var(--component-header-bg);
// Method 2 — Sass map (structural differences)
@if theme-cfg($theme, "header-sidenav-side", left) == right { right: 0; }
// Method 3 — 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 |
Utilities
@layer syx.utilities — highest specificity. Always wins
over atoms and molecules.
Typography Scale — .syx-type-*
.syx-type-display-1Display Hero.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
Spacer @deprecated
Legacy spacing classes kept for backward compatibility. Prefer
syx-mt-* / syx-pt-* from the spacing utility.
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.