Skip to content

feat(utilities)!: close the utility gaps and name the sizing and underline families after their properties - #1044

Merged
mrholek merged 3 commits into
v6-devfrom
feat/utilities-parity-v6
Sep 1, 2026
Merged

feat(utilities)!: close the utility gaps and name the sizing and underline families after their properties#1044
mrholek merged 3 commits into
v6-devfrom
feat/utilities-parity-v6

Conversation

@mrholek

@mrholek mrholek commented Sep 1, 2026

Copy link
Copy Markdown
Member

Comparing our utility map against twbs/bootstrap@v6-dev class by class (headings lie here — the same family is documented under different names in both) left 14 families with no class in our build. Nine turned out to be a different name for something we already ship. These are the rest.

New families

class sets why
.border-x, .border-y (+ -0) border-inline, border-block a facing pair of borders took two classes
.min-w-{0,100}, .min-h-{0,100} min-width, min-height only caps existed on this axis
.animation-{shake,pop}, .animation-none animation
.color-scheme-{light,dark} color-scheme

.min-w-0 is the one that earns its place. A flex child starts at min-width: auto and a .text-truncate child never breaks its line, so its content width is the whole string — the item pushes past its track instead of ellipsizing. The example on the width page shows both rows.

The animation keyframes are named animation-shake / animation-pop, not shake / pop, so a project declaring an animation under either bare name cannot redefine ours. Both stand down under prefers-reduced-motion: reduce while $enable-reduced-motion is on.

.color-scheme-* sets no colours of its own — it tells the browser which scheme to paint form controls, pickers and scrollbars in. The page says so explicitly, because the name reads like a theme switch and is not one.

Renames

Three families were named after an abbreviation rather than the property they set, which stopped scanning as one set once the floors landed.

v5 v6
.mw-100 .max-w-100
.mh-100 .max-h-100
.link-offset-* (+ -hover) .underline-offset-* (+ -hover)

Every floor class on the sizing axis already spelled the property out (.min-vw-100, .min-vh-100, .min-dvw-100), so .mw sitting next to the new .min-w read as two conventions for one property.

.link-offset-* sets text-underline-offset on any element, not only a link, and .underline-thickness-* beside it already carried the property-based name. Same three steps, same values, same -hover variants.

Each removed class is declared in build/class-api-removals.json with the class that replaces it, so the class API gate keeps answering for it.

Docs

New ## Min width / ## Min height sections, .border-x / .border-y on the border page, ## Animation on the motion page, a new color scheme page in the sidebar, and migration-guide entries for all of it. The motion page also gains the ## Customizing heading it was missing above ### Utilities API.

Verification

  • npm run css-compile — all ten new classes present in dist/css/coreui.css; both keyframes emitted outside the cascade layer, and the prefers-reduced-motion: reduce override in place.
  • node build/check-class-api.mjs — passes; the three removals match 8 baseline classes.
  • npx stylelint scss/_utilities.scss scss/utilities/_api.scss — clean.
  • npm run docs-build — 178 pages, no broken links.
  • bundlewatch — passes. Raises the two utilities ceilings: the new families put coreui-utilities.min.css 20 bytes over, and the unminified file had 40 bytes of headroom left, so it would have tripped on whichever change landed next. Now 19.43/19.75 kB and 17.75/18 kB.

Second pass

.link-underline-*.underline-*

v5 v6
.link-underline, .link-underline-danger .underline, .underline-danger
.link-underline-opacity-50 (+ -hover) .underline-opacity-50 (+ -hover)

Same reasoning as .link-offset-*: the class sets text-decoration-color on any element, not only a link, and .underline-offset-* / .underline-thickness-* beside it already carried the property-based name.

Deliberate divergence: upstream folds the opacity into the colour class (.underline-50); we keep .underline-opacity-50, because every other opacity family here reads .bg-opacity-* / .text-opacity-* / .link-opacity-*. One family spelling it differently is exactly what this pass removes.

The unrelated --cui-link-underline-offset token from Reboot keeps its name — it is not this utility.

New .rounded-size-*

Writes the radius into --cui-rounded-size without applying border-radius. This only means anything if something reads the token, so the unprefixed corner classes now do:

.rounded-top {
  border-top-left-radius: var(--cui-rounded-size, var(--cui-border-radius));
  ...
}

