Basic Input
A labelled text input with helper text wired to it via aria-describedby.
6 फ्रेमवर्कशुरुआती
A combobox that filters as you type, over a multi-select listbox.
<!--
A COMBOBOX, not a listbox with a search box bolted on. The difference is where
the roles go, and it matters:
- role="combobox" belongs on the INPUT, not on a wrapper. The input is the
control; wrapping it changes what gets announced and breaks the pairing with
aria-controls.
- aria-autocomplete="list" tells the user the typing filters a list rather
than completing the text inline.
- Because the input is a real form control, a real <label for> works. No
aria-labelledby gymnastics - this is the one popup pattern that gets a
genuine label.
- Focus STAYS in the input the whole time. Arrow keys move
aria-activedescendant, never DOM focus, or typing would stop working the
moment you arrowed into the list.
The result count is announced through a live region: filtering silently
redraws the screen, and a screen-reader user has no way to know that "sq" left
one row standing.
-->
<div class="msearch">
<label class="msearch__label" for="msearch-input">Skills</label>
<div class="msearch__control">
<svg class="msearch__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><circle cx="11" cy="11" r="7" /><path d="m20 20-3.5-3.5" /></svg>
<input
class="msearch__input"
id="msearch-input"
type="text"
role="combobox"
aria-expanded="false"
aria-controls="msearch-listbox"
aria-autocomplete="list"
aria-describedby="msearch-count"
autocomplete="off"
placeholder="Filter skills…"
/>
</div>
<p class="msearch__count" id="msearch-count" role="status" aria-live="polite">6 skills available. 2 selected.</p>
<ul class="msearch__listbox" id="msearch-listbox" role="listbox" aria-multiselectable="true" aria-label="Skills" hidden></ul>
</div>
<script>
document.querySelectorAll('.msearch').forEach(function (root) {
var DATA = ['React', 'TypeScript', 'Go', 'Rust', 'SQL', 'Figma'];
var input = root.querySelector('.msearch__input');
var listbox = root.querySelector('.msearch__listbox');
var count = root.querySelector('.msearch__count');
var chosen = ['React', 'TypeScript'];
var results = DATA.slice();
var active = 0;
function optionId(index) { return 'msearch-opt-' + index; }
function render() {
var query = input.value.trim().toLowerCase();
results = DATA.filter(function (item) { return item.toLowerCase().includes(query); });
if (active >= results.length) active = Math.max(0, results.length - 1);
listbox.innerHTML = '';
if (!results.length) {
var empty = document.createElement('li');
empty.className = 'msearch__empty';
// role="presentation": an empty-state row is not an option and must not
// be arrowable or selectable.
empty.setAttribute('role', 'presentation');
empty.textContent = 'No skills match “' + input.value + '”.';
listbox.appendChild(empty);
} else {
results.forEach(function (item, index) {
var li = document.createElement('li');
li.className = 'msearch__option' + (index === active ? ' msearch__option--active' : '');
li.id = optionId(index);
li.setAttribute('role', 'option');
li.setAttribute('aria-selected', String(chosen.includes(item)));
li.innerHTML =
'<span class="msearch__marker" aria-hidden="true"></span>' +
'<span class="msearch__option-label">' + item + '</span>' +
'<span class="msearch__tick" aria-hidden="true">✓</span>';
li.addEventListener('mousedown', function (event) {
// mousedown, not click, and prevented: a click would blur the input
// first, closing the popup out from under the pointer.
event.preventDefault();
toggle(item);
});
li.addEventListener('mousemove', function () { active = index; paintActive(); });
listbox.appendChild(li);
});
}
listbox.setAttribute('aria-activedescendant', results.length ? optionId(active) : '');
count.textContent =
(results.length === 0
? 'No skills match.'
: results.length + (results.length === 1 ? ' skill' : ' skills') + ' available.') +
' ' + chosen.length + ' selected.';
}
function paintActive() {
Array.prototype.slice.call(listbox.querySelectorAll('.msearch__option')).forEach(function (el, index) {
el.classList.toggle('msearch__option--active', index === active);
});
listbox.setAttribute('aria-activedescendant', results.length ? optionId(active) : '');
}
function setOpen(open) {
listbox.hidden = !open;
input.setAttribute('aria-expanded', String(open));
if (open) render();
}
function toggle(item) {
var at = chosen.indexOf(item);
if (at === -1) chosen.push(item);
else chosen.splice(at, 1);
render();
}
input.addEventListener('focus', function () { setOpen(true); });
input.addEventListener('input', function () {
active = 0;
setOpen(true);
render();
});
input.addEventListener('keydown', function (event) {
var open = input.getAttribute('aria-expanded') === 'true';
var key = event.key;
if (!open && (key === 'ArrowDown' || key === 'ArrowUp')) {
event.preventDefault();
setOpen(true);
return;
}
if (!open) return;
if (key === 'Escape') { event.preventDefault(); setOpen(false); return; }
if (key === 'Enter') {
event.preventDefault();
if (results[active]) toggle(results[active]);
return;
}
if (!results.length) return;
var next = active;
if (key === 'ArrowDown') next = active + 1;
else if (key === 'ArrowUp') next = active - 1;
else return;
event.preventDefault();
active = (next + results.length) % results.length;
paintActive();
});
document.addEventListener('mousedown', function (event) {
if (!root.contains(event.target)) setOpen(false);
});
render();
});
</script>| Prop | टाइप | डिफ़ॉल्ट | विवरण |
|---|---|---|---|
labelआवश्यक | string | - | लोड होते समय पढ़ा जाने वाला एक्सेसिबल लेबल। |
itemsआवश्यक | SelectItem[] | - | रेंडर की जाने वाली आइटम की सूची। |
valueआवश्यक | string[] | - | मेट्रिक की मौजूदा वैल्यू, पहले से फ़ॉर्मैट की हुई। |
onSelect | (next: string[]) => void | - | यूज़र द्वारा चुने गए मेन्यू आइटम के साथ चलता है। |
disabled | boolean | false | इंटरैक्शन रोकता है और कंट्रोल को धुँधला कर देता है। |
A combobox, not a listbox with a search box bolted on. `role="combobox"` goes on the input itself - wrapping it breaks the `aria-controls` pairing - and because the input is a real form control, this is the one popup pattern that gets a genuine `<label htmlFor>` rather than `aria-labelledby` gymnastics. DOM focus never leaves the input; arrows move `aria-activedescendant` only, or typing stops working the moment you arrow into the list. Two details worth copying: the active index is *clamped* rather than trusted, since the list shrinks under the cursor as you type and a stale id points at nothing; and rows use `onMouseDown` with `preventDefault`, because a click would blur the input and close the popup before the selection landed.