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 weight | Stem (em) | At 16px |
|---|---|---|
| Light 300 | 0.066 | 1.06px |
| Regular 400 | 0.088 | 1.41px |
| Medium 500 | 0.107 | 1.72px |
| Semibold 600 | 0.127 | 2.03px |
| Bold 700 | 0.146 | 2.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.
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.