Media / Container Query Builder
Edit CSS media queries AND container queries with compile-time STRUCTURE + FEATURE-VALUE validation. A mode prop ('media' | 'container') selects the dialect. The strict tier validates the optional leading modifier + media-type (only/not all|screen|print) or container name, the boolean combination of parenthesized feature tests — enforcing the CSS no-mixing-and-and-or-at-one-level rule — and each feature test's VALUE dimension against a known feature table: length features want a <length>, resolution features a <resolution>, aspect-ratio a <ratio> like 16/9, and enum features one of their keywords (orientation → portrait|landscape, pointer → none|coarse|fine, …). (width: 16/9) → never; (min-resolution: 600px) → never; (a) and (b) or (c) → never (mixes and/or). Container mode restricts the feature set to the size subset (width, height, inline-size, block-size, aspect-ratio) + orientation. Unknown/exotic features, calc() values, and deep nesting past a depth cap are deferred to the runtime parser (lenient by design).
Build a media query
Controlled value + onChange. Pick a media type, add parenthesized feature tests (min-width, orientation, hover, …), and the editor emits the @media condition string. A single and/or joiner keeps the condition valid by construction.
@media screen and (min-width: 600px) and (max-width: 900px)Feature tests, combiners, and a live match
Each row is one parenthesized feature test: a feature select, a shape (exists / : value / range), an operator, and the value (a unit-input for lengths, a keyword select for enums). Add rows, choose one and/or joiner, and toggle a top-level not. Switch the mode to swap between media and the container size-feature subset.
@media (min-width: 600px) and (orientation: landscape)Three usage tiers
From useState-and-go to compile-time query-grammar validation.
Pass any string
useState<string>. No compile-time validation; the runtime parser splits the condition and classifies every feature — including exotic features the strict tier does not know.
(prefers-color-scheme: dark)const [value, setValue] = useState<string>("(prefers-color-scheme: dark)")
Query-shaped hints
State typed as MediaQueryString — a query- shaped string (and the onChange return type). A QueryStringMap keys the output by mode, so the container dialect gets its own ContainerQueryString.
(hover) and (pointer: fine)const [value, setValue] = useState<MediaQueryString>("(hover) and (pointer: fine)")
Query grammar typed at compile time
cssMediaQuery() / cssContainerQuery() validate the structure, the and/or no-mix rule, and each feature's value dimension — resolving any violation to never before you run the code.
screen and (min-width: 600px)cssMediaQuery("screen and (min-width: 600px)") // ✓ // @ts-expect-error width wants <length>, not <ratio> cssMediaQuery("(width: 16/9)") // @ts-expect-error mixes and/or without parens cssMediaQuery("(a) and (b) or (c)")
Note: unknown/exotic features, calc() values, and deep nesting are deferred to the runtime parser (lenient by design). The strict tier gates the known feature set with typed values.
API
Public surface — component props, runtime helpers, and the type exports.
§ QueryBuilder / QueryBuilderPanel
<QueryBuilder
value: QueryString | (string & {})
onChange: (next: QueryString) => void
mode?: "media" | "container" // default "media"
className?: string
aria-label?: string
/>QueryBuilder is popover-wrapped; QueryBuilderPanel renders the same editor inline. Both are controlled. The mode prop selects the media or container dialect.
| Prop | Type | Description |
|---|---|---|
| value | QueryString | (string & {}) | Current query condition string. Required. An empty / unparseable value renders an empty test list. |
| onChange | (next: QueryString) => void | Fires when the query changes. Emits the canonical condition string. |
| mode | "media" | "container" | Dialect. Default "media". Container restricts the feature set to the size subset + orientation and shows a name input instead of a media-type select. |
§ Sub-components
<FeatureTestRow mode test onChange onRemove index? />
One parenthesized feature test: a feature select, a shape select (exists / : value / op value / range), operator selects, and value field(s) — a unit-input for lengths, a keyword select for enums, a plain input otherwise.
<MediaTypeSelect modifier mediaType onChange />
Media only: the only/not modifier + all|screen|print type (with an (any) no-type option).
<ContainerNameInput name onChange />
Container only: the optional container-name input.
<JoinerSelect value onChange />
The single and/or joiner for the flat test list — one joiner keeps the no-mix rule satisfied by construction.
<NotToggle checked onChange />
A top-level not toggle (negates the whole condition).
<QueryPreview value mode />
Live match indicator. Media mode reads window.matchMedia and updates on change; container mode shows an explanatory note (no global live-match API).
§ Runtime helpers
cssMediaQuery<S>(value: S & MediaQueryLiteral<S>): S
Call-site validator for a media query. Mirrors cssIf() / cssTransform().
cssContainerQuery<S>(value: S & ContainerQueryLiteral<S>): S
Call-site validator for a container query.
parseQuery(src, mode): { node: QueryNode | null; error: string | null }String → a parsed condition tree (strips the leading modifier/type or name), or an error. Parses unknown features too (superset of the strict tier).
parseQueryState(src, mode): QueryState | null
String → the flat editor state (lead + single joiner + not + test list). Drives the row editor.
formatQuery(node, mode): string · queryToString(state): string
Canonical re-serialization of a parsed node, and of the flat editor state.
featureKind(feature, mode) · featuresFor(mode) · enumOptionsFor(feature)
Table lookups: a feature's class (length/resolution/ratio/integer/enum/unknown); the select options for a mode (incl. min-/max- variants); an enum feature's keyword options.
matchesNow(query, mode): boolean | null
Media only — window.matchMedia(query).matches; null for container mode or when matchMedia is unavailable.
defaultFeatureTest(mode) · defaultQuery(mode)
Seed a fresh feature test / a one-test query state for a mode.
§ Types
- MediaQueryLiteral<S> / ContainerQueryLiteral<S>
- Strict validators — S if S is a structurally + dimensionally valid query, else never.
- MediaQueryString / ContainerQueryString / QueryString
- Suggestion unions (query-shaped strings). QueryString is the onChange return type.
- QueryStringMap / QueryMode
- Mode → output-string map, and the mode discriminant (media | container).
- FeatureOperator / MediaType / MediaModifier
- The operator union (< <= > >= =), the media types (all|screen|print), and modifiers (only|not).
- Orientation / Pointer / PrefersColorScheme / …
- Per-feature enum keyword unions (exported for IntelliSense).
- FeaturesOf<S> / FeatureCountOf<S>
- Tuple of the top-level feature names used in a query; and its length.
- FeatureTest / QueryNode / QueryState
- The internal discriminated-union state (a feature test by kind; a parsed node; the flat editor state). Exported for advanced use.
§ Strict-tier scope (validated vs deferred)
- Validated: balanced parens; the optional leading
only/not+ media-type or container name; the top-level boolean split with the no-mixing-and/or rule (mix requires parens); each feature is in the known table for the mode; each value's dimension matches the feature (<length>/<resolution>/<ratio>/<integer>) or its enum keyword. - Deferred (lenient → runtime parser): unknown/exotic features resolve to
neverin strict (use the casual tier); operator-direction consistency in a 3-part range; the illegal(min-width > 600px)combo;calc()/var()values; and nesting past a depth cap of 4. - Container style queries (
style(--x: 1)) are out of strict scope; the size-feature subset is validated, style queries are preserved verbatim by the runtime parser.
Drop it in
One command via the shadcn CLI.
$ pnpm dlx shadcn@latest add https://turtiesocks.github.io/ridiculous/r/query-builder.json