We import 53 icons from one library, and six of them ever reach a public page
Nakodo imports its interface icons from lucide-react. Counting the named imports across the repo: files importing lucide-react 52 distinct icons imported 53 imported by exactly one file 30 icon ele
Nakodo imports its interface icons from lucide-react. Counting the named imports across the repo:
files importing lucide-react 52
distinct icons imported 53
imported by exactly one file 30
icon elements rendered in JSX 132
Thirty icons with a single call site each is the shape you would expect from a library: you reach for the one that fits and you never need it again. The interesting number is how many of those 53 a visitor can actually see, so I swept all 76 URLs in the sitemap and counted the rendered class names.
curl -s https://nakodo.app/pricing | grep -o 'lucide-[a-z-]*' | sort | uniq -c
# 25 lucide-check
# 10 lucide-plus
# 1 lucide-menu
Across all 76 pages, six distinct icons ever leave the server:
menu 76 pages 1 per page
plus 66 pages 4 to 10 per page
copy 8 pages 1 to 6 per page
x 3 pages 3 per page
check 2 pages 21 and 25
mail 1 page 4
Forty-seven of the 53 live entirely behind the login. The public site is a static content site, and a static content site needs almost no iconography: a word is cheaper to render and easier to translate than a glyph that has to be explained.
What each of the six is doing
menu is the mobile navigation toggle, which is why it is on every single page, including the ten that ship nothing else: the eight hub pages such as /guides and /youtubers, plus /privacy and /terms. Those ten pages have no FAQ and nothing to copy, so one icon is their entire icon budget.
plus is the marker on an FAQ question, and on the four calculators it is also the add-a-row button. The FAQ one is my favourite icon in the app, because it is two symbols for the price of one:
<svg class="lucide lucide-plus size-4 shrink-0 text-muted-foreground
transition-transform group-open:rotate-45" aria-hidden="true">
The questions are native <details> elements, so group-open:rotate-45 turns the plus into a cross when the answer opens, with the transition doing the turn. No second icon, no state in JavaScript, and it works with JavaScript off.
copy sits on the copy-this-template buttons, which is why it appears on exactly the eight guides that carry a template, with a count that matches how many templates each one has: six on the outreach templates guide, five on the Instagram and TikTok one, four on the follow-up guide, and one each on five others.
x appears three times on three of the four free tools: one on each of the three video rows the engagement calculators start with, as a remove button that disables itself when a single row is left. The fourth tool is the sponsorship cost calculator, which has no rows to remove, so it ships none.
check and mail are the two highest counts in the sweep and the two rarest pages: 25 ticks on the plan features on /pricing, and 21 ticks plus four envelopes on the home page.
The size attribute in the markup is never the size on screen
Every rendered icon carries this:
<svg width="24" height="24" viewBox="0 0 24 24" stroke-width="2"
class="lucide lucide-check mt-0.5 size-4 shrink-0 ...">
The 24 is the library's default and the size-4 is what you actually see. Eighty of the 132 icon elements in the source carry no className at all, because the button variants size them from the outside:
// base
"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
// size variants
xs: "h-7 gap-1 px-2 text-xs [&_svg:not([class*='size-'])]:size-3",
sm: "h-8 gap-1.5 px-3 text-[13px] [&_svg:not([class*='size-'])]:size-3.5",
The :not([class*='size-']) is the part worth stealing. The rule sizes any icon in the button to match the button, and steps aside the moment the icon names a size itself, which it detects by looking for the substring in the class attribute. So an icon has no size by default, inherits one from whatever it sits in, and can override it locally with one class. Across the app the overrides are size-4 27 times, size-3.5 14, size-3 7 and size-5 twice.
Exactly one icon is addressed to a screen reader
I expected to find a rule about aria-hidden on decorative icons, and found instead that the library has one. From lucide-react 1.49.0:
...params.hasA11yProp === false ? {
[getAttributeName("aria-hidden")]: "true"
} : {},
Every icon is hidden from assistive technology unless you pass an accessibility prop, in which case the hiding switches itself off. That is the correct default for interface icons, which are nearly always a duplicate of a word next to them, and it means 35 of our 132 elements are repeating something the library already did.
One icon in the entire app passes an accessibility prop, in the support thread view:
{line.byEmail && <MailIcon className="size-3" aria-label="By email" />}
That envelope is the only glyph in the product carrying information that no neighbouring text carries: this message arrived as an email rather than being typed into the app. One icon out of 132, and it is the only one where the default stepped aside.
A live check on the rendered page agrees from the other direction: /pricing ships 62 <svg> elements, 36 of them lucide, and 61 of the 62 carry aria-hidden="true". The one that does not is a nested group inside a drawing:
<svg x="82" y="110" width="96" height="96" viewBox="0 0 96 96" overflow="visible">
Which is the other half of the inventory. The 26 non-library svgs on that page are ours: 27 files and 1,622 lines in the illustration directory, drawn on a shared 96 unit grid, and the platform marks are hand-drawn too because YouTube, Instagram and TikTok each require their own logo next to their own data. The icon library covers the verbs. It does not cover the brand, and it is not allowed to cover anybody else's.
The finding
aria-label on a bare <svg> is not reliably announced: the element needs role="img" for the label to be treated as its name, and an <span class="sr-only"> next to a hidden icon is the version that works everywhere. The app already has that pattern in its own attention mark, so the one labelled icon in the codebase is the one that should stop being clever. The 35 redundant aria-hidden attributes can go at the same time, since the library is already doing it.
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.