Lucide icons rendered as Foldkit virtual DOM via the h builder. Includes a generic renderer for any lucide icon plus a set of commonly used icons.

Dependencies:lucide
Preview
Style

Lucide icons rendered as Foldkit virtual DOM via the h builder. Install `@foldcn/icons` and import `icon(h, node, className?)` from it.

check
chevron-down
chevron-left
chevron-right
minus
x
src/demo/views/icons.ts54 lines
import type { Html, HtmlBuilder } from 'foldkit/html'

import { icon } from '../../generated/registry/lib/icons'
import { Check, ChevronDown, ChevronLeft, ChevronRight, Minus, X } from 'lucide'

import { defineSlice } from '../slice'
import type { Model, Message } from '../assemble'

const ICON_ROWS: ReadonlyArray<ReadonlyArray<[string, (h: HtmlBuilder<Message>) => Html]>> = [
  [
    ['check', (h) => icon(h, Check)],
    ['chevron-down', (h) => icon(h, ChevronDown)],
    ['chevron-left', (h) => icon(h, ChevronLeft)],
    ['chevron-right', (h) => icon(h, ChevronRight)],
    ['minus', (h) => icon(h, Minus)],
    ['x', (h) => icon(h, X)],
  ],
]

export const iconsView = (model: Model, h: HtmlBuilder<Message>): Html =>
  h.div(
    [h.Class('w-full max-w-lg')],
    [
      h.p(
        [h.Class('mb-4 text-sm text-muted-foreground')],
        [
          'Lucide icons rendered as Foldkit virtual DOM via the h builder. Install `@foldcn/icons` and import `icon(h, node, className?)` from it.',
        ],
      ),
      ...ICON_ROWS.map((row) =>
        h.div(
          [h.Class('mb-2 grid grid-cols-6 gap-2')],
          row.map(([label, render]) =>
            h.div(
              [
                h.Class(
                  'flex flex-col items-center gap-1 rounded-lg border border-border bg-card p-3 text-muted-foreground',
                ),
              ],
              [render(h), h.span([h.Class('text-[11px]')], [label])],
            ),
          ),
        ),
      ),
    ],
  )

export const slice = defineSlice({
  fields: {},
  init: {},
  messages: [],
  handlers: () => ({}),
})

This code is shown verbatim — it’s the exact file used for this preview. Much of the surrounding wiring (slice, fields, messages, handlers) belongs to this site’s demo harness; you only need the view and the state that matters for your own app. Adapt what you need.

Installation

Add this component to your project:

pnpm dlx shadcn@latest add @foldcn/icons

Source

The component ships as plain source — no build step, no wrapper. Copy it and make it yours.

registry/default/lib/icons.ts79 lines
import type { Attribute, ChildAttribute, Html, HtmlBuilder } from 'foldkit/html'
import type { IconNode, SVGProps } from 'lucide'

/**
 * Render a lucide icon as Foldkit virtual DOM.
 *
 * Lucide ships each icon as a node tree: `[["path", { d: "..." }], ...]`.
 * This renders that tree with the `h` builder, so icons are first-class
 * Foldkit VNodes with no string parsing involved.
 *
 * ```ts
 * import { icon } from '@foldcn/registry/styles/default/lib/icons'
 * import { ChevronDown } from 'lucide'
 *
 * // Default size (size-4 shrink-0)
 * icon(h, ChevronDown)
 *
 * // Custom size
 * icon(h, ChevronDown, 'size-3')
 * ```
 */

const svgElement =
  <M>(tag: string, h: HtmlBuilder<M>) =>
  (attributes: ReadonlyArray<Attribute<M> | ChildAttribute>): Html => {
    switch (tag) {
      case 'path':
        return h.path(attributes)
      case 'circle':
        return h.circle(attributes)
      case 'rect':
        return h.rect(attributes)
      case 'line':
        return h.line(attributes)
      case 'polyline':
        return h.polyline(attributes)
      case 'polygon':
        return h.polygon(attributes)
      default:
        return h.path(attributes)
    }
  }

const svgAttributes = <M>(
  className: string,
  h: HtmlBuilder<M>,
): ReadonlyArray<Attribute<M> | ChildAttribute> => [
  h.AriaHidden(true),
  h.Class(className),
  h.Xmlns('http://www.w3.org/2000/svg'),
  h.Fill('none'),
  h.ViewBox('0 0 24 24'),
  h.StrokeWidth('2'),
  h.Stroke('currentColor'),
  h.StrokeLinecap('round'),
  h.StrokeLinejoin('round'),
]

const nodeToAttributes = <M>(
  attrs: SVGProps,
  h: HtmlBuilder<M>,
): ReadonlyArray<Attribute<M> | ChildAttribute> =>
  Object.entries(attrs).map(([name, value]) => h.Attribute(name, String(value)))

const defaultIconClass = 'size-4 shrink-0'

export type IconPosition = 'inline-start' | 'inline-end'

export const icon = <M>(
  h: HtmlBuilder<M>,
  node: IconNode,
  className = defaultIconClass,
  position?: IconPosition,
): Html =>
  h.svg(
    [...svgAttributes(className, h), ...(position ? [h.DataAttribute('icon', position)] : [])],
    node.map(([tag, attrs]) => svgElement(tag, h)(nodeToAttributes(attrs, h))),
  )