Skip to content

HySelect ​

Usage examples on this page are written for Lit / plain HTML (<hy-select>). The same component ships as HySelect in @whitespaceux/harmony-react (native React 19), with typed wrappers for Angular, Solid, Svelte, and Vue. The API reference below applies to all frameworks.

A customizable select dropdown component that wraps the native HTML select element.

This component accepts slotted native <option> elements (optionally wrapped in <optgroup>, separated by <hr>) which are mirrored into a standard <select>. This approach provides enhanced styling, validation states, and accessibility features while maintaining native select behavior.

The picker. Where the browser supports the customizable select (appearance: base-select — Chrome / Edge 135+, Safari 27) and the device has a fine pointer, the option list is painted from Harmony tokens (a floating elevation-3 surface, hover / selected / disabled fills, the system chevron) and markup inside an option is preserved — a swatch before the label, a font sample in its own face. The checked option is cloned into the collapsed face (<selectedcontent>). Everywhere else — Firefox, older browsers, touch devices — the OS picker renders the options as text, exactly as before: the same markup degrades, nothing is required of the consumer.

Width. The select fills its container. A field narrower than its container states how many characters its longest label holds with chars (a closed set: 1–8, 10, 12, 16, 20, 24, 32, 48); the width follows the design system's font, type size and padding, and makes room for the chevron and the clear button. fit-options makes it exactly as wide as its widest option instead, as a native select is — in the classic picker and in the customizable one, whose face would otherwise follow the checked option (ignored above 50 options). width sets a fixed CSS width instead, for a size the layout dictates, and wins over both. None grows past the container.

A field or a settings row? ​

A field has its label above it, or beside it with layout="inline" (its helper and messages then sit under the control). A settings row — a setting's name with a line of description at the start and the control at the row's end, as on a preferences page — is a row of your own: a flex row with justify-content: space-between, the name and description as your own text, and the control named with aria-labelledby pointing at the name, with no label, helper-text or layout. Rebuilding a drawn settings page, keep the control at the row's end.

When to use

  • Picking one value from a short, fixed list: a country among a few, a size, a sort order.
  • A choice where space is tight and the options need not be compared.

Do

  • Order the options in a meaningful way: alphabetical, by size or by frequency.
  • Size the field with fit-options or chars, not a pixel width.

When not to use

  • More than about 15 options, or a list people search: hy-combobox.
  • Two to five options people should compare at a glance: hy-radio-group.
  • A single on or off choice: hy-switch or hy-checkbox.
  • Several values from one list: hy-multi-combobox.
  • An answer that cannot be listed in advance: hy-text-input.
  • A time of day: hy-date-field type="time", not separate hour and minute selects.

Accessibility: your part ​

  • With no visible label, name the select with aria-label or aria-labelledby on the host.

Examples ​

html
<hy-select label="Source ramp" value="brand">
  <option value="brand"><span class="swatch" aria-hidden="true"></span>Brand</option>
  <hr />
  <option value="neutral"><span class="swatch" aria-hidden="true"></span>Neutral</option>
</hy-select>
html
<hy-select label="Choose a color" placeholder="Select a color">
  <option value="red">Red</option>
  <option value="green">Green</option>
  <option value="blue">Blue</option>
</hy-select>
html
<hy-select label="Country" helper-text="Select your country of residence" required size="large">
  <option value="us">United States</option>
  <option value="ca">Canada</option>
  <option value="uk">United Kingdom</option>
</hy-select>
html
<hy-select label="Priority Level" value="medium" name="priority" size="small">
  <option value="low">Low Priority</option>
  <option value="medium">Medium Priority</option>
  <option value="high">High Priority</option>
  <option value="urgent" disabled>Urgent</option>
</hy-select>
html
<hy-select label="Account Type" error-message="Please select an account type" required>
  <option value="personal">Personal Account</option>
  <option value="business">Business Account</option>
  <option value="enterprise">Enterprise Account</option>
</hy-select>
html
<hy-select
  label="Region"
  value="ap"
  warning-message="Slower from Europe: pick a closer region if you can"
>
  <option value="eu">Europe</option>
  <option value="us">North America</option>
  <option value="ap">Asia Pacific</option>
</hy-select>
html
<hy-select
  label="Subscription Plan"
  disabled
  value="free"
  helper-text="Upgrade options are temporarily unavailable"
>
  <option value="free">Free Plan</option>
  <option value="pro">Pro Plan</option>
  <option value="enterprise">Enterprise Plan</option>
</hy-select>
html
<form>
  <hy-select name="department" label="Department" required>
    <option value="sales">Sales</option>
    <option value="marketing">Marketing</option>
    <option value="engineering">Engineering</option>
    <option value="support">Customer Support</option>
  </hy-select>

  <hy-select name="experience" label="Years of Experience" placeholder="Select experience level">
    <option value="0-2">0-2 years</option>
    <option value="3-5">3-5 years</option>
    <option value="6-10">6-10 years</option>
    <option value="10+">10+ years</option>
  </hy-select>
