Basic Input
A labelled text input with helper text wired to it via aria-describedby.
6 個のフレームワーク初級
An invalid select wired with `aria-invalid` and `aria-describedby` - not just a red border.
<!--
An invalid select. Three things do the work, and none of them is the red
border:
- aria-invalid="true" is what makes a screen reader say "invalid entry". A red
border says nothing to anyone who cannot see it.
- aria-describedby carries BOTH ids - the hint and the error. Order matters:
they are announced in the order listed, so the error comes last and is what
the user is left with. Replacing the hint with the error instead of
appending it would silently drop the instruction the user still needs.
- The message pairs an icon with the text. Red alone is a colour-only signal
and roughly one man in twelve cannot act on it.
The error text is NOT role="alert" here: it is already described by the
control, so an alert would announce it a second time on every focus.
-->
<div class="esel esel--invalid">
<span class="esel__label" id="esel-label">Billing country</span>
<button
class="esel__trigger"
id="esel-trigger"
type="button"
aria-haspopup="listbox"
aria-expanded="false"
aria-controls="esel-listbox"
aria-labelledby="esel-label esel-trigger"
aria-invalid="true"
aria-describedby="esel-hint esel-error"
>
<span class="esel__value esel__value--empty">Choose a country…</span>
<svg class="esel__chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="m6 9 6 6 6-6" /></svg>
</button>
<ul class="esel__listbox" id="esel-listbox" role="listbox" aria-labelledby="esel-label" tabindex="-1" hidden>
<li class="esel__option" id="esel-opt-de" role="option" data-value="de" aria-selected="false"><span class="esel__marker" aria-hidden="true"></span>Germany</li>
<li class="esel__option" id="esel-opt-fr" role="option" data-value="fr" aria-selected="false"><span class="esel__marker" aria-hidden="true"></span>France</li>
<li class="esel__option" id="esel-opt-nl" role="option" data-value="nl" aria-selected="false"><span class="esel__marker" aria-hidden="true"></span>Netherlands</li>
<li class="esel__option" id="esel-opt-es" role="option" data-value="es" aria-selected="false"><span class="esel__marker" aria-hidden="true"></span>Spain</li>
</ul>
<p class="esel__hint" id="esel-hint">Sets the tax rate on your invoices.</p>
<p class="esel__error" id="esel-error">
<svg class="esel__error-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="12" cy="12" r="10" /><path d="M12 8v4M12 16h.01" />
</svg>
Select a billing country to continue.
</p>
</div>
<script>
document.querySelectorAll('.esel').forEach(function (root) {
var trigger = root.querySelector('.esel__trigger');
var listbox = root.querySelector('.esel__listbox');
var valueEl = root.querySelector('.esel__value');
var options = Array.prototype.slice.call(root.querySelectorAll('.esel__option'));
var active = 0;
function paint() {
options.forEach(function (option, index) {
option.classList.toggle('esel__option--active', index === active);
});
listbox.setAttribute('aria-activedescendant', options[active] ? options[active].id : '');
}
function setOpen(open) {
listbox.hidden = !open;
trigger.setAttribute('aria-expanded', String(open));
if (open) paint();
}
function close() { setOpen(false); trigger.focus(); }
function commit(index) {
var option = options[index];
if (!option) return;
options.forEach(function (o) { o.setAttribute('aria-selected', String(o === option)); });
valueEl.textContent = option.textContent.trim();
valueEl.classList.remove('esel__value--empty');
active = index;
// Clearing the error is not cosmetic: aria-invalid and the describedby
// list have to come off together, or the control keeps announcing an
// error the user has already fixed.
root.classList.remove('esel--invalid');
trigger.removeAttribute('aria-invalid');
trigger.setAttribute('aria-describedby', 'esel-hint');
close();
}
trigger.addEventListener('click', function () {
setOpen(trigger.getAttribute('aria-expanded') !== 'true');
});
options.forEach(function (option, index) {
option.addEventListener('click', function () { commit(index); });
option.addEventListener('mousemove', function () { active = index; paint(); });
});
trigger.addEventListener('keydown', function (event) {
var open = trigger.getAttribute('aria-expanded') === 'true';
var key = event.key;
if (!open) {
if (key === 'ArrowDown' || key === 'ArrowUp' || key === 'Enter' || key === ' ') {
event.preventDefault();
setOpen(true);
}
return;
}
if (key === 'Escape') { event.preventDefault(); close(); return; }
if (key === 'Enter' || key === ' ') { event.preventDefault(); commit(active); return; }
if (key === 'Tab') { setOpen(false); return; }
var next = active;
if (key === 'ArrowDown') next = active + 1;
else if (key === 'ArrowUp') next = active - 1;
else if (key === 'Home') next = 0;
else if (key === 'End') next = options.length - 1;
else return;
event.preventDefault();
active = (next + options.length) % options.length;
paint();
});
document.addEventListener('mousedown', function (event) {
if (!root.contains(event.target)) setOpen(false);
});
});
</script>| Prop | 型 | デフォルト | 説明 |
|---|---|---|---|
label必須 | string | - | 読み込み中に読み上げられるアクセシブルなラベル。 |
items必須 | SelectItem[] | - | レンダリングする項目の配列。 |
value | string | - | 指標の現在値。フォーマット済みの文字列を渡します。 |
message | string | - | 通知の本文。 |
onSelect | (value: string) => void | - | ユーザーが選択したメニュー項目を引数に呼ばれます。 |
disabled | boolean | false | 操作を無効にし、コントロールを淡色表示にします。 |
The red border is the least important part. `aria-invalid="true"` is what makes a screen reader say "invalid entry"; colour says nothing to anyone who cannot see it. `aria-describedby` carries both the hint and the error, in that order, because they are announced in the order listed and the error should be what the user is left holding - replacing the hint with the error silently drops an instruction they still need. The presence of `message` IS the invalid state: there is no separate boolean to fall out of step with it, so a red control with no message cannot be expressed. Note the error text has no `role="alert"` - the control already describes it, and an alert would announce it twice on every focus.