Skip to content

CSS governance

Everything in Tabula's locality argument — an element's appearance is determinable from its own markup — is about the class attribute. A stylesheet reaches elements a different way: by selector. So a project's own .css files are the one surface where the whole model can be undone in a line, and until this layer existed no command in the system opened one.

This page is what governs them now, what it does not cover, and where the two gates can drift.

The five rules

Both gates run the same code — the rule kernel in packages/stylelint-plugin/src/kernel.ts. The stylelint plugin wraps it for your editor; tabula check:css walks files into it for CI. Written twice they would drift, and the drift would be silent in the direction that matters: you fix what the editor flags, CI flags something else, and the rule nobody's editor mentions stops being a rule.

Codestylelint ruleForbiddenWhy
TAB-E221tabula/no-apply@applyComposes a class list into a hand-written selector — the exact indirection the profile removes. Disavowed by Tailwind's own creator (draft-c §3.2 ✚16).
TAB-E222tabula/no-raw-rulesAny hand-authored rule in project CSS outside the sanctioned entry.card .title { color: red } styles by selector, so the element stops being readable from its own markup (draft-c §3.2 ✚18).
TAB-E223tabula/no-scoped-custom-property--tb-* / --d-* defined anywhere but :root/htmlT17 — the single most important check in the system (SPEC J6). The moment a token can be redefined on a <div>, every descendant using it becomes unreadable from its markup.
TAB-E224tabula/no-theme-inline@theme inlineT20inline substitutes token values into the utility at build time, compiling every conditional token away. Dark mode stops working with no diagnostic. It is the shadcn-ecosystem default, so an agent trained on that ecosystem will reach for it.
TAB-E225tabula/no-important-css!important in project CSSEvery utility is normalized to specificity (0,1,0) so rank alone decides conflicts. One !important puts a declaration outside that proof.

A project .css file may therefore contain: @import, @source, @utility, @custom-variant, @charset, a bodiless @layer a, b; order declaration, and comments. Nothing else.

The two gates

In CI / at build time

bash
tabula check:css                       # every project .css file + the emitted stylesheets
tabula build --check                   # drift + scan + check:css, the one command CI runs
tabula build --check --no-css          # drift + scan only; the CSS gate must be opted OUT of
tabula check:css --css-entry src/theme/entry.css   # designate a non-conventional entry

In the editor.stylelintrc.json, mirroring this repository's own:

json
{
  "plugins": ["@tabula-css/stylelint-plugin"],
  "rules": {
    "tabula/no-apply": true,
    "tabula/no-raw-rules": true,
    "tabula/no-scoped-custom-property": true,
    "tabula/no-theme-inline": true,
    "tabula/no-important-css": true
  },
  "overrides": [
    { "files": ["src/app.css"], "rules": { "tabula/no-raw-rules": [true, { "sanctionedEntry": true }] } }
  ]
}

@tabula-css/stylelint-plugin/config exports the rule block so you can spread it instead of copying it. stylelint is an optional peer dependency: the plugin's kernel has no stylelint in its import graph, which is what lets the CLI use it without acquiring an authoring tool as a runtime dependency.

The sanctioned entry

Every app needs one stylesheet that holds @import "../.tabula/source.css"; and the app's own root-level custom properties. That file — and only that file — is exempt from TAB-E222.

It is not exempt from the other four. A :root block in the entry is fine; --tb-color-accent: red in the entry is a TAB-E223 wherever it appears. See examples/reference-ui/src/app.css, which is the worked example.

How a file becomes the entry:

  • by convention — src/app.css, src/index.css, src/styles.css, app/globals.css and a few more (DEFAULT_CSS_ENTRIES in packages/cli/src/commands/check-css.ts);
  • or explicitly — tabula check:css --css-entry <path>, repeatable.

The one place the two gates can drift is this list: check:css reads it from flags and convention, stylelint reads it from an overrides block in your .stylelintrc. Keep them identical. If they disagree, the CI gate is the one that decides whether you ship.

Decisions and inconsistencies, recorded

1. Draft C's codes are not honoured. Draft C assigns these invariants PLANAR-E210 (T17), PLANAR-E211 (T20) and PLANAR-E212 (✚18). Those three numbers were already spent in this catalog on the build invariants I1/I2/I3 before the CSS layer was written. A code that means two things is worse than a code nobody recognises — tabula explain TAB-E212 can only print one teach text, and it would send a reader chasing rank collisions over a hand-written .card p. The family is minted at E221–E225 instead, and each catalog entry records the draft correspondence.

2. T17 reads differently for the emitted stylesheet, and it has to. T17 as written says every profile custom property may be defined only at :root. The profile's own output violates that on purpose: .ring-accent { --tb-ring-color: … } and .shadow-sm { --tb-shadow: … } are per-element composition properties, declared inherits: false with an initial value. So:

  • project CSS — any --tb-*/--d-* definition outside a root subject is a violation, with no exemptions. Project CSS has no legitimate reason to write a profile property at all; dyn() is the sanctioned channel for a per-element value and it writes an inline style, not a stylesheet.
  • emitted CSS — a definition outside a root subject is a violation only for a property that is defined at a root subject somewhere in the emitted set, i.e. a theme token. That catches what T17 exists to catch (a token redefined below the root) without failing the profile's own correct output.

The SPEC should say the second thing. It says the first. Recorded here rather than resolved by weakening the check.

3. @utility is an element binding. @utility card-shell { --tb-color-accent: red } compiles to a class that redefines a theme token on every element carrying it — the same T17 break, wearing the one at-rule project CSS is allowed to contain. The kernel treats a declaration inside @utility as element-scoped for this reason.

4. The entry list belongs in tabula.config.json. Its durable home is a css.entries key beside scan.sources in the config schema (packages/core/src/schemas/config.ts), whose additionalProperties: false means an unrecognised css key fails validation rather than being ignored. The task that built this layer did not own that file, so the list is convention plus --css-entry today. Moving it is a one-key change, and it would remove drift point named above.

5. An unparseable stylesheet is a finding, not a skip. A .css file PostCSS cannot parse is reported as TAB-E222. Skipping it would make "syntactically broken" the cheapest way past every rule on this page.

What this layer still does not cover

  • T18 — the axis attribute below the root. <div data-theme="dark"> is inert (the emitted selectors are :root[data-theme=…], so the attribute has no effect at all) and the ESLint rule tabula/no-axis-attribute-below-root is what would explain that to an author. In CSS, a .panel[data-theme="dark"] selector is caught as a hand-authored rule (TAB-E222) rather than as an axis violation — the right outcome by the wrong name.
  • A project-authored @source line — the closure hole, still open. @source "./src" in one of your stylesheets turns Tailwind's source scanning back on, and the theme namespaces are still defined, so bg-red-500 and p-4 start emitting again. check:css does not flag it, because draft-c §3.2 ✚18 lists @source among the at-rules project CSS may contain. That list and the closure argument in review A7 contradict each other, and this layer implements the list. Until that is resolved in the SPEC: keep the @import of .tabula/source.css the only Tailwind entry in your project, and add no @source line. grep -rn '@source' src is the check.
  • Imported third-party CSS. The gate walks the project tree, not node_modules. A vendored stylesheet you import is exactly as unreviewed as it was before.
  • Runtime <style> injection. Anything a component writes into the document at runtime is outside every gate here.

Released under the MIT License.