</form>

API ​

Properties ​

PropertyAttributeTypeDefaultDescription
labellabelstring''The label text displayed above the select.
helperTexthelper-textstring''Helper text displayed below the select.
valuevaluestring''The current value of the select. Until the consumer gives a value — by attribute or property — a slotted <option selected> gives it, as on a native select, each time the options change: a selected option that arrives later wins, after a pick or a form.reset() too. A given value is the consumer's, an empty one too, and no selected option replaces it. form.reset() returns to the value given in markup, or else to the <option selected>. React's HySelect takes defaultValue / value instead, as React's own <select> does.
namenamestring''The name attribute for the select.
placeholderplaceholderstring''Placeholder text when no option is selected.
sizesize'small' | 'medium' | 'large''medium'The size variant of the select (small, medium, or large).
layoutlayout'block' | 'inline''block'Row layout: block (default) stacks label above the select; inline puts label on the left and select on the right. Helper/error always sit under the control (col 2–end), never under the label. Set --hy-select-label-column-width on a wrapping container so stacked selects align.
charschars1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 10 | 12 | 16 | 20 | 24 | 32 | 48undefinedHow many characters the field holds — the way to make a select narrower than its container. One of 1–8, 10, 12, 16, 20, 24, 32, 48; the width is that many 0 glyphs in the select's own font, plus its padding, border, chevron and (while shown) clear button, so it follows every design system. Size it to the longest option label. Never wider than the container. A value outside the set is ignored (dev warning). width wins when both are set.
fitOptionsfit-optionsbooleanfalseThe select is exactly as wide as its widest option (and the placeholder), as a native select is — the way to size a select over a short, fixed list, where chars would leave room for zeros the labels never fill. The classic picker does it natively; the customizable picker's face lays every option out invisibly, so its width never follows the checked option. A clearable select keeps the clear button's room, so picking a value never widens it. Ignored above 50 options (dev warning). Wins over chars; width wins over it. Never wider than the container.
widthwidthstring | undefinedundefinedA fixed width for the select container, as any CSS width value (e.g. '300px', '20rem', '50%') — for a size the layout dictates rather than the content. The host fills its container by default. Never wider than the container. To size a select to its options, use chars.
disableddisabledbooleanfalseDisables the select.
requiredrequiredbooleanfalseMarks the select as required.
clearableclearablebooleanfalseShow a clear button on the trigger once a value is set, so an optional field can return to "nothing selected" with the pointer. Off by default (every surveyed select defaults off), and never shown on a required or disabled select. Keyboard users clear through the placeholder option, which is selectable whenever the select is not required.
clearLabelclear-labelstring'Clear selection'Accessible name of the clear button.
requiredLabelrequired-labelstring'required'The word announced for the required indicator (*) after the label. Use for internationalization.
errorerrorbooleanfalseError state: the value is not accepted. Sets aria-invalid and colors the focus ring. Setting error-message raises it and clearing the message lowers it; set on its own it keeps the helper text visible. Visual and for screen readers only — form validity comes from required.
errorMessageerror-messagestring''Error message shown under the select (replaces the helper text). Setting it raises error.
warningwarningbooleanfalseWarning state: the value is accepted but worth a second look. Shows warning-message in the warning color; never sets aria-invalid and never blocks submission. error wins when both are set.
warningMessagewarning-messagestring''Warning message shown under the select (replaces the helper text). Setting it raises warning.
autofocusautofocusbooleanfalseMarks this select as the auto-focus target. Read by hy-dialog (and other overlay containers) on show to choose initial focus. Also focuses on first connect when used outside an overlay, mirroring native HTML autofocus but routed through JS so SSR'd HTML doesn't pull focus during hydration.
form—HTMLFormElement | null——
validity—ValidityState——
validationMessage—string——

Events ​

EventDetailDescription
change—Fired when the selected value changes. Detail:
input—Fired during value input. Detail:

Slots ​

SlotDescription
defaultNative <option> elements (optionally wrapped in <optgroup>, separated by <hr>) that define the available choices. Markup inside an option is preserved where the browser paints the picker (appearance: base-select) and reduced to text elsewhere.

CSS Parts ​

PartDescription
baseThe select's base wrapper element
labelThe label element
containerThe select input container
selectThe native select element
faceThe collapsed face inside the select (a <button> holding <selectedcontent>) — painted only by a customizable picker
iconThe dropdown indicator icon
clear-buttonThe clear affordance (rendered when clearable, a value is set, and the select is neither required nor disabled)
helper-textThe helper text element
error-messageThe error message element
warning-messageThe warning message element
message-iconThe icon starting the error or warning message