One size class then drives whichever corners you round (.rounded-size-5 > .rounded-top). With no size in scope they fall back to --cui-border-radius, so existing markup renders unchanged, and the numbered classes (.rounded-3, .rounded-top-3) set their own value and ignore the token.

The derived colour families now take their opacity classes

.bg-*-subtle, .bg-*-muted, .border-*-subtle and .text-*-emphasis wrote the property directly, so .bg-opacity-50 did nothing on .bg-primary-subtle while working on .bg-primary:

/* before */ .bg-primary-subtle { background-color: var(--cui-primary-bg-subtle) }
/* after  */ .bg-primary-subtle { --cui-bg: var(--cui-primary-bg-subtle); --cui-bg-opacity: 1;
                                  background-color: color-mix(in srgb, var(--cui-bg) calc(var(--cui-bg-opacity) * 100%), transparent) }

At full opacity the output is identical.

Not in this PR

.fg-contrast-* is not a standalone class upstream — it belongs to a rename of the whole text-colour family (.text-primary.fg-primary, .text-opacity-50.fg-50, .text-reset.fg-reset, .text-*-emphasis.fg-emphasis-*), together with the word-order flip on .bg-*-subtle.bg-subtle-*. Measured blast radius across the three repos: 760 uses of .text-{color} (74 of them in React/Vue component source) plus 122 text-opacity / text-bg, before counting admin templates and user code. That is its own decision and its own PR.

Verification (second pass)

  • node build/check-class-api.mjs — passes. It caught that the new link-underline-* pattern swallowed the existing link-underline-dark / -light entries; they are folded in with their reason kept.
  • npx stylelint scss/_utilities.scss — clean.
  • npm run docs-build — 178 pages, no broken links.
  • bundlewatch — passes; ceilings raised again (coreui.min.css 65.88/66.25 kB, coreui-utilities.min.css 18.38/18.5 kB). Routing four families through color-mix() costs bytes the end-of-v6 size pass is the place to win back.

…rline families after their properties

Four families had no class in our build at all:

- `.border-x` / `.border-y` (and their `-0` forms) set the `border-inline` and
  `border-block` shorthands, so a facing pair of borders takes one class
  instead of two.
- `.min-w-{0|100}` and `.min-h-{0|100}` set a floor where only caps existed.
  `.min-w-0` is the one that earns its place: a flex child starts at
  `min-width: auto` and a `.text-truncate` child never breaks its line, so the
  item pushes past its track instead of ellipsizing until the floor drops to 0.
- `.animation-{shake|pop}` ship their keyframes with them. The keyframes carry
  the utility prefix so a project declaring a bare `shake` or `pop` animation
  cannot redefine them, and both stand down under
  `prefers-reduced-motion: reduce` while `$enable-reduced-motion` is on.
- `.color-scheme-{light|dark}` sets the CSS `color-scheme` property, which is
  what the browser reads to paint form controls, pickers and scrollbars. It
  sets no colours of its own, so it is not a theme switch.

Three existing families were named after an abbreviation rather than the
property they set, which stopped scanning as one set once the floors landed:

- `.mw-100` -> `.max-w-100`, `.mh-100` -> `.max-h-100`. Every floor class on
  that axis already spelled the property out (`.min-vw-100`, `.min-vh-100`,
  `.min-dvw-100`), so `.mw` sitting next to the new `.min-w` read as two
  conventions for one property.
- `.link-offset-*` -> `.underline-offset-*`, keeping the three steps and the
  `-hover` variants. The class sets `text-underline-offset` on any element, not
  only a link, and `.underline-thickness-*` beside it already carried the
  property-based name.

Every removed class is declared in class-api-removals.json with the class that
replaces it, so the class API gate keeps answering for it.

Documents all of it on the pages that own it: `## Min width` / `## Min height`,
`.border-x` / `.border-y` on the border page, `## Animation` on the motion page,
and a new color scheme page in the sidebar. Adds the `## Customizing` heading
the motion page was missing above its `### Utilities API` section.

Raises the two utilities bundlewatch ceilings: the new families put
coreui-utilities.min.css over its limit, and the unminified file had 40 bytes of
headroom left, so it would have tripped on whichever change landed next.
@coveralls

coveralls commented Sep 1, 2026

Copy link
Copy Markdown

Coverage Report for CI Build 33567782190

Coverage decreased (-0.007%) to 93.079%

Details

  • Coverage decreased (-0.007%) from the base build.
  • Patch coverage: No coverable lines changed in this PR.
  • 1 coverage regression across 1 file.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

