Appearance
| --mono-select-option-spacing | 2px | The seam between two rows, so a hovered row under the selected one stays a separate wash |
Select โ
A dropdown picker built on a custom button + listbox UI. Sizes, color and visual variants, validation states, slot-overridable label/helper/prefix/suffix, and a clearable trigger. Toggle Vue / CSS to switch the live demo and source together.
The look is Basecoat's .select / .combobox (see Theme): the trigger is the same box as input (1px --input border, xs shadow, 3px --ring focus ring), the panel is a --popover surface with a 1px --foreground/10 ring and an md shadow, options are text-sm rows that highlight on --muted and mark the selected one with a check at the end. Dark mode is built in.
Basic โ
Default select with label, placeholder and helper text.
Vue SFC
Opening and closing โ
What a click on the field does depends on whether it is searchable:
- Plain select โ the field is a button: a click opens the list, another click closes it (so do
โ/Enter/Spaceon the focused field). - Searchable select โ clicking into the input opens the list and focuses it, as you would expect of a box you type in, and typing opens it with results. A click in the input while the list is open never closes it (that is caret placement or text selection) โ close with the caret (
โ),Escor a click outside.
The caret opens and closes in both modes. Tabbing into either opens nothing.
Keyboard โ
Opening highlights the first option, so โ / โ start from a known place and Enter picks without needing an arrow press first. This works whether or not the select is searchable โ the trigger drives the list when there's no search field to hold focus.
| Keys | Does |
|---|---|
Enter / Space / โ (closed) | open the panel |
โ / โ | move the highlight, scrolling it into view |
Enter | select the highlighted option |
Esc | close |
The highlight is drawn as a leading bar on the option's left edge, distinct from the background that marks the selected option โ an option is often both at once. Tune it with --mono-select-cursor-width (default 3px) and --mono-select-cursor-color.
Sizes โ
xs โฆ xxl on the shared control ladder (--mono-control-height-*); the trigger text follows the button's steps (xs text-xs, sm/md/lg text-sm, xl text-base, xxl text-lg). Options stay text-sm at every size, like Basecoat's.
Vue SFC
Colors โ
Pick a colour and every variant takes it. color sets the focus colour โ the ring and the focused border. A resting trigger is always on --input, whatever its colour; primary focuses with --ring, secondary with --muted-foreground, the rest with their role token.
outlined is Basecoat's .select > button. filled is luma's (a transparent border on an --input/50 surface, no shadow) and underlined is sera's (square, only the bottom edge painted, no ring โ focus and validation recolour the line). Under the sera flavor the default variant is the underline.
Vue SFC
States โ
Disabled, readonly and required.
Vue SFC
Validation โ
valid, invalid and warning states with messages. invalid is Basecoat's aria-invalid treatment โ a permanent --destructive border and /20 ring, the label turns --destructive, the message carries role="alert"; the other two are the same pattern in --success / --warning.
Vue SFC
Clearable โ
Clear button shown when a value is selected.
One glyph. With something to clear, the clear button (โ) stands in for the caret, open or closed; otherwise โ nothing selected, or clearable off โ the caret (โ) is alone. The value wins over the open state: a searchable field with a value has no caret to close it with (see Opening and closing โ its body only opens), so it closes by picking, Escape or a click outside. A disabled or readonly field shows neither: it refuses every gesture, so a caret would promise an open it never delivers. The gutter the glyph sat in stays, so the text does not shift when the state toggles. The same rule <mono-tag-input> and <mono-dropdown-table> follow, so the three fields line up in a form.
Vue SFC
Slots โ
Custom label, helper and prefix/suffix content via slots.
Vue SFC
Event: change โ
Listen to change to react to selection.
Vue SFC
Event log โ
Live log of change and clear events.
Vue SFC
Customized โ
Override per-element styling with cssClass (Vue) or utility classes (CSS).
Vue SFC
Custom keys โ
Use key-value and display-value to feed natural-shape items (for example { id, name }) without pre-mapping. key-value names the property stored in modelValue; omit it to store the whole item object. display-value is either a property name (string) or a (item) => string function โ when unset it falls back to item.label.
Without key-value the whole item is the value, and it is matched by identity โ the object in modelValue has to be one of the objects in items. A reactive wrapper around it is fine (the select compares raw targets), but an item rebuilt between renders โ a refetch, a structuredClone, a JSON round trip โ is a different object and stops resolving. Pass key-value whenever items are re-created.
Vue SFC
DataSource (devextreme) โ
Bind a live devextreme DataSource with :data-source.prop. The select reads its items and stays reactive to the source's events โ so an external ds.filter(...) + ds.load() updates the list with no re-binding. With load-more="scroll" it pages the source one chunk at a time (here pageSize: 5). This example builds the source with mono-utils' monoCreateFetcher against a public OData endpoint (baseUrl, no configBaseUrl). Bind with .prop and use key-value / display-value to map the server fields.
Search โ
Add searchable and the trigger becomes a text box you type into โ with a bound DataSource it queries the server (debounced, search-value names the fields), otherwise it filters the items array client-side. search-operation picks the comparison (default contains), search-debounce the delay (default 300 ms) and search-placeholder the hint shown while open.
Search expressions โ
search-value is not limited to top-level field names. It takes the same grammar the data grid uses, as an array or as a comma-separated string โ the two are equivalent, so plain HTML loses nothing:
html
search-value="Company.Name,Transaction.[*].Price,*.[*].*"ts
:search-value.prop="['Company.Name', 'Transaction.[1].Name', 'Transaction.[*].Price', '*']"An entry can be:
| Entry | Matches |
|---|---|
LastName | a top-level field |
Company.Name | a nested field |
Transaction.[1].Name | one array element |
Transaction.[*].Name | any element of the array |
* | every top-level field |
Company.* | every field of Company |
*.* | every field of every nested object |
*.[*].* | every field of every array element |
Patterns read literally, segment by segment โ '*' covers the top level only, so pair it with '*.*' / '*.[*].*' to reach deeper. A field named explicitly always wins over a pattern, in either order, and is searched exactly once.
In the array form an entry may also be a { field, custom } clause builder, for a column contains can't search โ a boolean, or a code the user never types. Return null to opt the column out of a given term:
ts
:search-value.prop="[
'*',
{ field: 'Active', custom: ({ field, value }) => `${field} eq ${value === 'yes'}` },
]"Setting search-value explicitly means it alone decides what is searched.
Leave it off and the default is '*' โ every top-level field of an option โ so searching works without naming anything. display-value (when it's a string) and key-value stay in the list beside it, and the rendered display text is matched too, which is the only way to search a display-value function.
Two caveats on a bound DataSource: a * pattern is resolved against the rows already loaded, so it can only see fields present in that sample; and it only emits clauses for string columns, because contains(Price,'x') is not valid OData. That is exactly why display-value / key-value remain alongside '*' โ they are explicit entries, so they still work before the first page arrives and a numeric key-value stays searchable. Give any other non-text column an explicit entry or a custom builder.
To offer no search at all, drop searchable rather than emptying search-value.
Vue SFC
Grouped options โ
display-group is an array with one accessor per level (string field or function), used as the group key and header. Add the boolean group to bucket a paginated source client-side, or feed pre-grouped { key, items } data directly; group-sticky pins headers.
Two levels over a 1,200-row static array, load-more="scroll" revealing a chunk at a time.
Pre-grouped data with custom group-key / group-items field names.
Customized list rendering โ
display-value is a string and Lit escapes it, so an option can never carry markup โ an icon, a badge, a second line with its own styling. slot="list" hands the option rows to you instead: mono keeps the panel, the search box, the filtering, the grouping, the keyboard and the value, and you decide what each row looks like.
The slot is for markup, and only markup. Nothing behavioural moves to you: mono paints the selected and keyboard-cursor states on the lines you render, and owns every click.
Two things to know before you reach for it. It requires controlMonoForm โ what you loop is the control's resolved list (post-search, post-DataSource, groups flattened), which exists only inside the element and is reported back up onto form.items()[key].list. And mono places your wrapper as one block without ever reaching inside it, so render exactly one element per entry: that is how a click is paired back to its row.
A line you render IS an option: mono's own option styling applies to it, so it matches the panel around it โ font, padding, height, divider, hover, the selected tint, the keyboard cursor, and the indent under a group header โ without a single rule of your own.
State arrives as attributes on those lines โ data-mono-type, -level, -selected and -active โ so you restyle with CSS instead of binding classes, and your own rule always outranks mono's.
Identical to mono-tag-input's slot of the same name, with one difference that follows from the control: picking here sets the value and closes, rather than toggling one of many.
A flat list โ one element per option, with a badge and a (?) tooltip that display-value could never have carried.
Grouped, two levels deep. Grouping stays mono's โ it decides which groups exist and drops the ones the search emptied โ and hands you the headers in the same flat sequence as the rows, each with its level and its leaves.
Width & height โ
Set width, height, min-width, max-width, min-height or max-height โ each takes a CSS string ("260px", "80%") or a number (px). This sizes the field (distinct from dropdownHeight/dropdownMaxHeight, which size the popup).
Vue SFC
Panel size โ
The field and the dropdown panel are sized separately. By default the panel matches the field โ right for a picker whose options read as continuations of the field, wrong for a wide one.
:dropdown.prop="{ width, height, minWidth, maxWidth, minHeight, maxHeight }" sizes the panel alone. Each key takes a CSS string or a number (px). Setting width also stops the panel tracking the field.
vue
<mono-select label="Account" width="100%" :items.prop="items"
:dropdown.prop="{ maxWidth: '32rem', maxHeight: 280 }" />Prefer maxWidth to width on a full-width field: the panel then caps on a wide screen but still shrinks with the field on a narrow one.
height is an exact height and maxHeight the cap โ the same split the dropdown-height / dropdown-max-height attributes carry, and those stay for plain HTML, where there is no .prop binding to pass an object with. The widths land on the panel and the heights on its scrolling body, so a fixed height still scrolls rather than clipping.
The same object, with the same meanings, is accepted by <mono-tag-input> and <mono-dropdown-table>.
Vue SFC
Many selects on one page โ
A grid with an inline select per row mounts one element per row, so the per-instance cost is multiplied by the row count. Two things make that cheap, and both are automatic:
- The option list is only rendered while the dropdown is open. A closed select has its panel in the DOM but no
<button role="option">children. Previously all of them were rendered up front and merely hidden with CSS, which putrows x optionselements in the document โ 6,000 for a 200-row grid with 30 options each. - The outside-click / focus / Escape listeners are bound while open, not from the moment the element connects. 200 closed selects add no document listeners.
Two things you still control:
- Bind
itemsto a stable reference (a module constant, aref, or acomputed).items,display-group,search-valueandcss-classall content-compare, so rebuilding an equal one is free โ but a genuinely new array re-renders, as it should. - Prefer a function that is not rebuilt per render for
display-value. A fresh arrow (:display-value.prop="(i) => i.name") is a new value every parent render, and a function cannot be content-compared, so each render reaches the element. Hoist it to module scope or acomputedwhen a grid has one select per row.
If rows are only editable one at a time, v-if on the editing cell mounts one select instead of one per row โ cheaper still than any of the above.
Placement โ
The dropdown keeps itself inside the viewport, so a select near the bottom of the page is never clipped:
flip(defaulttrue) opens the list upward when there isn't room below.shift(defaulttrue) slides it horizontally so it doesn't overflow a screen edge.- When neither side fits the full list, it opens on the roomier side and shrinks to fit, scrolling internally. This caps
dropdownHeight/dropdownMaxHeight/dropdown.height/dropdown.maxHeightโ those stay the requested size whenever there is room for them.
Set :flip="false" / :shift="false" to pin the list below the field.
CSS Variables โ
Vue SFC
Themed through --mono-select-* custom properties (they inherit and pierce the shadow boundary โ and reach the portaled panel); an explicit --mono-select-ring-color override wins over the color prop. Re-skin globally via the tokens (--input, --ring, --popover, --muted, --radius โฆ) โ or switch flavor. Scroll areas use the shared --mono-scrollbar-* tokens.
| Variable | Default | Controls |
|---|---|---|
--mono-select-ring-color (alias --mono-select-focus-color) | --ring | Focus colour: the ring and the focused border (set by color) |
--mono-select-ring-width / --mono-select-ring-alpha | --mono-ring-width (3px) / --mono-ring-alpha (50%) | Ring geometry |
--mono-select-border-color (aliases --mono-select-border, --mono-select-rest-border) | --input | Resting trigger border |
--mono-select-side-border-color | = border | The top / left / right edges only (sera and underlined set transparent) |
--mono-select-bg (alias --mono-select-surface) / --mono-select-hover-bg | --mono-mode-surface / dark --input/50 | Trigger background, and its hover (dark only in vega) |
--mono-select-shadow | --mono-shadow-xs (outlined) | Resting trigger shadow |
--mono-select-radius / --mono-select-radius-<size> | --mono-radius-md | Trigger corner, for every size / one step |
--mono-select-height-<size> | --mono-control-height-<size> | Trigger height per step |
--mono-select-padding-x-<size> / --mono-select-padding-end-<size> (--mono-select-padding-x for all) | 2.5 / 2 ร --mono-spacing at md | Trigger inset, start / end |
--mono-select-padding-y | 0 | Trigger block inset (the height token owns the height) |
--mono-select-font-<size> / --mono-select-line-height (-<size>) | --mono-text-sm / its line height | Trigger text |
--mono-select-trigger-gap / --mono-select-icon-size | 1.5 ร spacing / 4 ร spacing | Gap between value and actions; chevron / clear glyph size |
--mono-select-gap | 3 ร --mono-spacing | Label โ trigger โ message spacing |
--mono-select-label-font-size / -font-weight / -line-height / -text-transform / -letter-spacing / -gap | --mono-text-sm / --mono-label-font-weight / 1 / none / normal / 2 ร spacing | Label |
--mono-select-message-font-size / -line-height | --mono-text-sm / --mono-leading-normal | Message |
--mono-select-clear-radius | calc(var(--radius) - 5px) | Chevron / clear button corner |
--mono-select-disabled-bg / --mono-select-readonly-bg | trigger bg / --muted | State surfaces |
--mono-select-filled-bg | --input/50 | filled surface |
--mono-select-outline-{bg,hover-bg,border-color,side-border-color,shadow,ring-width,radius,padding-x} | unset | The same knobs scoped to the outlined variant โ what the flavors set |
--mono-select-dropdown-bg / -color / -radius / -shadow / -ring / -padding | --popover / --popover-foreground / --mono-radius-md / --mono-shadow-md / 1px --foreground/10 / --mono-spacing | The panel |
--mono-select-option-{radius,padding-x,padding-end,padding-y,gap,font-size,line-height,font-weight,min-height} | --mono-radius-sm / 2 / 8 / 1.5 / 2 ร spacing / --mono-text-sm / normal / 0 | Option rows |
--mono-select-option-active-bg / -active-color | --muted / --foreground | Hover + keyboard cursor row |
--mono-select-check-icon / --mono-select-check-size / --mono-select-option-selected-font-weight | --check-icon / 0.875rem / inherit | The selected row's check mark |
--mono-select-group-{padding-x,padding-y,font-size,line-height,font-weight,text-transform,letter-spacing} | 2 / 1.5 ร spacing / --mono-text-xs / normal / none / normal | Group headers |
--mono-select-color (alias --mono-select-text), --mono-select-placeholder, --mono-select-muted | --foreground, --muted-foreground, --muted-foreground | Text, placeholder, chevron / message / group ink |
--mono-select-valid / --mono-select-invalid / --mono-select-warning | --success / --destructive / --warning | Validation colours |
--mono-select-primary โฆ --mono-select-info | the role tokens | The color presets |
Deprecated and honoured as no-ops until 2.0: --mono-select-focus-rgb, --mono-select-underline-glow (the underline is sera's line now), --mono-select-cursor-width / -color (the keyboard cursor is Basecoat's --muted row, the same as hover), --mono-select-background, the per-colour filled tints.
Types โ
Import
import { SelectProps } from 'mono-helper'| Prop | Value | Default | Description |
|---|---|---|---|
modelValue | unknown | โ | The currently selected value. |
value | unknown | โ | Alias for the selected value, kept in sync with modelValue. |
items | string | SelectItem[] | โ | List of selectable items, as an array or JSON string. |
dataSource | SelectDataSource<SelectItem> | โ | DataSource used to drive the item list, taking precedence over items. |
immediate | boolean | โ | Auto-load the DataSource on attach when it has no items yet. |
loadMore | '' | 'button' | 'scroll' | boolean | โ | Incremental loading mode: scroll, button, or off. |
pageSize | number | โ | Chunk size used when paging a plain items array. |
keyValue | string | โ | Item property name used as the model value. |
displayValue | string | (item: any) => string | โ | Item property name or function used to derive display text. |
displayGroup | SelectDisplayGroup | โ | Per-level accessors for grouped options (header label per group level). |
groupKey | string | โ | Field holding a group node's key (default 'key'). |
groupItems | string | โ | Field holding a group node's child array (default 'items'). |
searchable | boolean | โ | Turn the field itself into a search input (combobox); server query when a DataSource is bound. |
searchValue | string | MonoSearchExprEntry[] | โ | Field(s) the search matches (server query + client filter target). A comma string or an array; entries may be plain columns, paths (`Company.Name`, `Transaction.[*].Price`), `*` patterns (`'*'`, `'*.[*].*'`) or, in array form, `{ field, custom }` clause builders. |
searchOperation | string | โ | devextreme search operation for server search (default 'contains'). |
searchDebounce | number | โ | Debounce (ms) before a server search fires (default 300). |
searchPlaceholder | string | โ | Placeholder shown in the field while it is open/searching (falls back to `placeholder`). |
group | boolean | โ | Group a plain, paginated DataSource client-side (keeps scroll/load-more). |
groupSticky | boolean | โ | Keep group headers pinned to the top while their rows scroll. |
dropdown | SizeProps | โ | Sizes the popup PANEL only, independent of the field โ `:dropdown.prop="{ width: 460, maxHeight: 320 }"`. Without it the panel matches the field's width, which is right for a picker whose options read as continuations of the field, and wrong for a wide one. Setting `width` also stops the panel tracking the field. The widths land on the panel and the heights on its scrolling body, which is why a fixed `height` here still scrolls rather than clipping. |
dropdownHeight | string | number | โ | Fixed height of the scrollable dropdown list. |
dropdownMaxHeight | string | number | โ | Maximum height of the scrollable dropdown list. |
stayOpen | boolean | โ | Exempt this select from every automatic close โ clicking or focusing anything outside it, which includes opening another select. Not a lock: its own trigger, Escape and picking an item still close it. Set it on every select to allow several dropdowns open at once. |
size | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl' | โ | Visual size of the select. |
color | 'primary' | 'secondary' | 'success' | 'danger' | 'warning' | 'info' | 'teal' | 'purple' | 'neutral' | 'dark' | โ | Color theme applied to the select. |
variant | 'underlined' | 'outlined' | 'filled' | โ | Visual style variant of the select. |
label | string | โ | Text label shown above the select. |
placeholder | string | โ | Placeholder text shown when no value is selected. |
helperText | string | โ | Helper text shown below the select. |
validationState | 'warning' | 'default' | 'valid' | 'invalid' | โ | Validation state controlling the select's appearance. |
validationMessage | string | โ | Validation message shown below the select. |
errorMessage | string | โ | Error message shown below the select. |
successMessage | string | โ | Success message shown below the select. |
name | string | โ | Form field name submitted with the value. |
disabled | boolean | โ | Disables the select, preventing interaction. |
readonly | boolean | โ | Makes the select read-only, blocking value changes. |
required | boolean | โ | Marks the select as required and shows a required indicator. |
clearable | boolean | โ | Shows a clear button to reset the selected value. |
ariaLabelText | string | โ | Accessible label used when no visible label is present. |
ariaLabel | string | โ | โ |
width | string | number | โ | Explicit sizing of the field. Each accepts a CSS length string (`"320px"`, `"80%"`) or a number (interpreted as px). Use `width="100%"` for full width. (Distinct from `dropdownHeight`/`dropdownMaxHeight`, which size the popup.) |
height | string | number | โ | โ |
minWidth | string | number | โ | โ |
maxWidth | string | number | โ | โ |
minHeight | string | number | โ | โ |
maxHeight | string | number | โ | โ |
cssClass | SelectCssClass | โ | Custom CSS classes applied to internal select parts. |
visible | boolean | โ | Whether the control is shown. Defaults to `true`. |
visibleType | 'none' | 'invisible' | โ | How it hides when `visible` is `false`. Defaults to `'none'`. |