Untouched
class="card"
One hand-written file, 161 classes, every one of them a token with a class name on it. No generator, no config, no scanner, no arbitrary-value syntax and no build step — if a class is here, somebody chose to type it.
src/utilities.css is a single static file. There is nothing that produces it and
nothing that consumes it: no JIT engine, no content glob, no class scanner walking
your templates, no purge step, and no escape hatch for values the scale does not have. It ships
exactly as it is written, and what you read in the file is what lands in the browser.
That has one consequence worth stating plainly. The class list is finite and it is short. There are 161 classes across thirteen groups, and there is no syntax for asking for a hundred and sixty-second. That is not a limitation the file is apologising for — it is the design.
If it cannot be named, it is not needed.
A utility that cannot be given a name is a utility that does not correspond to a decision anybody made. If your design needs a value the scale does not have, that is a signal about the design or about the scale, and the answer is to change one of them — not to invent a class on the fly.
This is not a Tailwind clone and it is not a Bootstrap clone. No class name, scale value, or source from either was used in building it. The names come from the properties they set and the steps come from @arag's own space tokens and type tokens. Where a convention here happens to resemble one elsewhere, it is because both were reading the same CSS specification.
| Group | Classes | Hands through |
|---|---|---|
| Display | 10 | Keywords only, plus u-sr-only. |
| Flex | 17 | Keywords only. |
| Grid | 8 | --size-column-min, --size-card-min. |
| Gap | 10 | --space-0…--space-9. |
| Padding | 30 | --space-0…--space-9, three axes. |
| Margin | 16 | --space-0…--space-9, block axis only. |
| Type | 27 | The type, leading, weight and tracking scales. |
| Text colour | 8 | Semantic -text roles. |
| Background | 10 | Surface roles and the -subtle family fills. |
| Border | 6 | --border-width-* and the border colour roles. |
| Radius | 6 | --radius-*. |
| Shadow | 5 | --shadow-*. |
| Size | 8 | --size-measure, --size-container-*. Maximums only. |
| Total | 161 | Plus one [hidden] guard block that adds no new class. |
u-Components are bare. A card is .card, its footer is .card__footer,
and there is no prefix anywhere in the component layer. Utilities take the opposite decision and
prefix all 161, for two reasons that both pay off at the call site.
The first is that it makes a collision impossible. A component named .flex or
.hidden is not far-fetched, and a utility layer that could shadow one would make
every future component name a compatibility question. With the prefix, the two namespaces cannot
touch.
The second matters more day to day: it makes the markup readable. In
class="card u-pad-6" you can see at a glance which token is the thing and which
token is the adjustment, without knowing either name in advance.
<!-- component, then adjustments. The prefix does the sorting for you. -->
<article class="card u-pad-6 u-gap-3">
<h3 class="card__title u-text-lg u-wrap-balance">Дансны үлдэгдэл</h3>
<p class="u-color-muted u-margin-block-0">Төлбөрийн мэдээлэл амжилттай шинэчлэгдлээ.</p>
</article>The prefix also means a grep for u- in your templates returns every place the
utility layer is load-bearing, which is the audit you actually want to run before changing a
token.
index.css declares the order once, before anything is imported:
@layer reset, tokens, base, components, utilities, overrides;utilities comes after components, so a utility beats a component
whenever they set the same property — no specificity trick, no repeated selector, nothing
raising its own priority. And because your own application CSS is unlayered, it still beats
both. The whole override story is layer order, top to bottom, and nothing in this package fights
you for a property. The full order and why it is that order →
Below, both cards are .card. The second one carries two utilities that overwrite
the padding and background .card sets on itself. Same element, same specificity,
one class each — the layer decides.
class="card"
class="card u-pad-3 u-bg-sunken"
<article class="card">…</article>
<article class="card u-pad-3 u-bg-sunken">…</article>
/* @layer components */
.card {
padding: var(--card-padding); /* --space-6 */
background-color: var(--card-bg); /* --color-surface */
}
/* @layer utilities — later layer, so these win */
.u-pad-3 { padding: var(--space-3); }
.u-bg-sunken { background-color: var(--color-surface-sunken); }Every component exposes a local custom property API — --card-padding,
--button-radius and so on. Setting the local on the instance is usually the
better move, because it retunes the derived values with it (a card whose
--card-bg moves gets a matching hover state for free), where a utility overwrites
one computed property and leaves the derivations pointing at the old value. Reach for the
utility when you want exactly one property changed and nothing else.
More on the local API →
Ten classes. Nine set a display keyword; the tenth removes an element visually
while leaving it in the accessibility tree.
| Class | Declaration | Notes |
|---|---|---|
u-block | display: block | |
u-inline | display: inline | |
u-inline-block | display: inline-block | |
u-flex | display: flex | Pairs with the flex group. |
u-inline-flex | display: inline-flex | |
u-grid | display: grid | Pairs with the grid group. |
u-inline-grid | display: inline-grid | |
u-contents | display: contents | Removes the box, keeps the children. Use sparingly — it also removes the element from some accessibility trees. |
u-hidden | display: none | For a permanent removal. For something that toggles, use the hidden attribute instead — see the guard. |
u-sr-only | position, clip-path, --border-width-1 box | Visually gone, still announced. Borrows --border-width-1 for the 1px box rather than introducing a literal, because the 1px is the canonical value for the pattern and not a spacing decision. |
u-inline-block u-inline-block u-inline-block
There is a fourth span after these three
carrying u-sr-only. You cannot see it; a screen reader reads it.Гуйвуулга баталгаажлаа
<span class="u-inline-block u-pad-3 u-bg-sunken u-border-subtle u-radius-sm">…</span>
<span class="u-sr-only" lang="mn">Гуйвуулга баталгаажлаа</span>Seventeen classes, all keywords, none of them touching a length. u-flex itself
lives in the display group; everything here assumes it or
u-inline-flex is already on the element.
| Class | Declaration |
|---|---|
u-flex-row | flex-direction: row |
u-flex-col | flex-direction: column |
u-flex-wrap | flex-wrap: wrap |
u-flex-nowrap | flex-wrap: nowrap |
u-items-start | align-items: start |
u-items-center | align-items: center |
u-items-end | align-items: end |
u-items-stretch | align-items: stretch |
u-items-baseline | align-items: baseline |
u-justify-start | justify-content: start |
u-justify-center | justify-content: center |
u-justify-end | justify-content: end |
u-justify-between | justify-content: space-between |
u-flex-1 | flex: 1 1 0% |
u-flex-auto | flex: 1 1 auto |
u-flex-none | flex: 0 0 auto |
u-shrink | min-inline-size: 0 |
start and end rather than flex-start and
flex-end: the logical keywords follow the writing direction, which is the same rule
the rest of the package holds to. There is no u-items-* or
u-justify-* for left or right, and there will not be.
<div class="u-flex u-flex-wrap u-items-center u-justify-between u-gap-4
u-pad-4 u-bg-sunken u-border-subtle u-radius-md">
<strong lang="mn">Дансны үлдэгдэл</strong>
<span class="u-numeric u-weight-semibold">1,284,500.00</span>
</div>u-shrink is spelled that way
It sets min-inline-size: 0, not a flex-shrink value. A flex item's
automatic minimum size is its content, so flex-shrink alone will not take a child
below the width of its longest word. That is precisely the case a Mongolian label produces: a
Cyrillic string 15–25% longer than its Latin source refuses to shrink and pushes its
sibling off the row. u-shrink is the one-declaration fix, and it is named for what
it achieves rather than for the property it sets.
Eight classes. Two of them reflow on their own and are the ones to reach for; three are fixed column counts that do not.
| Class | Declaration | Token |
|---|---|---|
u-grid-auto | repeat(auto-fit, minmax(min(…, 100%), 1fr)) | --size-column-min · |
u-grid-cards | repeat(auto-fill, minmax(min(…, 100%), 1fr)) | --size-card-min · |
u-grid-2 | repeat(2, minmax(0, 1fr)) | — |
u-grid-3 | repeat(3, minmax(0, 1fr)) | — |
u-grid-4 | repeat(4, minmax(0, 1fr)) | — |
u-col-span-2 | grid-column: span 2 | — |
u-col-span-full | grid-column: 1 / -1 | — |
u-place-center | place-items: center | — |
The min(token, 100%) inside the minmax() is what keeps
u-grid-auto and u-grid-cards from overflowing a phone: at 375px the
column floor collapses to the container width instead of holding at 16rem. Neither needs a media
query, because the columns answer to the content's own minimum.
The fixed counts do not do that. At 375px, u-grid-2 is still two columns and a
Cyrillic label a fifth longer than its English source is the first thing to overflow. Use them
where the count is the point — a pair of side-by-side charts, a four-up legend — and
reach for u-grid-auto everywhere else.
<div class="u-grid u-grid-auto u-gap-4">
<div class="u-pad-4 u-bg-sunken u-border-subtle u-radius-md" lang="mn">Шинэ захиалга үүсгэх</div>
…
</div>Narrow this window and those three cells become two, then one, on their own.
Ten classes, one per space step, and nothing else. gap gets the full scale because
it is the one spacing mechanism that never collapses and never needs a last-child exception.
| Class | Token | Value |
|---|---|---|
u-gap-0 | --space-0 | |
u-gap-1 | --space-1 | |
u-gap-2 | --space-2 | |
u-gap-3 | --space-3 | |
u-gap-4 | --space-4 | |
u-gap-5 | --space-5 | |
u-gap-6 | --space-6 | |
u-gap-7 | --space-7 | |
u-gap-8 | --space-8 | |
u-gap-9 | --space-9 | |
One class sets both axes. There is no row-gap or column-gap utility, because a grid that wants them different is a layout with an opinion, and that belongs in your own CSS.
<div class="u-flex u-flex-wrap u-gap-1">…</div>
<div class="u-flex u-flex-wrap u-gap-5">…</div>
<div class="u-flex u-flex-wrap u-gap-8">…</div>Thirty classes: three axes × ten steps. The pattern is the whole table, so here is the pattern rather than thirty near-identical rows.
| Pattern | Declaration | Count | Range |
|---|---|---|---|
u-pad-N | padding: var(--space-N) | 10 | u-pad-0 … u-pad-9 |
u-pad-inline-N | padding-inline: var(--space-N) | 10 | u-pad-inline-0 … u-pad-inline-9 |
u-pad-block-N | padding-block: var(--space-N) | 10 | u-pad-block-0 … u-pad-block-9 |
N runs 0 through 9 and maps one-to-one onto --space-0 through
--space-9. Nothing is renamed, nothing is renumbered, and there is no step here
that the token scale does not have. u-pad-5 is
, the same value base.css reaches for and the
same value you would write by hand. The scale
→
The axes are logical: inline is the axis the text runs along and
block is the one it stacks along, whatever the writing mode. There is no
u-pad-left and there is no single-side padding utility at all — padding one
edge is a component decision, not a spacing adjustment.
<div class="u-bg-sunken u-border-subtle u-radius-md u-pad-2">…</div>
<div class="u-bg-sunken u-border-subtle u-radius-md u-pad-6">…</div>
<div class="u-bg-sunken u-border-subtle u-radius-md u-pad-inline-9 u-pad-block-2">…</div>Sixteen classes, and the shape is deliberately lopsided: the block axis gets the whole scale,
the inline axis gets 0 and auto and nothing else. That asymmetry is
one of the deliberate omissions, and it is the one most likely to
look like an oversight.
| Pattern | Declaration | Count |
|---|---|---|
u-margin-0 | margin: var(--space-0) | 1 |
u-margin-inline-0 | margin-inline: var(--space-0) | 1 |
u-margin-inline-auto | margin-inline: auto — centring, the one legitimate inline margin | 1 |
u-margin-block-N | margin-block: var(--space-N), N 0–9 | 10 |
u-margin-block-start-0 | margin-block-start: var(--space-0) | 1 |
u-margin-block-start-auto | margin-block-start: auto — pushes a footer to the bottom of a flex column | 1 |
u-margin-block-end-0 | margin-block-end: var(--space-0) | 1 |
The three single-edge classes are all zeroes and one auto, because that is what a
margin utility is actually needed for in practice: cancelling the rhythm
base.css puts on a paragraph or a heading, and pinning a footer. Adding a positive
value to one block edge is a rhythm decision, and rhythm belongs to the component.
A paragraph with u-margin-block-0. The rhythm
base.css gives it is cancelled, so this sits flush against the next line.
Flush.
<p class="u-margin-block-0">…</p>
<p class="u-margin-block-0 u-color-muted u-text-sm">Flush.</p>Twenty-seven classes across six sub-groups: size, leading, weight, tracking, family, alignment, plus the numeric and wrapping helpers. Every one hands through a token from the type scale.
| Class | Property | Token | Value |
|---|---|---|---|
u-text-xs | font-size | --text-xs | |
u-text-sm | font-size | --text-sm | |
u-text-base | font-size | --text-base | |
u-text-lg | font-size | --text-lg | |
u-text-xl | font-size | --text-xl | |
u-text-2xl | font-size | --text-2xl | |
u-text-3xl | font-size | --text-3xl | |
u-leading-tight | line-height | --leading-tight | |
u-leading-snug | line-height | --leading-snug | |
u-leading-normal | line-height | --leading-normal | |
u-leading-loose | line-height | --leading-loose | |
u-weight-regular | font-weight | --weight-regular | |
u-weight-medium | font-weight | --weight-medium | |
u-weight-semibold | font-weight | --weight-semibold | |
u-weight-bold | font-weight | --weight-bold | |
u-tracking-tight | letter-spacing | --tracking-tight | |
u-tracking-normal | letter-spacing | --tracking-normal | |
u-tracking-wide | letter-spacing | --tracking-wide | |
u-font-sans | font-family | --font-sans | — |
u-font-mono | font-family | --font-mono | — |
u-text-start | text-align | — | start |
u-text-center | text-align | — | center |
u-text-end | text-align | — | end |
u-numeric | font-variant-numeric, font-feature-settings | --font-numeric-tabular | Tabular figures. Put it on any column of numbers that should line up. |
u-wrap-balance | text-wrap | — | balance — for headings and short labels. |
u-wrap-pretty | text-wrap | — | pretty — for body copy; kills orphans. |
u-wrap-anywhere | overflow-wrap | — | anywhere — the last resort for an unbroken token. |
u-text-start and u-text-end, never left and
right. The alignment follows the writing direction the same way the padding axes
do.
Шинэ захиалга үүсгэх
Хэрэглэгчийн тохиргоо
Утасны дугаар
1,284,500.00 · 0.00 · 118,000.25
<p class="u-text-2xl u-weight-bold u-wrap-balance" lang="mn">Шинэ захиалга үүсгэх</p>
<p class="u-text-lg u-weight-medium" lang="mn">Хэрэглэгчийн тохиргоо</p>
<p class="u-text-sm u-color-muted u-tracking-wide" lang="mn">Утасны дугаар</p>
<p class="u-text-xs u-font-mono u-numeric">1,284,500.00 · 0.00 · 118,000.25</p>Eight classes, all semantic roles. There is no utility for a raw ramp step — that omission has its own entry.
| Class | Token | Sample |
|---|---|---|
u-color-default | --color-text | Гуйвуулга баталгаажлаа |
u-color-muted | --color-text-muted | Гуйвуулга баталгаажлаа |
u-color-subtle | --color-text-subtle | Гуйвуулга баталгаажлаа |
u-color-inverse | --color-text-inverse | Гуйвуулга баталгаажлаа |
u-color-brand | --color-brand-text | Гуйвуулга баталгаажлаа |
u-color-success | --color-success-text | Гуйвуулга баталгаажлаа |
u-color-warn | --color-warn-text | Гуйвуулга баталгаажлаа |
u-color-danger | --color-danger-text | Гуйвуулга баталгаажлаа |
Every one of those re-resolves in dark mode without the class changing. That is the entire argument for keeping the colour utilities semantic. How the roles switch →
Ten classes. Five surface roles, four family fills, and transparent.
| Class | Token | Job |
|---|---|---|
u-bg-page | --color-bg | The page ground. |
u-bg-surface | --color-surface | The default panel. |
u-bg-raised | --color-surface-raised | One step up. Pairs with a shadow. |
u-bg-sunken | --color-surface-sunken | One step down. Wells, code blocks, inset rows. |
u-bg-inverse | --color-surface-inverse | Pair with u-color-inverse. |
u-bg-brand | --color-brand-subtle | Pairs with u-color-brand. |
u-bg-success | --color-success-subtle | Pairs with u-color-success. |
u-bg-warn | --color-warn-subtle | Pairs with u-color-warn. |
u-bg-danger | --color-danger-subtle | Pairs with u-color-danger. |
u-bg-none | — | transparent. Cancels a background a component set. |
-subtle roles
The four family backgrounds point at --color-brand-subtle and friends rather
than at the solid fills, because a -subtle surface clears contrast against the
ordinary text colour and against the matching -text role. A solid fill does not:
it needs its own label colour to travel with it, and a background utility cannot carry one.
That pairing is a component's job. See the component
variants →
<span class="u-pad-4 u-radius-md u-bg-brand u-color-brand u-text-sm">…</span>
<span class="u-pad-4 u-radius-md u-bg-danger u-color-danger u-text-sm">…</span>Six classes. Three all-round weights at the three border colour roles, one canceller, and two single-edge rules that turn up constantly in real layouts.
| Class | Declaration |
|---|---|
u-border | var(--border-width-1) solid var(--color-border) |
u-border-subtle | var(--border-width-1) solid var(--color-border-subtle) |
u-border-strong | var(--border-width-1) solid var(--color-border-strong) |
u-border-none | var(--border-width-0) solid transparent |
u-border-block-end | border-block-end, subtle — a row divider. |
u-border-inline-start | border-inline-start, subtle — a quote or callout rule. |
u-border-none sets a zero-width transparent border rather than
border: none, so an element that gets it keeps its box model unchanged and can be
given a border back on hover or focus without the layout jumping by a pixel.
<div class="u-pad-4 u-border u-radius-md">…</div>
<div class="u-pad-4 u-border-inline-start" lang="mn">Гуйвуулга баталгаажлаа</div>Six classes, one per radius token, minus --radius-xs and
--radius-2xl — those two exist in tokens.css for components to
reach for and have never been wanted as a one-off adjustment.
| Class | Token | Value |
|---|---|---|
u-radius-none | --radius-none | |
u-radius-sm | --radius-sm | |
u-radius-md | --radius-md | |
u-radius-lg | --radius-lg | |
u-radius-xl | --radius-xl | |
u-radius-full | --radius-full | |
<span class="u-inline-block u-pad-4 u-bg-sunken u-border-subtle u-radius-full">full</span>Five classes, one per elevation token. --shadow-xl is not exposed as a utility:
the top of the shadow scale belongs to overlays, and an overlay is always a component.
| Class | Token | Usual job |
|---|---|---|
u-shadow-none | --shadow-none | Flattens a component that ships with elevation. |
u-shadow-xs | --shadow-xs | A hairline lift. Toolbars, segmented controls. |
u-shadow-sm | --shadow-sm | Cards that need to read as separate from the page. |
u-shadow-md | --shadow-md | Hover elevation, sticky headers. |
u-shadow-lg | --shadow-lg | Popovers and menus, when you are building one by hand. |
<div class="u-pad-5 u-bg-raised u-radius-lg u-shadow-md">u-shadow-md</div>Elevation in this system is asymmetric between themes — in dark mode a raised surface
lightens as well as casting a shadow, because a shadow on a dark ground is nearly invisible.
That is why u-bg-raised and a shadow utility are usually applied together.
Why elevation is asymmetric →
Eight classes. Six are maximums, one is a full width, one is a scroll container. There is no fixed-width utility in the file — that omission has its own entry.
| Class | Declaration | Token | Value |
|---|---|---|---|
u-measure | max-inline-size | --size-measure | |
u-container-sm | max-inline-size | --size-container-sm | |
u-container-md | max-inline-size | --size-container-md | |
u-container-lg | max-inline-size | --size-container-lg | |
u-container-xl | max-inline-size | --size-container-xl | |
u-container-2xl | max-inline-size | --size-container-2xl | |
u-full | inline-size: 100% | — | Relative, so it cannot clip anything. |
u-scroll-inline | overflow-x, overscroll-behavior-inline, max-inline-size: 100% | — | A wide child scrolls inside this instead of making the page scroll sideways. |
Pair a container class with u-margin-inline-auto to centre a column, which is the
one place an inline margin is unambiguously right.
That row is wider than this column. It scrolls on its own; the page does not.
<div class="u-scroll-inline u-border-subtle u-radius-md u-pad-3">
<div class="u-flex u-flex-nowrap u-gap-4">
<span class="u-flex-none u-pad-4 u-bg-sunken u-radius-sm" lang="mn">Шинэ захиалга үүсгэх</span>
…
</div>
</div>Four things a utility layer is normally expected to have and this one does not. None of them is an oversight, and each one is the direct consequence of a rule the system holds elsewhere. If you came here looking for a class and did not find it, this is almost certainly the section that explains why.
There is no text-overflow utility, no line clamp, no
white-space: nowrap. Not a shorter list than usual — none at all.
The reason is grammatical, not aesthetic. Mongolian Cyrillic is agglutinative: the case suffix at the end of a word is what tells you whether the word is the subject, the object, the destination or the possessor. An ellipsis eats the end of the string, which is exactly where that information lives. Truncating English costs you some words. Truncating Mongolian can leave a grammatically incoherent fragment that means something other than the original, and the reader has no way to tell which.
So the layer does not offer a one-class shortcut to it. The sanctioned answers are
u-wrap-balance for a heading, u-wrap-pretty for body copy, and
u-wrap-anywhere for an unbroken token such as a wallet address or a transaction
reference. All three let the text be fully present; they only argue about where the line
breaks.
Төлбөрийн мэдээлэл амжилттай шинэчлэгдлээ.
Төлбөрийн мэдээлэл амжилттай шинэчлэгдлээ.
Both panels are the same string at the same ceiling. The left one grows a second line and stays
readable. The right one keeps its single line by throwing away the verb ending — the part
that says the update succeeded. The clipping rule on the right is a docs-only rule
written for this comparison; it is not in utilities.css and it is not going to
be.
Write it in your own CSS. It is unlayered, it beats everything here, and it takes four declarations. What the system will not do is make it a one-class habit that spreads across a codebase before anyone has thought about the language it is clipping.
The size group offers maximums and one relative full width. There is no
inline-size utility at a fixed length, and no plan for one.
A fixed inline size is the single most reliable way to clip a Cyrillic label, because it makes
the decision before the text exists. A maximum does not: a box with
max-inline-size is free to be narrower when the content is short and is capped only
when the content is long, which is a ceiling on the layout rather than an assumption about the
string. u-full is relative and therefore cannot clip anything either — it
resolves against whatever the parent turns out to be.
The same rule runs through the rest of the package: --size-touch and
--size-control are applied as min-block-size and never as a height, so
a button that needs two lines gets two lines.
The floors, not heights, rule →
0 and autoThe block axis gets all ten space steps. The inline axis gets u-margin-inline-0
and u-margin-inline-auto, and that is the entire inline margin surface.
An inline margin on a text-bearing element is how a layout starts assuming a width. Once something is indented by a fixed amount on the side the text runs along, the space left for the text is a fixed subtraction from the container — and that subtraction was almost always sized against the English draft. It is the same assumption a fixed width makes, arrived at from the other direction, and it is exactly the assumption the bilingual rule forbids.
auto is exempt because it is not an assumption about a width: it is an instruction
to divide whatever space is left, equally, in both directions. That is centring, and centring
is fine.
<div class="u-container-sm u-margin-inline-auto u-pad-5
u-bg-sunken u-border-subtle u-radius-md u-text-center">
<span lang="mn">Хэрэглэгчийн тохиргоо</span>
</div>u-color-brand-600 does not exist. Neither does u-bg-neutral-200 or
any of the other 60-odd classes a ramp-per-step generator would produce.
A ramp step is an absolute colour. --color-brand-600 is the same colour in light
mode and in dark mode, by design — that is what makes it useful as a raw material for
building the semantic roles out of. Handed a class name, though, it stops being raw material and
becomes a light-mode-only class: readable on the light ground it was chosen against, and
unreadable the moment the theme flips.
The eight text colour and ten background utilities all point at semantic roles instead, and every one of them re-resolves when the theme changes without the markup moving. That is the whole reason the role layer exists, and exposing the ramp directly through a utility would route around it. The roles and what they are for →
The ramp tokens are public — --color-brand-600 is right there in
tokens.css and you can use it in your own CSS or in a
style attribute. Nothing is hidden. What is withheld is a class, because
a class is the form that spreads.
[hidden] guardThe file ends with a block that restates every display-setting utility with
[hidden] attached. It is eight selectors and one declaration, and it fixes a bug
that shipped in nine of the ten component files before anybody caught it.
The trap: reset.css contains [hidden] { display: none }, which is
what makes the HTML hidden attribute work. But reset is the first
layer. Anything that sets display in a later layer beats it, silently, and the
element stays visible while the accessibility tree has already been told it is gone.
Here it is worse than in the component files, because utilities sits one layer
higher still. <div class="u-flex" hidden> would have beaten
reset.css and any component's own guard.
/* The last block in utilities.css. */
.u-block[hidden],
.u-inline[hidden],
.u-inline-block[hidden],
.u-flex[hidden],
.u-inline-flex[hidden],
.u-grid[hidden],
.u-inline-grid[hidden],
.u-contents[hidden] {
display: none;
}u-hidden needs no entry — it already sets display: none, so
there is nothing for the attribute to lose to. And this is the reason to prefer the
hidden attribute over u-hidden for anything that toggles: the
attribute is one property in the DOM that both the renderer and the screen reader agree on,
where a class is a second source of truth that can drift out of sync with the
aria- state beside it.
There are two u-flex rows in
that preview. The second carries hidden and the guard is what keeps it
gone.
<div class="u-flex u-gap-4 …">…</div>
<div class="u-flex u-gap-4 …" hidden>…</div> <!-- stays gone -->Two honest cases, and neither is a failure of the layer.
You are writing the same three utilities on every instance. If every card in
your product carries u-pad-4 u-bg-sunken u-radius-md, that is not three
adjustments — that is a component with a name nobody has written down yet. Write the class,
put the three declarations in it, and the next person gets the decision instead of having to
re-derive it. A utility repeated across a codebase is a component in denial, and it is the kind
of duplication that goes stale one instance at a time.
You need a value the scale does not have. There is no arbitrary-value syntax
and there is not going to be one. Write the rule in your own CSS. It is unlayered, it sits above
all six of the system's layers, and it wins by existing — no
!important, no specificity fight, no override file. That is not a workaround; it is
the documented override mechanism. Why unlayered code
wins →
What both cases have in common is that the answer is to write CSS, not to make the utility layer bigger. The layer is 161 classes because 161 decisions were made. Every class added without a decision behind it makes the other 161 harder to find.