Select
Picks one value from a known list, wrapping the native <select> so it matches the rest of the form.
When to Use
Use when:
The user picks exactly one option from four or more known values — a category, a workshop type, a filter. The list is fixed and short enough to scroll.
Don't use when:
There are three or fewer options — use Radio Card Group so they're all visible. For dozens of values, use a searchable input; for on/off, use Toggle.
Variants
Basic
Options are flat value => label pairs. A placeholder adds a leading prompt option.
expand_more
<?= ws_select('category', [
'ideation' => 'Ideation',
'strategy' => 'Strategy',
'team' => 'Team Building',
], [
'label' => 'Category',
'placeholder' => 'Select a category…',
'required' => true,
]) ?>
info
The options shape here is not the same as
ws_input(type: 'select'), which wants a list of arrays. When you need a dropdown, this helper is the one to reach for — see Input.
Sizes
expand_more
expand_more
expand_more
<?= ws_select('field', $opts, ['label' => 'Small', 'size' => 'sm']) ?>Pre-selected value
Pass value to mark the current selection — essential when redisplaying a form after a validation failure.
expand_more
<?= ws_select('workshop_type', $types, [
'label' => 'Workshop type',
'value' => $plan['type'] ?? '',
]) ?>States
expand_more
Choose the option that best fits your needs
expand_more
Please select an option
expand_more
| State | Behavior |
|---|---|
| Default | Bordered field with the chevron drawn as a separate .ws-select__arrow element, since native dropdown arrows can't be styled. |
| Hover | Border shifts to --gray-300. |
| Focus | --input-border-focus plus the --input-ring halo, matching Input exactly. |
| Error | Derived from a non-empty error — there's no separate state option. Sets aria-invalid="true", points aria-describedby at the message, and renders it with role="alert". |
| Helper | Shown below the field. Like Input, the error takes the describedby slot when both are present. |
| Disabled | Native disabled: greyed, unfocusable, not submitted. |
| Open | The dropdown list is rendered by the OS, so its appearance isn't ours to style — and doesn't need to be. |
<?= ws_select('category', $opts, ['error' => 'Please select an option']) ?>
<?= ws_select('category', $opts, ['helper' => 'Helpful description']) ?>Real-World Usage
A category filter above a library listing: options built from the database, the current query-string value pre-selected, and a submit-on-change handler passed through attrs.
expand_more
<?php
$opts = ['all' => 'All categories'];
foreach ($categories as $c) {
$opts[$c['slug']] = $c['name'] . ' (' . $c['exercise_count'] . ')';
}
?>
<?= ws_select('category', $opts, [
'label' => 'Filter by category',
'value' => $_GET['category'] ?? 'all',
'attrs' => ['onchange' => 'this.form.submit()'],
]) ?>Options
| Option | Type | Default | Purpose |
|---|---|---|---|
$name | string | '' | First positional. Field name, and the default id. |
$selectOptions | array | [] | Second positional. Flat value => label pairs. |
label | string | null | Visible label. Effectively required. |
value | string | '' | Pre-selected value |
placeholder | string | null | Leading prompt option |
required | bool | false | Native required attribute |
disabled | bool | false | Disabled state |
error | string | null | Error message. Non-empty is the error state. |
helper | string | null | Help text below the field. Note: helper, not hint. |
size | string | 'md' | sm | md | lg |
id | string | $name | Element ID, used to bind the label |
class | string | '' | Additional CSS classes |
attrs | array | [] | Extra HTML attributes — where onchange and data-* go |
Accessibility
| Concern | Behavior |
|---|---|
| Semantics | A native <select>. Keyboard interaction, type-ahead, and the platform's own picker on mobile all come free — the main reason not to build a custom dropdown. |
| Labelling | <label for> bound to the field id. Without label the control is unnamed; a placeholder option doesn't count. |
| Errors | aria-invalid="true" on the field, aria-describedby pointing at the message, and role="alert" on the message so it's announced when it appears. |
| Keyboard | Native throughout: Tab to reach, arrows or typing to change, Enter/Esc to commit or cancel on platforms with a popup list. |
| Placeholder option | Rendered as a real option. If the field is required, make sure its value is empty so native validation rejects it. |
| Submit on change | Reloading the page on change moves focus and can strand keyboard users mid-list. Prefer an explicit apply button, or restore focus after the reload. |
| Contrast | Field text uses --text-dark and the arrow --gray-400. The arrow is decorative — the control is identifiable without it. |
Tokens
| Token | Used for |
|---|---|
--bg-surface | Field background |
--gray-200 / --gray-300 | Border, resting and hover |
--gray-400 | Chevron colour |
--gray-50 | Disabled fill |
--input-border-focus / --input-ring / --input-ring-width | Focus border and halo, shared with Input |
--focus-ring-width / --focus-ring-offset | Shared focus geometry |
--radius-xl | Field corners |
--text-dark | Selected value text |
--text-body / --text-ui / --text-meta / --text-h3 | Type scale across sizes |
--font-body | Body face |
--transition-fast | Border and ring transitions |
CSS Classes
| Class | Purpose |
|---|---|
.ws-select | The native <select> |
.ws-select--sm / --lg | Size modifiers (md emits nothing) |
.ws-select-wrap | Positioning context for the field and its arrow |
.ws-select__arrow | The chevron overlay |
.ws-field | Outer field wrapper, shared with Input |
.ws-field--error | Error state on the wrapper |
.ws-field__label / __error / __helper | Label, error, and helper text, shared with Input |
info
ws_input(type: 'select') emits .ws-select-wrapper — a different class from this component's .ws-select-wrap. Another reason to standardise on ws_select() for dropdowns.
Files
| File | Purpose |
|---|---|
includes/components/helpers.php | ws_select() helper function |
includes/components/select.php | Template — option loop and ARIA wiring |
includes/components/components.css | Styles (.ws-select and shared .ws-field rules) |