What happens
An icon class from any collection other than lucide renders on a native screen as a plain circle. Nothing is logged, at build time or at runtime, so the first sign is seeing it on a device.
We shipped one to a phone. A 48px accent icon came out as a bare ring, and it was caught by looking at the screenshot, not by any check.
Repro
In a native screen, with @iconify-json/hugeicons installed (it arrives with @stacksjs/defaults):
<i class="h-12 w-12 i-hugeicons-workout-run"></i>Build and run: a circle. The web build of the same template draws the runner correctly, since ts-css resolves any installed collection.
Mechanism
native/compiler/icons.ts, resolveIconName:
const prefixed = /^i-([a-z0-9]+)-(.+)$/.exec(value) ?? ...
const collection = prefixed ? prefixed[1] : 'lucide'
if (collection === 'lucide') {
const symbol = LUCIDE_TO_SF[icon]
if (symbol) return { symbol, iconify: `i-lucide-${icon}`, known: true }
...
}
return { symbol: FALLBACK_SYMBOL, iconify: ..., known: false } // FALLBACK_SYMBOL = 'circle'collection === 'lucide' is the only branch that consults a table, so every other collection takes the fallback by construction — not because a name is missing, but because the collection isn't lucide. There is no way to express a mapping for one, even where an exact SF Symbol exists: hugeicons:workout-run is figure.run, the same symbol footprints already maps to.
The silence is the actual bug
known: false is computed and then discarded. Its only consumer is layout-chrome.ts, which uses it to pick between the symbol and the raw class — never to report. No console.warn in the file, and nothing downstream logs it.
The doc comment on LUCIDE_TO_SF says otherwise:
Anything missing falls back to
circleand the compiler says which name it did not know.
It doesn't. We checked a full build:ios log for any mention of the icon, the collection, or a fallback: nothing.
What makes this stand out is how close the counterexample is. In native/runtime/native-host.ts:
:474 console.warn(`[STX Native] unknown-class: ${id}: ${name}`)
:477 const icon = resolveIconName(classes.icon)An unrecognised class warns. Three lines later an icon resolves to the fallback and says nothing — in the same function, for the same kind of authoring mistake. unknown-binding warns too. Icons are the gap.
Suggested shape
Separable, in order of value:
- Say it. Emit the
unknown-iconwarning the docs already promise, at compile time where the template and name are in hand — matchingunknown-class. This alone turns a device-only discovery into a build line. - Let other collections map. Key the table by collection, or let a project supply its own entries, so an installed collection isn't fallback-by-construction.
- Consider a better fallback. A circle is indistinguishable from a deliberate ring. Something visibly wrong, or the SF Symbol
questionmark.square.dashed, reads as a mistake.
Worth noting (1) stands on its own: with a warning, the current behaviour is defensible — without one, a valid, installed collection degrades invisibly between web and native builds of the same template.