@arag 0.1.0

Utilities

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.

What this layer is

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.

Governing rule

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.

The thirteen groups, and the real counts.
Group Classes Hands through
Display10Keywords only, plus u-sr-only.
Flex17Keywords only.
Grid8--size-column-min, --size-card-min.
Gap10--space-0--space-9.
Padding30--space-0--space-9, three axes.
Margin16--space-0--space-9, block axis only.
Type27The type, leading, weight and tracking scales.
Text colour8Semantic -text roles.
Background10Surface roles and the -subtle family fills.
Border6--border-width-* and the border colour roles.
Radius6--radius-*.
Shadow5--shadow-*.
Size8--size-measure, --size-container-*. Maximums only.
Total161Plus one [hidden] guard block that adds no new class.

Why every class is prefixed 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.

Where the layer sits

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.

Untouched

class="card"

Overridden

class="card u-pad-3 u-bg-sunken"

Markup, and what is happening in the cascade
<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); }
Prefer the local first

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 →

Display

Ten classes. Nine set a display keyword; the tenth removes an element visually while leaving it in the accessibility tree.

ClassDeclarationNotes
u-blockdisplay: block
u-inlinedisplay: inline
u-inline-blockdisplay: inline-block
u-flexdisplay: flexPairs with the flex group.
u-inline-flexdisplay: inline-flex
u-griddisplay: gridPairs with the grid group.
u-inline-griddisplay: inline-grid
u-contentsdisplay: contentsRemoves the box, keeps the children. Use sparingly — it also removes the element from some accessibility trees.
u-hiddendisplay: noneFor a permanent removal. For something that toggles, use the hidden attribute instead — see the guard.
u-sr-onlyposition, clip-path, --border-width-1 boxVisually 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.Гуйвуулга баталгаажлаа

Markup
<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>

Flex

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.

ClassDeclaration
u-flex-rowflex-direction: row
u-flex-colflex-direction: column
u-flex-wrapflex-wrap: wrap
u-flex-nowrapflex-wrap: nowrap
u-items-startalign-items: start
u-items-centeralign-items: center
u-items-endalign-items: end
u-items-stretchalign-items: stretch
u-items-baselinealign-items: baseline
u-justify-startjustify-content: start
u-justify-centerjustify-content: center
u-justify-endjustify-content: end
u-justify-betweenjustify-content: space-between
u-flex-1flex: 1 1 0%
u-flex-autoflex: 1 1 auto
u-flex-noneflex: 0 0 auto
u-shrinkmin-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.

Дансны үлдэгдэл 1,284,500.00
Markup
<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>
Why 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.

Grid

Eight classes. Two of them reflow on their own and are the ones to reach for; three are fixed column counts that do not.

ClassDeclarationToken
u-grid-autorepeat(auto-fit, minmax(min(…, 100%), 1fr))--size-column-min ·
u-grid-cardsrepeat(auto-fill, minmax(min(…, 100%), 1fr))--size-card-min ·
u-grid-2repeat(2, minmax(0, 1fr))
u-grid-3repeat(3, minmax(0, 1fr))
u-grid-4repeat(4, minmax(0, 1fr))
u-col-span-2grid-column: span 2
u-col-span-fullgrid-column: 1 / -1
u-place-centerplace-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.

Шинэ захиалга үүсгэх
Хэрэглэгчийн тохиргоо
Дансны үлдэгдэл
Markup
<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.

Gap

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.

ClassTokenValue
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.

u-gap-1 u-gap-1 u-gap-1
u-gap-5 u-gap-5 u-gap-5
u-gap-8 u-gap-8 u-gap-8
Markup
<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>

Padding

Thirty classes: three axes × ten steps. The pattern is the whole table, so here is the pattern rather than thirty near-identical rows.

PatternDeclarationCountRange
u-pad-Npadding: var(--space-N)10u-pad-0u-pad-9
u-pad-inline-Npadding-inline: var(--space-N)10u-pad-inline-0u-pad-inline-9
u-pad-block-Npadding-block: var(--space-N)10u-pad-block-0u-pad-block-9
The scale is the space scale, exactly

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.

u-pad-2
u-pad-6
u-pad-inline-9 u-pad-block-2
Markup
<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>

Margin

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.

PatternDeclarationCount
u-margin-0margin: var(--space-0)1
u-margin-inline-0margin-inline: var(--space-0)1
u-margin-inline-automargin-inline: auto — centring, the one legitimate inline margin1
u-margin-block-Nmargin-block: var(--space-N), N 0–910
u-margin-block-start-0margin-block-start: var(--space-0)1
u-margin-block-start-automargin-block-start: auto — pushes a footer to the bottom of a flex column1
u-margin-block-end-0margin-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.

Markup
<p class="u-margin-block-0">…</p>
<p class="u-margin-block-0 u-color-muted u-text-sm">Flush.</p>

Type

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.

ClassPropertyTokenValue
u-text-xsfont-size--text-xs
u-text-smfont-size--text-sm
u-text-basefont-size--text-base
u-text-lgfont-size--text-lg
u-text-xlfont-size--text-xl
u-text-2xlfont-size--text-2xl
u-text-3xlfont-size--text-3xl
u-leading-tightline-height--leading-tight
u-leading-snugline-height--leading-snug
u-leading-normalline-height--leading-normal
u-leading-looseline-height--leading-loose
u-weight-regularfont-weight--weight-regular
u-weight-mediumfont-weight--weight-medium
u-weight-semiboldfont-weight--weight-semibold
u-weight-boldfont-weight--weight-bold
u-tracking-tightletter-spacing--tracking-tight
u-tracking-normalletter-spacing--tracking-normal
u-tracking-wideletter-spacing--tracking-wide
u-font-sansfont-family--font-sans
u-font-monofont-family--font-mono
u-text-starttext-alignstart
u-text-centertext-aligncenter
u-text-endtext-alignend
u-numericfont-variant-numeric, font-feature-settings--font-numeric-tabularTabular figures. Put it on any column of numbers that should line up.
u-wrap-balancetext-wrapbalance — for headings and short labels.
u-wrap-prettytext-wrappretty — for body copy; kills orphans.
u-wrap-anywhereoverflow-wrapanywhere — 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

Markup
<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>

Text colour

Eight classes, all semantic roles. There is no utility for a raw ramp step — that omission has its own entry.

ClassTokenSample
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 →

Background

Ten classes. Five surface roles, four family fills, and transparent.

ClassTokenJob
u-bg-page--color-bgThe page ground.
u-bg-surface--color-surfaceThe default panel.
u-bg-raised--color-surface-raisedOne step up. Pairs with a shadow.
u-bg-sunken--color-surface-sunkenOne step down. Wells, code blocks, inset rows.
u-bg-inverse--color-surface-inversePair with u-color-inverse.
u-bg-brand--color-brand-subtlePairs with u-color-brand.
u-bg-success--color-success-subtlePairs with u-color-success.
u-bg-warn--color-warn-subtlePairs with u-color-warn.
u-bg-danger--color-danger-subtlePairs with u-color-danger.
u-bg-nonetransparent. Cancels a background a component set.
Why the family fills are the -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 →

u-bg-brand u-bg-success u-bg-warn u-bg-danger u-bg-inverse
Markup
<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>

Border

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.

ClassDeclaration
u-bordervar(--border-width-1) solid var(--color-border)
u-border-subtlevar(--border-width-1) solid var(--color-border-subtle)
u-border-strongvar(--border-width-1) solid var(--color-border-strong)
u-border-nonevar(--border-width-0) solid transparent
u-border-block-endborder-block-end, subtle — a row divider.
u-border-inline-startborder-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.

u-border
u-border-subtle
u-border-strong
Гуйвуулга баталгаажлаа
Markup
<div class="u-pad-4 u-border u-radius-md">…</div>
<div class="u-pad-4 u-border-inline-start" lang="mn">Гуйвуулга баталгаажлаа</div>

Radius

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.

ClassTokenValue
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
none sm md lg xl full
Markup
<span class="u-inline-block u-pad-4 u-bg-sunken u-border-subtle u-radius-full">full</span>

Shadow

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.

ClassTokenUsual job
u-shadow-none--shadow-noneFlattens a component that ships with elevation.
u-shadow-xs--shadow-xsA hairline lift. Toolbars, segmented controls.
u-shadow-sm--shadow-smCards that need to read as separate from the page.
u-shadow-md--shadow-mdHover elevation, sticky headers.
u-shadow-lg--shadow-lgPopovers and menus, when you are building one by hand.
u-shadow-xs
u-shadow-sm
u-shadow-md
u-shadow-lg
Markup
<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 →

Size

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.

ClassDeclarationTokenValue
u-measuremax-inline-size--size-measure
u-container-smmax-inline-size--size-container-sm
u-container-mdmax-inline-size--size-container-md
u-container-lgmax-inline-size--size-container-lg
u-container-xlmax-inline-size--size-container-xl
u-container-2xlmax-inline-size--size-container-2xl
u-fullinline-size: 100%Relative, so it cannot clip anything.
u-scroll-inlineoverflow-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.

Markup
<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>

The deliberate omissions

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.

No truncation utility of any kind

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.

u-wrap-pretty — what the layer offers

Төлбөрийн мэдээлэл амжилттай шинэчлэгдлээ.

Truncation — what the layer refuses

Төлбөрийн мэдээлэл амжилттай шинэчлэгдлээ.

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.

If you genuinely need to clip

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.

No fixed-width utility

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 →

Inline margins get only 0 and auto

The 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.

Хэрэглэгчийн тохиргоо
Markup
<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>

No raw-ramp colour utilities

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 →

If you want the ramp anyway

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.

Living with the layer

The [hidden] guard

The 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.

Markup
<div class="u-flex u-gap-4 …">…</div>
<div class="u-flex u-gap-4 …" hidden>…</div>  <!-- stays gone -->

When not to reach for a utility

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.