or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

accessibility.mdanimation.mdcore-utilities.mdform-controls.mdhooks.mdindex.mdinteractive-components.mdlayout-components.mdlist-components.mdmedia-components.mdplatform-apis.mdstylesheet.mdsystem-integration.mdtext-input.md

accessibility.mddocs/

0

# Accessibility

1

2

React Native's accessibility API adapted for web with comprehensive support for screen readers, reduced motion preferences, focus management, and WCAG-compliant accessibility features.

3

4

## AccessibilityInfo

5

6

Accessibility information API providing methods to query accessibility settings, manage focus, and communicate with assistive technologies on web platforms.

7

8

```javascript { .api }

9

const AccessibilityInfo: {

10

isScreenReaderEnabled: () => Promise<boolean>;

11

isReduceMotionEnabled: () => Promise<boolean>;

12

addEventListener: (eventName: string, handler: Function) => { remove: () => void };

13

removeEventListener: (eventName: string, handler: Function) => void;

14

setAccessibilityFocus: (reactTag: number) => void;

15

announceForAccessibility: (announcement: string) => void;

16

fetch: () => Promise<boolean>; // deprecated

17

};

18

```

19

20

### isScreenReaderEnabled()

21

Query whether a screen reader is currently enabled (always returns true on web).

22

23

```javascript { .api }

24

AccessibilityInfo.isScreenReaderEnabled(): Promise<boolean>

25

```

26

27

### isReduceMotionEnabled()

28

Query whether the user prefers reduced motion based on system settings.

29

30

```javascript { .api }

31

AccessibilityInfo.isReduceMotionEnabled(): Promise<boolean>

32

```

33

34

### addEventListener()

35

Add event listeners for accessibility-related changes.

36

37

```javascript { .api }

38

AccessibilityInfo.addEventListener(

39

eventName: 'reduceMotionChanged' | 'screenReaderChanged',

40

handler: (enabled: boolean) => void

41

): { remove: () => void }

42

```

43

44

### setAccessibilityFocus()

45

Set accessibility focus to a specific element (web implementation is no-op).

46

47

```javascript { .api }

48

AccessibilityInfo.setAccessibilityFocus(reactTag: number): void

49

```

50

51

### announceForAccessibility()

52

Announce a message to screen readers (web implementation is no-op, use aria-live regions instead).

53

54

```javascript { .api }

55

AccessibilityInfo.announceForAccessibility(announcement: string): void

56

```

57

58

**Usage:**

59

```javascript

60

import { AccessibilityInfo } from "react-native-web";

61

62

function AccessibilityDemo() {

63

const [isScreenReaderEnabled, setIsScreenReaderEnabled] = React.useState(false);

64

const [isReduceMotionEnabled, setIsReduceMotionEnabled] = React.useState(false);

65

const [announcements, setAnnouncements] = React.useState([]);

66

67

React.useEffect(() => {

68

// Query initial accessibility settings

69

AccessibilityInfo.isScreenReaderEnabled().then(setIsScreenReaderEnabled);

70

AccessibilityInfo.isReduceMotionEnabled().then(setIsReduceMotionEnabled);

71

72

// Listen for reduced motion changes

73

const subscription = AccessibilityInfo.addEventListener(

74

'reduceMotionChanged',

75

(enabled) => {

76

setIsReduceMotionEnabled(enabled);

77

console.log('Reduced motion preference changed:', enabled);

78

}

79

);

80

81

return () => subscription.remove();

82

}, []);

83

84

const announceToScreenReader = (message) => {

85

// Web-specific implementation using aria-live regions

86

setAnnouncements(prev => [...prev, { id: Date.now(), message }]);

87

88

// Also call the API (no-op on web, but maintains compatibility)

89

AccessibilityInfo.announceForAccessibility(message);

90

91

// Remove announcement after it's been read

92

setTimeout(() => {

93

setAnnouncements(prev => prev.filter(a => a.message !== message));

94

}, 3000);

95

};

96

97

const handleButtonPress = () => {

98

announceToScreenReader('Button was pressed successfully');

99

};

100

101

return (

102

<View style={styles.container}>

103

<Text style={styles.title}>Accessibility Information</Text>

104

105

{/* Screen reader status */}

106

<View style={styles.statusContainer}>

107

<Text style={styles.label}>Screen Reader:</Text>

108

<Text style={styles.value}>

109

{isScreenReaderEnabled ? 'Enabled' : 'Disabled'}

110

</Text>

111

</View>

112

113

{/* Reduced motion status */}

114

<View style={styles.statusContainer}>

115

<Text style={styles.label}>Reduced Motion:</Text>

116

<Text style={styles.value}>

117

{isReduceMotionEnabled ? 'Preferred' : 'Not Preferred'}

118

</Text>

119

</View>

120

121

{/* Interactive elements */}

122

<TouchableOpacity

123

style={styles.button}

124

onPress={handleButtonPress}

125

accessibilityRole="button"

126

accessibilityLabel="Announce test button"

127

accessibilityHint="Tap to test screen reader announcement"

128

>

129

<Text style={styles.buttonText}>Test Announcement</Text>

130

</TouchableOpacity>

131

132

<TouchableOpacity

133

style={styles.button}

134

onPress={() => announceToScreenReader('This is a custom announcement')}

135

accessibilityRole="button"

136

accessibilityLabel="Custom announcement button"

137

>

138

<Text style={styles.buttonText}>Custom Announcement</Text>

139

</TouchableOpacity>

140

141

{/* Aria-live region for announcements */}

142

<View

143

style={styles.announcements}

144

aria-live="polite"

145

aria-atomic="true"

146

>

147

{announcements.map(announcement => (

148

<Text

149

key={announcement.id}

150

style={styles.srOnly}

151

>

152

{announcement.message}

153

</Text>

154

))}

155

</View>

156

</View>

157

);

158

}

159

160

// Enhanced accessibility utilities

161

class WebAccessibilityManager {

162

static mediaQuery = null;

163

static listeners = new Set();

164

165

static init() {

166

if (typeof window !== 'undefined' && !this.mediaQuery) {

167

this.mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)');

168

this.mediaQuery.addEventListener('change', this.handleMediaChange);

169

}

170

}

171

172

static handleMediaChange = (event) => {

173

this.listeners.forEach(callback => {

174

callback(event.matches);

175

});

176

};

177

178

static addReducedMotionListener(callback) {

179

this.init();

180

this.listeners.add(callback);

181

182

// Call immediately with current value

183

if (this.mediaQuery) {

184

callback(this.mediaQuery.matches);

185

}

186

187

return {

188

remove: () => {

189

this.listeners.delete(callback);

190

}

191

};

192

}

193

194

static async getReducedMotionPreference() {

195

this.init();

196

return this.mediaQuery ? this.mediaQuery.matches : false;

197

}

198

199

// Focus management

200

static setFocus(element) {

201

if (element && typeof element.focus === 'function') {

202

element.focus();

203

return true;

204

}

205

return false;

206

}

207

208

static trapFocus(container) {

209

const focusableElements = container.querySelectorAll(

210

'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'

211

);

212

213

if (focusableElements.length === 0) return { release: () => {} };

214

215

const firstElement = focusableElements[0];

216

const lastElement = focusableElements[focusableElements.length - 1];

217

218

const handleTabKey = (e) => {

219

if (e.key !== 'Tab') return;

220

221

if (e.shiftKey) {

222

if (document.activeElement === firstElement) {

223

e.preventDefault();

224

lastElement.focus();

225

}

226

} else {

227

if (document.activeElement === lastElement) {

228

e.preventDefault();

229

firstElement.focus();

230

}

231

}

232

};

233

234

container.addEventListener('keydown', handleTabKey);

235

firstElement.focus();

236

237

return {

238

release: () => {

239

container.removeEventListener('keydown', handleTabKey);

240

}

241

};

242

}

243

244

// Screen reader announcements

245

static createAnnouncementRegion() {

246

let region = document.getElementById('accessibility-announcements');

247

248

if (!region) {

249

region = document.createElement('div');

250

region.id = 'accessibility-announcements';

251

region.setAttribute('aria-live', 'polite');

252

region.setAttribute('aria-atomic', 'true');

253

region.style.position = 'absolute';

254

region.style.left = '-10000px';

255

region.style.width = '1px';

256

region.style.height = '1px';

257

region.style.overflow = 'hidden';

258

document.body.appendChild(region);

259

}

260

261

return region;

262

}

263

264

static announce(message, priority = 'polite') {

265

const region = this.createAnnouncementRegion();

266

region.setAttribute('aria-live', priority);

267

268

// Clear and set new message

269

region.textContent = '';

270

setTimeout(() => {

271

region.textContent = message;

272

}, 100);

273

274

// Clear after announcement

275

setTimeout(() => {

276

region.textContent = '';

277

}, 3000);

278

}

279

280

// High contrast detection

281

static detectHighContrast() {

282

if (typeof window === 'undefined') return false;

283

284

// Check for Windows high contrast mode

285

const testDiv = document.createElement('div');

286

testDiv.style.backgroundImage = 'url(data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==)';

287

testDiv.style.position = 'absolute';

288

testDiv.style.left = '-9999px';

289

document.body.appendChild(testDiv);

290

291

const hasBackgroundImage = window.getComputedStyle(testDiv).backgroundImage !== 'none';

292

document.body.removeChild(testDiv);

293

294

return !hasBackgroundImage;

295

}

296

297

// Color scheme preference

298

static getColorSchemePreference() {

299

if (typeof window === 'undefined') return 'light';

300

301

const darkModeQuery = window.matchMedia('(prefers-color-scheme: dark)');

302

return darkModeQuery.matches ? 'dark' : 'light';

303

}

304

}

305

306

// Accessible component examples

307

function AccessibleModal({ visible, onClose, title, children }) {

308

const modalRef = React.useRef(null);

309

const [focusTrap, setFocusTrap] = React.useState(null);

310

311

React.useEffect(() => {

312

if (visible && modalRef.current) {

313

const trap = WebAccessibilityManager.trapFocus(modalRef.current);

314

setFocusTrap(trap);

315

316

// Announce modal opening

317

WebAccessibilityManager.announce(`${title} dialog opened`);

318

319

return () => {

320

trap.release();

321

WebAccessibilityManager.announce('Dialog closed');

322

};

323

}

324

}, [visible, title]);

325

326

if (!visible) return null;

327

328

return (

329

<View

330

style={styles.modalOverlay}

331

accessibilityRole="dialog"

332

accessibilityModal={true}

333

accessibilityLabel={title}

334

>

335

<View

336

ref={modalRef}

337

style={styles.modalContent}

338

accessibilityViewIsModal={true}

339

>

340

<View style={styles.modalHeader}>

341

<Text style={styles.modalTitle} accessibilityRole="heading">

342

{title}

343

</Text>

344

<TouchableOpacity

345

onPress={onClose}

346

style={styles.closeButton}

347

accessibilityRole="button"

348

accessibilityLabel="Close dialog"

349

>

350

<Text>×</Text>

351

</TouchableOpacity>

352

</View>

353

354

<View style={styles.modalBody}>

355

{children}

356

</View>

357

</View>

358

</View>

359

);

360

}

361

362

function AccessibleForm() {

363

const [formData, setFormData] = React.useState({

364

name: '',

365

email: '',

366

message: ''

367

});

368

const [errors, setErrors] = React.useState({});

369

370

const validateForm = () => {

371

const newErrors = {};

372

373

if (!formData.name.trim()) {

374

newErrors.name = 'Name is required';

375

}

376

377

if (!formData.email.trim()) {

378

newErrors.email = 'Email is required';

379

} else if (!/\S+@\S+\.\S+/.test(formData.email)) {

380

newErrors.email = 'Email is invalid';

381

}

382

383

setErrors(newErrors);

384

385

if (Object.keys(newErrors).length > 0) {

386

WebAccessibilityManager.announce('Form has errors. Please review and correct.');

387

return false;

388

}

389

390

return true;

391

};

392

393

const handleSubmit = () => {

394

if (validateForm()) {

395

WebAccessibilityManager.announce('Form submitted successfully');

396

// Handle form submission

397

}

398

};

399

400

return (

401

<View style={styles.form}>

402

<Text style={styles.formTitle} accessibilityRole="heading">

403

Contact Form

404

</Text>

405

406

<View style={styles.fieldGroup}>

407

<Text style={styles.label}>

408

Name *

409

</Text>

410

<TextInput

411

style={[styles.input, errors.name && styles.inputError]}

412

value={formData.name}

413

onChangeText={(name) => setFormData(prev => ({ ...prev, name }))}

414

accessibilityLabel="Name"

415

accessibilityRequired={true}

416

accessibilityInvalid={!!errors.name}

417

accessibilityErrorMessage={errors.name}

418

/>

419

{errors.name && (

420

<Text style={styles.errorText} accessibilityRole="alert">

421

{errors.name}

422

</Text>

423

)}

424

</View>

425

426

<View style={styles.fieldGroup}>

427

<Text style={styles.label}>

428

Email *

429

</Text>

430

<TextInput

431

style={[styles.input, errors.email && styles.inputError]}

432

value={formData.email}

433

onChangeText={(email) => setFormData(prev => ({ ...prev, email }))}

434

keyboardType="email-address"

435

accessibilityLabel="Email address"

436

accessibilityRequired={true}

437

accessibilityInvalid={!!errors.email}

438

accessibilityErrorMessage={errors.email}

439

/>

440

{errors.email && (

441

<Text style={styles.errorText} accessibilityRole="alert">

442

{errors.email}

443

</Text>

444

)}

445

</View>

446

447

<View style={styles.fieldGroup}>

448

<Text style={styles.label}>

449

Message

450

</Text>

451

<TextInput

452

style={[styles.input, styles.textArea]}

453

value={formData.message}

454

onChangeText={(message) => setFormData(prev => ({ ...prev, message }))}

455

multiline={true}

456

numberOfLines={4}

457

accessibilityLabel="Message"

458

accessibilityHint="Optional message or comments"

459

/>

460

</View>

461

462

<TouchableOpacity

463

style={styles.submitButton}

464

onPress={handleSubmit}

465

accessibilityRole="button"

466

accessibilityLabel="Submit contact form"

467

>

468

<Text style={styles.submitButtonText}>Submit</Text>

469

</TouchableOpacity>

470

</View>

471

);

472

}

473

474

// Motion-aware animation wrapper

475

function MotionAwareAnimation({ children, animation, reducedMotionFallback }) {

476

const [prefersReducedMotion, setPrefersReducedMotion] = React.useState(false);

477

478

React.useEffect(() => {

479

const subscription = WebAccessibilityManager.addReducedMotionListener(

480

setPrefersReducedMotion

481

);

482

return subscription.remove;

483

}, []);

484

485

const animationToUse = prefersReducedMotion

486

? (reducedMotionFallback || { duration: 0 })

487

: animation;

488

489

return children(animationToUse);

490

}

491

492

const styles = StyleSheet.create({

493

container: {

494

flex: 1,

495

padding: 20,

496

backgroundColor: '#fff'

497

},

498

title: {

499

fontSize: 24,

500

fontWeight: 'bold',

501

marginBottom: 20,

502

accessibilityRole: 'heading'

503

},

504

statusContainer: {

505

flexDirection: 'row',

506

marginBottom: 10,

507

alignItems: 'center'

508

},

509

label: {

510

fontWeight: 'bold',

511

marginRight: 8,

512

minWidth: 120

513

},

514

value: {

515

flex: 1

516

},

517

button: {

518

backgroundColor: '#007AFF',

519

padding: 12,

520

borderRadius: 8,

521

marginVertical: 8,

522

alignItems: 'center'

523

},

524

buttonText: {

525

color: 'white',

526

fontWeight: '600'

527

},

528

announcements: {

529

position: 'absolute',

530

left: -10000,

531

width: 1,

532

height: 1,

533

overflow: 'hidden'

534

},

535

srOnly: {

536

position: 'absolute',

537

left: -10000,

538

width: 1,

539

height: 1,

540

overflow: 'hidden'

541

},

542

543

// Modal styles

544

modalOverlay: {

545

position: 'absolute',

546

top: 0,

547

left: 0,

548

right: 0,

549

bottom: 0,

550

backgroundColor: 'rgba(0,0,0,0.5)',

551

justifyContent: 'center',

552

alignItems: 'center'

553

},

554

modalContent: {

555

backgroundColor: 'white',

556

borderRadius: 12,

557

padding: 20,

558

minWidth: 300,

559

maxWidth: 500,

560

maxHeight: '80%'

561

},

562

modalHeader: {

563

flexDirection: 'row',

564

justifyContent: 'space-between',

565

alignItems: 'center',

566

marginBottom: 16

567

},

568

modalTitle: {

569

fontSize: 18,

570

fontWeight: 'bold'

571

},

572

closeButton: {

573

padding: 8,

574

borderRadius: 4

575

},

576

modalBody: {

577

flex: 1

578

},

579

580

// Form styles

581

form: {

582

padding: 20

583

},

584

formTitle: {

585

fontSize: 20,

586

fontWeight: 'bold',

587

marginBottom: 20

588

},

589

fieldGroup: {

590

marginBottom: 16

591

},

592

input: {

593

borderWidth: 1,

594

borderColor: '#ccc',

595

borderRadius: 4,

596

padding: 12,

597

fontSize: 16

598

},

599

inputError: {

600

borderColor: '#ff0000'

601

},

602

textArea: {

603

height: 100,

604

textAlignVertical: 'top'

605

},

606

errorText: {

607

color: '#ff0000',

608

fontSize: 14,

609

marginTop: 4

610

},

611

submitButton: {

612

backgroundColor: '#28a745',

613

padding: 16,

614

borderRadius: 8,

615

alignItems: 'center',

616

marginTop: 20

617

},

618

submitButtonText: {

619

color: 'white',

620

fontSize: 16,

621

fontWeight: '600'

622

}

623

});

624

```

625

626

## Web-Specific Implementation

627

628

React Native Web's AccessibilityInfo implementation leverages web standards and browser APIs to provide comprehensive accessibility support:

629

630

**Key Features:**

631

- **Media Query Integration**: Uses `prefers-reduced-motion` CSS media query for motion preferences

632

- **ARIA Support**: Provides utilities for ARIA live regions, roles, and properties

633

- **Focus Management**: Implements focus trapping and programmatic focus control

634

- **Screen Reader Compatibility**: Works with NVDA, JAWS, VoiceOver, and other assistive technologies

635

- **High Contrast Detection**: Detects Windows high contrast mode and other accessibility preferences

636

637

**Implementation Details:**

638

- `isScreenReaderEnabled()` always returns `true` as web assumes screen reader availability

639

- `isReduceMotionEnabled()` uses `(prefers-reduced-motion: reduce)` media query

640

- `announceForAccessibility()` requires custom ARIA live region implementation

641

- Events are based on CSS media query change listeners

642

- Focus management uses native DOM APIs

643

644

**Best Practices:**

645

- Always provide alternative text for images using `accessibilityLabel`

646

- Use semantic roles (`accessibilityRole`) for proper element identification

647

- Implement focus management for modals and dynamic content

648

- Respect `prefers-reduced-motion` for animations

649

- Provide error messages and validation feedback

650

- Use sufficient color contrast ratios (minimum 4.5:1)

651

- Ensure keyboard navigation works throughout the application

652

653

## Types

654

655

```javascript { .api }

656

interface AccessibilityInfoStatic {

657

isScreenReaderEnabled(): Promise<boolean>;

658

isReduceMotionEnabled(): Promise<boolean>;

659

addEventListener(

660

eventName: 'reduceMotionChanged' | 'screenReaderChanged',

661

handler: (enabled: boolean) => void

662

): { remove(): void };

663

removeEventListener(eventName: string, handler: Function): void;

664

setAccessibilityFocus(reactTag: number): void;

665

announceForAccessibility(announcement: string): void;

666

fetch(): Promise<boolean>; // deprecated

667

}

668

669

type AccessibilityEventHandler = (enabled: boolean) => void;

670

671

interface AccessibilitySubscription {

672

remove(): void;

673

}

674

675

interface FocusTrap {

676

release(): void;

677

}

678

```