Optical alignment in UI: 7 spacing fixes math gets wrong
Round letters overshoot flat ones by 1% to 3% of cap height, and UI inherits the same problem. Seven optical fixes, and what CSS now handles for you.
In this piece
A play triangle sits in a 40px round button, centered with place-items: center. Every number agrees. On screen the triangle leans left. Optical alignment is the correction that closes the gap between what a layout measures and what an eye reads, and in most product interfaces it never gets applied.
Type designers have been applying it for centuries. A round O is cut taller than a flat H so both read at the same size, an overshoot of roughly 1% to 3% of the cap height, with Peter Karow’s production manuals putting about 3% on an O and 5% on an A. Interfaces inherit the same problem the moment a circle, a triangle, or a line of text goes inside a box that only knows its own extents.
Why does a mathematically centered UI look off-center?
The eye centers mass. The layout engine centers extents. For a rectangle the two coincide, so squares center correctly for free and everything else drifts. A triangle carries its weight on the flat edge, so geometric centering puts too much of it on the heavy side. A circle inscribed in a square covers about 79% of the square (pi/4), so at identical bounding boxes a round avatar reads smaller than a square thumbnail. A line of text sits in a line box that includes ascender and descender space the glyphs may never use, so vertical centering happens against invisible metrics instead of against the letters.
None of this is taste. It is a measurable difference between two things a machine treats as identical, and it is the reason experienced designers move shapes off their true center.
The seven places it goes wrong in a product UI
1. The icon inside a round button
The play triangle is the canonical case: flat mass on the left, a point on the right, so the geometric center reads as pushed left. We shift it 3% to 6% of the icon width toward the point and check at rendered size, not zoomed. For a 24px glyph that is a single pixel: transform: translateX(1px). The same applies to any glyph with an asymmetric silhouette, a send arrow, a bookmark, a speech bubble with a tail.
The shift belongs to the component. An instance that nudges its own icon is a bug report against the button, not a fix.
2. The label inside a button or chip
Vertical padding is applied to the line box, not to the letters. A 14px label in a 40px button with 13px of padding on both sides ends up sitting low, because the descender space under the baseline is empty for a word like Save and full for a word like Upgrade plan. CSS now addresses this directly: text-box-trim with text-box-edge cuts the leading above the cap height and below the baseline, so padding starts measuring letters. Support shipped in Chrome and Edge 133 and in Safari 18.2, with Firefox still missing as of mid-2026, which keeps it outside Baseline. Put it behind @supports (text-box: trim-both cap alphabetic) and keep asymmetric padding, usually 1px to 2px less at the bottom, as the fallback.
3. Optical size on variable fonts
A typeface drawn for 12px and the same typeface drawn for 48px are different drawings: thinner joins, tighter spacing, smaller apertures as the size grows. Variable fonts carry that as the opsz axis, and font-optical-sizing: auto is the browser default, so it works as soon as the font file has the axis. Two things break it: font-variation-settings written by hand, which overrides the automatic value, and static font files shipped from a build pipeline that dropped the axis. Check the axis is present in the file you actually serve, not in the one in the design tool.
4. Tracking that moves with size and case
One letter-spacing value across a type scale is wrong at both ends. Our defaults: add 0.04em to 0.06em on uppercase labels at 12px, keep 0 through the body range, and remove 0.01em to 0.02em on display sizes above 40px. Uppercase needs the air because caps have flat sides and no ascender rhythm to separate them; display sizes need it removed because the same relative spacing grows with the type. Store the value per step of the scale, next to size and line height, the way we treat any other step in a typographic scale.
5. Round shapes standing next to square ones
A 40px circular avatar next to a 40px square thumbnail reads smaller, by the 79% area difference. We scale round elements 4% to 8% up against square neighbours in the same row, then look at the row at 100%. Icon systems solve the same problem with keyline grids: circular and square glyphs sit inside the keyline, while tall and wide ones are allowed to extend into the padding so every icon reads at one size.
6. Padding around a trailing glyph
Icons ship with whitespace baked into the viewBox. A button with 16px of padding on both sides and a chevron at the end has 16px plus whatever the chevron carries inside its own box, so the right side looks loose. Measure the ink, not the SVG: trim the viewBox in the icon set, or subtract 2px to 4px from the trailing padding in the component that pairs text with an icon.
7. Punctuation at the start of a line
An opening quote at the top of a pull quote breaks the left edge, because the mark is small, high, and mostly whitespace. Print solves it by hanging the mark outside the text block. CSS has hanging-punctuation, supported in Safari only, and the last value is implemented nowhere as of 2026. For a pull quote that has to work in every browser, set the mark in a ::before with a negative margin, or use a negative text-indent on the first line.
Why teams keep shipping it
Three causes, in the order we see them.
- The tools center by bounding box. Figma alignment and CSS
place-itemsboth work on extents. Nothing in the default path knows about visual mass, so the uncorrected version is the one that costs zero effort. - The correction lives in an instance override. A designer nudges a triangle inside one instance, the value never becomes a token, and the next screen starts from the uncorrected component. This is component drift arriving through the smallest possible door.
- Review compares the build to the spec. If the spec was drawn from geometry, the build matches the spec and both look wrong on screen. Nobody in the chain is checking against the rendered pixel.
What it costs
It does not break a flow and it does not fail an audit. The cost is the review comment nobody can localize: the screen looks unfinished, and three people spend an afternoon rearranging the layout when the problem is one pixel inside one button. The cost compounds, because the uncorrected component is copied into every new screen, and it is cheap to fix at the component level and slow to fix once forty screens carry the same misread.
One constraint on the fixes themselves: when the correction tightens padding, keep the hit area. WCAG 2.2 sets a minimum target size of 24 by 24 CSS pixels at Level AA, and an optical trim that pushes an icon button under it trades a craft problem for a compliance problem.
How to fix what already shipped
- Screenshot the three densest screens at 100% zoom. Optical errors disappear when you zoom in, which is why they survive design review on a 27-inch display.
- Mirror the screenshot horizontally. Flipping the image resets the pattern recognition that has been compensating for the error, and misalignment jumps out.
- Blur it by 8px. Detail goes, mass distribution stays. Anything that looks lopsided in the blur is lopsided in the original.
- Fix in the component, never in the instance. One change to the icon button beats forty overrides, and it is the only version that survives the next redesign.
- Name every correction. A constant called
--optical-shift-triangle: 1pxis reviewable, greppable, and removable. A rawtranslateX(1px)in a stylesheet is folklore. - Re-measure hit areas after the pass. Optical trims shrink boxes.
How to keep it out
Four rules hold up in practice. Optical corrections belong to components, so an instance-level nudge gets treated as a defect in the component. Tracking lives in the type scale, one value per step, not per usage. Modern trimming goes behind @supports with the padding fallback intact, so Firefox users get the old behaviour rather than a broken one. And design review adds one question: does anything look off when the screen is mirrored.
The corrections are small, old, and mostly encodable. What cannot be encoded is the last check, which stays the same as it was in metal type: put the thing on screen at the size people will see it, and look at it.
Sources
- Overshoot (typography)
- Overshoot, Typography.Guru glossary
- text-box-trim, MDN
- CSS Text Box support table, Can I Use
- font-optical-sizing, MDN
- Optical size, the hidden superpower of variable fonts
- hanging-punctuation, MDN
- Understanding SC 2.5.8 Target Size (Minimum), W3C
- Mathematical and optical alignment in UI design
- Optical adjustment: logic vs designers
Frequently asked questions
Is optical alignment just eyeballing it?+
No. Most of it is bounded by numbers that type design settled long ago: overshoot of 1% to 3% of cap height, a circle covering 79% of its bounding square, tracking that moves with size and case. Those values can be written into components and tokens and reviewed like any other code. What stays subjective is the last step, checking the rendered screen at the size a user sees it, because font metrics and icon sets differ enough that no single constant covers every case.
How much should I shift an icon inside a round button?+
Start at 3% to 6% of the icon width, toward the visual point of the shape, and only for glyphs with asymmetric mass: play triangles, send arrows, bookmarks, tailed speech bubbles. On a 24px icon that lands on a single pixel. Symmetric glyphs (a circle, a square, a plus, a gear) need no shift at all, and adding one makes them worse. Verify at the rendered size on a normal display, not zoomed to 400% in the design tool.
Can CSS handle optical alignment automatically in 2026?+
Partly. Text trimming (text-box-trim with text-box-edge) removes the leading that makes labels sit low, and font-optical-sizing applies the opsz axis of a variable font at rendered size without any work. Neither is complete: text trimming is missing in Firefox as of mid-2026, and hanging-punctuation ships in Safari only. Icons get nothing, because the browser has no notion of the visual mass inside an SVG, so shape corrections stay a component-level decision.
Related services
Studio
Start a project.
We write about what we build. Tell us what you want to build.