Icons are the vocabulary of user interfaces. A trash can means delete. A pencil means edit. A magnifying glass means search. Without icons, every action needs a text label. With icons, the interface communicates instantly, crossing language barriers -- particularly important for a language built in Abidjan for developers across Africa.
Sessions 049 and 209 integrated a complete icon library into FlinUI. Over 1,000 icons from the Lucide icon set, rendered as inline SVG, available with the same zero-import philosophy as every other FlinUI component. No font files to load. No icon sprite sheets to configure. Just and the icon appears.
The Icon Component
<Icon name="home" />
<Icon name="user" size={24} />
<Icon name="check" color="green" />
<Icon name="alert-triangle" size={20} color="warning" />
<Icon name="settings" size={32} stroke_width={1.5} />The Icon component accepts four props:
| Prop | Type | Default | Description |
|---|---|---|---|
name | text | required | Icon name from the Lucide set |
size | int | 24 | Width and height in pixels |
color | text | "currentColor" | Fill/stroke color |
stroke_width | float | 2 | SVG stroke width |
Using "currentColor" as the default color means icons inherit their color from the surrounding text. An icon inside a element will be the primary color. An icon inside a will be the danger color. This default eliminates the most common icon styling need without any explicit configuration.
Why Lucide
We evaluated four icon libraries:
| Library | Icons | Size | License | Style |
|---|---|---|---|---|
| Feather | 287 | 24KB | MIT | Minimal stroke |
| Heroicons | 292 | 35KB | MIT | Solid and outline |
| Font Awesome | 7,800+ | 400KB+ | Mixed (free/pro) | Varied |
| Lucide | 1,000+ | 150KB | ISC | Clean stroke |
Lucide won on three criteria:
Coverage. 1,000+ icons cover every common UI need. Feather and Heroicons, while beautifully designed, are missing icons for many domains (finance, medical, education, e-commerce).
License. Lucide uses the ISC license -- permissive, simple, and compatible with any use. Font Awesome's dual-license model (free tier plus paid pro tier) would create confusion for FLIN developers.
Design consistency. Every Lucide icon uses the same 24x24 grid, 2px stroke width, and rounded line caps. They look uniform when mixed, which is essential for a component library where icons appear in buttons, navigation items, form labels, and status indicators.
Lineage. Lucide is a fork of Feather Icons with active community development. It inherits Feather's exceptional design quality while adding the breadth that Feather lacks.
Integration Architecture
Icons are stored as SVG path data in a registry file. When renders, the component looks up the path data by name and constructs an inline SVG:
// Icon.flin (simplified)
name = props.name
size = props.size || 24
color = props.color || "currentColor"
stroke = props.stroke_width || 2// Look up the SVG path data path_data = icon_registry[name]
{if path_data != none} {else} ? {/if} ```
The icon_registry is a map from icon names to SVG path strings. For the "home" icon:
icon_registry["home"] = '<path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/>'The tag (covered in article 094) injects the SVG path data as raw HTML, bypassing FLIN's HTML escaping. This is safe because the path data comes from the icon registry (a compile-time constant), not from user input.
Inline SVG vs. Icon Fonts
FlinUI renders icons as inline SVG rather than using icon fonts (like Font Awesome's webfont approach). The reasons:
Scalability. Inline SVG scales perfectly at any size. Icon fonts can produce blurry edges at non-standard sizes because the browser's text rendering engine is optimized for text, not icons.
Color control. Inline SVG supports stroke and fill independently. Icon fonts render as text glyphs and can only be colored with color -- you cannot have a two-tone icon with a font.
No flash of invisible text. Icon fonts require a font file download. Until the font loads, icons are invisible (FOIT -- Flash of Invisible Text) or display as unicode squares. Inline SVG renders immediately because the path data is embedded in the HTML.
Tree shaking. When using inline SVG, only the icons actually used in the application are included. With an icon font, the entire font file (containing all icons) must be downloaded even if the application uses only five icons.
Accessibility. Inline SVG can include and aria-label attributes for screen readers. Icon fonts are invisible to assistive technology unless wrapped in additional ARIA markup.
Icon Categories
The 1,000+ Lucide icons are organized by domain:
Navigation: home, menu, arrow-left, arrow-right, chevron-down, external-link
Actions: plus, minus, x, check, edit, trash, copy, save, download, upload
Communication: mail, phone, message-circle, send, bell, at-sign
Media: play, pause, volume, camera, image, film, music
Files: file, folder, archive, clipboard, paperclip
Users: user, users, user-plus, user-minus, user-check
Data: bar-chart, pie-chart, trending-up, activity, database
Development: code, terminal, git-branch, bug, cpu, server
Commerce: shopping-cart, credit-card, dollar-sign, package, truck
Social: github, twitter, facebook, instagram, linkedin
Misc: settings, search, filter, globe, map, clock, calendar, starUsing Icons in Components
Icons integrate naturally with other FlinUI components:
// Button with icon
<Button variant="primary">
<Icon name="save" size={16} /> Save
</Button>// Navigation with icons
// Alert with icon
// Empty state with large icon
Many FlinUI components accept an icon prop directly, handling the rendering internally:
// The icon prop is a shorthand
<SidebarItem icon="home">Dashboard</SidebarItem>// Equivalent to
String Method Integration
Session 050's string methods play a role in icon handling. The starts_with and remove_prefix methods enable icon namespace detection:
// Some components need to detect icon prefixes
fn resolve_icon(icon_name: text) {
{if icon_name.starts_with("lucide-")}
return icon_name.remove_prefix("lucide-")
{else if icon_name.starts_with("custom-")}
return lookup_custom_icon(icon_name.remove_prefix("custom-"))
{else}
return icon_name
{/if}
}This pattern enables extensibility -- developers can add custom icon sets with a namespace prefix, and the icon system resolves them alongside the built-in Lucide icons.
Performance: Icon Registry Size
The complete Lucide icon registry (1,000+ icons as SVG path strings) is approximately 150KB of FLIN source. Compiled into bytecode, it is approximately 80KB. This is loaded once when the application starts and shared across all icon references.
For comparison: - Lucide as a JavaScript library: ~290KB (all icons) - Lucide as a web font: ~180KB - Font Awesome Free font: ~400KB - FlinUI icon registry: ~80KB (compiled)
The registry is smaller than the alternatives because it stores only the SVG path data -- no JavaScript module wrappers, no font metadata, no glyph tables. Just the raw coordinates that define each icon's shape.
Custom Icons
Developers can add custom icons to the registry:
// Register a custom icon
register_icon("my-logo", '<circle cx="12" cy="12" r="10"/><path d="M12 6v12M6 12h12"/>')// Use it like any other icon
register_icon adds an entry to the icon registry. The custom icon is then available everywhere in the application, with the same props (size, color, stroke_width) as built-in icons.
For complete icon sets, a .flin file can register all icons at once:
// custom-icons.flin
register_icon("brand-logo", '<path d="..."/>')
register_icon("brand-icon-1", '<path d="..."/>')
register_icon("brand-icon-2", '<path d="..."/>')This file is automatically discovered by the component system (if placed in a search path) and executed on startup. No import needed -- just drop the file in the project.
One Thousand Icons, Zero Configuration
Accessibility: Icons That Speak
Icons need to be accessible. A sighted user sees a trash can and understands "delete." A screen reader user hears nothing unless the icon has an accessible label.
FlinUI's Icon component handles this automatically:
// Decorative icon (next to text, no label needed)
<Button><Icon name="save" size={16} /> Save</Button>
// The button text "Save" provides the accessible name// Standalone icon (no text, needs label)
// Icon with explicit title
When the title prop is provided, the Icon component adds a element inside the SVG and an aria-labelledby attribute referencing it. Screen readers announce the title text when the icon receives focus.
For decorative icons (icons that appear next to text and do not carry independent meaning), the SVG includes aria-hidden="true" to prevent screen readers from announcing the SVG structure. This is set automatically when no title prop is provided.
The Icon Search Problem
With 1,000+ icons, finding the right one is a challenge. What is the icon for "calendar"? Is it calendar, calendar-days, calendar-check, or calendar-range? For the FlinUI documentation site, we built an interactive icon browser:
search = ""
filtered = icon_names.where(name => name.contains(search.lower))
This component searches across all icon names in real-time. Type "arrow" and you see all 20+ arrow variants. Type "chart" and you see all chart-related icons. The filter uses FLIN's built-in where and contains methods -- no search library needed.
Over 1,000 icons. Inline SVG rendering. Scalable to any size. Colorable with any token. Accessible with aria labels. Tree-shaken to include only used icons. Available with and nothing else.
Icons are the most frequently used visual element in web applications after text. Making them available without configuration, without downloads, without build steps is one of the decisions that makes FlinUI feel like it was designed for building real applications, not demos.
---
This is Part 87 of the "How We Built FLIN" series, documenting how a CEO in Abidjan and an AI CTO integrated 1,000+ icons into a UI component library.
Series Navigation: - [86] The Layout System - [87] Icons Library Integration (you are here) - [88] FlinUI Enterprise Components - [89] Scoped CSS and Computed Styles