You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Close the Custom Media Queries entry in the README TODO with a media module that defines reusable named conditions once, emits native @custom-media rules, and lets the existing settings.atRule conditions reference those names.
The module is opt-in. Configurations without it keep their current CSS, JSON, TypeScript, and Style Dictionary output.
Motivation
Media conditions are duplicated as raw strings in every settings.atRule. The Colors example repeats @media (prefers-color-scheme: dark) for the dark theme and for each palette override, and example/basic/cssforge.config.ts repeats it once per dark palette color. Changing one breakpoint means finding every copy.
Media Queries Level 5 gives the same reason for the rule: repeated media queries are an editing hazard, and naming one turns a multi-site edit into a single line.
Current limitation
WithCondition carries only selector and atRule strings, and conditionalBuilder() emits the atRule value verbatim as a block opener. The reference half therefore already parses today: atRule: "@media (--dark)" produces @media (--dark) { ... }. What is missing is the definition, its placement, and validation of the name.
The definition cannot ride the current path. generateCSS() opens :root { at line 489, appends each module root output inside it, and appends only selector-based outside output after the closing brace at line 565. An atRule value of "@custom-media --dark (prefers-color-scheme: dark);" would emit @custom-media --dark (prefers-color-scheme: dark); { inside :root, and @custom-media belongs at the top level of the stylesheet.
Design choices that the implementation should settle explicitly:
The section name and its placement next to colors, spacing, and typography are a schema decision. The key to name mapping (narrow-window becomes --narrow-window) should stay predictable and validated.
Grammar: @custom-media <extension-name> [ <media-query-list> | true | false ] ;. The name is a dashed ident and whitespace separates it from the value. at-custom-media-parsing.html marks @custom-media --query(max-width: 30em) invalid while @custom-media --query (max-width: 30em), --query (color), (hover), --query not all and (hover: hover), --query true, and --query false are valid.
A definition is referenced in boolean context as @media (--name). Normal and range contexts are syntax errors.
Evaluation is logical, not textual substitution. @custom-media --modern (color), (hover) used as @media (--modern) and (width > 1024px) means ((color) or (hover)) and (width > 1024px). The generated CSS must keep the definition and the reference, so a later build-time transform can decide how to lower it.
Cycles are forbidden, and the specification drops every custom media query in a loop. A diagnostic naming the loop is better than CSS that silently stops matching.
Repeated definitions of one name resolve by the rule in scope at evaluation time, which differs between the published Working Draft and current MDN description. Emitting all definitions before their first use satisfies both readings, and config keys cannot express redefinition, so a duplicate can only come from two keys normalizing to one name and should be reported like the collision in assertNoKeyCollisions().
@custom-media also qualifies @import, so definitions belong in a stylesheet prelude ahead of any @import and ahead of any referencing @media.
Platform reality
The rule is not Baseline. Firefox 148 implements it behind the layout.css.custom-media.enabled preference, Chrome tracks crbug.com/40781325, and Safari tracks webkit.org/b/233820, both without an implementation.
In a browser without support the definition is an unknown at-rule that is dropped, and @media (--name) holds an unknown media feature, which MQ5 error handling turns into not all. A theme override behind a named condition then never matches, without an error.
Consumers on everything except Firefox need a build-time transform, for example PostCSS Custom Media or Lightning CSS.
Every example Playwright project declares only a chromium project, for example example/vanilla-react-css/playwright.config.ts, so the current browser tests cannot prove that @media (--name) matches.
Acceptance criteria
The media section is opt-in: a configuration without it produces byte-identical CSS, JSON, TypeScript, and Style Dictionary output, proven against the existing snapshots.
Definitions are emitted at the top level of the stylesheet, ahead of :root { and ahead of every @media that references them, never inside a selector or at-rule block.
Emitted text follows the grammar, including the space between name and value, verbatim true and false, and comma separated lists. The valid and invalid cases from at-custom-media-parsing.html are covered at the library seam.
A definition that references an unknown name fails with the configuration path and the missing name.
A reference cycle fails with a readable path such as media.a → media.b → media.a and writes no stylesheet.
settings.atRule: "@media (--name)" wraps palette, gradient, and theme declarations in that condition, and existing raw atRule strings keep their current output.
An atRule string that references a name cssforge does not define is reported at the configuration path that wrote it, while a plain native condition such as @media (min-width: 60em) stays valid and unvalidated.
JSON, TypeScript, and Style Dictionary modes define what happens to the section, and the definitions neither appear as tokens nor collide with token paths.
The CLI (--mode css and --mode all) writes a stylesheet where every definition precedes its uses, proven by a CLI output test.
README documentation covers the config shape, the generated CSS through an md:generate block, the transform requirement, and the fallback behavior without @custom-media. The package README and docs pages are regenerated with moon run cssforge:readme-update and the docs sync-docs task, including the page list in sync-readme.ts, the sync-docs outputs, and the docs sidebar.
Verification records either a Firefox Playwright project with the preference enabled proving that a themed declaration applies only while the condition matches, or an explicit decision to rely on the transform path plus the documented unsupported-browser fallback. A Chromium-only run cannot prove this behavior.
The README TODO entry is checked off once the module lands.
Verification
moon run cssforge:test
moon run cssforge:typecheck
moon run cssforge:format
moon run cssforge:readme-check
moon run vanilla-react-css:cssforge-generate for the example integration, once an example uses a named condition
Non-goals
Expanding or substituting custom media queries during generation. Textual substitution does not reproduce the specification's logical evaluation, and the transform belongs to PostCSS or Lightning CSS.
A JavaScript runtime or CSS.customMedia script API.
General media query validation for raw atRule strings beyond the names cssforge itself defines.
Migrating the existing examples and README examples to named conditions, which can follow separately.
Goal
Close the
Custom Media Queriesentry in the README TODO with amediamodule that defines reusable named conditions once, emits native@custom-mediarules, and lets the existingsettings.atRuleconditions reference those names.The module is opt-in. Configurations without it keep their current CSS, JSON, TypeScript, and Style Dictionary output.
Motivation
Media conditions are duplicated as raw strings in every
settings.atRule. The Colors example repeats@media (prefers-color-scheme: dark)for the dark theme and for each palette override, andexample/basic/cssforge.config.tsrepeats it once per dark palette color. Changing one breakpoint means finding every copy.Media Queries Level 5 gives the same reason for the rule: repeated media queries are an editing hazard, and naming one turns a multi-site edit into a single line.
Current limitation
WithConditioncarries onlyselectorandatRulestrings, andconditionalBuilder()emits theatRulevalue verbatim as a block opener. The reference half therefore already parses today:atRule: "@media (--dark)"produces@media (--dark) { ... }. What is missing is the definition, its placement, and validation of the name.The definition cannot ride the current path.
generateCSS()opens:root {at line 489, appends each modulerootoutput inside it, and appends only selector-basedoutsideoutput after the closing brace at line 565. AnatRulevalue of"@custom-media --dark (prefers-color-scheme: dark);"would emit@custom-media --dark (prefers-color-scheme: dark); {inside:root, and@custom-mediabelongs at the top level of the stylesheet.Proposed API direction
Design choices that the implementation should settle explicitly:
colors,spacing, andtypographyare a schema decision. The key to name mapping (narrow-windowbecomes--narrow-window) should stay predictable and validated.@media (--name)strings keep configs free of helper functions and preserve the native escape hatch that feat(validation): add conservative diagnostics for selector- and condition-scoped token references #30 relies on. A helper that builds the string from a key would be optional.Constraints from the specification
@custom-media <extension-name> [ <media-query-list> | true | false ] ;. The name is a dashed ident and whitespace separates it from the value. at-custom-media-parsing.html marks@custom-media --query(max-width: 30em)invalid while@custom-media --query (max-width: 30em),--query (color), (hover),--query not all and (hover: hover),--query true, and--query falseare valid.@media (--name). Normal and range contexts are syntax errors.@custom-media --modern (color), (hover)used as@media (--modern) and (width > 1024px)means((color) or (hover)) and (width > 1024px). The generated CSS must keep the definition and the reference, so a later build-time transform can decide how to lower it.assertNoKeyCollisions().@custom-mediaalso qualifies@import, so definitions belong in a stylesheet prelude ahead of any@importand ahead of any referencing@media.Platform reality
layout.css.custom-media.enabledpreference, Chrome trackscrbug.com/40781325, and Safari trackswebkit.org/b/233820, both without an implementation.@media (--name)holds an unknown media feature, which MQ5 error handling turns intonot all. A theme override behind a named condition then never matches, without an error.example/vanilla-react-css/playwright.config.ts, so the current browser tests cannot prove that@media (--name)matches.Acceptance criteria
mediasection is opt-in: a configuration without it produces byte-identical CSS, JSON, TypeScript, and Style Dictionary output, proven against the existing snapshots.:root {and ahead of every@mediathat references them, never inside a selector or at-rule block.trueandfalse, and comma separated lists. The valid and invalid cases fromat-custom-media-parsing.htmlare covered at the library seam.media.a → media.b → media.aand writes no stylesheet.settings.atRule: "@media (--name)"wraps palette, gradient, and theme declarations in that condition, and existing rawatRulestrings keep their current output.atRulestring that references a name cssforge does not define is reported at the configuration path that wrote it, while a plain native condition such as@media (min-width: 60em)stays valid and unvalidated.describeScope()records the reference text, so@media (--narrow)and@media (max-width: 30em)remain distinct scopes even when they denote the same condition. Document that behavior and keep the feat(validation): add conservative diagnostics for selector- and condition-scoped token references #30 diagnostics consistent with it.--mode cssand--mode all) writes a stylesheet where every definition precedes its uses, proven by a CLI output test.md:generateblock, the transform requirement, and the fallback behavior without@custom-media. The package README and docs pages are regenerated withmoon run cssforge:readme-updateand the docssync-docstask, including the page list insync-readme.ts, thesync-docsoutputs, and the docs sidebar.Verification
moon run cssforge:testmoon run cssforge:typecheckmoon run cssforge:formatmoon run cssforge:readme-checkmoon run vanilla-react-css:cssforge-generatefor the example integration, once an example uses a named conditionNon-goals
CSS.customMediascript API.atRulestrings beyond the names cssforge itself defines.