Text-box trim on a label: where it helps, where it does nothing, and why centered caps still look low

Putting text-box: trim-both cap alphabetic on the element that holds the words makes the space above and below the capitals predictable. Put it on the flex container and nothing happens, and a mixed-case word still doesn't sit in the middle of its button.

Lab · Impeccable · typeset · Proposal: a short Optical alignment section in typeset.md · Issue #1262 on GitHub →

Try it

Icon plus labelfour faces

ProblemProposed fixNo effectSide effectBaselineOffsets in CSS px, + = low

With the label trimmed, align-items: center puts the caps on the icon's center in every face

16 px icon, "Save", inline-flex, gap: 8px, padding: 10px 16px, line-height: 20px. Untrimmed, where the caps land depends on how the face splits ascent and descent. Trim on the button is left out here: it measures the same as untrimmed in every face, as in the group above. Chips are set from the measurement: an offset under 0.4 px is device-pixel snapping and gets no Problem chip. System faces depend on your OS and may fall back.

01

The problem

What goes wrong with a label in a box today, measured on this page, in your browser. Each row links to its before/after pair.

02

What the proposal does

The same cases, today beside the issue's guidance. Proposed text: trim the label element, not the flex container; expect the shrink; judge the rendered word; match a fallback's cap height with size-adjust; leave trim off for CJK.

Where the trim goes.ks-button shape

Trim the label element, never the flex container

inline-flex, align-items: center, min-height: 40px, padding: 0 20px, line-height: 1, "Save changes". On this button the caps were already centered, so all three look alike at 1×. What differs is the label box (teal): trim on the button leaves it 14 px tall; trim on a display: block label shrinks it to the cap band.

.button       { display: inline-flex; align-items: center; min-height: 40px; padding: 0 20px; line-height: 1; }
.button.trim  { text-box: trim-both cap alphabetic; }   /* no effect: the text is an anonymous flex item */
.button .label      { display: block; }
.button .label.trim { text-box: trim-both cap alphabetic; }   /* works */
Centered caps vs a centered word

The x-height's center sits below the cap center; cap trim does not move it

The probe's button: padding: 10px 16px, line-height: 1, "Save changes". The lowercase bulk of the word is what reads low. The proposal: judge the rendered component, and if a label reads low, move it with asymmetric padding-block that keeps the total. The loupes are cropped to the start of the label so they can be magnified further. The third figure shows one such nudge (9 px / 11 px), measured live; whether it reads better is a judgement call, which is the point. The ink center is a canvas estimate, and canvas and a pixel scan disagree, so it settles nothing either.

Auto-height controlsline-height 20px

Trim removes line-height minus cap height; re-derive padding-block or set min-height

Padding 10px 16px, no min-height, "Save changes", aligned at the top so the shrink shows. The third figure keeps the trim and adds half the removed height back to each side of the padding.

Fallback face on a trimmed boxArial Bold for Inter

Match the fallback's cap height with size-adjust; ascent-override and its siblings move the untrimmed line box, not a trimmed one

All four trimmed, padding: 10px 16px, line-height: 1. Overrides and size-adjust are computed live from Inter's metrics. Honest scale: the difference is 0.16 px at 14 px, under a device pixel even magnified, so the figures will look the same; the brackets carry the measured heights. It grows with font size. Needs "Arial Bold" installed.

A CJK label保存する

cap alphabetic uses Latin metrics; leave trim off for CJK labels

Purple lines are the label's ink bounds from canvas measureText; the fallback face is whatever your system picks. Trimmed, the ideographs run past the cap band into the padding; untrimmed, the box keeps room for them.

03

Where it shows up

Ordinary compositions where the same rules apply, at real size and ×2. Numbers are measured live on the outlined element in each; where a composition has nothing to fix in your faces, the chip says so.

Compositions

Toolbar of mixed buttons (Segoe UI)

Icon + label, label only, icon only. Measured on the first button: caps vs icon center.

Card header with an icon (Segoe UI 16/24)

Icon, title, a trailing link. Measured: the title's caps vs the icon center.

Input and button in one row

The button's label is trimmed. Without re-deriving the padding it comes up short of the input beside it. Measured: button height vs input height.

Tag row (Inter 600 12/1.5)

Pills with padding: 3px 9px. Measured on the first tag: caps vs box center and the pill height; the trimmed version adds the removed height back as padding.

Mixed-language button row

"Save" beside 保存する with the same trim class on both. Measured on the CJK button: ink above the cap line.

Probe notes method, font metrics and the measurements behind the issue
loading fonts…

