Components

BadgeSelector

Badge-based single select compound component with built-in search and CRUD

Overview

BadgeSelector is a compound component for badge-based single selection. It has built-in search, create, edit, and delete functionality.

Features

  • Item search and filtering
  • Inline item creation
  • Item edit/delete
  • Keyboard navigation
  • Close on outside click
  • Customizable trigger
  • TypeScript support

Anatomy

import { BadgeSelector } from '@illog/ui'
 
<BadgeSelector.Root
  items={items}
  selectedItem={selectedItem}
  onSelectItem={handleSelectItem}
  onClearItem={handleClearItem}
  onCreateItem={handleCreateItem}
  onDeleteItem={handleDeleteItem}
  onUpdateItem={handleUpdateItem}
>
  <BadgeSelector.Trigger>
    {/* Custom trigger button */}
  </BadgeSelector.Trigger>
 
  <BadgeSelector.Content>
    <BadgeSelector.Search />
    <BadgeSelector.List />
  </BadgeSelector.Content>
</BadgeSelector.Root>

API Reference

Root

Root component that manages state and provides context.

PropTypeRequiredDescription
itemsBadgeItem[]YesAll available items
selectedItemBadgeItem | nullYesCurrently selected item
onSelectItem(itemId: string) => Promise<void>YesItem select handler
onClearItem() => Promise<void>YesClear selection handler
onCreateItem(data: Partial<OmittedBadgeItem>) => Promise<string>YesItem create handler
onDeleteItem(itemId: string) => Promise<void>YesItem delete handler
onUpdateItem(itemId: string, data: Partial<OmittedBadgeItem>) => Promise<void>YesItem update handler
maxNameLengthnumberNoMax name length (default: 100)
childrenReactNodeYesMust include Trigger and Content

Type Definitions:

type BadgeItem = {
  id: string
  name: string
  color: BadgeColor
  createdAt: Date
  updatedAt: Date | null
  deletedAt: Date | null
}
 
type OmittedBadgeItem = Omit<BadgeItem, 'id' | 'createdAt' | 'updatedAt' | 'deletedAt'>

Trigger

Trigger element that opens and closes the dropdown.

PropTypeRequiredDescription
asChildbooleanNoMerge props with child element (default: false)
childrenReactNodeYesTrigger button content

Data Attributes: data-state: "open" | "closed"

Content

Container that renders dropdown content via a portal.

PropTypeRequiredDescription
childrenReactNodeYesUsually Search and List
classNamestringNoCSS class

Input field for filtering and creating items.

PropTypeRequiredDescription
placeholderstringNoPlaceholder text (default: "Search...")
maxLengthnumberNoMax input length (default: 100)

List

Renders the filtered item list. No props.

Examples

Custom Trigger with Badge

<BadgeSelector.Root {...handlers}>
  <BadgeSelector.Trigger asChild>
    <div style={{ cursor: 'pointer' }}>
      {selectedItem ? (
        <Badge item={selectedItem} />
      ) : (
        <button>Select Item</button>
      )}
    </div>
  </BadgeSelector.Trigger>
  <BadgeSelector.Content>
    <BadgeSelector.Search />
    <BadgeSelector.List />
  </BadgeSelector.Content>
</BadgeSelector.Root>

Complete Example

import { useState } from 'react'
import { BadgeSelector, Divider } from '@illog/ui'
 
export default function MyComponent() {
  const [items, setItems] = useState<BadgeItem[]>([])
  const [selectedItem, setSelectedItem] = useState<BadgeItem | null>(null)
 
  return (
    <BadgeSelector.Root
      items={items}
      selectedItem={selectedItem}
      onSelectItem={async (id) => {
        setSelectedItem(items.find((i) => i.id === id) ?? null)
      }}
      onClearItem={async () => setSelectedItem(null)}
      onCreateItem={async (data) => {
        const newItem = { ...data, id: Date.now().toString() }
        setItems([...items, newItem])
        return newItem.id
      }}
      onDeleteItem={async (id) => setItems(items.filter((i) => i.id !== id))}
      onUpdateItem={async (id, data) => {
        setItems(items.map((i) => (i.id === id ? { ...i, ...data } : i)))
      }}
    >
      <BadgeSelector.Trigger asChild>
        <button>{selectedItem?.name ?? 'Select Item'}</button>
      </BadgeSelector.Trigger>
      <BadgeSelector.Content>
        <BadgeSelector.Search />
        <Divider />
        <BadgeSelector.List />
      </BadgeSelector.Content>
    </BadgeSelector.Root>
  )
}

Accessibility

Keyboard Interactions

KeyAction
EnterSelect item / create new item (while searching)
BackspaceClear selection (when search is empty)
EscapeClose dropdown
Space (trigger)Toggle dropdown
Enter (trigger)Toggle dropdown

ARIA

  • Trigger: aria-expanded, aria-haspopup="listbox", role="button"
  • Search: Auto-focused on open, proper input semantics
  • Content: role="listbox"

Focus Management

  • Focus returns to trigger when dropdown closes
  • Auto-focuses search input when dropdown opens
  • Full keyboard navigation without mouse

Custom API

useBadgeSelectorContext

Access internal state and handlers in custom components:

import { useBadgeSelectorContext } from '@illog/ui'
 
function CustomBadgeList() {
  const {
    isOpen, searchTerm, selectedItem, filteredItems,
    canCreateNew, previewColor,
    setIsOpen, setSearchTerm,
    selectItem, clearItem, createItem, deleteItem, updateItem,
  } = useBadgeSelectorContext()
 
  return (
    <div>
      {filteredItems.map((item) => (
        <button key={item.id} onClick={() => selectItem(item)}>
          {item.name}
        </button>
      ))}
    </div>
  )
}
PropertyTypeDescription
isOpenbooleanDropdown open state
searchTermstringCurrent search term
selectedItemBadgeItem | nullSelected item
filteredItemsBadgeItem[]Items matching search
canCreateNewbooleanWhether a new item can be created
previewColorBadgeColorPreview color for new item
selectItem(item: BadgeItem) => Promise<void>Select item
clearItem() => Promise<void>Clear selection
createItem() => Promise<void>Create new item
deleteItem(itemId: string) => Promise<void>Delete item
updateItem(itemId: string, data) => Promise<void>Update item