SmartIcon and SmartIconPicker
smart-naive-icon— an offline-first icon package for Vue 3 + Naive UI, built on Iconify:SmartIconis the renderer andSmartIconPickerthe picker. It is a standalone npm package thatsmart-admin-webdeclares as a peer dependency (^2.0.0), consumed through just three thin wrappers inside the kernel package.
The menu table has an icon field: put a string in it and the sidebar, breadcrumb, and buttons can draw the matching icon. Picking an icon, storing it, and rendering it — SmartAdmin hands all three to this package. It's offline-first: icons render from local data bundled into the app, with no requests to Iconify's online API. This page is about the package itself — its value contract, how to add icon sets, how to drop in local SVGs, and how to translate its copy. How icons are registered app-wide in SmartAdmin and how AppIcon renders them across the site belongs to the Theme & Icons page; the integration conventions aren't repeated here.
Picking an icon in menu management
Only three files in the kernel package touch this package, all under web/packages/admin/src:
setupIcons()inlib/icons.ts, called once bycreateSmartAdmin(), registers the offline icon sets and local SVGs globally;components/IconPicker/index.vuewrapsSmartIconPickerand is the app's picker, used on the menuiconfield under System → Menu Management;components/AppIcon.vuewrapsSmartIconand is the app's renderer — every icon drawn across the site goes through it.
The last two are exported from smart-admin-web as IconPicker and AppIcon, so an app's pages can use them too. The menu-management page (views/system/menu/index.vue) ties the two ends together: pick with the picker in the form, display with the renderer in a table column.
<!-- form: pick an icon and store it in form.icon -->
<IconPicker :model-value="form.icon ?? ''" @update:model-value="(v: string) => (form.icon = v)" />
<!-- table column: draw the stored string -->
<AppIcon :icon="row.icon" :size="18" />The v-model value (written here as model-value) is just a string, shaped like ph:house-duotone — i.e. prefix:icon-name; a local SVG is local:icon-name. That string goes into the database's icon field as-is, and handing it back to AppIcon renders it. The whole contract is this one line: one field stores one string, and both the picker and the renderer understand it.
SmartAdmin's picker wrapper doesn't pass collections again — setupIcons() already registered them globally, so it just reuses them; it does only one thing the package itself doesn't handle: computing vue-i18n copy into labels and injecting it (see below).
What offline-first means
"Offline-first" doesn't mean the component runs offline — it means the icon sets you register render only from the local data bundled into your build, and never touch api.iconify.design. Each set (@iconify-json/<prefix>) is a separate lazy-loaded chunk in your build, pulled in only the first time you open its tab or first render an icon from it.
The cost is size: every set you register adds a chunk, and big sets aren't cheap (Phosphor ≈ 946 KB gz, Lucide ≈ 85 KB gz), so register on demand rather than dumping them all in at once. In return, in a deployment with no internet or restricted egress, icons behave exactly as they do online. The package also keeps one online fallback: type an unregistered Iconify name by hand and it loads online temporarily when connected — that's for emergencies, not the norm.
Registering more icon sets
setupSmartIcon is the package's single configuration entry point — configure every set at once. SmartAdmin's setupIcons() is one wrapper around it (the kernel package's lib/icons.ts):
import { setupSmartIcon } from 'smart-naive-icon'
setupSmartIcon({
collections: [
{ prefix: 'ph', name: 'Phosphor', loader: () => import('@iconify-json/ph/icons.json').then((m) => m.default) },
{ prefix: 'lucide', name: 'Lucide', loader: () => import('@iconify-json/lucide/icons.json').then((m) => m.default) },
// one more set: first npm i @iconify-json/<prefix>, then add a loader line with the same prefix
],
preloadPrefix: '__none__', // the package has no "don't preload" switch, so pass a prefix that can never match
})Each collection is a tab in the picker, and the first in collections is the one open by default (ph in SmartAdmin). Browse the sets you want at icon-sets.iconify.design, note the prefix, and install the matching @iconify-json/<prefix>. Stored values carry the prefix (ant-design:home-outlined), so no matter which tab you originally picked from, AppIcon can always match it.
SmartAdmin doesn't use preloadPrefix to warm an entire collection — the full ph set is 946 KB gz, too much to eat on first paint. It takes a different path instead: a ph subset is registered synchronously at startup, and only names outside it fall back to AppIcon lazy-loading the full ph set. There are two subsets — the kernel's ships with the package, and an app generates its own from its src with smart-admin-icons — see Theme & Icons.
If you never call setupSmartIcon at all, the package ships one Lucide set as a fallback (it lists @iconify-json/lucide as a runtime dependency), usable on import — SmartAdmin doesn't take that default path, instead explicitly registering four sets: ph / lucide / ep / ant-design. That list lives in the kernel package; icons an app needs beyond it go in as local SVGs, covered next.
Dropping in your own SVGs
When a design's icon isn't in any Iconify set, register it as a local SVG. Under Vite, use a glob to read a directory as raw strings; the filename (minus .svg) becomes the icon name:
import { registerLocalIcons } from 'smart-naive-icon'
registerLocalIcons(
import.meta.glob('/src/assets/svg/*.svg', { query: '?raw', import: 'default', eager: true }),
)
// src/assets/svg/star.svg -> stored as local:starSmartAdmin folds this step into setupIcons()'s localIcons option. An app hands the same glob to createSmartAdmin({ icons }), and it's registered alongside the kernel's own SVGs:
createSmartAdmin({
icons: import.meta.glob('./assets/svg/*.svg', { query: '?raw', import: 'default', eager: true }),
})Drop an SVG into the app's src/assets/svg/, restart dev, and it shows up on the picker's "Local" tab.
Copy and localization
The package carries no i18n framework of its own: every visible label in SmartIconPicker comes from a single labels object (English by default), and you override just the keys you need. SmartAdmin's picker wrapper computes vue-i18n copy into labels and passes it in (the kernel package's components/IconPicker/index.vue); wrapped in a computed, the picker's copy switches along with the language:
<SmartIconPicker v-model="icon" :labels="{ placeholder: 'Choose an icon', title: 'Icon' }" />Overridable keys: placeholder / title / search / local / online / onlinePlaceholder / use / offlineHint / loading / empty / more (which carries a {n} placeholder, replaced with the count at render time).
Using it outside SmartAdmin
It's a standalone package, so it also installs into any other Vue 3 + Naive UI project. It doesn't bundle Vue or Naive UI, providing them as peer dependencies: vue ^3.3, naive-ui ^2.34, @iconify/vue ^4 || ^5; styles are injected by the component automatically, with no separate CSS import. Browser-only (it uses navigator.onLine and v-html), and the picker must sit inside the app's <n-config-provider> for the theme variables to resolve.
npm i smart-naive-iconFor SmartIconPicker's full props list (collections / localIcons / cap / clearable, etc.), the SmartIcon API, and SSR/Nuxt notes, see the package README.