SLASHED — Architecture

Layer order

Declared once in core/layers.css:

@layer
  slashed.tokens,
  slashed.reset,
  slashed.base,
  slashed.forms,
  slashed.components,
  slashed.layout,
  slashed.macros,
  slashed.utilities,
  slashed.states,
  slashed.themes,
  slashed.motion,
  slashed.accessibility,
  slashed.print,
  slashed.legacy,
  slashed.overrides;

Priority increases left → right. Unlayered consumer CSS beats all layers. slashed.overrides ships empty. slashed.legacy is for backward-compatibility fallbacks and sits just below slashed.overrides so user overrides always win.

slashed.macros sits between layout and utilities. Macros may compose with primitives and components, but a single-property utility still wins on the same selector.


File structure

core/
  layers.css                   @layer declaration only
  tokens.css                   slashed.tokens
  tokens.layout.css            slashed.tokens  (semantic layout tokens)
  tokens.macros.css     slashed.tokens  (semantic macro tokens)
  reset.css                    slashed.reset
  base.css                     slashed.base
  themes.css                   slashed.themes
  layout.css                   slashed.layout
  macros.css            slashed.macros  (recipes / patterns)
  states.css                   slashed.states
  motion.css                   slashed.motion
  accessibility.css            slashed.accessibility
  print.css                    slashed.print
optional/
  tokens.components.css   slashed.tokens  (component tokens for btn + card)
  customize-example.css   slashed.themes, slashed.overrides  (copy-and-customise rebrand/config/overrides starter; not bundled)
  forms.css               slashed.forms  (classless native form-control styling)
  components.css          slashed.components  (btn + card — the only two components)
  utilities.css           slashed.utilities  (curated subset active — heading/text-size/hover/list-reset/marker-colour/alternate-selection/sticky)
  legacy.css              slashed.legacy

optional/components.css and optional/tokens.components.css ship exactly two components — .sf-btn and .sf-card, live since v0.7.0. These are the only components SLASHED carries: the framework stays BEM-first and token-first and does not maintain a broad component library. See docs/components.md for the component reference.

optional/utilities.css ships a curated subset — the heading (.sf-h1.sf-h6), text-size (.sf-text-*), hover-transform (.sf-hover-*), list-reset, marker-colour, alternate-selection, and sticky helpers are active; the rest stay commented out. SLASHED is BEM-first by design, so it ships only this curated subset of utility classes in 0.x.


Class taxonomy

Every shipped class falls into exactly one of seven categories. The category answers a single question for the consumer; this is the decision tree to use when adding a new class.

# Category Question it answers File Layer
1 Layout primitives “How are children arranged?” core/layout.css slashed.layout
2 Macro-classes (recipes) “What does this element DO / look like?” core/macros.css slashed.macros
3 Animation primitives “How does this element move?” core/motion.css slashed.motion
4 State utilities (.sf-is-*) “What state is this element in?” core/states.css slashed.states
5 A11y utilities & patterns “How does this work for AT users?” core/accessibility.css slashed.accessibility
6 Print utilities (.print-*) “How does this print?” core/print.css slashed.print
7 Components (opt-in) “Pre-built visual block.” optional/components.css slashed.components

Decision tree for adding a new class

1. Does it answer "where do my children go?"
   YES → core/layout.css                 (slashed.layout)
   NO  → step 2

2. Does it modify an element's runtime state?
      (hover, active, disabled, loading…)
   YES → core/states.css (.sf-is-*/.sf-has-*)  (slashed.states)
   NO  → step 3

3. Is it an a11y pattern or a11y utility?
      (sr-only, sf-focus-parent, clickable-parent…)
   YES → core/accessibility.css          (slashed.accessibility)
   NO  → step 4

4. Is it a single-property animation trigger?
   YES → core/motion.css                 (slashed.motion)
   NO  → step 5

5. Is it an opinionated visual component?
      (button, card, alert…)
   YES → optional/components.css         (slashed.components)
   NO  → core/macros.css          (slashed.macros)  ← default

Macros are the default fallback for behavioural/visual recipes that aren’t layout, state, a11y, animation, or component.

Layout primitives vs macros

Test Primitive Macro / recipe
Used as the only class on an element? Often (<ul class="sf-cluster">) Rarely — usually composed (class="card sf-clickable-parent")
Defines arrangement of children? Yes (display, gap, grid-template) Sometimes, but secondary
Has tokenised knobs (--sf-X-gap etc.)? Yes, central to the API Sometimes — minimal
Touches pseudo-elements / states? Rarely Often (::after, :hover, :focus-within)
Container queries inside? Often Rarely
Sole responsibility “Where do my children go?” “What does this DO / look like?”

Heuristic: a primitive is a noun (a stack, a grid); a macro is a verb/adjective (clickable, scrollable, prose).

Two conceptually-macro classes stay in layout.css in 0.x for compatibility:


Layers

slashed.tokens — custom properties only, :root only. No element rules. All values consumers might override are tokens. Spread across multiple files: tokens.css (core — brand/status/semantic/numeric palette/alpha tokens), tokens.layout.css (layout primitives), tokens.macros.css (macros), and tokens.components.css (optional component-level tokens).

