CSS styling
Style individual UI components with the validated rules channel
The rules channel is the CSS/Code mode's main tool: a validated CSS dialect that targets named UI parts and sets allowlisted properties on them. You write it in the studio's rules editor (visual form or CSS text — both edit the same model), preview it live, and publish. Developers can drive the same channel over HTTP — see the developer API.
Because everything is validated against a closed contract, a rule can restyle your customers' surfaces but can never leak outside them, import external resources, or break other tenants.
The syntax
Selectors use bare part names — no classes, no DOM knowledge required:
/* every Button, everywhere */
Button {
border-radius: 9999px;
font-weight: 600;
}
/* a specific variant */
Button.destructive {
background-color: #e5484d;
}
/* interaction states are pseudo-classes */
Button:hover {
background-color: var(--brand-hover);
}
/* part-specific states use [state="..."] */
SelectTrigger[state="open"] {
border-color: var(--brand);
}
/* scope a rule to one color mode with a .dark / .light prefix */
.dark Card {
background-color: #1c1f24;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
/* responsive rules go in a min-width media query */
@media (min-width: 768px) {
Greeting {
font-size: 2rem;
}
}The selector grammar, completely:
| Form | Meaning |
|---|---|
Part | Target every instance of a part |
Part.variant | One of the part's declared variants |
Part:hover | A universal interaction state: :hover, :focus-visible, :active, :disabled |
Part[state="x"] | A part-specific state (e.g. open, checked, active) |
.dark Part / .light Part | Apply in one color mode only |
@media (min-width: Npx) { … } | Apply at a breakpoint and up: 640 (sm), 768 (md), 1024 (lg), 1280 (xl) |
Anything else — descendant chains, ids, attribute selectors of your own, unknown parts or states — is a validation error you'll see in the editor.
The parts catalog
The closed set of styleable parts. Parts with variants can be narrowed with .variant; parts with states accept [state="…"] on top of the universal :hover / :focus-visible / :active / :disabled.
| Part | Variants | States | What it is |
|---|---|---|---|
Button | default, destructive, outline, secondary, ghost, link | — | All standard buttons |
Input | — | — | Single-line text input |
Textarea | — | — | Multi-line text input |
SelectTrigger | — | open, closed | Select dropdown trigger |
Checkbox | — | checked, unchecked | Checkbox control |
Switch | — | checked, unchecked | Toggle switch |
Card | — | — | Raised content card |
DialogContent | — | open, closed | Modal dialog surface |
PopoverContent | — | open, closed | Popover surface |
Tooltip | — | — | Tooltip surface |
TabsList | — | — | Tab strip container |
TabsTrigger | — | active, inactive | Individual tab |
Avatar | — | — | User/workspace avatar |
DropdownMenuContent | — | — | Dropdown menu surface |
DropdownMenuItem | — | highlighted, disabled | Dropdown menu row |
NavItem | — | active | Sidebar navigation row |
SidebarRoot | — | — | Dashboard sidebar container |
WorkspaceSwitcher | — | — | Workspace switcher trigger |
MobileTopBar | — | — | Mobile header bar |
ProjectCard | — | — | Dashboard project grid card |
Greeting | — | — | Dashboard greeting headline |
CreditsWidget | compact, full | — | Credit balance widget |
SearchOverlay | — | — | Dashboard search card |
SettingsSection | default, danger | — | Settings page section |
SettingsRow | — | — | Settings label/control row |
StepIndicator | — | — | Wizard step dots |
PromptBox | dark, light | — | AI prompt input container |
EditorHeader | — | — | Editor top chrome |
PublishButton | — | — | Editor publish CTA |
ChatBubble | — | — | User chat message bubble |
UsageChart | — | — | Usage bar chart container |
The catalog grows over time; the live list is always available from the contract endpoint.
Properties and values
Only these properties can be set, and every value must fully match its grammar — there's no passthrough:
| Properties | Grammar | Accepts |
|---|---|---|
background-color, color, border-color, outline-color | color | #hex (3/6/8), rgb()/rgba()/hsl()/hsla(), transparent, or var(--token) for any published token |
border-width, border-top/right/bottom/left-width, padding-top/right/bottom/left, gap, font-size, letter-spacing | length | 0, or a number with px, rem, em, or % |
border-radius, padding | lengths | 1–4 space-separated lengths |
border-style | keyword | none, solid, dashed, dotted |
box-shadow | shadow | none, or 1–4 comma-separated layers (inset? 2–4 lengths, then a color) |
font-weight | weight | 100–900 in hundreds, normal, bold |
line-height | line-height | Unitless 0.8–3, or a length with a unit |
text-transform | keyword | none, uppercase, lowercase, capitalize |
text-decoration-line | keyword | none, underline |
opacity | opacity | 0–1, up to 3 decimals |
transition-duration | time | Digits + s or ms, e.g. 150ms |
Referencing tokens instead of hex where possible (var(--brand-hover) rather than a hard-coded shade) keeps your rules working when you later change the palette.
Validation: strict where it matters
- Unknown part, variant, state, mode, or breakpoint → error. A typo in targeting is something you need to see.
- Invalid value for a known property → error. A silently dropped value would be a confusing no-op.
- Unknown property → dropped with a warning. The allowlist grows over time, so old rules shouldn't break on new keys.
- Values containing
;,{,},\,/*,url(,@,<, or>are rejected outright.
A legibility linter also runs on submit: blocking problems (like unreadable contrast against your own theme) stop the draft; softer notes come back as warnings.
Caps
| Cap | Value |
|---|---|
| Rules | 300 |
| Properties per rule | 20 |
| Compiled CSS size | 100,000 bytes |
Publish flow
Rules save as immutable, content-addressed drafts — the same rules always produce the same hash. You preview a draft in the studio, then publish the exact hash you previewed. Every publish lands in the version history with one-click restore. The same lifecycle is scriptable through the developer API.