# Combobox Component Usage
## Overview
The combobox component combines a text input with a dropdown list, allowing users to filter and select from a list of options. It's ideal for large datasets where users need to search for specific items. The component uses JavaScript for filtering, keyboard navigation, and selection.
## JavaScript Requirements
### Required Scripts
```html
<!-- Include Basecoat CSS and JavaScript -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/basecoat-css@latest/dist/basecoat.cdn.min.css">
<script src="https://cdn.jsdelivr.net/npm/basecoat-css@latest/dist/js/basecoat.min.js" defer></script>
<script src="https://cdn.jsdelivr.net/npm/basecoat-css@latest/dist/js/combobox.min.js" defer></script>
```
## HTML Structure
### Basic Combobox
```html
<div class="combobox">
<button
type="button"
class="btn-outline w-full justify-between"
role="combobox"
aria-expanded="false"
aria-haspopup="listbox"
aria-controls="framework-listbox"
>
<span data-value>Select framework...</span>
<svg class="size-4 shrink-0 opacity-50" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="m7 15 5 5 5-5"></path>
<path d="m7 9 5-5 5 5"></path>
</svg>
</button>
<div role="listbox" id="framework-listbox" aria-label="Frameworks">
<header>
<input type="text" placeholder="Search framework..." aria-label="Search frameworks">
</header>
<div data-empty class="hidden">No framework found.</div>
<div role="option" data-value="react">React</div>
<div role="option" data-value="vue">Vue</div>
<div role="option" data-value="angular">Angular</div>
<div role="option" data-value="svelte">Svelte</div>
<div role="option" data-value="solid">Solid</div>
</div>
</div>
```
### Combobox with Icons
```html
<div class="combobox">
<button
type="button"
class="btn-outline w-full justify-between"
role="combobox"
aria-expanded="false"
aria-haspopup="listbox"
>
<span data-value>Select status...</span>
<svg class="size-4 shrink-0 opacity-50"><!-- chevrons-up-down --></svg>
</button>
<div role="listbox" aria-label="Status options">
<header>
<input type="text" placeholder="Search status..." aria-label="Search status">
</header>
<div data-empty class="hidden">No status found.</div>
<div role="option" data-value="backlog">
<svg class="size-4 text-muted-foreground"><!-- circle icon --></svg>
Backlog
</div>
<div role="option" data-value="todo">
<svg class="size-4 text-blue-500"><!-- circle icon --></svg>
Todo
</div>
<div role="option" data-value="in-progress">
<svg class="size-4 text-yellow-500"><!-- timer icon --></svg>
In Progress
</div>
<div role="option" data-value="done">
<svg class="size-4 text-green-500"><!-- check-circle icon --></svg>
Done
</div>
<div role="option" data-value="cancelled">
<svg class="size-4 text-red-500"><!-- x-circle icon --></svg>
Cancelled
</div>
</div>
</div>
```
## Component Elements
### Trigger Button
```html
<button
type="button"
class="btn-outline w-full justify-between"
role="combobox"
aria-expanded="false"
aria-haspopup="listbox"
aria-controls="listbox-id"
>
<span data-value>Placeholder text...</span>
<svg class="size-4"><!-- chevrons-up-down icon --></svg>
</button>
```
- `role="combobox"`: Indicates the element is a combobox
- `aria-expanded`: Tracks the open/closed state
- `aria-haspopup="listbox"`: Indicates a listbox popup
- `aria-controls`: References the listbox ID
- `data-value`: Displays the selected value
### Listbox Container
```html
<div role="listbox" id="listbox-id" aria-label="Options">
<header>
<input type="text" placeholder="Search..." aria-label="Search options">
</header>
<div data-empty class="hidden">No results found.</div>
<!-- Options -->
</div>
```
### Option Items
```html
<div role="option" data-value="value" aria-selected="false">
Option Label
</div>
<!-- Selected option -->
<div role="option" data-value="selected" aria-selected="true">
<svg class="size-4"><!-- check icon --></svg>
Selected Option
</div>
<!-- Disabled option -->
<div role="option" data-value="disabled" aria-disabled="true">
Disabled Option
</div>
```
## Implementation Examples
### Country Selector
```html
<div class="combobox">
<button
type="button"
class="btn-outline w-[280px] justify-between"
role="combobox"
aria-expanded="false"
aria-haspopup="listbox"
>
<span data-value>Select country...</span>
<svg class="size-4 shrink-0 opacity-50"><!-- chevrons-up-down --></svg>
</button>
<div role="listbox" aria-label="Countries" class="max-h-[300px] overflow-auto">
<header>
<input type="text" placeholder="Search country..." aria-label="Search countries">
</header>
<div data-empty class="hidden">No country found.</div>
<div role="group" aria-label="Popular">
<span class="px-2 py-1.5 text-xs font-medium text-muted-foreground">Popular</span>
<div role="option" data-value="us">🇺🇸 United States</div>
<div role="option" data-value="uk">🇬🇧 United Kingdom</div>
<div role="option" data-value="ca">🇨🇦 Canada</div>
</div>
<hr role="separator">
<div role="group" aria-label="All countries">
<span class="px-2 py-1.5 text-xs font-medium text-muted-foreground">All Countries</span>
<div role="option" data-value="af">🇦🇫 Afghanistan</div>
<div role="option" data-value="al">🇦🇱 Albania</div>
<div role="option" data-value="dz">🇩🇿 Algeria</div>
<!-- More countries... -->
</div>
</div>
</div>
```
### Assignee Selector
```html
<div class="combobox">
<button
type="button"
class="btn-outline w-[250px] justify-between"
role="combobox"
aria-expanded="false"
aria-haspopup="listbox"
>
<span data-value class="flex items-center gap-2">
<svg class="size-4 text-muted-foreground"><!-- user icon --></svg>
Assign to...
</span>
<svg class="size-4 shrink-0 opacity-50"><!-- chevrons-up-down --></svg>
</button>
<div role="listbox" aria-label="Team members">
<header>
<input type="text" placeholder="Search team member..." aria-label="Search team members">
</header>
<div data-empty class="hidden">No team member found.</div>
<div role="option" data-value="john">
<img src="/avatars/john.png" alt="" class="size-6 rounded-full">
<div class="flex-1">
<p class="text-sm font-medium">John Doe</p>
<p class="text-xs text-muted-foreground">john@example.com</p>
</div>
</div>
<div role="option" data-value="jane">
<img src="/avatars/jane.png" alt="" class="size-6 rounded-full">
<div class="flex-1">
<p class="text-sm font-medium">Jane Smith</p>
<p class="text-xs text-muted-foreground">jane@example.com</p>
</div>
</div>
<div role="option" data-value="bob">
<img src="/avatars/bob.png" alt="" class="size-6 rounded-full">
<div class="flex-1">
<p class="text-sm font-medium">Bob Johnson</p>
<p class="text-xs text-muted-foreground">bob@example.com</p>
</div>
</div>
</div>
</div>
```
### Language Selector
```html
<div class="combobox">
<button
type="button"
class="btn-outline justify-between"
role="combobox"
aria-expanded="false"
aria-haspopup="listbox"
>
<span data-value>English</span>
<svg class="size-4 shrink-0 opacity-50"><!-- chevrons-up-down --></svg>
</button>
<div role="listbox" aria-label="Languages">
<header>
<input type="text" placeholder="Search language..." aria-label="Search languages">
</header>
<div data-empty class="hidden">No language found.</div>
<div role="option" data-value="en" aria-selected="true">
<svg class="size-4"><!-- check icon --></svg>
English
</div>
<div role="option" data-value="es">
<svg class="size-4 invisible"><!-- placeholder --></svg>
Spanish
</div>
<div role="option" data-value="fr">
<svg class="size-4 invisible"><!-- placeholder --></svg>
French
</div>
<div role="option" data-value="de">
<svg class="size-4 invisible"><!-- placeholder --></svg>
German
</div>
<div role="option" data-value="ja">
<svg class="size-4 invisible"><!-- placeholder --></svg>
Japanese
</div>
<div role="option" data-value="zh">
<svg class="size-4 invisible"><!-- placeholder --></svg>
Chinese
</div>
</div>
</div>
```
### Timezone Selector with Filtering
```html
<div class="combobox">
<button
type="button"
class="btn-outline w-[300px] justify-between"
role="combobox"
aria-expanded="false"
aria-haspopup="listbox"
>
<span data-value>Select timezone...</span>
<svg class="size-4 shrink-0 opacity-50"><!-- chevrons-up-down --></svg>
</button>
<div role="listbox" aria-label="Timezones" class="max-h-[300px] overflow-auto">
<header>
<input type="text" placeholder="Search timezone..." aria-label="Search timezones">
</header>
<div data-empty class="hidden">No timezone found.</div>
<div role="option" data-value="utc" data-keywords="UTC Coordinated Universal Time">
(UTC+00:00) Coordinated Universal Time
</div>
<div role="option" data-value="est" data-keywords="EST Eastern Standard New York">
(UTC-05:00) Eastern Time (US & Canada)
</div>
<div role="option" data-value="pst" data-keywords="PST Pacific Standard Los Angeles">
(UTC-08:00) Pacific Time (US & Canada)
</div>
<div role="option" data-value="gmt" data-keywords="GMT Greenwich Mean London">
(UTC+00:00) Greenwich Mean Time
</div>
<div role="option" data-value="cet" data-keywords="CET Central European Paris Berlin">
(UTC+01:00) Central European Time
</div>
<div role="option" data-value="jst" data-keywords="JST Japan Standard Tokyo">
(UTC+09:00) Japan Standard Time
</div>
</div>
</div>
```
## JavaScript Events
### Selection Event
```javascript
const combobox = document.querySelector('.combobox');
combobox.addEventListener('change', function(e) {
const selectedValue = e.detail.value;
const selectedLabel = e.detail.label;
console.log('Selected:', selectedValue, selectedLabel);
});
```
### Filtering
The component automatically filters options based on:
- Option text content
- `data-value` attribute
- `data-keywords` attribute (for additional search terms)
```html
<!-- Option searchable by "USA", "United States", or "America" -->
<div role="option" data-value="us" data-keywords="USA America">
United States
</div>
```
### Programmatic Control
```javascript
// Get selected value
function getSelectedValue(comboboxElement) {
const selected = comboboxElement.querySelector('[role="option"][aria-selected="true"]');
return selected ? selected.getAttribute('data-value') : null;
}
// Set selected value programmatically
function setSelectedValue(comboboxElement, value) {
const options = comboboxElement.querySelectorAll('[role="option"]');
options.forEach(option => {
const isSelected = option.getAttribute('data-value') === value;
option.setAttribute('aria-selected', isSelected);
});
const selectedOption = comboboxElement.querySelector(`[data-value="${value}"]`);
if (selectedOption) {
const trigger = comboboxElement.querySelector('[role="combobox"] [data-value]');
trigger.textContent = selectedOption.textContent.trim();
}
}
```
## Accessibility Guidelines
### Keyboard Navigation
- **Arrow Down**: Open menu, move to next option
- **Arrow Up**: Move to previous option
- **Enter**: Select focused option
- **Escape**: Close menu
- **Home**: Move to first option
- **End**: Move to last option
- **Type characters**: Filter options
### Screen Reader Support
```html
<div class="combobox">
<label id="framework-label" class="label mb-2">Framework</label>
<button
type="button"
role="combobox"
aria-labelledby="framework-label"
aria-expanded="false"
aria-haspopup="listbox"
aria-controls="framework-list"
>
<span data-value>Select...</span>
</button>
<div role="listbox" id="framework-list" aria-labelledby="framework-label">
<!-- options -->
</div>
</div>
```
## Best Practices
### When to Use Combobox
- Large lists that need filtering
- User/assignee selection
- Location/timezone selection
- Tag/category selection
- Any searchable dropdown
### When to Use Alternatives
- Small lists (5-7 items): Use Select
- Multiple selection: Use Combobox with multi-select or Checkbox group
- Free-form input: Use Input with autocomplete
### Performance
- For very large lists (100+ items), consider virtual scrolling
- Debounce filter input for API-backed searches
- Lazy load options when possible
### UX Guidelines
- Always show placeholder text
- Provide clear empty state messages
- Show selected option clearly
- Support keyboard navigation
- Consider mobile touch targets
## Form Integration
### Within a Form
```html
<form class="form space-y-4">
<div class="grid gap-2">
<label for="framework-combo" class="label">Framework</label>
<div class="combobox">
<button
type="button"
id="framework-combo"
class="btn-outline w-full justify-between"
role="combobox"
aria-expanded="false"
>
<span data-value>Select framework...</span>
<svg class="size-4 opacity-50"><!-- icon --></svg>
</button>
<input type="hidden" name="framework" value="">
<div role="listbox" aria-label="Frameworks">
<!-- options -->
</div>
</div>
</div>
<button type="submit" class="btn">Submit</button>
</form>
```
### Hidden Input for Form Submission
```javascript
combobox.addEventListener('change', function(e) {
const hiddenInput = this.querySelector('input[type="hidden"]');
if (hiddenInput) {
hiddenInput.value = e.detail.value;
}
});
```
## Jinja/Nunjucks Macro
### Using the Macro
```jinja
{% from "combobox.njk" import combobox %}
{% set options = [
{ value: "react", label: "React" },
{ value: "vue", label: "Vue" },
{ value: "angular", label: "Angular" },
{ value: "svelte", label: "Svelte" }
] %}
{{ combobox(
id="framework-select",
placeholder="Select framework...",
search_placeholder="Search framework...",
empty_text="No framework found.",
options=options
) }}
```