Prompt Select
A dropdown that reads as the end of a sentence — "I need help with [Goal…]" — for conversational navigation.
When to Use
Variants
Default
Brand-bordered dropdown with the serif-italic prompt. The first option doubles as the placeholder, so give it an empty value.
<?= ws_prompt_select('I need help with', 'goalSelect', [
'' => 'Goal…',
'team-building' => 'Team Building',
'ideation' => 'Ideation',
]) ?>Muted
Grey border and subdued prompt, for a secondary instance or one sitting on an already-busy surface.
<?= ws_prompt_select('Filter by', 'filterSelect', $opts, ['variant' => 'muted']) ?>Responsive
Below 480px the row stacks: the prompt moves above the dropdown at a reduced size, and the dropdown goes full width. Nothing to configure.
States
| State | Behavior |
|---|---|
| Default | Native <select> with the brand border and a custom chevron. |
| Focus | --focus-ring-width and --focus-ring-offset, the platform's shared focus geometry. |
| Changed | The onchange string runs as an inline handler. Because this component navigates rather than collects, that's usually where the routing happens — try it above. |
| Pre-selected | value marks the current option. Useful when the prompt reflects a filter already applied. |
| Disabled | Not supported — there's no disabled option. |
| Error | Not supported. Another reason this isn't a form control: there's nowhere to put validation. |
<?= ws_prompt_select('Most popular', 'popularSelect', $opts, [
'onchange' => 'goToPopular(this)',
]) ?>Real-World Usage
The homepage hero's intent router: goals come from the database, and selecting one navigates straight to that goal's collection. Note the aria-label passed through to give the select an accessible name — see Accessibility for why that's necessary.
<?= ws_prompt_select('I need help with', 'heroGoal', $goalOptions, [
'onchange' => 'if (this.value) location.href = "/library/goals/" + this.value;',
]) ?>
<script>
// The prompt is a <span>, so name the control explicitly.
document.getElementById('heroGoal')
.setAttribute('aria-label', 'I need help with');
</script>Options
| Option | Type | Default | Purpose |
|---|---|---|---|
$label | string | '' | First positional. The prompt, rendered serif-italic. Write it so the option completes the sentence. |
$name | string | '' | Second positional. Field name and default ID. |
$selectOptions | array | [] | Third positional. Flat value => label pairs, same shape as Select. |
value | string | '' | Pre-selected value |
onchange | string | '' | Inline JS handler, typically the navigation |
id | string | $name | Element ID |
class | string | '' | Additional CSS classes on the wrapper |
variant | string | 'default' | default | muted |
There's no attrs option, so attributes can't be passed to the select directly — set them from JS using the id.
Accessibility
| Concern | Behavior |
|---|---|
| Semantics | A native <select>, so keyboard operation, type-ahead, and the mobile picker are all native. |
| Labelling | Missing. The prompt renders as <span class="ws-prompt-select__label">, not a <label for>, and no aria-label is emitted. The select therefore has no accessible name — a screen reader announces an unnamed combo box. Set aria-label from JS using the element's id, as shown above. |
| Click target | Because the prompt isn't a real label, clicking it doesn't open the dropdown — a usability loss for everyone. |
| Keyboard | Tab to reach, arrows or typing to change. Standard. |
| Navigate on change | Changing the value navigates away. Keyboard users move through options with arrows, which fires change on each one — on some browsers that means navigating before they reach their choice. Prefer a Go button, or navigate on blur instead. |
| Placeholder option | Give the first option an empty value so the handler can guard against it, as in the example. |
| Contrast | Default and muted variants both pair tested border and text tokens. |
<label for="{id}"> rather than a span (which solves naming and click-to-open at once), and accept an attrs option so callers can pass ARIA without JavaScript.
Tokens
| Token | Used for |
|---|---|
--color-primary | Default variant border |
--gray-200 | Muted variant border |
--bg-surface | Select background |
--color-ink / --color-ink-muted | Prompt and muted-prompt colour |
--focus-ring-width / --focus-ring-offset | Focus ring geometry |
--text-body / --text-ui | Prompt and option type scale |
CSS Classes
| Class | Purpose |
|---|---|
.ws-prompt-select | Flex wrapper, stacking below 480px |
.ws-prompt-select--muted | Muted variant |
.ws-prompt-select__label | Serif-italic prompt (a <span> — see Accessibility) |
.ws-prompt-select__dropdown | Positioning context for the select and chevron |
.ws-prompt-select__select | The native <select> |
Files
| File | Purpose |
|---|---|
includes/components/helpers.php | ws_prompt_select() helper function |
includes/components/prompt-select.php | Template — prompt, select, and chevron markup |
includes/components/components.css | Styles (.ws-prompt-select rules) |