slashed.reset — browser normalization. Minimal var() usage (only with hardcoded fallbacks for critical layout values like scroll-padding-top).

slashed.base — element defaults, all values via var(). A minimal, readable foundation for flow and inline text — not a classless UI kit. SLASHED is BEM-first: the token API is the product, and the base holds the line at three tiers:

Native form controls are out of base entirely — they live in the opt-in slashed.forms layer.

slashed.forms — opt-in classless styling for native form controls (input, select, textarea, button, checkbox/radio, fieldset, label) from optional/forms.css. Element-level only, no classes. Reads --sf-field-border-color so the .sf-is-* validation states in slashed.states recolour fields. Skip the file entirely if you prefer full BEM control.

slashed.components — UI blocks: .sf-btn and .sf-card, live since v0.7.0. These are the only two components SLASHED ships — it stays BEM-first and does not carry a broad component library. Every value goes through var(). Requires tokens.components.css.

slashed.layout — layout primitives: .sf-stack, .sf-cluster, .sf-sidebar, .sf-cover, .sf-grid, .sf-container, .sf-content-grid, .sf-icon, etc. Layout tokens declared in tokens.layout.css, overridable per-instance via style="--sf-stack-gap: …".

slashed.macros — recipes / patterns: .sf-prose, .sf-not-prose, .sf-flow, .sf-truncate, .sf-line-clamp-{2,3,N}, .sf-equal-height, .sf-aspect, .sf-scroll-shadow, .sf-scroll-snap, .sf-overflow-fade, .sf-no-tap-highlight. Tokens for these classes live in core/tokens.macros.css.

slashed.utilities — single-purpose helpers. SLASHED is BEM-first and ships only a curated subset in 0.x (heading, text-size, hover-transform, list-reset, marker-colour, alternate-selection, sticky, and visibility — see optional/utilities.css’s header comment for the current active set); most definitions in the file are staged (commented out) behind that same curation call.

slashed.states.sf-is-* markers. Exclusive prefix — utilities never use it (visibility helpers that don’t encode a runtime condition, like .sf-invisible/.sf-visible, live in slashed.utilities instead — see states.md § “Prefer native state”).

slashed.themes — token reassignments only. Lives in core/themes.css. Holds @media (prefers-color-scheme: dark) and the [data-theme="light|dark"] selectors that flip color-scheme and --sf-is-dark. Sits above slashed.{states, utilities, components} so theme overrides cannot be beaten by an equal-specificity component or utility rule. Consumers can extend this layer with forced-colors swaps, brand-palette scopes, or any other token-only reassignment (see optional/customize-example.css).

slashed.motion — animation tokens, keyframes, transition utilities. No component selectors. Gated behind @media (prefers-reduced-motion: no-preference). Transition tokens live in core/tokens.css — see motion.md. .sf-color-pulse animates --sf-color-primary-source-light lightness, demonstrating @property colour interpolation in oklch.

slashed.accessibility:focus-visible, .sr-only, .skip-link, reduced-motion resets, plus the a11y patterns .sf-focus-parent and .sf-clickable-parent. High in the stack to override motion without relying solely on !important. Selective !important used only where override is a genuine accessibility barrier (focus ring, reduced motion, sr-only). .sr-only uses overflow: clip (modern consensus — avoids creating a new scroll container unlike the legacy overflow: hidden).

slashed.print@media print only. Contains @page rule consuming --sf-print-* tokens. Authored colour is preserved by default; consumers opt into ink-on-paper via .print-no-color or force colour via .print-color-exact. !important is reserved for selectors whose semantics require defeating consumer CSS: the hide-list (nav, aside, button, input, select, textarea, dialog, [popover], .no-print), details > summary, and the two opt-in colour classes.

slashed.legacy@supports not (…) fallbacks for the ~2022 baseline (first browsers with CSS cascade layers). Sits above slashed.print so fallbacks win, but below slashed.overrides so user CSS always wins.

slashed.overrides — ships empty. Consumer escape hatch.


Tokens

Naming conventions

BEM consumer-API tokens

Many tokens — --sf-shadow-*, --sf-blur, --sf-gap, --sf-gradient-*, --sf-scrollbar-*, --sf-optical-sizing — are not consumed by the framework itself. They exist for your own BEM classes (.card { box-shadow: var(--sf-shadow-m) }) and are exercised in demo/index.html and validated by tests/.

Print helpers use the .print-* prefix: .print-only (show only on paper), .no-print (hide on paper), .print-color-exact (force colour), and .print-no-color (force ink-saving flatten). See the slashed.print layer.

Naming exceptions

A small number of classes are intentionally unprefixed (no sf- or sf-is-). They are short, universally understood terms where a prefix would add noise without reducing collision risk in practice.

Class Layer Rationale
.sr-only accessibility Industry-standard screen-reader name
.sr-only-focusable accessibility Companion to .sr-only
.skip-link accessibility Common a11y pattern name
.no-motion accessibility Reads as a behaviour toggle
.no-print print Reads as a behaviour toggle
.print-only print Reads as a behaviour toggle
.print-color-exact print Self-documenting intent
.print-no-color print Self-documenting intent

Specificity

unlayered consumer CSS       highest
slashed.overrides
slashed.legacy
slashed.print
slashed.accessibility
slashed.motion
slashed.themes
slashed.states
slashed.utilities
slashed.macros
slashed.layout
slashed.components
slashed.forms
slashed.base
slashed.reset
slashed.tokens               lowest

Selectors stay low-specificity (single class, :root, element). Consumer overrides win without !important.


Extension points

Mechanism Example
Token override :root { --sf-color-primary: #your-color }
Instance token style="--sf-button-radius: var(--sf-radius-full)"
Layer override @layer slashed.overrides { … }
Unlayered CSS Author normally
Theme [data-theme="dark"] { --sf-color-primary: … }

Responsive design

  1. Fluid tokens--sf-space-*, --sf-text-* use clamp(). No @media needed.
  2. Intrinsic primitives.sf-grid, .sf-sidebar, .sf-switcher, .sf-cluster, .sf-equal, .sf-content-grid carry no @container query at all: they respond to their own width through intrinsic sizing (minmax(min(…),1fr), flex-basis, column-width), which is both container-relative and viewport-independent for free. Prefer this pattern.
  3. Container-query primitives.sf-grid-cols-*, .sf-alternate, .sf-bento need a fixed column count at a threshold, which intrinsic sizing can’t express, so they use @container. .sf-container declares the named container sf-layout (see Container query contract below); .sf-alternate declares its own sf-alternate. Their breakpoints are hard-coded in rem (root-relative, viewport-independent) — never em, which inside @container resolves against the container’s own fluid font-size and would re-couple the query to the viewport.
  4. Breakpoints — a last resort. The framework ships no breakpoint tokens (custom properties can’t be used in @media/@container conditions); hard-code a rem value in your own query if you truly need one.

Container query contract

The framework declares two named containers:

Selector Container name Type Where consumed
.sf-container sf-layout inline-size open — consumers can target with @container sf-layout (...) in their own CSS
.sf-alternate sf-alternate inline-size framework-internal — the zigzag flip query is bound to this name so a nested .sf-alternate flips on its own width, not its outer container’s

The .sf-bento and .sf-grid-cols-* (fixed-column and ratio) primitives use anonymous @container (...) queries on purpose. They are designed to react to whatever the nearest inline-size ancestor reports — typically .sf-container, but legitimately also a parent .sf-bento, .sf-alternate, or any user-declared container. This portability is the feature: drop .sf-grid-cols-3 inside any inline-size context and it adapts.

If precision is needed in a consumer layout — e.g. a .sf-grid-cols-3 that must respond to the outer .sf-container and ignore an intermediate container — the consumer wraps the grid in their own named container or uses @container sf-layout (...) directly in user CSS.


Bundle configuration

The optimal bundle (dist/slashed.optimal.css) — the recommended default — includes core files plus classless form styling in order:

core/layers.css
core/tokens.css
core/tokens.layout.css
core/tokens.macros.css
core/reset.css
core/base.css
core/themes.css
core/layout.css
core/macros.css
core/states.css
core/motion.css
core/accessibility.css
core/print.css
optional/forms.css

Each bundle is emitted readable and minified with a source map (*.css, *.min.css, *.min.css.map) via lightningcss; the minifier does not down-level modern colour syntax (no targets), so light-dark() and oklch(from …) survive. npm run build reports raw/gzip/brotli sizes. Custom hand-built bundles must load core/layers.css first.

Four tiered bundles — each also emitted as a layer-flattened .flat variant, so bundle.config.json lists eight outputs in total — are built by scripts/bundle.js:

Bundle Contents
slashed.optimal.css all core/ + forms
slashed.full.css optimal + tokens.components + components (btn + card — the only two) + utilities (curated subset active, rest staged)

optional/legacy.css is not bundled by default — add it explicitly when you need back-compat shims. Because every rule sits in an @layer, concatenation order within a bundle does not affect the cascade. The bundler strips local @import statements (the explicit file list resolves them), so the tokens.components import inside components.css is inlined by listing the token file first. components.css and tokens.components.css ship .sf-btn / .sf-card and their tokens — the only two components; utilities.css ships a curated subset (heading, text-size, hover, list-reset, marker, selection, and sticky helpers active, rest commented). Consumers can also build à la carte: raw core/ plus hand-picked optional files. Each bundle also has a layer-flattened .flat variant.


Performance

Plain CSS, no runtime. The notable costs:

Known intentional tradeoffs

Deliberate behaviours, documented so they aren’t mistaken for bugs.


Deferred

@starting-style and CSS anchor positioning are only meaningful attached to a specific element/component transition (dialog, popover, tooltip). SLASHED ships only .sf-btn and .sf-card and stays BEM-first, so these belong to consumer components rather than the framework and are out of scope here. The component-free part — scroll-driven animation range tokens (--sf-scroll-timeline-range-*) — already ships in core/tokens.css.