The control-sizing contract
Three sizes — sm (22px), md (30px, the default) and lg (36px). A control's height is never hard-coded. It is the sum of its line-height, twice its vertical padding and twice its 1px border. Keeping the parts as separate tokens means padding-y can stay density-aware while line-height holds the text box steady.
:root {
/* sm → 22px · md → 30px · lg → 36px */
--wr-control-padding-y-sm: 0.125rem; /* 2px → 16 + 2×2 + 2×1 = 22 */
--wr-control-padding-y-md: 0.25rem; /* 4px → 20 + 2×4 + 2×1 = 30 */
--wr-control-padding-y-lg: 0.3125rem; /* 5px → 24 + 2×5 + 2×1 = 36 */
--wr-control-padding-x-sm: 0.5rem; /* 8px */
--wr-control-padding-x-md: 0.75rem; /* 12px */
--wr-control-padding-x-lg: 1rem; /* 16px */
--wr-control-line-height-sm: 1rem; /* 16px */
--wr-control-line-height-md: 1.25rem; /* 20px */
--wr-control-line-height-lg: 1.5rem; /* 24px */
--wr-control-font-size-sm: var(--wr-text-xs); /* 12px */
--wr-control-font-size-md: var(--wr-text-sm); /* 14px */
--wr-control-font-size-lg: var(--wr-text-base); /* 16px */
--wr-control-radius-sm: 5px;
--wr-control-radius-md: 6px;
--wr-control-radius-lg: 7px;
}The size ladder
Each box below is a bare <div> styled only with the contract tokens — no fixed heights. The rendered height equals line-height + 2×padding-y + 2×1px border, i.e. 22 / 30 / 36px.
How a control derives its height
A control points its own --wr-<name>-* knobs at the contract, then renders padding and line-height from them. The sm / lg modifiers simply re-point the same knobs at the matching tier — there is no separate height rule to keep in sync. Because only padding-y is multiplied by --wr-density-y, switching density keeps the whole catalogue aligned.
/* A control never hard-codes its height. It sums the three contract
parts, and multiplies padding-y by the density factor so the whole
catalogue tracks density together. From the button styles: */
.wr-btn {
--wr-btn-padding-y: var(--wr-control-padding-y-md);
--wr-btn-line-height: var(--wr-control-line-height-md);
--wr-btn-font-size: var(--wr-control-font-size-md);
--wr-btn-radius: var(--wr-control-radius-md);
line-height: var(--wr-btn-line-height);
font-size: var(--wr-btn-font-size);
/* height = line-height + 2×padding-y(×density) + 2×1px border */
padding: calc(var(--wr-btn-padding-y) * var(--wr-density-y, 1))
calc(var(--wr-btn-padding-x) * var(--wr-density-x, 1));
}
/* The 'sm' / 'lg' modifiers just re-point the same knobs at the
matching tier of the contract — so button, input and select line
up pixel-for-pixel in a row at every size. */
.wr-btn--sm {
--wr-btn-padding-y: var(--wr-control-padding-y-sm);
--wr-btn-line-height: var(--wr-control-line-height-sm);
--wr-btn-font-size: var(--wr-control-font-size-sm);
--wr-btn-radius: var(--wr-control-radius-sm);
}The contract tokens
| Name | Description | Type | Default |
|---|---|---|---|
--wr-control-padding-y-{sm,md,lg} | Vertical padding per size. Multiplied by --wr-density-y at consume time, so it stays the only density-aware part of the height. | length | 2px · 4px · 5px |
--wr-control-padding-x-{sm,md,lg} | Horizontal padding per size. Multiplied by --wr-density-x at consume time. | length | 8px · 12px · 16px |
--wr-control-line-height-{sm,md,lg} | The text box height — the fixed core the padding is added around. | length | 16px · 20px · 24px |
--wr-control-font-size-{sm,md,lg} | Label size per tier — 12 / 14 / 16px. Aliases the type scale so it stays in step with typography. | length | `--wr-text-xs` · `--wr-text-sm` · `--wr-text-base` |
--wr-control-radius-{sm,md,lg} | Corner radius per control size. Climbs gently so larger controls read a touch rounder. | length | 5px · 6px · 7px |
The shared size API
Every form control takes the same size input — sm, md (default) or lg. One mental model across the catalogue: pick a size once and button, input and select agree on it.
<!-- One size scale everywhere — sm · md (default) · lg. The attribute
name differs on the input directive: it is `size`, so it cannot clash
with the native `<input size>`. -->
<button wr-btn size="sm">Small</button>
<button wr-btn size="md">Medium</button>
<button wr-btn size="lg">Large</button>
<input wrInput size="sm" placeholder="Small" />
<wr-select size="lg" placeholder="Large">…</wr-select>import { input } from '@angular/core';
// Each control re-exports its own size alias, but they share one shape.
export type WrButtonSize = 'sm' | 'md' | 'lg';
export type WrInputSize = 'sm' | 'md' | 'lg';
export type WrSelectSize = 'sm' | 'md' | 'lg';
// In the component the input defaults to 'md' and only emits a class
// when it differs — so the base styles cover the common case.
readonly size = input<WrButtonSize>('md');
// host: const size = this.size();
// if (size !== 'md') parts.push(`wr-btn--${size}`);The radius scale
Two families. Controls use the tight --wr-control-radius-{sm,md,lg} ramp (5 / 6 / 7px) so they read crisp at small sizes. Containers and overlays use --wr-border-radius-*, anchored on a 10px base for panels — cards, dropdowns, dialogs, toasts — with sm (6px) for nested rows, lg (16px) for large surfaces and pill (50rem) for fully-rounded ends. Circles (avatars, radio dots, FABs) use border-radius: 50% directly — there is no token for them.
:root {
/* Controls — the squircle radii that sit on buttons / inputs / selects. */
--wr-control-radius-sm: 5px;
--wr-control-radius-md: 6px;
--wr-control-radius-lg: 7px;
/* Containers & overlays — cards, dropdowns, dialogs, toasts… */
--wr-border-radius-sm: 0.375rem; /* 6px — nested items (e.g. dropdown rows) */
--wr-border-radius-base: 0.625rem; /* 10px — the default panel radius */
--wr-border-radius-lg: 1rem; /* 16px — large surfaces */
--wr-border-radius-pill: 50rem; /* fully-rounded ends (pill buttons, tags) */
}
/* Circles are not a token — avatars, radio dots and FABs use 50% directly. */
.wr-avatar { border-radius: 50%; }Disabled
Part of the control contract too — a control that will not respond has one fade strength, not seven.
| Name | Description | Type | Default |
|---|---|---|---|
--wr-disabled-opacity | How far a control fades when it will not respond. One number, because the catalog had seven — 0.6 in eighteen places, then 0.5, 0.7, 0.55, 0.4, 0.35 and 0.65 across 29 entry points, with date-picker and calendar each disagreeing with themselves three ways inside one file. Override it in a component block, not per declaration: wr-calendar does, because a day from the adjacent month is still navigable at 0.55, so a disabled day has to be fainter than that or the two swap meanings. | 0.6 | — |
The radius tokens
| Name | Description | Type | Default |
|---|---|---|---|
--wr-control-radius-sm | Small control corners (size="sm" button / input / select). | length | 5px |
--wr-control-radius-md | Default control corners. | length | 6px |
--wr-control-radius-lg | Large control corners. | length | 7px |
--wr-border-radius-sm | Nested surfaces inside a panel — e.g. dropdown / context-menu rows. | length | 0.375rem (6px) |
--wr-border-radius-base | The default container / overlay radius — cards, dropdowns, dialogs, toasts, popovers. | length | 0.625rem (10px) |
--wr-border-radius-lg | Large surfaces that want a softer frame. | length | 1rem (16px) |
--wr-border-radius-pill | Fully-rounded ends — pill buttons, tags, the pill shape modifier. | length | 50rem |