Blog · October 25, 2026

Matching icon stroke weight to the text beside it.

You drop a popular line-icon set into a settings row, and something looks off. The icons look bolder than the labels next to them, or occasionally thinner, and nudging the size never quite fixes it. Size isn’t the problem. The icon’s stroke and the text’s stems are the same kind of line, and the eye compares them directly. Once you measure both, the right stroke width is a small calculation.

Strokes and stems are the same measurement

Every letter is built from strokes. The vertical one in a lowercase “l” or “n” is called the stem, and its width is the single number that most decides how heavy a weight looks. A line icon is also drawn from strokes, of one uniform width. When the two sit on the same baseline, your eye reads them as one row of marks and notices at once when one set is thicker.

So the question “does this icon match my text?” becomes concrete: is the icon’s stroke, in rendered pixels, close to the text’s stem, in rendered pixels?

What the defaults actually draw

We measured Inter, the variable font our app UI uses, by reading the outline of its lowercase “l” straight from the font file at each weight, using Inter’s text optical size. Stem width scales with font size, so it’s most useful as a fraction of the em:

Inter weightStem (em)At 16px
Light 3000.0661.06px
Regular 4000.0881.41px
Medium 5000.1071.72px
Semibold 6000.1272.03px
Bold 7000.1462.34px

Now the icons. Lucide and Feather draw on a 24-unit grid with a default stroke of 2 units, so an icon shown at 24px has a 2px stroke. Next to 16px Regular body text, whose stems are about 1.4px, that icon is drawn at the weight of Semibold. That’s the heaviness you noticed. Heroicons’ outline set uses 1.5 units on the same grid, which at 24px lands between Regular and Medium, and that’s a large part of why it tends to look calmer beside body copy.

Neither default is wrong. A 2px stroke is a good match for 16px Semibold, the weight many buttons and tab labels use. The mistake is to use one icon setting for every context in the app.

Scaling an icon changes its weight

An SVG icon’s stroke width is in grid units, not pixels, so it scales with the icon. Render that 24-unit Lucide icon at 20px and the 2-unit stroke becomes 1.67px. At 16px it becomes 1.33px. That’s why shrinking an icon sometimes seems to fix the mismatch: it quietly thins the stroke, and you get the right weight at the wrong size.

To get the weight right at the size you actually want, work backwards from the text:

// stroke in grid units, for an icon drawn on a 24-unit grid
strokeWidth = textStemPx × 24 / iconPx

// 16px Regular label (1.41px stem), 20px icon
1.41 × 24 / 20 = 1.69

// 16px Semibold button (2.03px stem), 20px icon
2.03 × 24 / 20 = 2.44

Most SVG icon libraries let you pass a stroke width per instance, so this can live in your icon wrapper rather than in every call site: give it the text style it sits beside and let it work out the stroke. Lucide also has an absoluteStrokeWidth option that treats the stroke as pixels regardless of size, which is useful when one icon appears at several sizes and should keep the same line weight everywhere.

Measuring your own font

Every family has its own stem ratio, and switching from Inter to a geometric sans or a rounded face can move it noticeably. You don’t need special tools to measure it. Set a lowercase “l” at 100px in your font and weight, take a screenshot, zoom in and count the pixels across the stem. Divide by 100 and you have the stem per em. Do it for the two or three weights your app actually uses and write the numbers next to your type scale.

If you want it exact, the font file has the answer: the width of the “l” glyph’s outline divided by the font’s units per em. That’s how the table above was produced, and it takes about ten lines with the fontTools Python library.

Icon fonts and system symbols do this for you

Some icon systems treat weight as a property of the type, which removes most of the arithmetic.

SF Symbols come in nine weights that correspond to the weights of the San Francisco system font, plus small, medium and large scales sized relative to the text. When you style a symbol with the same font as its label, it picks up the matching weight. The common mistake on iOS is to break that link by setting a fixed frame and a separate weight for the image, so the symbol stops following Dynamic Type and its label.

Material Symbols is a variable font with a weight axis from 100 to 700 and an optical size axis from 20 to 48dp. Google’s documentation describes optical size as the way to “automatically adjust the stroke weight when you increase or decrease the symbol size”, which addresses the scaling problem above directly. Set the weight to the same number as your text and let optical size follow the rendered size.

Then adjust by eye, in two places

A matched stroke is the starting point; two situations reliably need a small correction.

Light icons on dark backgrounds. Light strokes appear to spread into the dark around them, so a matched icon looks heavier in dark mode than in light. Material’s guidance on its grade axis says the same thing: “To reduce glare for a light symbol on a dark background, use a low grade.” With SVG icons, take roughly a tenth off the stroke in dark mode and compare the two side by side. Our post on white type on dark covers the same effect in text.

Filled icons. A filled glyph carries far more ink than a line icon or a word of the same size, so stroke matching doesn’t apply to it. Filled icons work as an emphasis state, such as the selected tab, precisely because they are heavier. Pair them with the outlined version of the same icon so the shape stays the same and only the weight changes.

Short version. Measure your font’s stem per em for the weights you use. For each place an icon sits beside text, set the stroke to that stem in rendered pixels, using stem × 24 / iconPx for a 24-unit grid. Put the calculation in your icon component, thin slightly for dark mode, and keep filled icons for emphasis.

Store screenshots are a place where this shows: the UI is scaled down, an icon badge or feature row sits beside a big headline, and every mismatch in weight is magnified. The principle is the one behind optical alignment: what matters is how heavy each mark looks, not the box it sits in.

Design your screenshots → Why “perfectly centered” looks slightly off