/ component

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).

/ basic-usage

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)
/ condition-builder

Feature tests, combiners, and a live match

query mode

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.

(:
)
(:)
(min-width: 600px) and (orientation: landscape)
preview
matchMedia unavailable

Live result from window.matchMedia(), updated as the viewport changes.

produced value
@media (min-width: 600px) and (orientation: landscape)
matches now? unknown (live via window.matchMedia — resize the window)
/ types

Three usage tiers

From useState-and-go to compile-time query-grammar validation.

01 casual
string

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)")
02 intellisense
MediaQueryString

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)")
03 strict
MediaQueryLiteral<S>

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

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.

PropTypeDescription
valueQueryString | (string & {})Current query condition string. Required. An empty / unparseable value renders an empty test list.
onChange(next: QueryString) => voidFires 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 never in 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.
/ install

Drop it in

One command via the shadcn CLI.

$ pnpm dlx shadcn@latest add https://turtiesocks.github.io/ridiculous/r/query-builder.json