@k2b/ui
Tag editing
Reusable tag presentation, entity management, and assignment composition.
Tag owns compact presentation; TagEditor owns accessible create/edit/delete interaction while the application owns persistence and confirmation. MultiSelectInput assigns existing tags and accepts custom option/value rendering.
- Platform
- Design
TSX
Copyconst [tags, setTags] = createSignal<TagEditorItem[]>(initialTags); <Tag color="#0891b2">Platform</Tag><Tag color="#8b5cf6" icon="ti ti-palette">Design</Tag><Tag size="sm">Neutral</Tag><Tag color="#2563eb" icon="ti ti-point" selected size="lg">Selected</Tag> <TagEditor items={tags()} onCreate={async (value) => setTags((items) => [...items, { id: crypto.randomUUID(), ...value }])} onUpdate={async (tag, value) => setTags((items) => items.map((item) => item.id === tag.id ? { ...item, ...value } : item))} onDelete={async (tag) => setTags((items) => items.filter((item) => item.id !== tag.id))}/> <MultiSelectInput label="Assigned tags" value={selected} onValueChange={setSelected} options={tags().map((tag) => ({ value: tag.id, label: tag.name, color: tag.color }))} renderValue={(option) => <strong>{option.label}</strong>} renderOption={(option) => <TagOption option={option} />} searchPlaceholder="Search tags..." emptyLabel="No tags available" noResultsLabel="No matching tags" clearable/>Tag, TagEditor, and MultiSelectInput separate presentation, entity management, and assignment.
Use tag editing
Use TagEditor for managed tag entities with stable ids. Use MultiSelectInput to assign existing ids. Keep TagsInput for a freeform comma-separated string list.
Import
import { MultiSelectInput, Tag, TagEditor, type TagEditorItem } from "@k2b/ui";Ownership
Tag owns compact passive and selected presentation. Set selected when the
surrounding control represents the current value; the selected treatment uses
a stronger surface, label weight, and a check, so it does not depend on color
alone. Passing either true or false reserves the same icon slot and keeps
the tag geometry stable while selection changes. Tag remains
presentation-only: the surrounding link or button owns href, activation,
aria-current, or aria-pressed.
TagEditor is controlled and backend-free. It owns create/edit/delete interaction, busy state, focus, and inline errors. The application owns persistence, authorization, uniqueness, confirmation, toasts, sorting, and reconciliation. A rejected async callback keeps the editor open. A missing callback hides that capability.
Use onDirtyChange when a surrounding settings surface guards navigation or
closing. It reports whether the currently open create or edit form differs from
its initial value and returns to false after save, cancel, or cleanup. It does
not report pending persistence or compare the controlled items collection.
MultiSelectInput accepts renderOption and renderValue for richer labels while retaining the package-owned selection and removal semantics.
Accessibility
Edit and delete buttons name the affected tag. Forms use the common field contract and announce errors. Do not rely on tag color alone; keep a readable name.
Runtime
Interactive editing and selection require hydration. The initial list and tags render on the server.
Example
<Tag color="#2563eb" icon="ti ti-point" selected size="lg">
Platform
</Tag>
<TagEditor
items={tags()}
onCreate={createTag}
onUpdate={updateTag}
onDelete={async (tag) => {
if (await confirmDelete(tag)) await deleteTag(tag);
}}
onDirtyChange={setTagEditorDirty}
/>
<MultiSelectInput
label="Assigned tags"
value={tagIds}
onValueChange={setTagIds}
options={tags().map((tag) => ({ value: tag.id, label: tag.name, color: tag.color }))}
/>