DSSL v0.2: “First Light”, ratified
The v0.1 draft was put to a seven-point vote. This document is the ratified v0.2 result, and every decision below is implemented by the engine serving this page. The engine itself has kept growing since: it is now at v0.15.0 (see the changelog for what the site demanded).
Decision log
Seven questions, seven answers, the draft spec was amended to match.
| # | Question | Decision | Consequence |
|---|---|---|---|
| 1 | Syntax style | Braces | C-shaped blocks; semicolons optional, newline-terminated declarations |
| 2 | Styling engine | Own engine | No CSS output for regular rules, selectors matched against the live DOM, declarations applied directly; pseudo-elements excepted (shadow CSS) |
| 3 | Bare numbers | px | padding: 16 is 16px; unitless preserved where CSS demands it |
| 4 | Cascade | Layers + order | No specificity, no !important; five layers; pin as the only escape hatch |
| 5 | Vocabulary | Moderate sugar | All CSS names valid + a small semantic set (row, grid, fluid, …) |
| 6 | Conditions | CSS shapes | Nested state blocks (hover, focus…) + @media / @container; @signal added as the JS bridge |
| 7 | Website shape | Docs site | Multi-view docs with live examples, playground, spec and changelog |
1 · Architecture
DSSL is a styling language plus engine. Source files use the .dssl extension. The engine, four dependency-free ES modules (parser, resolver, engine, API), fetches stylesheets, parses them, resolves tokens, and computes styles for elements in the live DOM. It survives DOM mutations via a MutationObserver, re-evaluates states on interaction events, and re-runs media conditions through matchMedia listeners.
Regular rules produce no CSS: winning declarations are applied to elements directly. Pseudo-elements, which cannot be expressed inline, are the single exception, they compile to a shadow stylesheet that references the same tokens. A tiny opt-in reset (data-dssl-reset) is likewise generated CSS, zero-specificity via :where().
.dssl source
└─ parser.js braces syntax → node tree (loud, line-numbered errors)
└─ resolver.js tokens · semantic expansion · behaviors · flatten
└─ engine.js match → cascade (layer, order) → element.style
states via events · @media via matchMedia
motion via Web Animations API
└─ shadow CSS ::before/::after/::selection + reset only2 · Syntax
- Blocks open with
header {and close with}; one-liners allowed. - Declarations are
prop: value, newline- or;-terminated. Valueless semantics (truncate) anduse nameneed no colon. - Comments:
//and block comments. Strings are"…"or'…'. - Values continue across lines while parens are open, or the line ends with a top-level comma or colon.
- Bare numbers are px;
16/1.4size-over-lineheight infont;token/40is alpha.
3 · Tokens
tokens { } blocks define named values in nested groups (color.ink, space.4). They compile to --dssl-* custom properties on :root. Reference resolution is strict: an identifier that matches no token and no known keyword is a compile error with a suggestion. Tokens may be defined conditionally (inside @media/@signal), that is how theming works. Bundles share one token registry in load order.
4 · Cascade
For every element, the engine collects all structurally-matching rules whose conditions pass, sorts them by (layer, source order), and applies declarations in sequence, later declarations overwrite earlier ones on the element. Layers, lowest first:
tokens < base < components < utilities < pinAuthor rules default to components. @layer re-assigns a subtree. pin blocks apply with !important-grade priority as the single sanctioned escape hatch. Selectors keep their native meaning, ids and pseudo-classes select exactly as in CSS, they simply carry no weight in conflict resolution.
5 · Conditions
- Element states, nested blocks named for the pseudo-class (
hover,focus-visible,active,disabled,checked, …). The engine wires the events; rules re-evaluate live viamatches(). - @media, any media query; bare numbers become px (
(width >= 768)). - @container, evaluated against the nearest ancestor with
container-typeset, tracked by the engine with a ResizeObserver. - @signal,
name(truthy),name == value,name != value; driven from JS. - Conditions compose by nesting; siblings AND together. Raw pseudo-classes in selector text (
.tab:hover) also work.
6 · Motion
motion name { } declares keyframes (from, to, at N%); a single terminal block interpolates from the element's natural state. animate: name duration easing fill runs it via the Web Animations API, the engine deduplicates re-runs so recomputes never restart animations; DSSL.replay() does that on demand. Reduced-motion disables animations unless essential: true.
7 · Signals
DSSL.signal(name, value) sets a named value in the engine and mirrors it onto <html> as data-dssl-signal-<name>, inspectable, framework-free theming and UI state. Clearing with null returns conditions to their system-preference defaults.
8 · Errors
Unknown properties, unknown tokens, unknown motions, malformed conditions and bad header shapes are compile errors carrying file, line, message and, where the edit distance is close, a suggestion. Error objects are structured (DSSL.errors()) so tools like the playground can render them. Uncaught selector evaluation failures are contained per-rule and reported once.
9 · Deliberate limits (v0.2)
- No
@import/module graph: bundles merge in load order. - Behaviors take no parameters; no loops or user functions.
- Comma selector lists do not combine with nested state blocks (bind states per-rule instead).
- Pseudo-element rules accept declarations only.
- No minification or build step, the engine is the pipeline.
10 · Versioning
DSSL follows semver for the language surface: new semantic properties, condition kinds and API calls are minor versions; breaking grammar changes are major. The engine reports DSSL.version, currently 0.2.0, “First Light”, ratified and grown during the construction of the site you are reading.