1 previously-covered line in 1 file lost coverage.

File Lines Losing Coverage Coverage
js/src/navigation.ts 1 64.63%

Coverage Stats

Coverage Status
Relevant Lines: 9474
Covered Lines: 9049
Line Coverage: 95.51%
Relevant Branches: 5351
Covered Branches: 4750
Branch Coverage: 88.77%
Branches in Coverage %: Yes
Coverage Strength: 388.54 hits per line

💛 - Coveralls

mrholek and others added 2 commits September 2, 2026 00:30
…let the derived colors take their opacity

Three more gaps against the utility set, all in families we already ship.

**`.link-underline-*` is `.underline-*`.** `.link-underline-danger` becomes
`.underline-danger`, and the opacity steps become `.underline-opacity-*`. The
class sets `text-decoration-color` on any element, not only a link, and it now
sits beside `.underline-offset-*` and `.underline-thickness-*`, which already
carried that name. Deliberate divergence: the opacity keeps its `-opacity-`
segment rather than folding into the colour class, because every other opacity
family here reads `.bg-opacity-*` / `.text-opacity-*` / `.link-opacity-*`, and
one family spelling it differently is what this pass removes. The unrelated
`--cui-link-underline-offset` token from Reboot keeps its name.

**New `.rounded-size-*`.** It writes the radius into `--cui-rounded-size`
without applying `border-radius`, and the unprefixed corner classes now read
that token, so one size class drives whichever corners you round. Without the
indirection the utility would compile and do nothing, so `.rounded`,
`.rounded-top`, `.rounded-end`, `.rounded-bottom` and `.rounded-start` fall back
to `--cui-border-radius` when no size is in scope — existing markup renders
unchanged, and the numbered classes keep setting their own value.

**The derived colour families compose with their opacity classes.**
`.bg-*-subtle`, `.bg-*-muted`, `.border-*-subtle` and `.text-*-emphasis` each
wrote the property directly, so `.bg-opacity-50` did nothing on
`.bg-primary-subtle` while working on `.bg-primary`. They now declare
`--cui-bg` / `--cui-border` / `--cui-text` and resolve through the same
`color-mix()` as the base families; at full opacity the output is identical.

Folds the `link-underline-dark` / `link-underline-light` removal entries into
the `link-underline-*` pattern that now covers them, keeping the note that those
two have no renamed form because the theme colours are gone.

Raises the ceilings again: routing four families through `color-mix()` costs
bytes that a size pass at the end of v6 is the place to win back.
…prefixes first

The docs job runs `npm ci && npm run docs-build` and never rebuilds the
stylesheet, while <UtilityClasses /> reads `dist/css/coreui.css` — the committed
one, refreshed only by the occasional `chore: rebuild dist`. So every PR that
adds a utility class fails the docs build until that rebuild lands, and the
error names the class rather than the staleness. `docs-build` now runs
`css-compile` first, so the table is generated from the branch it documents.

