Radius Tiers
A semantic vocabulary for border-radius. Three tiers (commit, modify, contain) let components, brand themes, and inline overrides speak the same language without anyone reaching for a raw pixel value.
Three Tiers
Every primitive in the library belongs to exactly one of three tiers. The tier dictates the
component's default radius and how it responds to a wrapping context. Brands tune the visual
identity by changing the three token values once, e.g. @theme { --radius-commit: var(--radius-lg); } (Override Pathway);
component code never touches a raw pixel value.
commit
Pill (9999px). Action, identity, status. Buttons, ButtonGroup, Badge, SegmentGroup, Toggle, Stepper.
modify
Small (~4px). Tap surface, editable. Input, Select, Combobox, Textarea, Checkbox, Tab, Menu-Item.
contain
Barely-rounded (~2px). Architectural surface, panel. Card, Alert, Dialog, Drawer, Tooltip, Popover, Toolbar (whose children inherit modify).
The tier tokens
/* foundation.css — the tier tokens */
@theme {
--radius-commit: 9999px; /* pill — Button, ButtonGroup, Badge */
--radius-modify: var(--radius-sm); /* tap surface — Input, Select, Tab */
--radius-contain: var(--radius-xs); /* container — Card, Alert, Dialog */
--radius-bridge: var(--radius-md); /* middle rung — adjacency + small content surfaces */
}The token names are also the public-API vocabulary: a component reads tier="commit" and applies rounded-commit. The contract is symmetric.
One exception: radio indicators keep their circle via --radius-control (see Override Pathway).
Tier-aware Components
Interactive primitives take a tier prop and fall back
to a wrapping TierContext when the prop is unset (the Blocks index carries the per-component defaults). The defaults worth knowing:
| Component | Default tier | Family | Why |
|---|---|---|---|
| Button | commit | Action | Identity declaration: buttons read as decisive. |
| Toggle | commit | Action | Track radius mirrors Button visual weight. |
| SegmentGroup | commit | Navigation | Track + indicator both follow tier for inline action-strip ergonomics. |
| Stepper | commit | Navigation | Indicator + separator are progress-marker affordances. |
| RadioGroup | commit | Form | Indicator + dot read as commit-tier affordances even inside Form-family. |
| Checkbox | modify | Form | Box is the canonical input-tap surface: modify is its native tier. |
| Tab | modify | Navigation | Editorial-leaning navigation surface; commits would feel too loud. |
Prop and context contract
<script>
import { Button, Toolbar } from '@urbicon-ui/blocks';
</script>
<!-- Default: tier="commit" — pill -->
<Button>Launch</Button>
<!-- Explicit: tier="modify" — soft square -->
<Button tier="modify">Launch</Button>
<!-- Wrap in a Toolbar with tier="modify" — children inherit -->
<Toolbar tier="modify">
<Button>Launch</Button> <!-- inherits modify -->
<Button tier="commit">Reset</Button> <!-- explicit override -->
</Toolbar>Context Cascade
A wrapping component with a tier prop publishes that
tier through TierContext. Every tier-aware child that
does not set its own tier prop inherits the wrapping value.
Switch the toolbar tier below to see Button, Toggle, Checkbox, and Badge respond in lockstep.
Children with no tier prop inherit commit. Set tier="commit" on a single child to opt-out of the cascade.
Override Pathway
The tier tokens are CSS custom properties. Override them at any scope: global @theme for brand identity, a wrapping selector for a
stage-specific look, or an inline style attribute for one-off
experiments. The same Button code re-flows across all three.
One deliberate exception: the radio indicator reads --radius-control, not the tier's radius. Its circle is
the only thing distinguishing it from a checkbox, so squaring the commit tier for austere
buttons used to square the radios along with them. The token defaults to the same 9999px, so nothing changes until you set --radius-control yourself. The checkbox keeps following
the tier: a pill-shaped box there is a look you asked for by writing tier="commit", not one a button theme imposed on you.
--radius-commit: The override sits on the parent <div style>:
every rounded-commit utility inside that subtree picks
up the new value.
Brand-level + scoped overrides
/* Brand-level override — applies everywhere */
@theme {
--radius-commit: var(--radius-lg); /* squared pill */
--radius-modify: var(--radius-xs); /* tighter inputs */
}
/* Scoped override — only inside .editorial-stage */
.editorial-stage {
--radius-commit: var(--radius-md);
}Bridge Token
--radius-bridge is the middle rung (var(--radius-md) by default) for the two cases where contain is too hard and commit too soft. The first is adjacency: a floating panel anchored to a commit-tier (pill) trigger is
container-tier content, but its radius wants to pair with the trigger, so a Menu panel under a
pill Button reads as connected rather than as a stranded contain-surface.
The second is optical size: radius scales with the area it turns, so the 2px
edge that reads as precise on a 600px Card reads as a plain rectangle on a ~200px tile. A
small tinted surface is content, not architecture, and takes the middle rung: the ChatMessage bubble, Textarea at tier="commit" (a pill would be absurd on a multi-line
field), and <Card tier="bridge">, which is how a
consumer says "this tile is too small for the container radius" without hand-setting a rounded-* class and splitting the contain family.
Anything that genuinely is a panel, dialog or container stays on contain. Brands tune bridge via the foundation token
like any other tier. See Menu for the adjacency case and Card for the optical one.