Inrochtaineacht
Comhpháirteanna a thógáil atá inúsáidte ag gach duine, lena n-áirítear úsáideoirí le míchumais a bhfuil ag brath ar theicneolaíochtaí cúnta.
Inrochtaineacht (a11y) ní gné roghnach í—is éard atá inti ná riachtanas bunúsach do chomhpháirteanna gréasáin nua-aimseartha. Caithfidh gach comhpháirt a bheith inúsáidte ag gach duine, lena n-áirítear daoine le míchumais radhairc, mótair, éisteachta, nó chognaíocha.
Is liosta neamhiomlán é an treoir seo de phrionsabail agus de phatrúin inrochtaineachta ar chóir duit iad a leanúint agus tú ag tógáil comhpháirteanna. Ní treoir chuimsitheach í, ach ba chóir di léargas a thabhairt duit ar na cineálacha fadhbanna ar chóir duit a bheith ar an eolas fúthu.
Má úsáideann tú linter le rialacha inrochtaineachta láidre cosúil le Ultracite, is dócha go mbailítear na cineálacha fadhbanna sin go huathoibríoch, ach tá sé fós tábhachtach na prionsabail a thuiscint.
Prionsabail Bhunúsacha
1. HTML Seimeantach ar dtús
Tosaigh i gcónaí leis an eilimint HTML is oiriúnaí. Soláthraíonn HTML seimeantach gnéithe inrochtaineachta tógtha isteach nach mbíonn i bhfeidhmiúcháin saincheaptha go minic.
// ❌ Don't reinvent the wheel
<div onClick={handleClick} className="button">
Click me
</div>
// ✅ Use semantic elements
<button onClick={handleClick}>
Click me
</button>Tagann eilimintí seimeantacha le fógraí róil oiriúnacha, idirghníomhaíocht méarchlár, bainistíocht fócas, agus rannpháirtíocht i bhfoirmeacha.
2. Nascleanúint Méarchláir
Caithfidh gach eilimint idirghníomhach a bheith inrochtana trí mhéarchlár. Ba chóir do dhaoine a bheith in ann nascleanúint, gníomhú, agus idirghníomhú leis an bhfeidhmiúlacht go léir ag baint úsáide as an méarchlár amháin.
// ✅ Complete keyboard support
function Menu() {
const handleKeyDown = (e: React.KeyboardEvent) => {
switch(e.key) {
case 'ArrowDown':
focusNextItem();
break;
case 'ArrowUp':
focusPreviousItem();
break;
case 'Home':
focusFirstItem();
break;
case 'End':
focusLastItem();
break;
case 'Escape':
closeMenu();
break;
}
};
return (
<div role="menu" onKeyDown={handleKeyDown}>
{/* menu items */}
</div>
);
}3. Tacaíocht do Léitheoirí Scáileáin
Déan cinnte go ndéantar ábhar agus idirghníomhaíochtaí a fhógairt i gceart do léitheoirí scáileáin ag baint úsáide as airíonna ARIA nuair is gá.
// ✅ Proper ARIA labeling
<nav aria-label="Main navigation">
<ul>
<li><a href="/" aria-current="page">Home</a></li>
<li><a href="/about">About</a></li>
</ul>
</nav>
// ✅ Dynamic content announcements
<div aria-live="polite" aria-atomic="true">
{isLoading && <span>Loading results...</span>}
{results && <span>{results.length} results found</span>}
</div>4. Inrochtaineacht Amhairc
Tacaigh le húsáideoirí le míchumais radhairc trí chaighdeán codarsnachta cuí, taispeáin innéacs fócas, agus méideanna téacs freagracha.
/* ✅ Visible focus indicators */
button:focus-visible {
outline: 2px solid var(--color-focus);
outline-offset: 2px;
}
/* ✅ Sufficient color contrast (4.5:1 for normal text, 3:1 for large text) */
.text {
color: #333; /* Against white: 12.6:1 ratio */
background: white;
}
/* ✅ Responsive text sizing */
.text {
font-size: 1rem; /* Respects user preferences */
}Pátrúin ARIA
Ag tuiscint ARIA
Cuireann ARIA (Accessible Rich Internet Applications) faisnéis sheimeantach faoi eilimintí ar fáil do theicneolaíochtaí cúnta. Úsáid ARIA chun HTML seimeantach a fheabhsú, ní chun é a chur in ionad.
Tá cúpla riail ann ba chóir duit a leanúint:
- Ná húsáid ARIA má tá HTML seimeantach ar fáil
- Ná déan seimeantachas dúchasach a athrú mura bhfuil sé riachtanach
- Caithfidh gach eilimint idirghníomhach a bheith inrochtana trí mhéarchlár
- Ná foláireamh eilimintí fócasacha ó theicneolaíochtaí cúnta
- Caithfidh uimhreacha idirghníomhacha go léir ainmneacha inrochtana a bheith acu
Aitreabhtáin ARIA Coitianta
Róil
Sainmhínigh cad é eilimint:
// Widget roles
<div role="button" tabIndex={0} onClick={handleClick}>
Custom Button
</div>
// Landmark roles
<div role="navigation" aria-label="Breadcrumb">
{/* breadcrumb items */}
</div>
// Live region roles
<div role="alert">
Error: Invalid email address
</div>Stáit
Cur síos a dhéanamh ar staid reatha eilimint:
// Checked state
<div
role="checkbox"
aria-checked={isChecked}
tabIndex={0}
>
Accept terms
</div>
// Expanded state
<button
aria-expanded={isOpen}
aria-controls="panel-1"
>
Toggle Panel
</button>
<div id="panel-1" hidden={!isOpen}>
Panel content
</div>
// Selected state
<li
role="option"
aria-selected={isSelected}
>
Option 1
</li>Airíonna
Soláthraigh faisnéis bhreise:
// Labels and descriptions
<input
aria-label="Search"
aria-describedby="search-help"
type="search"
/>
<span id="search-help">Press Enter to search</span>
// Relationships
<button aria-controls="modal-1">Open Modal</button>
<div id="modal-1" role="dialog">{/* modal content */}</div>
// Required and invalid
<input
aria-required="true"
aria-invalid={hasError}
aria-errormessage="email-error"
/>
<span id="email-error">Please enter a valid email</span>Pátrúin Comhpháirte
Modal/Dialóg
Éilíonn modals bainistíocht fócas cúramach agus greamú méarchláir:
function Modal({ isOpen, onClose, children }) {
const modalRef = useRef<HTMLDivElement>(null);
const previousFocus = useRef<HTMLElement | null>(null);
useEffect(() => {
if (isOpen) {
// Store current focus
previousFocus.current = document.activeElement as HTMLElement;
// Focus first focusable element in modal
const firstFocusable = modalRef.current?.querySelector<HTMLElement>(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
firstFocusable?.focus();
// Prevent body scroll
document.body.style.overflow = 'hidden';
} else {
// Restore focus
previousFocus.current?.focus();
document.body.style.overflow = '';
}
return () => {
document.body.style.overflow = '';
};
}, [isOpen]);
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Escape') {
onClose();
}
if (e.key === 'Tab') {
// Trap focus within modal
const focusables = modalRef.current?.querySelectorAll<HTMLElement>(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
if (focusables && focusables.length > 0) {
const firstFocusable = focusables[0];
const lastFocusable = focusables[focusables.length - 1];
if (e.shiftKey && document.activeElement === firstFocusable) {
e.preventDefault();
lastFocusable.focus();
} else if (!e.shiftKey && document.activeElement === lastFocusable) {
e.preventDefault();
firstFocusable.focus();
}
}
}
};
if (!isOpen) return null;
return (
<div
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
ref={modalRef}
onKeyDown={handleKeyDown}
className="modal"
>
<button
onClick={onClose}
aria-label="Close dialog"
className="close-button"
>
×
</button>
{children}
</div>
);
}Roghchlár Síos
Teastaíonn airíonna ARIA cuí agus nascleanúint méarchláir ó roghchláir anuas:
function DropdownMenu({ items }) {
const [isOpen, setIsOpen] = useState(false);
const [selectedIndex, setSelectedIndex] = useState(-1);
const menuRef = useRef<HTMLUListElement>(null);
const handleKeyDown = (e: React.KeyboardEvent) => {
switch(e.key) {
case 'ArrowDown':
e.preventDefault();
if (!isOpen) {
setIsOpen(true);
setSelectedIndex(0);
} else {
setSelectedIndex(prev =>
prev < items.length - 1 ? prev + 1 : 0
);
}
break;
case 'ArrowUp':
e.preventDefault();
if (isOpen) {
setSelectedIndex(prev =>
prev > 0 ? prev - 1 : items.length - 1
);
}
break;
case 'Enter':
case ' ':
e.preventDefault();
if (!isOpen) {
setIsOpen(true);
} else if (selectedIndex >= 0) {
items[selectedIndex].onClick();
setIsOpen(false);
}
break;
case 'Escape':
setIsOpen(false);
setSelectedIndex(-1);
break;
}
};
return (
<div className="dropdown">
<button
aria-haspopup="true"
aria-expanded={isOpen}
aria-controls="dropdown-menu"
onKeyDown={handleKeyDown}
onClick={() => setIsOpen(!isOpen)}
>
Menu
</button>
{isOpen && (
<ul
id="dropdown-menu"
role="menu"
ref={menuRef}
onKeyDown={handleKeyDown}
>
{items.map((item, index) => (
<li
key={item.id}
role="menuitem"
tabIndex={-1}
aria-selected={index === selectedIndex}
onClick={() => {
item.onClick();
setIsOpen(false);
}}
>
{item.label}
</li>
))}
</ul>
)}
</div>
);
}Cluaisíní
Éilíonn comhéadais cluaisíní patrúin ARIA agus nascleanúint méarchláir ar leith:
function Tabs({ tabs, defaultTab = 0 }) {
const [activeTab, setActiveTab] = useState(defaultTab);
const handleKeyDown = (e: React.KeyboardEvent, index: number) => {
let newIndex = index;
switch(e.key) {
case 'ArrowLeft':
newIndex = index > 0 ? index - 1 : tabs.length - 1;
break;
case 'ArrowRight':
newIndex = index < tabs.length - 1 ? index + 1 : 0;
break;
case 'Home':
newIndex = 0;
break;
case 'End':
newIndex = tabs.length - 1;
break;
default:
return;
}
e.preventDefault();
setActiveTab(newIndex);
// Focus the newly selected tab
const tabElement = document.getElementById(`tab-${newIndex}`);
tabElement?.focus();
};
return (
<div className="tabs">
<div role="tablist" aria-label="Tabs">
{tabs.map((tab, index) => (
<button
key={tab.id}
id={`tab-${index}`}
role="tab"
aria-selected={activeTab === index}
aria-controls={`panel-${index}`}
tabIndex={activeTab === index ? 0 : -1}
onClick={() => setActiveTab(index)}
onKeyDown={(e) => handleKeyDown(e, index)}
>
{tab.label}
</button>
))}
</div>
{tabs.map((tab, index) => (
<div
key={tab.id}
id={`panel-${index}`}
role="tabpanel"
aria-labelledby={`tab-${index}`}
hidden={activeTab !== index}
tabIndex={0}
>
{tab.content}
</div>
))}
</div>
);
}Foirmeacha
Teastaíonn lipéid shoiléire, teachtaireachtaí earráide, agus aiseolas bailíochtúcháin ó fhoirmeacha:
function AccessibleForm() {
const [errors, setErrors] = useState({});
return (
<form aria-label="Contact form">
<div className="form-group">
<label htmlFor="email">
Email Address
<span aria-label="required">*</span>
</label>
<input
id="email"
type="email"
aria-required="true"
aria-invalid={!!errors.email}
aria-describedby={errors.email ? "email-error" : "email-help"}
/>
<span id="email-help" className="help-text">
We'll never share your email
</span>
{errors.email && (
<span id="email-error" role="alert" className="error">
{errors.email}
</span>
)}
</div>
<fieldset>
<legend>Notification Preferences</legend>
<div>
<input
id="notify-email"
type="checkbox"
name="notifications"
value="email"
/>
<label htmlFor="notify-email">Email notifications</label>
</div>
<div>
<input
id="notify-sms"
type="checkbox"
name="notifications"
value="sms"
/>
<label htmlFor="notify-sms">SMS notifications</label>
</div>
</fieldset>
<button type="submit">Submit</button>
</form>
);
}Bainistíocht Fócas
Fócas le Feiceáil
Taispeáin innéacsanna fócas ach amháin do nascleanúint méarchláir:
/* Remove default outline */
*:focus {
outline: none;
}
/* Show outline only for keyboard focus */
*:focus-visible {
outline: 2px solid var(--color-focus);
outline-offset: 2px;
}
/* Custom focus styles for specific components */
.button:focus-visible {
box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.5);
}Greamú Fócas
Coinnigh fócas laistigh de réigiún sonraithe:
function useFocusTrap(ref: React.RefObject<HTMLElement>, isActive: boolean) {
useEffect(() => {
if (!isActive || !ref.current) return;
const element = ref.current;
const focusableSelector =
'a[href], button, textarea, input[type="text"], input[type="radio"], input[type="checkbox"], select, [tabindex]:not([tabindex="-1"])';
const focusableElements = element.querySelectorAll<HTMLElement>(focusableSelector);
const firstFocusable = focusableElements[0];
const lastFocusable = focusableElements[focusableElements.length - 1];
const handleTabKey = (e: KeyboardEvent) => {
if (e.key !== 'Tab') return;
if (e.shiftKey) {
if (document.activeElement === firstFocusable) {
e.preventDefault();
lastFocusable?.focus();
}
} else {
if (document.activeElement === lastFocusable) {
e.preventDefault();
firstFocusable?.focus();
}
}
};
element.addEventListener('keydown', handleTabKey);
firstFocusable?.focus();
return () => {
element.removeEventListener('keydown', handleTabKey);
};
}, [ref, isActive]);
}Atógáil Fócas
Filleadh ar an eilimint cheart tar éis idirghníomhaíochtaí a chríochnú:
function useRestoreFocus() {
const previousFocus = useRef<HTMLElement | null>(null);
const saveFocus = () => {
previousFocus.current = document.activeElement as HTMLElement;
};
const restoreFocus = () => {
previousFocus.current?.focus();
};
return { saveFocus, restoreFocus };
}Réigiúin Beo
Fógair athruithe ar ábhar dinimiciúil do léitheoirí scáileáin:
Teachtaireachtaí Stádais
// Polite announcement (waits for screen reader to finish)
<div role="status" aria-live="polite">
{savedMessage && "Settings saved successfully"}
</div>
// Assertive announcement (interrupts screen reader)
<div role="alert" aria-live="assertive">
{errorMessage && `Error: ${errorMessage}`}
</div>
// Loading states
<div aria-live="polite" aria-busy={isLoading}>
{isLoading ? "Loading..." : `${items.length} items loaded`}
</div>Táscairí Dul Chun Cinn
function ProgressBar({ value, max = 100 }) {
return (
<div
role="progressbar"
aria-valuenow={value}
aria-valuemin={0}
aria-valuemax={max}
aria-label="Upload progress"
>
<div
className="progress-fill"
style={{ width: `${(value / max) * 100}%` }}
/>
<span className="sr-only">
{Math.round((value / max) * 100)}% complete
</span>
</div>
);
}Dath agus Codarsnacht
Riachtanais Chodarsnacha
Lean treoirlínte WCAG maidir le codarsnacht datha:
/* Normal text (< 18pt or < 14pt bold) */
.text {
color: #595959; /* 7:1 ratio against white */
background: white;
}
/* Large text (≥ 18pt or ≥ 14pt bold) */
.heading {
color: #767676; /* 4.5:1 ratio against white */
font-size: 1.5rem;
font-weight: bold;
}
/* Non-text elements (icons, borders) */
.icon {
color: #949494; /* 3:1 ratio against white */
}Neamhspleáchas Datha
Ná húsáid dath amháin chun faisnéis a chur in iúl:
// ❌ Color only
<span className="text-red-500">Error</span>
// ✅ Color with text/icon
<span className="text-red-500">
<ErrorIcon aria-hidden="true" />
<span>Error: Invalid input</span>
</span>
// ✅ Multiple indicators
<input
className={hasError ? 'border-red-500' : 'border-gray-300'}
aria-invalid={hasError}
aria-describedby={hasError ? 'error-message' : undefined}
/>
{hasError && (
<span id="error-message" className="text-red-500">
<ErrorIcon aria-hidden="true" /> This field is required
</span>
)}Inrochtaineacht Soghluaiste
Spriocanna Teagmhála
Déan cinnte go bhfuil spriocanna teagmhála mór go leor:
/* Minimum 44x44px for iOS, 48x48dp for Android */
.button {
min-height: 44px;
min-width: 44px;
padding: 12px 16px;
}
/* Add invisible touch area for small icons */
.icon-button {
position: relative;
padding: 8px;
}
.icon-button::before {
content: '';
position: absolute;
top: -8px;
right: -8px;
bottom: -8px;
left: -8px;
}Radharc agus Zúmáil
Láimhseáil úsáideoirí a cheadú zúmáil a úsáid:
<!-- ✅ Allows zooming -->
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- ❌ Prevents zooming -->
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">Bearnaí Coitianta
Téacs Áitliosta mar Lipéid
Ná bain úsáid as áiteacha ina n-aonar mar lipéid:
// ❌ Placeholder disappears when typing
<input placeholder="Email address" />
// ✅ Persistent label
<label>
Email address
<input type="email" />
</label>
// ✅ Floating label pattern
<div className="form-field">
<input id="email" placeholder=" " />
<label htmlFor="email">Email address</label>
</div>Cnaipí Folamh
Tabhair téacs inrochtana i gcónaí do chnaipí siombail:
// ❌ No accessible name
<button onClick={handleDelete}>
<TrashIcon />
</button>
// ✅ Screen reader text
<button onClick={handleDelete} aria-label="Delete item">
<TrashIcon aria-hidden="true" />
</button>
// ✅ Visually hidden text
<button onClick={handleDelete}>
<TrashIcon aria-hidden="true" />
<span className="sr-only">Delete item</span>
</button>Eilimintí Foirme faoi Mhíchumas
Níl eilimintí faoi mhíchumas inchliceáilte, rud a d'fhéadfadh mearbhall a chur ar úsáideoirí:
// ❌ User can't understand why button is disabled
<button disabled={!isValid}>
Submit
</button>
// ✅ Use aria-disabled and explain
<button
aria-disabled={!isValid}
aria-describedby="submit-help"
onClick={isValid ? handleSubmit : undefined}
className={!isValid ? 'opacity-50 cursor-not-allowed' : ''}
>
Submit
</button>
<span id="submit-help">
{!isValid && 'Please fill in all required fields'}
</span>