<UtilityClasses /> walks its `utility` list in order and skips classes an earlier
family already claimed, and its prefix match is greedy: `underline` also matches
`.underline-offset-*`, `.underline-opacity-*` and `.underline-thickness-*`, and
`rounded` matches `.rounded-size-*`. Listing the narrow prefixes first leaves the
broad one only what it owns, which fills three tables that had been rendering
empty — one of them before this branch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@mrholek
mrholek merged commit 25cff80 into v6-dev Sep 1, 2026
9 checks passed
mrholek added a commit that referenced this pull request Sep 1, 2026
…rline families after their properties (#1044)

* feat(utilities)!: close the utility gaps and name the sizing and underline families after their properties

Four families had no class in our build at all:

- `.border-x` / `.border-y` (and their `-0` forms) set the `border-inline` and
  `border-block` shorthands, so a facing pair of borders takes one class
  instead of two.
- `.min-w-{0|100}` and `.min-h-{0|100}` set a floor where only caps existed.
  `.min-w-0` is the one that earns its place: a flex child starts at
  `min-width: auto` and a `.text-truncate` child never breaks its line, so the
  item pushes past its track instead of ellipsizing until the floor drops to 0.
- `.animation-{shake|pop}` ship their keyframes with them. The keyframes carry
  the utility prefix so a project declaring a bare `shake` or `pop` animation
  cannot redefine them, and both stand down under
  `prefers-reduced-motion: reduce` while `$enable-reduced-motion` is on.
- `.color-scheme-{light|dark}` sets the CSS `color-scheme` property, which is
  what the browser reads to paint form controls, pickers and scrollbars. It
  sets no colours of its own, so it is not a theme switch.

Three existing families were named after an abbreviation rather than the
property they set, which stopped scanning as one set once the floors landed:

- `.mw-100` -> `.max-w-100`, `.mh-100` -> `.max-h-100`. Every floor class on
  that axis already spelled the property out (`.min-vw-100`, `.min-vh-100`,
  `.min-dvw-100`), so `.mw` sitting next to the new `.min-w` read as two
  conventions for one property.
- `.link-offset-*` -> `.underline-offset-*`, keeping the three steps and the
  `-hover` variants. The class sets `text-underline-offset` on any element, not
  only a link, and `.underline-thickness-*` beside it already carried the
  property-based name.

Every removed class is declared in class-api-removals.json with the class that
replaces it, so the class API gate keeps answering for it.

Documents all of it on the pages that own it: `## Min width` / `## Min height`,
`.border-x` / `.border-y` on the border page, `## Animation` on the motion page,
and a new color scheme page in the sidebar. Adds the `## Customizing` heading
the motion page was missing above its `### Utilities API` section.

Raises the two utilities bundlewatch ceilings: the new families put
coreui-utilities.min.css over its limit, and the unminified file had 40 bytes of
headroom left, so it would have tripped on whichever change landed next.

* feat(utilities)!: rename the underline family, add rounded-size, and let the derived colors take their opacity

Three more gaps against the utility set, all in families we already ship.

**`.link-underline-*` is `.underline-*`.** `.link-underline-danger` becomes
`.underline-danger`, and the opacity steps become `.underline-opacity-*`. The
class sets `text-decoration-color` on any element, not only a link, and it now
sits beside `.underline-offset-*` and `.underline-thickness-*`, which already
carried that name. Deliberate divergence: the opacity keeps its `-opacity-`
segment rather than folding into the colour class, because every other opacity
family here reads `.bg-opacity-*` / `.text-opacity-*` / `.link-opacity-*`, and
one family spelling it differently is what this pass removes. The unrelated
`--cui-link-underline-offset` token from Reboot keeps its name.

**New `.rounded-size-*`.** It writes the radius into `--cui-rounded-size`
without applying `border-radius`, and the unprefixed corner classes now read
that token, so one size class drives whichever corners you round. Without the
indirection the utility would compile and do nothing, so `.rounded`,
`.rounded-top`, `.rounded-end`, `.rounded-bottom` and `.rounded-start` fall back
to `--cui-border-radius` when no size is in scope — existing markup renders
unchanged, and the numbered classes keep setting their own value.

**The derived colour families compose with their opacity classes.**
`.bg-*-subtle`, `.bg-*-muted`, `.border-*-subtle` and `.text-*-emphasis` each
wrote the property directly, so `.bg-opacity-50` did nothing on
`.bg-primary-subtle` while working on `.bg-primary`. They now declare
`--cui-bg` / `--cui-border` / `--cui-text` and resolve through the same
`color-mix()` as the base families; at full opacity the output is identical.

Folds the `link-underline-dark` / `link-underline-light` removal entries into
the `link-underline-*` pattern that now covers them, keeping the note that those
two have no renamed form because the theme colours are gone.

Raises the ceilings again: routing four families through `color-mix()` costs
bytes that a size pass at the end of v6 is the place to win back.

* fix(docs): compile the CSS the utility tables read, and order narrow prefixes first

The docs job runs `npm ci && npm run docs-build` and never rebuilds the
stylesheet, while <UtilityClasses /> reads `dist/css/coreui.css` — the committed
one, refreshed only by the occasional `chore: rebuild dist`. So every PR that
adds a utility class fails the docs build until that rebuild lands, and the
error names the class rather than the staleness. `docs-build` now runs
`css-compile` first, so the table is generated from the branch it documents.

<UtilityClasses /> walks its `utility` list in order and skips classes an earlier
family already claimed, and its prefix match is greedy: `underline` also matches
`.underline-offset-*`, `.underline-opacity-*` and `.underline-thickness-*`, and
`rounded` matches `.rounded-size-*`. Listing the narrow prefixes first leaves the
broad one only what it owns, which fills three tables that had been rendering
empty — one of them before this branch.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants