Previews

No matching results.

x
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
<div class="tw:p-6 tw:max-w-7xl tw:mx-auto tw:space-y-8">
<div class="callout">
<p class="tw:mb-1 tw:text-sm tw:font-mono">
<strong>COMBOBOX:</strong> Filter-as-you-type single-select (ARIA 1.2)<br>
<strong>COMPOSES:</strong> .form-control | .dropdown-menu | .dropdown-item
</p>
<p class="tw:mb-0 tw:text-sm">
Typing filters the list; arrows move the active option; Enter selects; Escape
restores the committed value. Free text that matches no option rolls back on
close — the field can only ever hold a real option (error prevention, C05).
</p>
</div>
<div class="tw:border-t tw:pt-6">
<h2 class="h4 tw:mb-4">Basic</h2>
<div class="callout tw:mb-4">
<p class="tw:mb-0 tw:text-sm">
<strong>Basic:</strong> label + visible input + hidden value field. The hidden
input (<code>data-combobox-target="value"</code>) is the form field; the visible
input carries only the display text.
</p>
</div>
<div class="tw:max-w-sm" data-controller="combobox" id="combobox-school">
<label for="combobox-school-input" class="form-label">School</label>
<div class="combobox">
<input type="text" id="combobox-school-input" class="form-control"
role="combobox" aria-autocomplete="list" aria-expanded="false"
aria-controls="combobox-school-listbox"
placeholder="Start typing a school name"
autocomplete="off"
data-combobox-target="input"
data-action="input->combobox#filterInput focus->combobox#open keydown->combobox#keydown">
<i class="fa-regular fa-chevron-down combobox-chevron" aria-hidden="true"></i>
<input type="hidden" name="school_id" data-combobox-target="value">
<ul class="dropdown-menu dropdown-menu-scrollable" id="combobox-school-listbox"
role="listbox" aria-label="Schools" data-combobox-target="menu">
<li class="dropdown-item" role="option" data-value="1"
data-combobox-target="option" data-action="click->combobox#select">Ashcroft High School</li>
<li class="dropdown-item" role="option" data-value="2"
data-combobox-target="option" data-action="click->combobox#select">Barnhill Community College</li>
<li class="dropdown-item" role="option" data-value="3"
data-combobox-target="option" data-action="click->combobox#select">Cedar Park Academy</li>
<li class="dropdown-item" role="option" data-value="4"
data-combobox-target="option" data-action="click->combobox#select">Danesfield Sixth Form</li>
<li class="dropdown-item" role="option" data-value="5"
data-combobox-target="option" data-action="click->combobox#select">Eastbrook Grammar School</li>
<li class="combobox-empty" hidden data-combobox-target="empty">No schools match — check the spelling or try fewer letters.</li>
</ul>
</div>
</div>
<div class="tw:mt-4 tw:text-sm tw:text-gray-600">
<code><div data-controller="combobox"><div class="combobox"><input class="form-control" role="combobox" ><ul class="dropdown-menu" role="listbox"></ul></div></div></code>
</div>
</div>
<div class="tw:border-t tw:pt-6">
<h2 class="h4 tw:mb-4">Async (remote source)</h2>
<div class="callout tw:mb-4">
<p class="tw:mb-1 tw:text-sm">
<strong>Async mode:</strong> set <code>data-combobox-url-value</code> and the
controller debounces input, then fetches <code>{url}?q={query}</code> and renders
the JSON <code>{value, text}[]</code> response as options — same commit, keyboard,
and rollback contract as the static list above. Below
<code>data-combobox-min-chars-value</code> a <code>.combobox-hint</code> row prompts
for more input; while a request is pending a <code>.combobox-loading</code> row
(<code>.spinner-border-sm</code>) shows; a failed fetch reuses
<code>.combobox-empty</code> with retry copy. A persistent visually-hidden
<code>data-combobox-target="status"</code> span announces each transition
("Type at least N characters…", "Searching…", "N results available", "No results
found", or the error copy) to screen readers — recommended over
<code>aria-live</code> on the visually-toggled rows themselves, since a live-region
node that gets <code>hidden</code> toggled on/off is an unreliable announcement
trigger.
</p>
<p class="tw:mb-0 tw:text-sm">
<strong>Preview mock:</strong> these examples have no real backend, so each carries
an inline <code><script type="application/json" data-combobox-target="fixture"></code>
sibling (a static <code>{results, delay, error}</code> fixture). When present, the
controller reads it instead of calling <code>fetch</code> — same visible delay and
superseded-request behavior, though the mechanics differ slightly (the fixture
cancels its timer on abort rather than rejecting like a real aborted
<code>fetch</code>). Production usage simply omits the fixture and points
<code>url</code> at a real same-origin endpoint.
</p>
</div>
<div class="tw:grid tw:grid-cols-1 tw:sm:grid-cols-2 tw:gap-6">
<div data-controller="combobox" id="combobox-async-school"
data-combobox-url-value="/api/schools/search"
data-combobox-min-chars-value="2"
data-combobox-debounce-value="250">
<label for="combobox-async-school-input" class="form-label">School (remote search)</label>
<div class="combobox">
<input type="text" id="combobox-async-school-input" class="form-control"
role="combobox" aria-autocomplete="list" aria-expanded="false"
aria-controls="combobox-async-school-listbox"
placeholder="Type at least 2 letters"
autocomplete="off"
data-combobox-target="input"
data-action="input->combobox#filterInput focus->combobox#open keydown->combobox#keydown">
<i class="fa-regular fa-chevron-down combobox-chevron" aria-hidden="true"></i>
<input type="hidden" name="school_id" data-combobox-target="value">
<span class="tw:sr-only" aria-live="polite" data-combobox-target="status"></span>
<script type="application/json" data-combobox-target="fixture">
{"delay": 350, "results": [
{"value": "101", "text": "Ashcroft High School"},
{"value": "102", "text": "Barnhill Community College"},
{"value": "103", "text": "Cedar Park Academy"},
{"value": "104", "text": "Danesfield Sixth Form"},
{"value": "105", "text": "Eastbrook Grammar School"}
]}
</script>
<ul class="dropdown-menu dropdown-menu-scrollable" id="combobox-async-school-listbox"
role="listbox" aria-label="Schools" data-combobox-target="menu">
<li class="combobox-hint" hidden data-combobox-target="hint">Type at least 2 characters to search.</li>
<li class="combobox-loading" hidden data-combobox-target="loading">
<span class="spinner-border spinner-border-sm" aria-hidden="true"></span> Searching schools…
</li>
<li class="combobox-empty" hidden data-combobox-target="empty">No schools match — check the spelling or try fewer letters.</li>
</ul>
</div>
<p class="tw:mt-2 tw:text-sm tw:text-gray-600">
Try: 1 letter (hint) → 2+ letters of a school name (loading → results) →
<code>zzz</code> (no results).
</p>
</div>
<div data-controller="combobox" id="combobox-async-error"
data-combobox-url-value="/api/schools/search"
data-combobox-min-chars-value="1"
data-combobox-debounce-value="200">
<label for="combobox-async-error-input" class="form-label">School (network failure demo)</label>
<div class="combobox">
<input type="text" id="combobox-async-error-input" class="form-control"
role="combobox" aria-autocomplete="list" aria-expanded="false"
aria-controls="combobox-async-error-listbox"
placeholder="Type to trigger a failed search"
autocomplete="off"
data-combobox-target="input"
data-action="input->combobox#filterInput focus->combobox#open keydown->combobox#keydown">
<i class="fa-regular fa-chevron-down combobox-chevron" aria-hidden="true"></i>
<input type="hidden" name="school_id" data-combobox-target="value">
<span class="tw:sr-only" aria-live="polite" data-combobox-target="status"></span>
<script type="application/json" data-combobox-target="fixture">
{"delay": 300, "error": true}
</script>
<ul class="dropdown-menu dropdown-menu-scrollable" id="combobox-async-error-listbox"
role="listbox" aria-label="Schools" data-combobox-target="menu">
<li class="combobox-hint" hidden data-combobox-target="hint">Type at least 1 character to search.</li>
<li class="combobox-loading" hidden data-combobox-target="loading">
<span class="spinner-border spinner-border-sm" aria-hidden="true"></span> Searching schools…
</li>
<li class="combobox-empty" hidden data-combobox-target="empty">No schools match — check the spelling or try fewer letters.</li>
</ul>
</div>
<p class="tw:mt-2 tw:text-sm tw:text-gray-600">
This example's fixture always simulates a failed request — type any letter to see
the loading row, then the retry-copy error state.
</p>
</div>
</div>
<div class="tw:mt-4 tw:text-sm tw:text-gray-600">
<code><div data-controller="combobox" data-combobox-url-value="/api/schools/search" data-combobox-min-chars-value="2" data-combobox-debounce-value="250"><span class="tw:sr-only" aria-live="polite" data-combobox-target="status"></span><li class="combobox-hint" hidden data-combobox-target="hint"><li class="combobox-loading" hidden data-combobox-target="loading"><span class="spinner-border spinner-border-sm"></span></li></div></code>
</div>
</div>
<div class="tw:border-t tw:pt-6">
<h2 class="h4 tw:mb-4">Preselected value</h2>
<div class="callout tw:mb-4">
<p class="tw:mb-0 tw:text-sm">
<strong>Preselected:</strong> set the visible input's <code>value</code>, the hidden
input's <code>value</code>, and <code>.active</code> on the matching option.
</p>
</div>
<div class="tw:max-w-sm" data-controller="combobox" id="combobox-subject">
<label for="combobox-subject-input" class="form-label">Subject</label>
<div class="combobox">
<input type="text" id="combobox-subject-input" class="form-control"
role="combobox" aria-autocomplete="list" aria-expanded="false"
aria-controls="combobox-subject-listbox"
value="Mathematics" autocomplete="off"
data-combobox-target="input"
data-action="input->combobox#filterInput focus->combobox#open keydown->combobox#keydown">
<i class="fa-regular fa-chevron-down combobox-chevron" aria-hidden="true"></i>
<input type="hidden" name="subject_id" value="20" data-combobox-target="value">
<ul class="dropdown-menu dropdown-menu-scrollable" id="combobox-subject-listbox"
role="listbox" aria-label="Subjects" data-combobox-target="menu">
<li class="dropdown-item" role="option" data-value="10"
data-combobox-target="option" data-action="click->combobox#select">English Literature</li>
<li class="dropdown-item active" role="option" data-value="20"
data-combobox-target="option" data-action="click->combobox#select">Mathematics</li>
<li class="dropdown-item" role="option" data-value="30"
data-combobox-target="option" data-action="click->combobox#select">Physics</li>
<li class="dropdown-item" role="option" data-value="40"
data-combobox-target="option" data-action="click->combobox#select">Psychology</li>
<li class="combobox-empty" hidden data-combobox-target="empty">No subjects match — try fewer letters.</li>
</ul>
</div>
</div>
</div>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
<div class="tw:p-6 tw:max-w-7xl tw:mx-auto tw:space-y-8">
<%# Introductory callout %>
<div class="callout">
<p class="tw:mb-1 tw:text-sm tw:font-mono">
<strong>COMBOBOX:</strong> Filter-as-you-type single-select (ARIA 1.2)<br>
<strong>COMPOSES:</strong> .form-control | .dropdown-menu | .dropdown-item
</p>
<p class="tw:mb-0 tw:text-sm">
Typing filters the list; arrows move the active option; Enter selects; Escape
restores the committed value. Free text that matches no option rolls back on
close — the field can only ever hold a real option (error prevention, C05).
</p>
</div>
<%# Basic %>
<div class="tw:border-t tw:pt-6">
<h2 class="h4 tw:mb-4">Basic</h2>
<div class="callout tw:mb-4">
<p class="tw:mb-0 tw:text-sm">
<strong>Basic:</strong> label + visible input + hidden value field. The hidden
input (<code>data-combobox-target="value"</code>) is the form field; the visible
input carries only the display text.
</p>
</div>
<div class="tw:max-w-sm" data-controller="combobox" id="combobox-school">
<label for="combobox-school-input" class="form-label">School</label>
<div class="combobox">
<input type="text" id="combobox-school-input" class="form-control"
role="combobox" aria-autocomplete="list" aria-expanded="false"
aria-controls="combobox-school-listbox"
placeholder="Start typing a school name"
autocomplete="off"
data-combobox-target="input"
data-action="input->combobox#filterInput focus->combobox#open keydown->combobox#keydown">
<i class="fa-regular fa-chevron-down combobox-chevron" aria-hidden="true"></i>
<input type="hidden" name="school_id" data-combobox-target="value">
<ul class="dropdown-menu dropdown-menu-scrollable" id="combobox-school-listbox"
role="listbox" aria-label="Schools" data-combobox-target="menu">
<li class="dropdown-item" role="option" data-value="1"
data-combobox-target="option" data-action="click->combobox#select">Ashcroft High School</li>
<li class="dropdown-item" role="option" data-value="2"
data-combobox-target="option" data-action="click->combobox#select">Barnhill Community College</li>
<li class="dropdown-item" role="option" data-value="3"
data-combobox-target="option" data-action="click->combobox#select">Cedar Park Academy</li>
<li class="dropdown-item" role="option" data-value="4"
data-combobox-target="option" data-action="click->combobox#select">Danesfield Sixth Form</li>
<li class="dropdown-item" role="option" data-value="5"
data-combobox-target="option" data-action="click->combobox#select">Eastbrook Grammar School</li>
<li class="combobox-empty" hidden data-combobox-target="empty">No schools match — check the spelling or try fewer letters.</li>
</ul>
</div>
</div>
<div class="tw:mt-4 tw:text-sm tw:text-gray-600">
<code><div data-controller="combobox"><div class="combobox"><input class="form-control" role="combobox" ><ul class="dropdown-menu" role="listbox"></ul></div></div></code>
</div>
</div>
<%# Async — remote source %>
<div class="tw:border-t tw:pt-6">
<h2 class="h4 tw:mb-4">Async (remote source)</h2>
<div class="callout tw:mb-4">
<p class="tw:mb-1 tw:text-sm">
<strong>Async mode:</strong> set <code>data-combobox-url-value</code> and the
controller debounces input, then fetches <code>{url}?q={query}</code> and renders
the JSON <code>{value, text}[]</code> response as options — same commit, keyboard,
and rollback contract as the static list above. Below
<code>data-combobox-min-chars-value</code> a <code>.combobox-hint</code> row prompts
for more input; while a request is pending a <code>.combobox-loading</code> row
(<code>.spinner-border-sm</code>) shows; a failed fetch reuses
<code>.combobox-empty</code> with retry copy. A persistent visually-hidden
<code>data-combobox-target="status"</code> span announces each transition
("Type at least N characters…", "Searching…", "N results available", "No results
found", or the error copy) to screen readers — recommended over
<code>aria-live</code> on the visually-toggled rows themselves, since a live-region
node that gets <code>hidden</code> toggled on/off is an unreliable announcement
trigger.
</p>
<p class="tw:mb-0 tw:text-sm">
<strong>Preview mock:</strong> these examples have no real backend, so each carries
an inline <code><script type="application/json" data-combobox-target="fixture"></code>
sibling (a static <code>{results, delay, error}</code> fixture). When present, the
controller reads it instead of calling <code>fetch</code> — same visible delay and
superseded-request behavior, though the mechanics differ slightly (the fixture
cancels its timer on abort rather than rejecting like a real aborted
<code>fetch</code>). Production usage simply omits the fixture and points
<code>url</code> at a real same-origin endpoint.
</p>
</div>
<div class="tw:grid tw:grid-cols-1 tw:sm:grid-cols-2 tw:gap-6">
<div data-controller="combobox" id="combobox-async-school"
data-combobox-url-value="/api/schools/search"
data-combobox-min-chars-value="2"
data-combobox-debounce-value="250">
<label for="combobox-async-school-input" class="form-label">School (remote search)</label>
<div class="combobox">
<input type="text" id="combobox-async-school-input" class="form-control"
role="combobox" aria-autocomplete="list" aria-expanded="false"
aria-controls="combobox-async-school-listbox"
placeholder="Type at least 2 letters"
autocomplete="off"
data-combobox-target="input"
data-action="input->combobox#filterInput focus->combobox#open keydown->combobox#keydown">
<i class="fa-regular fa-chevron-down combobox-chevron" aria-hidden="true"></i>
<input type="hidden" name="school_id" data-combobox-target="value">
<span class="tw:sr-only" aria-live="polite" data-combobox-target="status"></span>
<script type="application/json" data-combobox-target="fixture">
{"delay": 350, "results": [
{"value": "101", "text": "Ashcroft High School"},
{"value": "102", "text": "Barnhill Community College"},
{"value": "103", "text": "Cedar Park Academy"},
{"value": "104", "text": "Danesfield Sixth Form"},
{"value": "105", "text": "Eastbrook Grammar School"}
]}
</script>
<ul class="dropdown-menu dropdown-menu-scrollable" id="combobox-async-school-listbox"
role="listbox" aria-label="Schools" data-combobox-target="menu">
<li class="combobox-hint" hidden data-combobox-target="hint">Type at least 2 characters to search.</li>
<li class="combobox-loading" hidden data-combobox-target="loading">
<span class="spinner-border spinner-border-sm" aria-hidden="true"></span> Searching schools…
</li>
<li class="combobox-empty" hidden data-combobox-target="empty">No schools match — check the spelling or try fewer letters.</li>
</ul>
</div>
<p class="tw:mt-2 tw:text-sm tw:text-gray-600">
Try: 1 letter (hint) → 2+ letters of a school name (loading → results) →
<code>zzz</code> (no results).
</p>
</div>
<div data-controller="combobox" id="combobox-async-error"
data-combobox-url-value="/api/schools/search"
data-combobox-min-chars-value="1"
data-combobox-debounce-value="200">
<label for="combobox-async-error-input" class="form-label">School (network failure demo)</label>
<div class="combobox">
<input type="text" id="combobox-async-error-input" class="form-control"
role="combobox" aria-autocomplete="list" aria-expanded="false"
aria-controls="combobox-async-error-listbox"
placeholder="Type to trigger a failed search"
autocomplete="off"
data-combobox-target="input"
data-action="input->combobox#filterInput focus->combobox#open keydown->combobox#keydown">
<i class="fa-regular fa-chevron-down combobox-chevron" aria-hidden="true"></i>
<input type="hidden" name="school_id" data-combobox-target="value">
<span class="tw:sr-only" aria-live="polite" data-combobox-target="status"></span>
<script type="application/json" data-combobox-target="fixture">
{"delay": 300, "error": true}
</script>
<ul class="dropdown-menu dropdown-menu-scrollable" id="combobox-async-error-listbox"
role="listbox" aria-label="Schools" data-combobox-target="menu">
<li class="combobox-hint" hidden data-combobox-target="hint">Type at least 1 character to search.</li>
<li class="combobox-loading" hidden data-combobox-target="loading">
<span class="spinner-border spinner-border-sm" aria-hidden="true"></span> Searching schools…
</li>
<li class="combobox-empty" hidden data-combobox-target="empty">No schools match — check the spelling or try fewer letters.</li>
</ul>
</div>
<p class="tw:mt-2 tw:text-sm tw:text-gray-600">
This example's fixture always simulates a failed request — type any letter to see
the loading row, then the retry-copy error state.
</p>
</div>
</div>
<div class="tw:mt-4 tw:text-sm tw:text-gray-600">
<code><div data-controller="combobox" data-combobox-url-value="/api/schools/search" data-combobox-min-chars-value="2" data-combobox-debounce-value="250"><span class="tw:sr-only" aria-live="polite" data-combobox-target="status"></span><li class="combobox-hint" hidden data-combobox-target="hint"><li class="combobox-loading" hidden data-combobox-target="loading"><span class="spinner-border spinner-border-sm"></span></li></div></code>
</div>
</div>
<%# Preselected %>
<div class="tw:border-t tw:pt-6">
<h2 class="h4 tw:mb-4">Preselected value</h2>
<div class="callout tw:mb-4">
<p class="tw:mb-0 tw:text-sm">
<strong>Preselected:</strong> set the visible input's <code>value</code>, the hidden
input's <code>value</code>, and <code>.active</code> on the matching option.
</p>
</div>
<div class="tw:max-w-sm" data-controller="combobox" id="combobox-subject">
<label for="combobox-subject-input" class="form-label">Subject</label>
<div class="combobox">
<input type="text" id="combobox-subject-input" class="form-control"
role="combobox" aria-autocomplete="list" aria-expanded="false"
aria-controls="combobox-subject-listbox"
value="Mathematics" autocomplete="off"
data-combobox-target="input"
data-action="input->combobox#filterInput focus->combobox#open keydown->combobox#keydown">
<i class="fa-regular fa-chevron-down combobox-chevron" aria-hidden="true"></i>
<input type="hidden" name="subject_id" value="20" data-combobox-target="value">
<ul class="dropdown-menu dropdown-menu-scrollable" id="combobox-subject-listbox"
role="listbox" aria-label="Subjects" data-combobox-target="menu">
<li class="dropdown-item" role="option" data-value="10"
data-combobox-target="option" data-action="click->combobox#select">English Literature</li>
<li class="dropdown-item active" role="option" data-value="20"
data-combobox-target="option" data-action="click->combobox#select">Mathematics</li>
<li class="dropdown-item" role="option" data-value="30"
data-combobox-target="option" data-action="click->combobox#select">Physics</li>
<li class="dropdown-item" role="option" data-value="40"
data-combobox-target="option" data-action="click->combobox#select">Psychology</li>
<li class="combobox-empty" hidden data-combobox-target="empty">No subjects match — try fewer letters.</li>
</ul>
</div>
</div>
</div>
</div>