Pillbox
The Pillbox component transforms inputs into multi-select tag interfaces. Perfect for tagging, categorizing, skill selection, email recipients, and any scenario where users need to select or create multiple items. Each selection appears as a removable "pill" with full keyboard support and validation options.
Key Features
- Multi-Select Tags - Visual pill-based selection with easy removal
- Searchable Dropdown - Filter available options as you type
- Creatable Mode - Allow users to create custom tags
- Validation - Max items, duplicates prevention, custom validators
- Keyboard Navigation - Backspace to remove, arrow keys, Enter to add
- Value Management - Separate value/label support for complex data
- Customizable Rendering - Custom pill and option templates
When to Use
- Tagging and categorization systems
- Skill or interest selection
- Email recipient fields (To, CC, BCC)
- Multi-select filters and facets
- Product attribute selection
Basic Usage
// Select from predefined options
const pillbox = Domma.elements.pillbox('#tags', {
data: ['React', 'Vue', 'Angular', 'Svelte'],
value: ['React'],
maxItems: 5,
onChange: (values) => console.log('Selected:', values)
});
// Allow creating custom tags
Domma.elements.pillbox('#custom-tags', {
data: ['JavaScript', 'Python', 'Java'],
creatable: true,
duplicates: false
});
Quick Start
Simple Tag Selector
Select your favourite programming languages:
<input type="text" id="tech-stack">
<script>
const languages = [
{ value: 'js', label: 'JavaScript' },
{ value: 'ts', label: 'TypeScript' },
{ value: 'py', label: 'Python' },
{ value: 'java', label: 'Java' },
{ value: 'go', label: 'Go' },
{ value: 'rust', label: 'Rust' }
];
Domma.elements.pillbox('#tech-stack', {
data: languages,
value: ['js'],
maxItems: 3,
onChange: (values) => {
console.log('Selected languages:', values);
}
});
</script>
Tutorial
Let's build an email recipient field with validation and custom rendering.
0 recipient(s) selected
Key Points: The pillbox validates emails, prevents duplicates, enforces a max limit, and allows creating custom email addresses not in the contacts list.
Examples
Example 1: Creatable Tags (Free-Form)
Create custom tags that aren't in the predefined list:
Type any skill and press Enter to add it
Domma.elements.pillbox('#skills-input', {
data: ['JavaScript', 'React', 'Node.js', 'MongoDB'],
creatable: true,
searchable: true,
duplicates: false,
onCreate: (skill) => {
console.log('Created new skill:', skill);
}
});
Example 2: Size Variants
Control the size of pills and input:
// Small
Domma.elements.pillbox('#small', {
data: items,
size: 'small'
});
// Large
Domma.elements.pillbox('#large', {
data: items,
size: 'large'
});
Example 3: Max Items Limit
Limit the number of selections (try adding more than 3):
Domma.elements.pillbox('#limited-input', {
data: ['Option 1', 'Option 2', 'Option 3', 'Option 4', 'Option 5'],
maxItems: 3,
maxItemsMessage: 'You can only select {max} items',
onMaxReached: () => {
alert('Maximum items reached!');
}
});
Example 4: Custom Pill Rendering
Customize how pills appear with icons and colors:
const team = [
{ value: '1', label: 'Alice', role: 'Admin' },
{ value: '2', label: 'Bob', role: 'Developer' },
{ value: '3', label: 'Carol', role: 'Designer' }
];
Domma.elements.pillbox('#team-select', {
data: team,
renderPill: (item) => {
const icon = item.role === 'Admin' ? '👑' :
item.role === 'Developer' ? '💻' : '🎨';
return `${icon} ${item.label} <span style="opacity: 0.6;">(${item.role})</span>`;
},
renderOption: (item) => {
const icon = item.role === 'Admin' ? '👑' :
item.role === 'Developer' ? '💻' : '🎨';
return `
<div style="display: flex; align-items: center; gap: 0.5rem;">
<span>${icon}</span>
<div>
<div><strong>${item.label}</strong></div>
<div style="font-size: 0.75rem; opacity: 0.7;">${item.role}</div>
</div>
</div>
`;
}
});
Example 5: Programmatic Control
Control pills via JavaScript API:
const pillbox = Domma.elements.pillbox('#controlled-pillbox', {
data: ['Alpha', 'Beta', 'Gamma', 'Delta', 'Epsilon']
});
// Add pill programmatically
pillbox.addPill('Alpha', 'Alpha');
// Remove pill by value
pillbox.removePill('Alpha');
// Remove last pill
pillbox.removePillAt(pillbox.getCount() - 1);
// Clear all
pillbox.clear();
// Get current values
const values = pillbox.getValue();
console.log(values); // ['Beta', 'Gamma']
// Set values programmatically
pillbox.setValue(['Delta', 'Epsilon']);
Best Practices
Accessibility
- Labels: Always provide visible labels for pillbox inputs
- ARIA: Component includes proper ARIA attributes automatically
- Keyboard: Ensure Backspace, Arrow keys, Enter, and Escape work
- Focus Management: Pills should be removable via keyboard
UX Guidelines
- Max Items: Set reasonable limits (3-10) to prevent overwhelming UI
- Clear Feedback: Show validation errors clearly (onValidationError)
- Duplicate Prevention: Enable
duplicates: falsein most cases - Searchable: Keep searchable enabled for lists > 10 items
- Placeholders: Use descriptive placeholders ("Add skills...", "Type email...")
Data Management
- Value/Label: Use objects with value/label for complex data
- Validation: Implement
validatePillfor format checks (emails, URLs) - Initial Values: Set
valueoption for pre-selected items - onChange: Track changes to sync with your app state/backend
Common Pitfalls
- Avoid: No max limit on creatable fields - users may add too many
- Avoid: Allowing duplicates without clear reason - creates confusion
- Avoid: Not validating created pills - can lead to invalid data
- Avoid: Hiding the clear button on fields that accumulate many items
API Reference
Options
| Option | Type | Default | Description |
|---|---|---|---|
data |
Array | [] |
Available options (strings or {value, label} objects) |
value |
Array | [] |
Initial selected values |
placeholder |
String | 'Add items...' |
Input placeholder text |
searchable |
Boolean | true |
Enable dropdown search filtering |
creatable |
Boolean | false |
Allow creating custom tags |
maxItems |
Number | null |
Maximum number of pills allowed |
duplicates |
Boolean | false |
Allow duplicate values |
clearable |
Boolean | true |
Show clear-all button |
size |
String | 'medium' |
Size variant: 'small', 'medium', 'large' |
renderPill |
Function | null |
Custom pill renderer: (item) => HTML |
renderOption |
Function | null |
Custom option renderer: (item) => HTML |
pillTemplate |
String | '{label}' |
Default pill HTML template |
validatePill |
Function | null |
Validator: (value) => true | string (error message) |
maxItemsMessage |
String | 'Maximum {max} items allowed' |
Message when max items reached |
duplicateMessage |
String | 'Item already exists' |
Message when duplicate detected |
noResultsMessage |
String | 'No results found' |
Empty dropdown message |
onAdd |
Function | null |
Callback when pill added: (value, element) => {} |
onRemove |
Function | null |
Callback when pill removed: (value, element) => {} |
onChange |
Function | null |
Callback on value change: (values) => {} |
onCreate |
Function | null |
Callback when custom pill created: (value) => {} |
onMaxReached |
Function | null |
Callback when max items reached: () => {} |
onValidationError |
Function | null |
Callback on validation error: (error, value) => {} |
Methods
| Method | Returns | Description |
|---|---|---|
getValue() |
Array | Get array of current values |
setValue(values) |
this | Set values (clears existing pills first) |
addPill(value, label) |
this | Add a single pill |
removePill(value) |
this | Remove pill by value |
removePillAt(index) |
this | Remove pill at index |
clear() |
this | Remove all pills |
getCount() |
Number | Get number of pills |
setData(data) |
this | Update available options |
open() |
this | Open dropdown |
close() |
this | Close dropdown |
isOpen() |
Boolean | Check if dropdown is open |
focus() |
this | Focus the input |
enable() |
this | Enable the component |
disable() |
this | Disable the component |
destroy() |
void | Remove component and clean up |
CSS Classes
| Class | Description |
|---|---|
.dm-pillbox |
Wrapper container |
.dm-pillbox-container |
Pills + input container |
.dm-pillbox-input |
The text input |
.dm-pill |
Individual pill element |
.dm-pill-remove |
Remove button on pill |
.dm-pillbox-clear |
Clear all button |
.dm-pillbox-dropdown |
Dropdown container |
.dm-pillbox-options |
Options list (ul) |
.dm-pillbox-option |
Individual option (li) |
.dm-pillbox-option.active |
Currently focused option |
.dm-pillbox-empty |
Empty state message |
.dm-pillbox-sm |
Small size variant |
.dm-pillbox-lg |
Large size variant |
.dm-pillbox.disabled |
Disabled state |
Config Engine
Use Domma's declarative configuration system to initialize pillbox components from JSON config.
Basic Configuration
$.setup({
'#skills-input': {
component: 'pillbox',
options: {
data: ['JavaScript', 'React', 'Node.js', 'MongoDB'],
maxItems: 5,
duplicates: false
}
}
});
With Creatable Mode
$.setup({
'#tags-input': {
component: 'pillbox',
options: {
data: ['Frontend', 'Backend', 'DevOps'],
creatable: true,
searchable: true,
onCreate: (tag) => {
console.log('Created new tag:', tag);
}
}
}
});
Multiple Instances
$.setup({
'#skills': {
component: 'pillbox',
options: {
data: ['Python', 'Java', 'C++'],
size: 'small',
maxItems: 3
}
},
'#interests': {
component: 'pillbox',
options: {
data: ['Music', 'Sports', 'Reading'],
size: 'large',
creatable: true
}
}
});
Model Integration
Pillbox integrates seamlessly with Domma's reactive Model system (M) for two-way data binding. When bound to a model, the pillbox automatically syncs with model changes and updates the model when user input changes.
Basic Model Binding
// Create a model with an array field
const tagsModel = M.create({
selectedTags: { type: 'array', default: [] }
});
// Bind pillbox to model
Domma.elements.pillbox('#tags', {
data: ['React', 'Vue', 'Angular', 'Svelte'],
model: tagsModel,
modelKey: 'selectedTags'
});
// Display model value elsewhere
M.bind(tagsModel, 'selectedTags', '#tag-display', {
format: (tags) => tags.length > 0 ? tags.join(', ') : 'None selected'
});
Two-Way Synchronisation
Changes to the model automatically update the pillbox, and vice versa:
// Model changes update the pillbox
tagsModel.set('selectedTags', ['React', 'Vue']); // Pills appear
// Adding/removing pills updates the model
// User adds pill → model.set() called → other bindings update
Config Engine Integration
const userModel = M.create({
skills: { type: 'array', default: [] }
});
$.setup({
'#skills': {
component: 'pillbox',
options: {
data: ['JavaScript', 'Python', 'Java'],
model: userModel,
modelKey: 'skills'
}
}
});
Multiple Bindings
Multiple elements can bind to the same model field for synchronised updates:
const formModel = M.create({
tags: { type: 'array', default: [] }
});
// Pillbox component
Domma.elements.pillbox('#tags-input', {
data: tagOptions,
model: formModel,
modelKey: 'tags'
});
// Display tag count
M.bind(formModel, 'tags', '#tag-count', {
format: (tags) => `${tags.length} tag(s) selected`
});
// Display as JSON
M.bind(formModel, 'tags', '#tags-json', {
format: (tags) => JSON.stringify(tags, null, 2)
});
Related Elements
Explore related Domma components: