mirror of
https://github.com/vercel/components.build.git
synced 2026-09-14 20:06:39 +08:00
55fae375a4
* Update Geistdocs * Update geistdocs.tsx * Run translation script * Delete toc.tsx * Update route.ts * Update route.ts
811 lines
19 KiB
Plaintext
811 lines
19 KiB
Plaintext
---
|
||
title: Accesibilitate
|
||
description: Construirea de componente utilizabile de către toată lumea, inclusiv utilizatorii cu dizabilități care se bazează pe tehnologii asistive.
|
||
---
|
||
|
||
Accesibilitatea (a11y) nu este o caracteristică opțională — este o cerință fundamentală pentru componentele web moderne. Fiecare componentă trebuie să fie utilizabilă de toată lumea, inclusiv de persoanele cu dizabilități vizuale, motorii, auditive sau cognitive.
|
||
|
||
Acest ghid este o listă neexhaustivă de principii și modele de accesibilitate pe care ar trebui să le urmați atunci când construiți componente. Nu este un ghid complet, dar ar trebui să vă ofere o idee despre tipurile de probleme de care ar trebui să țineți cont.
|
||
|
||
Dacă folosiți un linter cu reguli stricte de accesibilitate precum [Ultracite](https://www.ultracite.ai), astfel de probleme vor fi probabil detectate automat, dar este totuși important să înțelegeți principiile.
|
||
|
||
## Principii de bază
|
||
|
||
### 1. HTML semantic mai întâi
|
||
|
||
Începeți întotdeauna cu cel mai potrivit element HTML. HTML-ul semantic oferă funcții de accesibilitate încorporate pe care implementările personalizate le pot rata frecvent.
|
||
|
||
```tsx
|
||
// ❌ Don't reinvent the wheel
|
||
<div onClick={handleClick} className="button">
|
||
Click me
|
||
</div>
|
||
|
||
// ✅ Use semantic elements
|
||
<button onClick={handleClick}>
|
||
Click me
|
||
</button>
|
||
```
|
||
|
||
Elementele semantice includ anunțuri de rol adecvate, interacțiune cu tastatura, gestionarea focusului și participare la formulare.
|
||
|
||
### 2. Navigare cu tastatura
|
||
|
||
Fiecare element interactiv trebuie să fie accesibil de la tastatură. Utilizatorii ar trebui să poată naviga, activa și interacționa cu toate funcționalitățile folosind doar tastatura.
|
||
|
||
```tsx
|
||
// ✅ 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. Suport pentru cititoarele de ecran
|
||
|
||
Asigurați-vă că tot conținutul și interacțiunile sunt anunțate corect cititoarelor de ecran, folosind atribute ARIA atunci când este necesar.
|
||
|
||
```tsx
|
||
// ✅ 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. Accesibilitate vizuală
|
||
|
||
Sprijiniți utilizatorii cu deficiențe de vedere prin contrast adecvat, indicatori de focus și redimensionare reactivă a textului.
|
||
|
||
```css
|
||
/* ✅ 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 */
|
||
}
|
||
```
|
||
|
||
## Modele ARIA
|
||
|
||
### Înțelegerea ARIA
|
||
|
||
ARIA (Accessible Rich Internet Applications) oferă informații semantice despre elemente pentru tehnologiile asistive. Folosiți ARIA pentru a îmbunătăți, nu pentru a înlocui, HTML-ul semantic.
|
||
|
||
Are câteva reguli pe care ar trebui să le respectați:
|
||
|
||
1. Nu folosiți ARIA dacă puteți folosi HTML semantic
|
||
2. Nu schimbați semantica nativă decât dacă este necesar
|
||
3. Toate elementele interactive trebuie să fie accesibile de la tastatură
|
||
4. Nu ascundeți elementele focusabile de tehnologiile asistive
|
||
5. Toate elementele interactive trebuie să aibă nume accesibile
|
||
|
||
### Atribute ARIA comune
|
||
|
||
#### Roluri
|
||
|
||
Definește ce este un element:
|
||
|
||
```tsx
|
||
// 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ări
|
||
|
||
Describ starea curentă a unui element:
|
||
|
||
```tsx
|
||
// 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>
|
||
```
|
||
|
||
#### Proprietăți
|
||
|
||
Oferă informații adiționale:
|
||
|
||
```tsx
|
||
// 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>
|
||
```
|
||
|
||
## Modele de componente
|
||
|
||
### Modal/Dialog
|
||
|
||
Modalurile necesită gestionare atentă a focusului și blocarea tastaturii:
|
||
|
||
```tsx
|
||
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>
|
||
);
|
||
}
|
||
```
|
||
|
||
### Meniu derulant
|
||
|
||
Meniurile derulante au nevoie de atribute ARIA adecvate și navigare cu tastatura:
|
||
|
||
```tsx
|
||
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>
|
||
);
|
||
}
|
||
```
|
||
|
||
### Taburi
|
||
|
||
Interfețele cu taburi necesită modele ARIA specifice și navigare cu tastatura:
|
||
|
||
```tsx
|
||
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>
|
||
);
|
||
}
|
||
```
|
||
|
||
### Formulare
|
||
|
||
Formularele au nevoie de etichete clare, mesaje de eroare și feedback de validare:
|
||
|
||
```tsx
|
||
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>
|
||
);
|
||
}
|
||
```
|
||
|
||
## Gestionarea focusului
|
||
|
||
### Focus vizibil
|
||
|
||
Afișați indicatori de focus doar pentru navigarea cu tastatura:
|
||
|
||
```css
|
||
/* 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);
|
||
}
|
||
```
|
||
|
||
### Blocarea focusului
|
||
|
||
Păstrați focusul într-o regiune specifică:
|
||
|
||
```tsx
|
||
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]);
|
||
}
|
||
```
|
||
|
||
### Restabilirea focusului
|
||
|
||
Returnați focusul la elementul potrivit după interacțiuni:
|
||
|
||
```tsx
|
||
function useRestoreFocus() {
|
||
const previousFocus = useRef<HTMLElement | null>(null);
|
||
|
||
const saveFocus = () => {
|
||
previousFocus.current = document.activeElement as HTMLElement;
|
||
};
|
||
|
||
const restoreFocus = () => {
|
||
previousFocus.current?.focus();
|
||
};
|
||
|
||
return { saveFocus, restoreFocus };
|
||
}
|
||
```
|
||
|
||
## Regiuni live
|
||
|
||
Anunțați modificările dinamice de conținut cititoarelor de ecran:
|
||
|
||
### Mesaje de stare
|
||
|
||
```tsx
|
||
// 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>
|
||
```
|
||
|
||
### Indicatori de progres
|
||
|
||
```tsx
|
||
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>
|
||
);
|
||
}
|
||
```
|
||
|
||
## Culoare și contrast
|
||
|
||
### Cerințe de contrast
|
||
|
||
Urmăriți ghidurile WCAG pentru contrastul culorilor:
|
||
|
||
```css
|
||
/* 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 */
|
||
}
|
||
```
|
||
|
||
### Independența culorii
|
||
|
||
Nu transmiteți informații folosind numai culoarea:
|
||
|
||
```tsx
|
||
// ❌ 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>
|
||
)}
|
||
```
|
||
|
||
## Accesibilitate pe mobil
|
||
|
||
### Ținte tactile
|
||
|
||
Asigurați-vă că țintele tactile sunt suficient de mari:
|
||
|
||
```css
|
||
/* 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;
|
||
}
|
||
```
|
||
|
||
### Viewport și zoom
|
||
|
||
Permiteți utilizatorilor să mărească/ micșoreze:
|
||
|
||
```html
|
||
<!-- ✅ 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">
|
||
```
|
||
|
||
## Capcane comune
|
||
|
||
### Textul placeholder folosit ca etichetă
|
||
|
||
Nu folosiți placeholder-ul ca singura etichetă:
|
||
|
||
```tsx
|
||
// ❌ 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>
|
||
```
|
||
|
||
### Butoane goale
|
||
|
||
Furnizați întotdeauna text accesibil pentru butoanele cu pictograme:
|
||
|
||
```tsx
|
||
// ❌ 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>
|
||
```
|
||
|
||
### Elemente de formular dezactivate
|
||
|
||
Elementele dezactivate nu pot primi focus, ceea ce poate confuza utilizatorii:
|
||
|
||
```tsx
|
||
// ❌ 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>
|
||
``` |