Download PARITY.md from EHS-Studio/dssl: direct link, hf CLI and curl.
- Browser
- Download file 21.5 kB
-
https://huggingface.co/EHS-Studio/dssl/resolve/main/PARITY.md
- Command line
-
hf download hf://EHS-Studio/dssl/PARITY.md
-
curl -L -o PARITY.md https://huggingface.co/EHS-Studio/dssl/resolve/main/PARITY.md
PARITY.md: DSSL vs CSS, honestly
The point of this table is to stop two bad arguments: "DSSL is missing everything" and "DSSL replaces all of CSS". Both are false. Here is the ledger. Status: shipped / partial / planned / by-design-no.
| CSS feature | In DSSL | Status | Notes |
|---|---|---|---|
| Type / class / id / attribute selectors | native (rides browser matching) | shipped | |
Combinators (>, +, ~, descendant) |
native | shipped | |
| Nesting | native, & substitution |
shipped | predates CSS nesting; no build step needed |
:nth-child(), :nth-of-type(), :not(), :is(), :where(), :has() |
native | shipped | the browser does the matching; v0.7 docs say so loudly |
Pseudo-classes (:hover, :focus...) |
engine-native states as nested blocks | shipped | real events, enter + leave, descendants re-evaluated; seen (first viewport entry) has no CSS equivalent |
| Pseudo-elements | one compiled shadow stylesheet | shipped | the ONE place DSSL emits real CSS; inline styles cannot reach them |
@media queries |
@media (max-width: 860), sugar: dark, light, reduced-motion, bare widths, <= ranges |
shipped | |
@container queries |
@container (min-width: 480) |
shipped | nearest ancestor (or self) with container-type |
@layer cascade layers |
@layer base|components|utilities + pin {} |
shipped | cascade is (layer, source order); no specificity, no !important |
@supports |
raw { } passes it through verbatim |
partial | no sugar; the engine feature-detects itself, and dssl import moves it into raw { } |
@font-face |
raw { } passes it through verbatim (semicolons required inside) |
partial | font.* tokens can name any loaded family; a missing ; in raw is a warning with a line number |
@keyframes |
motion name { from {} to {} } via WAAPI |
shipped | animate: name 600 ease-out infinite alternate |
| View transitions | not implemented | planned | |
| Scroll-driven animations | not implemented | planned | pairs naturally with seen + stagger |
Custom properties (--*) |
pass through unvalidated, plus dotted token re-declaration per subtree | shipped | --color.accent: color.err re-themes a subtree via inheritance |
| Design tokens | first-class: tokens { }, numeric keys, alpha suffix /85, scoped per file and subtree |
shipped | no CSS-native equivalent at this ergonomics level |
| Flexbox / Grid | raw properties pass through + row: column: grid: stack: sugar |
shipped | shorthands write display; the compiler warns when that clobbers an explicit display: |
place-items / alignment |
center: both |
shipped | the engine promotes a formatting context when missing, so alignment cannot silently no-op |
| Transitions | per-property: transition: 200 snap transform |
shipped | honors reduced-motion |
| Background patterns and noise | texture: (40 presets including ten wood species, 11 generators, your own) |
shipped | there is no CSS shorthand for "grain on top of my background"; this one keeps the author's background and removes cleanly. Runtime-only |
| Glow, particles, fire | aura-glow, aura-particle, glow-particle, aura-glow-particle, aura: |
shipped | CSS has box-shadow and filter: drop-shadow, and nothing for particles. No presets by design. Runtime-only |
Styling <input type="range"> |
slider: writes the ::-webkit-slider-* and ::-moz-range-* rules together |
shipped | Chrome tested; the Firefox rules are generated but not run yet. Histogram sliders via graph |
| Accessibility hints in the compiler | contrast, removed focus outline, small targets (warnings) | shipped | CSS says nothing; these never fail a build unless you pass --strict |
| Moving existing CSS over | dssl import styles.css |
shipped | converts the mechanical differences and reports the rest; the output always compiles |
print styles (@media print) |
not implemented | planned | |
!important |
by design no | by-design-no | pin { } is the must-win block; specificity wars are the thing DSSL deletes |
| Cascade | (layer, source order) | shipped | later wins; identical selectors resolve by order, deterministically |
| Static extraction / view-source styling | dssl build emits real CSS (v0.8.0); prerender snapshot for live pages planned |
partially shipped | freeze() already makes big static pages zero-cost after first paint |
| DevTools story | DSSL.explain(el) returns the cascade as data |
shipped | matched rules with file/line, winning declaration per property, pseudo rules; browser-extension panel planned |
| Work without JavaScript | dssl build emits the static subset as real CSS (v0.8.0); @signal/animate: stay runtime-only and counted |
shipped for the static subset; prerender snapshot still planned for live pages | the honest trade has an exit now |
The exit story
Abandoning DSSL leaves you: a DOM of valid inline styles, one small <style> element
(pseudo-element rules), and token values sitting in --dssl-* custom properties. The page
keeps working with the engine removed. No other styling replacement can say that, because
no other replacement styles with the platform's own primitives.
And since v0.8.0 there is a second exit: dssl build styles.dssl -o styles.css
compiles the static subset to a real stylesheet before you ever ship, no runtime,
no inline styles, CSP-clean, works without JavaScript. Tokens become custom properties,
states become pseudo-classes, layers become @layer, motions become @keyframes,
pin becomes !important, raw { } passes through verbatim, seen applies its final
state. @signal rules and animate: stay runtime-only and are counted in the report,
so the boundary is explicit, not folklore.
What DSSL adds that CSS does not have
- live tokens and signals: change one value or fire one bridge call, the whole DOM re-resolves
seenreveal-on-scroll andstaggerchoreography with zero JavaScript- the deform family composing into the transform channel
- gradients as sentences with compile-time stop validation
- the overlap policy: translucent boxes refuse to cover things unless you say so
- loud errors with did-you-mean, and a warnings channel for cascade-order footguns
freeze()/unfreeze(): a live engine that can be parked at zero cost
v0.16.2
DSSL.lint's css-loses skips the output of dssl build (recognised by its @layer tokens, base, components, utilities, pin statement), because its reset is built to sit under the engine. The docs page of this site went from 4,314 false findings (0.16.0), to 111 (0.16.1), to none from its own CSS.
v0.16.1
DSSL.lint's css-loses ignores a CSS rule whose value is the same as the inline value DSSL wrote (found on this site, which ships compiled CSS and runs the engine: 4,314 false findings on the docs page).
v0.16.0: round three
| Item from the game builder | Status |
|---|---|
Normal CSS loses to DSSL's inline styles (needed !important) |
done: yield: hands properties back; DSSL.lint finds the losers (css-loses). Still true: without yield, an inline style beats a stylesheet. |
env() and font-variant-numeric: tabular-nums refused |
done: the known words and properties come from the MDN CSS data |
radius { pill: 999 card: 22 } read as one value, error at line 0 |
done: two tokens; errors point at the token's line |
| Aura glows the box, not the shape | done: follows clip-path, border-radius, rotate/scale/transform, or around ...; alpha for images and canvases |
| More aura types | done: inner glow, rays, ripples, 9 particle shapes, palette, spin, wind, orbit |
| More textures | done: 33 presets, 9 generators |
Not verified: Firefox and Safari. around alpha needs an <img>, <canvas> or <video> (a div with a background-image is not readable). The shape follows the element's own rotate, scale and transform, not a rotation on a parent.
v0.15.0: the second game review
| Review item | Status |
|---|---|
| Aura redraws 1.5 times a second under reduced motion | done: follows what moves, rests when nothing does |
| Aura emitters linger when hidden | done: invisible emitters (opacity 0, display none, visibility hidden, on it or an ancestor) stop |
One canvas on <body>, never behind a hill |
done as z N per layer; interleaves with the root stacking context only |
| Silent file-order losses | done: dssl check, console and DSSL.lint name rules a later rule always beats |
| Styling is asynchronous | done: DSSL.flush(), await DSSL.settled(), DSSL.computed() |
| Signals typed | done |
DSSL.lint noisy |
done: invisible text skipped, outline and tight shadow count |
DSSL.signal costs 50 ms |
done: only the elements its rules select are restyled |
Several animate: per element |
done |
| Scoped signals | done (DSSL.signal(name, value, el)) |
outline-shape |
done (square corners, uses filter) |
DSSL.play on pseudo-elements |
done |
CLI --engine |
done |
| Test helpers (final value, pause, step) | done |
rotate: 5, nested state in ::after, font: trap, gradient docs |
done |
Not verified: Firefox and Safari. Scoped signals on ::before/::after use a marker attribute the engine keeps on the element.
v0.14.0: made for games
From a review by an agent that built a game on DSSL. Twelve problems and ten wishes; each has an assertion in scripts/test-v0140.mjs.
| Review item | Status |
|---|---|
Reduced motion silently kills every animate: |
done: console says so once; essential, DSSL.motion('full'), data-dssl-motion |
gradient: and background-image: overwrite each other |
done: warning; layers with + |
animation-delay etc. compile and do nothing |
done: warning that names the replacement; delay/jitter/iterations words |
| Motions scoped to one file | done: shared registry, same as tokens |
paint-order: stroke fill, conic-gradient(from 0deg ...) rejected |
done |
shadow: pop, radius: pill for custom tokens |
done |
| Console noise "folded border-color" | done: removed |
| Lint misses cross-rule contrast, false align warning | done: DSSL.lint(root) computes it on the live page; the align warning moved to dssl build's report where it applies |
| Docs drift | done: docs checked by a test against the real API and file list |
0% { } keyframes |
done |
| Engine overwrites inline styles | done: author styles win |
Static subtree, play(), scoped aura, check index.html, richer @signal, gradient sugar, motions on pseudo-elements, text-stroke, per-element variation, runtime lint |
done |
Not verified: Firefox and Safari (all checks ran in Chrome). The engine is still not at CSS parity for startup (see v0.13.0 below).
v0.13.0: the compiled page is the page
| Promise | Status | Evidence |
|---|---|---|
| With JavaScript off, a compiled page renders its static styles correctly on first paint | done | test-static-parity.mjs: 7 pages, 5,000+ elements, identical to the engine (demo elements for engine-only features excluded); the site itself ships this way |
| Startup within 10% of equivalent CSS | done for the compiled page | bench-compiled.mjs: +0% (default build), -4% (--inline-tokens) |
| Churn at parity | done with --inline-tokens (0%); default build +7% churn, +20% on an inherited-value restyle |
same benchmark |
| The same for the runtime engine | no: load 17x, churn 4x | the engine applies inline styles; a hybrid mode that skips what the CSS already did is not built |
| SSR | done | public/dssl/server.js (no DOM), real React renderToStaticMarkup tested; the site links compiled CSS in its server-rendered head |
| Strict CSP | done | no <style> made by script, zero violations under style-src 'self' with the engine, texture, slider, tilt, aura and inspector; textures need img-src data: |
| Devtools show .dssl source lines | done for built CSS | --sourcemap; Chrome's own DevTools shows card.dssl:4 (checked in a real DevTools window). Runtime inline styles cannot carry a source: use DSSL.inspect() |
Bugs the comparison found, now fixed: compiled selectors had CSS specificity (now :where()), a later border-style or
border-width was dropped by the engine, a margin or padding shorthand did not reset an earlier longhand, the reset was unlayered.
v0.12.0: the rest of the wood list
Fonts, richer gradients, an inspector, hot reload and mirrored tiling, from the same critic's list (rows marked "done in 0.12.0" below).
v0.11.0 (wood): rings, species, planks, baked textures
Written after another AI built a wood site in DSSL and listed what hurt. What came of it:
| Their note | What DSSL does now |
|---|---|
texture: wood is one generic grain |
oak, walnut, cherry, maple, pine, mahogany, ebony, birch, plus oak-planks and walnut-planks |
| Stretched noise with a contour layer is not wood | the rings generator draws growth rings as curves, with figure, warp, pores and knots |
| No way to draw planks | planks <px>: boards with seams and a slightly different board each |
| Seamless tiling, stable seed | integer harmonics and a seeded generator: same seed, same board everywhere; tested for seams |
counter-reset and counter() rejected |
they work |
| Contrast false alarms on translucent backgrounds | judged on the light page, white and black; warns only if it fails on all |
| Flash of unstyled content | dssl-ready class on <html>, and dssl build for the static part (the wood page does this) |
Textures missing from dssl build |
baked into the CSS when colours are known; the report names the ones that could not be |
@font-face in raw {} |
done in 0.12.0: a fonts { } block (Google families, variable ranges, your own files) |
| Gradients only cover simple cases | done in 0.12.0: repeating, colour spaces, midpoints, hard bands, mesh, layers |
| Dev-tools inspector for which rule produced a style | done in 0.12.0 as an in-page panel, DSSL.inspect(). A real browser DevTools panel would need an extension and is still not done. |
| Hot reload | done in 0.12.0: DSSL.watch() and dssl dev |
| The wood tile repeats every 576px | texture: ... symmetrical mirrors the tile, so it never shows a seam |
v0.10.0: glass, tilt, and a silent colour bug
| Need | DSSL | Status | Notes |
|---|---|---|---|
| Frosted glass | glass: 18 saturate 1.6 |
shipped | plain CSS, survives dssl build; CSS needs backdrop-filter plus a matching background |
| Pointer-following 3D tilt | tilt: 10 lift 1.04 glare |
shipped | engine only; flat under reduced motion, ignores touch; not combinable with bend or fold |
| Alpha on a named colour | white/12 |
fixed | used to compile to white/12px and be dropped by the browser with no error |
v0.9.0: texture, aura, sliders, and what testing them found
New language: texture, the aura family, slider, lint warnings, dssl import, and an
interactive designer in the editor extension. Writing real-browser tests for them found bugs
that were fixed in the same release:
aura-glow: nonealso wiped the particles slot. Eachnonenow clears only its own slot.- With reduced motion the loop kept redrawing a still glow 60 times a second. It now redraws on scroll, resize and a slow poll only (and still follows the page when you scroll).
- A glow was re-blurred every frame (11 ms for 40 glows). Each glow is now painted once into a cached bitmap and re-lit per frame (0.6 ms).
freeze()stopped aura but not the texture shimmer. It stops both.- A slider clause with a leftover word (
thumb 16 #fff shape bar) was silently ignored. It is an error. - A typo in an aura (
speedd 4) was swallowed as a colour and never got its did-you-mean. dssl builddid not count engine-native declarations inside a rule that kept other declarations, so it said nothing about the aura or texture it dropped. It now lists them.- The downloadable package and the editor extension each copied a hardcoded list of engine files, so a new module would have crashed them. They copy the whole folder now, and a test guards it.
importCssthrew on a stray@. It never throws now.
The contrast lint also found four real contrast failures in this site's own stylesheets.
v0.8.2: what rebuilding the site found
- Cross-stylesheet cascade. "Later wins" held inside one sheet and not between
sheets: every sheet numbered its rules from zero and the engine compared those
numbers across sheets, so a later sheet lost to an earlier one whenever the
earlier one had more rules above the rule in question. Every rule now has one
position in load order. A consequence: a later
tokens { }block now overrides a token an earlier sheet set inside@mediaor@signal. fluidworks on the longhands (padding-top,margin-block,letter-spacing,border-radiusand the rest), not only on the four-sided shorthands.
v0.8.1 follow-up corrections
A re-test of 0.8.0 found two bugs, and more showed up while writing the tests:
fluidleft out the intercept of its line, sofluid 16 24was 19.6px on a 375px phone instead of 16.1px and reached 24px at about 840px instead of 1200px. Fixed, and the tests now evaluate the emittedclamp()at real window widths. The same fix repaired shrinking sizes (fluid 24 16, which produced a backwardsclamp()) and rem sizes (the slope was 16 times too small). Percentages and mixed units are now refused with a message instead of producing nonsense.- A multi-line
raw { }started with the textundefined, and anything written on the same line asraw {was dropped. The guide's own example also had no semicolons, which is not valid CSS. All three fixed, and a missing semicolon insideraw { }is now a warning with the right line number. dssl buildnow takes--strict, likedssl check.- The
dsslcommand in the downloadable package crashed on start (module not found), because it looked for the engine inpublic/dssl/and the package keeps it indssl/. It now finds it in either place, and a test runs it from the package layout.
v0.8.0 review-driven corrections
The second external review (repro snippets, headless browser) found five bugs and asked for the static build first. All fixed or shipped:
row: gap 8 between center, the guide's own example, kept itsbetween(every place word used to write both axes; the later word overwrotejustify-contentsilently). Place words now own an axis: between/around/evenly are main-axis, stretch/baseline cross-axis, center/start/end fill free axes; conflicts and duplicates warn loudly.@font-face { ... }crashed the compiler with an uncaught throw. Unknown at-rules now skip their entire balanced block with one clean error.@supports (...) { .a { ... } }used to error AND still apply.aunconditionally. The whole block is skipped now; nothing leaks.- Unquoted
url(a.png)false-errored ("unknown value a.png");url()is atomic now, and the missing background words (no-repeat,repeat-x/y,space,round,scroll,local) joined the vocabulary. color: red !importantused to error AND emitred !important(which the browser then silently dropped).!importantis now one loud error, nothing is emitted, and the hint namespin { }as the actual answer.animation: spin ...suggested "did you mean span"; it now passes motion names through, warns when the motion is undefined, and bare durations are ms.transition: color .2s ease(CSS order) is an error whose hint shows the exact DSSL conversion (transition: 200 ease color).
And the wishlist items that shipped: raw { } / @css { } verbatim passthrough,
dssl build + dssl check (the CLI, with --json and CI exit codes),
DSSL.toCSS(), headless-Chromium computed-style tests for every guide example
(scripts/test-guide-browser.mjs), and font-size: fluid 16 24 clamp sugar.
v0.7.2 review-driven corrections
An external agent review reproduced a batch of real bugs; all of them are fixed and regression-tested (scripts/test-v072.mjs). Rows affected:
- Nested state blocks (hover/focus/... inside a rule) now sort AFTER the parent rule's own declarations, so nested overrides win the cascade. Nested @media, @signal and @layer blocks also keep their condition and layer (they used to be dropped, making nested dark mode silently dead).
- Declarations and a nested block can share one line (
.card { bg: red; hover { } }) without the declaration being swallowed. - Raw hex alpha is real:
#ff0000/50compiles to color-mix (was invalid /50px). - Time properties take bare numbers as ms (
transition-delay: 200-> 200ms). grid: 3 gap 12works;rows Njoined the shorthand.seen { ... }rejects motion-arrow syntax loudly and points at the real forms.- SVG and modern properties added: fill, stroke (and friends), text-wrap, content-visibility.
::part()rides the shadow pipeline;::slotted()is rejected with an explanation (it only exists inside shadow trees).- The gate prefilter now actually runs before native matches().
- State events scope candidate recomputation to the hovered subtree and its ancestors instead of the whole page (a 1k-card hover went from ~4,100 recomputes per event to the touched chain).
- checkbox/details states hear
changeandtoggleevents, so keyboard toggles and programmatic dispatch restyle immediately.