Combobox
A searchable or multi-value combobox is not shipped by the framework — the native
<select>and<datalist>stay the baseline. Theme an external combobox with Actual tokens instead.
The .select class keeps the native baseline with a CSS chevron, and the optional
custom-select module adds the appearance: base-select native picker where the
browser supports it. Beyond those, apply the framework's judgment from the
data grid template to the problem: when a
real product needs a JavaScript combobox (searchable select, removable tags,
autocomplete suggestions), load a dedicated component and bridge its custom
properties onto Actual's tokens.
Related terms: combo box, combobox, autocomplete, datalist, searchable select, enhanced select, multiple select, tags, chips, tag input, pill input.
Theme bridge
The combobox template themes
@lekoala/combobox entirely with Actual
tokens: the control mirrors .input, chips re-derive the .badge soft recipe,
and the picker popover follows --surface-raised and --shadow-popout. No
adapter, no compatibility layer — a handful of --cb-* assignments and shared
mix tokens.
<label class="field actual-combobox">
<span class="field-label">Skills</span>
<combo-box placeholder="Search or add a skill…">
<select class="select" name="skills[]" multiple>
<option value="design" selected>Design</option>
<option value="css" selected>CSS</option>
<option value="javascript" selected>JavaScript</option>
</select>
</combo-box>
</label>
The card in the template documents the full token map of this recipe.
Lightweight pass-through
Most of the skin is three one-line token bridges. The visible vocabulary —
surface, border, radius, focus ring, danger, hover overlay and the --soft-*
mix — stays the Actual one, so light/dark mode, the preset themes and their
contrast contracts apply to the combobox for free:
.actual-combobox {
--cb-bg: var(--surface);
--cb-color: var(--text);
--cb-border-color: var(--border);
--cb-border-radius: var(--radius);
--cb-focus-color: var(--focus);
--cb-muted-color: var(--text-muted);
--cb-error-color: var(--danger);
--cb-chip-bg: transparent;
--cb-control-height: var(--control-size);
--cb-option-min-height: var(--control-size);
}
.actual-combobox .cb-control:focus-within,
.actual-combobox .cb-text-control:focus {
outline: 2px solid transparent;
box-shadow: var(--focus-ring-shadow);
}
.actual-combobox .cb-chip-remove:focus-visible {
outline: 2px solid transparent;
box-shadow: 0 0 0 2px var(--focus-ring);
}
.actual-combobox .cb-chip {
border: var(--border-width) solid
color-mix(in oklab, var(--surface) var(--soft-border-mix), var(--primary));
border-radius: var(--radius-full);
background: color-mix(in oklab, var(--surface) var(--soft-bg-mix), var(--primary));
color: color-mix(in oklab, var(--primary) var(--soft-fg-mix), var(--text));
font-size: var(--font-size-xs);
font-weight: var(--font-weight-semibold);
}
The composite focus ring follows any internal focus, not just the search
input: focus a chip or its remove × and the control shows the same field ring,
with the local :focus-visible indicator on the chip or its × on top.
The same approach works for any widget that exposes a custom-property skin — keep the widget's behavior, restyle its tokens through the shared vocabulary.