Figures are magnified (factor in the corner) over the real 1× button; numbers are measured live from the 1× copy, in CSS px, + = low. Guides span each row: the first figure's lines run across, and thick segments mark where another figure differs. Each group's bar stays in view while you look: toggle guides and trim there.

Chrome Canary 159.0.8128.2 (Windows, 125% display scale, devicePixelRatio 1.25), measured after the fonts loaded: Inter 600 14px self-hosted on the probe page (from Google Fonts on this page, same metrics: cap height 10.19 px at 14px), system faces by font-family override. Offsets come from layout rects and the fonts' cap-height and x-height metrics, not from rendered glyphs. At this scale anything under about 0.4 px is pixel snapping; border: 1px computes to 0.8 px, so border-box heights read 31.79 rather than 32.

Method on this page: cap and x-height come from the height of a block trimmed with cap / ex at 200 px (the browser's own value, scaled); ascent, descent and CJK ink from canvas measureText; the baseline from a zero-size inline-block marker. Magnified figures are transform: scale() copies of the measured 1× element, so their geometry is the 1× geometry times the factor. All figures recompute on resize.

Font metrics at 14 px, measured here:

Probe markup:

<style>
  button { font: 600 14px/1 Inter; padding: 10px 16px; border: 1px solid; }
  .trim { text-box: trim-both cap alphabetic; }
  .icon { display: inline-flex; align-items: center; gap: 8px; line-height: 20px; }
</style>
<button>Save changes</button> <button class="trim">Save changes</button>
<button class="icon trim"><svg width="16" height="16"></svg>Save</button>
<button class="icon"><svg width="16" height="16"></svg><span class="trim" style="display: block">Save</span></button>

Cap center relative to the box center, and to a 16px icon's center in the inline-flex button, in px, + = caps low (this page also adds Arial: -0.61 default, 0.00 label trimmed, vs the icon):

FaceCaps vs box, defaultCaps vs box, trimmedCaps vs icon, defaultCaps vs icon, label trimmed
Inter-0.3-0.2+0.10.0
Segoe UI+0.7-0.3+1.10.0
Georgia-0.9-0.4-0.50.0
  • For Inter, trim on the inline-flex button itself gave the untrimmed value, to 0.01 px, at line-heights 1, 20px and 1.5. Untrimmed, where the caps land depends on how the face splits ascent and descent, so the sign changes between faces. Trimmed, the label box is the cap band, so the caps are centered by construction (the -0.2 to -0.4 left in the box column is snapping).
  • x-height: its center sits below the cap center by half of cap height minus x-height, about 1.3 px for Inter at 14px. It measured 1.0 px below the box center untrimmed and 1.2 px trimmed. ex alphabetic centers the x-height instead (-0.4 px) and lifts the caps 1.7 px high.
  • Ink: canvas measureText and a pixel scan both put "Save changes" 1 to 2 px low in both states, but disagree on whether trim helps; a descender stretches the ink box without moving the word's weight.
  • Shrink on an auto-height control = line-height minus cap height, to 0.01 px: 3.81 px at line-height 1 (10.7%), 6.61 px at normal (17.2%), 9.81 px at 20px (23.6%), 10.81 px at 1.5 (25.4%). With height: 36px, and with trim plus min-height: 36px, it stayed 36.
  • A <span class=trim> inside a block button does nothing in Chrome until it gets display: block.
  • A wrapped label loses only the first line's top and the last line's bottom; line pitch is unchanged.
  • CJK (保存する, fallback face unidentified): cap alphabetic uses Latin metrics, so the ideographs run about 2 px into the 10 px padding above and below (this page: ink 1.85 px above the cap line, 1.31 px below the baseline, unchanged by trim).
  • Fallback faces on a trimmed box: ascent-override, descent-override and line-gap-override set to Inter's values (96.875%, 24.12%, 0%) left the trimmed border-box height where the plain fallback had it (Arial Bold 31.63 px, Segoe UI 31.40, Inter 31.79). size-adjust set to the ratio of cap heights (101.64% for Arial Bold, 103.9% for Segoe UI) matched Inter in both. The overrides still fix the untrimmed baseline (Arial 22.0 to 22.8 px from the top). The shift is small at 14px, 0.2 to 0.4 px, and grows with size.
  • impeccable detect --no-config (0.1.18) on the probe: no alignment findings, and the 62-rule catalog has no alignment rule. Positive controls fired.
  • Support, from MDN browser-compat-data 8.1.7: text-box in Chrome and Edge 133, Safari 18.2, Firefox 154; trim on inline boxes in Firefox 154 and Safari 26.5 (partial), not Chrome. Firefox, Safari and the ideographic-ink edge are untested.

Investigation, probe and demo prepared with AI assistance (Claude Code) under the author's direction.