CSS grammar (level 1)

Last Updated: 2026-05-30 Status: 🟒 APPROVATO β€” allineato al contratto v0.5.0 (renderer-side chain landed 2026-05-30). Maintainer: subtask css_builder/ (closed 2026-05-12).

CSS level 1 grammar. Covers rules, selectors, custom properties, and the @media / @supports / @import at-rules.

Purpose

Produce CSS stylesheets programmatically. Pairs with CssRenderer for serialization. Supports the round-trip via CssBuilder.from_css(...) (parse CSS source into builder code).

Quick start

from genro_builders.contrib.css import CssBuilderHandler


class Theme(CssBuilderHandler):
    def main(self, root):
        sheet = root.stylesheet()
        sheet.rule(color="red", padding="10px")\
             .selector(class_="card")\
             ._.selector(class_="panel")\
             ._.cssvar("primary", value="#3498db")


t = Theme(); t.create()
print(t.render())

Output:

.card, .panel {
  color: red;
  padding: 10px;
  --primary: #3498db;
}

Elements

Element

Type

Sub-tags

Notes

stylesheet

container

rule[],importcss[]

Top-level container.

rule

container

selector[],cssvar[]

A CSS rule. Property declarations go as kwargs.

selector

leaf

β€”

A selector clause (.card, #main, etc.). Multiple selectors under the same rule produce a selector list.

selector_list

container

selector[]

Explicit selector list (rarely needed; rule auto-builds it from its children).

cssvar

leaf

β€”

A CSS custom property declaration (--name: value).

importcss

leaf

β€”

@import url(...) at the top of a stylesheet.

@media and @supports are passed as kwargs on rule, not as separate elements: rule(media="(max-width: 600px)", ...).

Common patterns

Property declarations as kwargs

A rule accepts CSS properties as kwargs. Hyphenated CSS names use underscore in Python:

sheet.rule(color="red", font_size="14px", text_align="center")

The renderer converts font_size β†’ font-size on output.

Selectors as children

Each selector is an explicit child element:

rule = sheet.rule(color="red")
rule.selector(class_="card")
rule.selector(class_="panel")
rule.selector(_id="main")

This produces a selector list: .card, .panel, #main { color: red; }.

The class_, id, attr kwargs map to CSS selector syntax (a class, an id, an attribute selector); classes takes a list for a multi-class compound. Pseudo-classes/elements attach directly inside class_ (e.g. class_="card:hover"); anything the structured kwargs can’t express goes through the opaque raw suffix.

Chaining with ._

sheet.rule(color="red")\
     .selector(class_="card")\
     ._.selector(class_="panel")\
     ._.cssvar("primary", value="#3498db")

See ../builders/patterns.md.

Round-trip from CSS source

The builder exposes two classmethods for the reverse direction:

from genro_builders.contrib.css import CssBuilder

# Parse a CSS string into builder Python source:
python_code = CssBuilder.from_css(".card { color: red; }")

# Or parse a file:
python_code = CssBuilder.from_css_file("theme.css")

The output is a complete Python module containing a CssBuilderHandler subclass whose main(self, root) rebuilds the input.

Render

CSS rendering does not use the universal rendered_item walk because CSS needs top-level composition (cssvar grouping, @import ordering, nested @media/@supports blocks). The CSS renderer overrides RendererBase.render(source, **opts) with a top-level dispatch that calls into internal helpers (_render_top_sequence, _render_top_node, …) and emits a stylesheet; CssBuilderHandler.render drives this whole-stylesheet walk instead of the generic render_children. The result is still finalized through the standard finalize (a single method: it joins the fragments and consumes the target).

The only mode is css. Output is multi-line, indented with two spaces, one property per line. No minification mode yet.

Validation rules

  • selector kwargs are validated eagerly with regex: invalid selector syntax raises at create() time.

  • rule rejects unknown CSS shorthand kwargs only when explicitly spelled out by the schema; most kwargs pass through as declarations.

  • importcss only valid at the top of a stylesheet.

Validation errors raise ValueError at create() time with a clear pointer to the offending kwarg.

Worked examples

01_introduction/ under ../../src/genro_builders/contrib/css/examples/ is a three-view example covering rules, selector lists, custom properties, and @media kwargs.

Known limitations

  • At-rules beyond @media, @supports, @import are not supported in level 1: @keyframes, @font-face, @property, @layer, @scope will be handled in a future subtask.

  • No CSS nesting (the level-3 nested rules proposal). Each rule is a separate top-level child of the stylesheet.

  • No source maps.

  • No CSS minification mode.

References

  • Source: src/genro_builders/contrib/css/.

  • Renderer: src/genro_builders/contrib/css/css_renderer.py.

  • Schema: src/genro_builders/contrib/css/css_elements.py.

  • Reverse parser: src/genro_builders/contrib/css/_reverse.py.

  • Contract: roadmap/architecture-contract.md.