CSS Custom Properties ​

The documented hooks. Every override hook the component reads — one per token it paints — is listed in @whitespaceux/harmony/theme-api.json, and @whitespaceux/harmony/component-bindings.json (the Harmony MCP get-bindings tool) says which part and state each one paints.

PropertyDefaultDescription
--hy-select-border-danger-restvar(--hy-border-danger-rest)Border (at rest and on hover) and keyboard focus ring in the error state.
--hy-select-border-warning-restvar(--hy-border-warning-rest)Border in the warning state, while the field is not focused.
--hy-select-foreground-warning-restvar(--hy-foreground-warning-rest)Warning message text and icon color.
--hy-select-gap-layout-2xsvar(--hy-gap-layout-2xs)Gap between the message icon and its text.
--hy-select-label-gap-form-smvar(--hy-gap-form-sm)Space between the label and the select (the form gap every field puts under its label).
--hy-select-radius-form-smvar(--hy-radius-form-sm)Clear-button corner radius
--hy-select-label-column-width—Width of the label column in layout="inline" mode. No default — set on a wrapping container so stacked selects align.
--hy-select-control-height-smvar(--hy-control-height-sm)Outer height for small size variant. Per-component override in the form-control height contract.
--hy-select-control-height-mdvar(--hy-control-height-md)Outer height for medium size variant.
--hy-select-control-height-lgvar(--hy-control-height-lg)Outer height for large size variant.
--hy-select-motion-transition-hover-durationvar(--hy-motion-transition-hover-duration)Transition duration for hover and focus state feedback
--hy-select-motion-transition-hover-easingvar(--hy-motion-transition-hover-easing)Transition easing for hover and focus state feedback
--hy-select-motion-transition-expand-durationvar(--hy-motion-transition-expand-duration)Transition duration for the chevron's rotation while the customizable picker is open
--hy-select-motion-transition-expand-easingvar(--hy-motion-transition-expand-easing)Transition easing for the chevron's rotation
--hy-select-motion-transition-enter-durationvar(--hy-motion-transition-enter-duration)Transition duration for the customizable picker's entrance
--hy-select-motion-transition-enter-easingvar(--hy-motion-transition-enter-easing)Transition easing for the customizable picker's entrance
--hy-select-motion-transition-exit-durationvar(--hy-motion-transition-exit-duration)Transition duration for the customizable picker's exit
--hy-select-motion-transition-exit-easingvar(--hy-motion-transition-exit-easing)Transition easing for the customizable picker's exit
--hy-select-background-surface-elevation-3var(--hy-background-surface-elevation-3)Surface of the customizable picker.
--hy-select-picker-border-subtle-restvar(--hy-border-subtle-rest)Hairline of the customizable picker.
--hy-select-radius-overlay-floating-mdvar(--hy-radius-overlay-floating-md)Corner radius of the customizable picker.
--hy-select-shadow-elevation-3var(--hy-shadow-elevation-3)Shadow of the customizable picker.
--hy-select-picker-padding-form-smvar(--hy-padding-form-sm)Inset around the option rows of the customizable picker.
--hy-select-picker-min-size—Block size the customizable picker keeps before it flips to the other side of the trigger (a per-instance layout constraint — literal by design). Default: 8rem
--hy-select-picker-max-rows—Rows the customizable picker shows before it scrolls (a number, not a length — so the cap never lands between two rows).
--hy-select-gap-row-list-xs-blockvar(--hy-gap-row-list-xs-block)Gap between two options in the customizable picker, so a hovered row never touches the selected one (default: the row-list xs gap, the sidebar's 4 px at standard).
--hy-select-picker-max-size—Block size the customizable picker scrolls beyond; overrides the row count. Default: exactly --hy-select-picker-max-rows rows plus the picker's inset and hairline
--hy-select-padding-layout-mdvar(--hy-padding-layout-md)Gap the customizable picker keeps from the viewport edge.
--hy-select-radius-row-list-smvar(--hy-radius-row-list-sm)Corner radius of an option row in the customizable picker.
--hy-select-background-neutral-hovervar(--hy-background-neutral-hover)Hover and keyboard-cursor wash of an option row in the customizable picker.
--hy-select-group-label-text-transformnoneCase of the option group labels (none unless set: shown as written; uppercase for capitals). Display only — the label text stays as written.

Methods ​

focus() ​

Sets focus on the select.

Parameters:

  • options - Optional focus options
javascript
const select = document.querySelector('hy-select');
select.focus();

blur() ​

Removes focus from the select.

javascript
const select = document.querySelector('hy-select');
select.blur();

Built with Lit. Documented with VitePress.