Skip to content

Repository files navigation

Lucide Icon Picker

A Contentful App Framework app that replaces freeform text entry for icon names with a searchable, visual picker over the Lucide icon set. It's a single entry-field app that adapts to the field it's assigned to:

  • Short Text field → picks a single icon; the field stores its exact PascalCase name (e.g. ArrowRight, ChartNoAxesCombined).
  • Object (JSON) field → an icon + text repeater: an array of { "icon": "ArrowRight", "text": "Fast" } rows, reorderable by drag and drop, each row using the same icon picker.

Which one renders is decided at runtime from sdk.field.type — there's nothing to configure per installation.

Built for a FontAwesome → Lucide migration across several Astro sites that render icons via @lucide/astro's icons namespace: icons[name]. This app guarantees the stored name matches that lookup exactly.

image

How the name/preview data stays in sync with @lucide/astro

Every Lucide framework package (lucide-react, @lucide/astro, lucide-vue-next, ...) is generated from the same icon set and published in lockstep — at a given Lucide release, all framework packages ship the same version number and the same icon names. This app depends on lucide-react (pinned to an exact version in package.json, not a range) purely to get:

  • Object.keys(icons) — the canonical list of valid icon names, used for search and validation.
  • The actual icons[name] React components — used to render live SVG previews, guaranteed pixel-identical to what @lucide/astro renders since both packages share the same path data.

When the Astro sites upgrade their @lucide/astro version, bump this app's lucide-react dependency to the identical version (npm install lucide-react@<version>) so the two icon sets never drift apart. @lucide/astro itself is never installed or imported here — this app is plain React and never renders Astro components.

Field behavior

src/App.tsx is the only implemented location (entry-field); it checks sdk.field.type and renders one of two editors:

Short Text (Symbol/Text) — src/locations/Field.tsx

  • Shows the currently selected icon's live preview + humanized name.
  • "Choose icon" / "Change" expands an inline search + virtualized grid of all ~1738 icons (src/components/IconGridPicker.tsx), filtered as you type. Selecting a tile writes that icon's exact name to the field and collapses the picker.
  • If the stored value doesn't match any known Lucide icon name (e.g. a typo, or a leftover value from before this app was installed), a warning banner is shown so the editor notices and can fix it — the picker itself can only ever write valid names.
  • "Clear icon" empties the field.

Object (JSON) — src/locations/IconTextRepeaterField.tsx

  • Stores an array of { icon: string; text: string } rows in one field.
  • Each row has a compact icon button (src/components/IconPickerButton.tsx), a text input, and a delete button, all the same height so the row reads as one aligned control instead of mismatched button/input sizes. Clicking the icon button expands the grid picker in normal document flow below the row (not a floating popover) — Contentful's field-iframe auto-resizer only measures in-flow layout height, so an absolutely-positioned overlay gets clipped and needs scrolling to reach.
  • At most one row's picker is open at a time (opening one closes any other), and a row with no icon yet opens its picker automatically — on first mount and right after "Add item" — so picking an icon for a new row doesn't need an extra click-to-open step that would otherwise cause a layout shift.
  • Rows reorder via real drag and drop (@dnd-kit, with Forma36's DragHandle), including keyboard support (focus the handle, arrow keys to move, Enter/Space to lift and drop) — not just up/down buttons.
  • "Add item" appends a blank row; the field is cleared entirely (not stored as []) once the last row is removed.

No installation parameters and no per-instance config either way — the same app definition can be assigned to any Short Text or Object field in any content type, across any space.

Local development

npm install
npm start

npm start creates/updates the app definition (prompts for Contentful login on first run) and starts the Vite dev server at http://localhost:3000.

Wiring into a Contentful space

  1. npm run create-app-definition (or npm start) to create the app definition in your organization. When prompted for locations, select only App configuration screen: no / Entry field.
  2. In the app definition's frontend settings, set the URL to http://localhost:3000 for local development.
  3. Install the app into a space (Apps → the app → Install).
  4. On the content type that has (or will have) a Lucide icon field, assign this app as the field's appearance/editor:
    • Single icon → a Short Text field.
    • Icon + text repeater → an Object (JSON object) field.
  5. Open an entry with that field to use the picker.

For a deployed version, npm run build then npm run upload bundles and activates the app on Contentful (no separate hosting needed), after which you can point the app definition's frontend at the hosted bundle instead of localhost.

Scripts

  • npm start / npm run dev — local dev server
  • npm run build — production build to build/
  • npm test / npm run test:ci — unit tests (Vitest + Testing Library)
  • npm run create-app-definition — create/select the app definition
  • npm run add-locations — add locations to an existing app definition
  • npm run upload — bundle and upload to Contentful

About

A Contentful App that adds a Lucide icon picker as a Short Text field.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages