Composite
Select Hybrid
A custom-styled dropdown for choosing one option from a list. Renders a trigger button and a popover list using the native Popover API where supported. Keyboard-accessible by default — typeahead, arrow keys, Home/End, Escape — and fully WAI-ARIA listbox compliant.
About
Overview
Use a Select when the options need richer rendering than a native <select> can offer — icons, descriptions, group separators, custom layouts. For plain text options under ~10 items in a form, prefer the native element via Form Field; the platform's behavior is hard to beat for accessibility and mobile pickers.
The trigger is a button, not an input. Selects don't accept typed input; they show a list and let the user pick. If you need free-text + suggestions, use Combobox instead.
Parts
Anatomy
A Select has three parts in the DOM: the trigger that's always visible, the listbox content that floats on open, and the items inside.
Assembled
<ren-select> wrapper. Holds trigger + content. Owns selection state and exposes a name/value API for forms.
.ren-select-trigger button with aria-haspopup="listbox". Shows the selected value or a placeholder. Click or Enter opens the list.
.ren-select-content with role="listbox". Floats on open via the Popover API. Closes on Escape, click outside, or selection.
.ren-select-item with role="option" and data-value. Optional data-highlighted for keyboard focus, aria-selected for the active choice.
Live
Demo
Click the trigger to open. Use Up/Down to navigate, Enter to select, Escape to close.
- Argentina
- Brazil
- Chile
- Mexico
- United States
<ren-select name="country" placeholder="Select a country">
<button class="ren-select-trigger">
<span class="ren-select-value"></span>
<span class="ren-select-icon">…</span>
</button>
<div class="ren-select-content" popover>
<div class="ren-select-item" data-value="ar">Argentina</div>
<div class="ren-select-item" data-value="br">Brazil</div>
<div class="ren-select-item" data-value="cl">Chile</div>
<div class="ren-select-item" data-value="mx">Mexico</div>
<div class="ren-select-item" data-value="us">United States</div>
</div>
</ren-select>
Shapes
Variants
Three sizes via size="sm|md|lg" on <ren-select> — generates .ren-select-sm, -md (default), or -lg. The trigger height matches the matching .ren-input size so selects line up with text fields in the same form.
Each .ren-select-item can hold any markup — an icon, a description, a badge. The component just routes selection by data-value.
Add a secondary line per item with .ren-select-item-description. Useful for Plan pickers (name + price) or Locale pickers (name + native script).
Add disabled on the trigger to lock the whole field. aria-disabled on individual .ren-select-item elements skips them in keyboard navigation.
Reference
API
CSS classes
| Class | Effect |
|---|---|
.ren-select | Wrapper. Establishes the relative positioning context for the popover. |
.ren-select-trigger | The button users see. Should be a real <button type="button">. Holds aria-haspopup="listbox" and aria-expanded. |
.ren-select-value | The slot where the selected option's display text appears. |
.ren-select-placeholder | Replaces .ren-select-value styling when nothing is selected — muted color. |
.ren-select-icon | The chevron at the trigger's end. Rotates 180° while the listbox is open. |
.ren-select-content | The floating list. Use with popover attribute and role="listbox". Auto-positioned by the component. |
.ren-select-item | One option. Use with role="option" and a data-value. aria-selected="true" marks the chosen one. |
.ren-select-sm / -lg | Size modifiers. Default trigger is medium (44 px touch target). |
Web Component attributes
| Attribute | Type | Default | Notes |
|---|---|---|---|
name | string | — | Form field name. Submitted via a hidden input so the value travels with the form. |
value | string | — | Initial selected value. Must match a data-value on one of the items. |
placeholder | string | "Select an option" | Trigger text when nothing is selected. |
placement | "top" | "right" | "bottom" | "left" | "bottom" | Preferred side for the listbox. Reflected to data-side; may flip when viewport space requires it. |
size | "sm" | "md" | "lg" | "md" | Visual size variant. |
disabled | boolean | false | Prevents opening. Adds aria-disabled. |
JavaScript API
| Member | Description |
|---|---|
value | Getter / setter. Reads or assigns the selected value. Setting triggers ren-change. |
selectedItem | Getter. The currently selected .ren-select-item element (or null). |
open() / close() | Programmatically toggle the listbox. |
isOpen | Boolean getter. True while the listbox is visible. |
Events
| Event | Detail |
|---|---|
ren-change | Fires when the selection changes. event.detail: { value, item }. |
ren-open / ren-close | Fire when the listbox opens or closes. No detail. |
Inclusive by default
Accessibility
Implements the WAI-ARIA Listbox pattern via the trigger + popover combination.
Keyboard
Labelling
Wrap the Select inside a Form Field so the label, description and error get auto-wired. Or set aria-label directly on the trigger if there's no visible label.
Avoid putting too many items in one Select. Past ~15, switch to a Combobox with search. Typeahead helps but doesn't scale to hundreds.
Patterns
Examples
Inside a Form Field
<ren-field>
<label>Country</label>
<ren-select name="country" placeholder="Select your country">
<button class="ren-select-trigger">
<span class="ren-select-value"></span>
<span class="ren-select-icon">…</span>
</button>
<div class="ren-select-content" popover>
<div class="ren-select-item" data-value="ar">Argentina</div>
<div class="ren-select-item" data-value="br">Brazil</div>
</div>
</ren-select>
</ren-field>
React to selection
document.querySelector('ren-select')
.addEventListener('ren-change', (e) => {
console.log('Selected', e.detail.value);
updatePreview(e.detail.value);
});
Items with icons + descriptions
<div class="ren-select-item" data-value="basic">
<span class="ren-select-item-icon"><!-- icon SVG --></span>
<span>
<strong>Basic</strong>
<span class="ren-select-item-description">$9 per month</span>
</span>
</div>