♿ Accessibility Guidelines for Interactive Demos¶
Comparative positioning note
This document is written from the perspective of Microsoft Azure, Cloud Scale Analytics, and CSA Loom. Any description of third-party or competing products, services, pricing, or capabilities is derived from publicly available documentation and sources believed accurate at the time of writing, and is provided for general comparison only. We do not claim expertise in, or authority over, any non-Microsoft product or service; the respective vendor's official documentation is the authoritative source for their offerings, which may change over time. Nothing here is intended to disparage any vendor — where a competing product has genuine advantages, we aim to note them honestly. Verify all third-party details against the vendor's current official documentation before making decisions.
📋 Overview¶
This guide defines accessibility standards and best practices for all interactive demonstrations and code playgrounds in the CSA-in-a-Box documentation. All interactive components must be accessible to users with disabilities, following WCAG 2.1 Level AA compliance standards.
🎯 Accessibility Principles¶
POUR Principles¶
All interactive demos must adhere to the four POUR principles:
- Perceivable - Information must be presentable to users in ways they can perceive
- Operable - User interface components must be operable by all users
- Understandable - Information and operation must be understandable
- Robust - Content must be robust enough to work with current and future technologies
🔑 Core Requirements¶
Keyboard Navigation¶
All interactive elements must be fully accessible via keyboard:
Required Keyboard Support¶
// Keyboard navigation implementation
class AccessibleComponent {
setupKeyboardNavigation() {
this.element.addEventListener('keydown', (e) => {
switch(e.key) {
case 'Tab':
this.handleTabNavigation(e);
break;
case 'Enter':
case ' ': // Space
this.handleActivation(e);
break;
case 'Escape':
this.handleEscape(e);
break;
case 'ArrowUp':
case 'ArrowDown':
case 'ArrowLeft':
case 'ArrowRight':
this.handleArrowNavigation(e);
break;
}
});
}
handleTabNavigation(event) {
// Ensure proper tab order
const focusableElements = this.getFocusableElements();
// Implement custom tab order if needed
}
getFocusableElements() {
return this.element.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
}
}
Focus Management¶
// Focus indicator management
class FocusManager {
constructor() {
this.setupFocusIndicators();
this.setupFocusTrap();
}
setupFocusIndicators() {
// Ensure visible focus indicators
const style = document.createElement('style');
style.textContent = `
:focus {
outline: 3px solid #0078D4;
outline-offset: 2px;
}
:focus:not(:focus-visible) {
outline: none;
}
:focus-visible {
outline: 3px solid #0078D4;
outline-offset: 2px;
}
`;
document.head.appendChild(style);
}
setupFocusTrap(containerElement) {
// Trap focus within modal/dialog
const focusableElements = containerElement.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const firstElement = focusableElements[0];
const lastElement = focusableElements[focusableElements.length - 1];
containerElement.addEventListener('keydown', (e) => {
if (e.key === 'Tab') {
if (e.shiftKey && document.activeElement === firstElement) {
e.preventDefault();
lastElement.focus();
} else if (!e.shiftKey && document.activeElement === lastElement) {
e.preventDefault();
firstElement.focus();
}
}
});
}
}
Screen Reader Support¶
ARIA Labels and Roles¶
<!-- Interactive demo container -->
<div role="region" aria-label="Azure Synapse Cost Calculator" id="cost-calculator">
<!-- Input controls -->
<div class="input-group">
<label for="data-volume">
Data Volume (TB/month)
<span class="sr-only">Use slider to select data volume between 0 and 100 terabytes</span>
</label>
<input
type="range"
id="data-volume"
min="0"
max="100"
value="10"
aria-valuemin="0"
aria-valuemax="100"
aria-valuenow="10"
aria-valuetext="10 terabytes per month"
aria-describedby="data-volume-help"
/>
<output id="data-volume-output" aria-live="polite">10 TB</output>
<p id="data-volume-help" class="help-text">
Select the amount of data processed monthly
</p>
</div>
<!-- Results section -->
<div class="results" role="status" aria-live="polite" aria-atomic="true">
<h3 id="results-heading">Estimated Monthly Cost</h3>
<div aria-labelledby="results-heading">
<p>Total: <span aria-label="$2,350 per month">$2,350</span></p>
</div>
</div>
<!-- Action buttons -->
<div class="actions">
<button
type="button"
aria-label="Reset calculator to default values"
aria-describedby="reset-help"
>
Reset
</button>
<span id="reset-help" class="sr-only">
This will clear all inputs and restore default values
</span>
</div>
</div>
Live Regions¶
// Announce changes to screen readers
class LiveRegionAnnouncer {
constructor() {
this.createLiveRegion();
}
createLiveRegion() {
const liveRegion = document.createElement('div');
liveRegion.setAttribute('role', 'status');
liveRegion.setAttribute('aria-live', 'polite');
liveRegion.setAttribute('aria-atomic', 'true');
liveRegion.className = 'sr-only';
document.body.appendChild(liveRegion);
this.liveRegion = liveRegion;
}
announce(message, priority = 'polite') {
this.liveRegion.setAttribute('aria-live', priority);
this.liveRegion.textContent = message;
// Clear after announcement
setTimeout(() => {
this.liveRegion.textContent = '';
}, 1000);
}
announceError(errorMessage) {
this.announce(`Error: ${errorMessage}`, 'assertive');
}
announceSuccess(successMessage) {
this.announce(successMessage, 'polite');
}
}
// Usage example
const announcer = new LiveRegionAnnouncer();
announcer.announce('Cost calculation updated: $2,350 per month');
announcer.announceSuccess('Configuration saved successfully');
announcer.announceError('Invalid data volume. Please enter a value between 0 and 100');
Color and Contrast¶
Minimum Contrast Ratios¶
- Normal text: 4.5:1 minimum
- Large text (18pt+): 3:1 minimum
- UI components: 3:1 minimum
- Focus indicators: 3:1 minimum
Color-Blind Friendly Palettes¶
/* Accessible color palette */
:root {
/* Primary colors with sufficient contrast */
--color-primary: #0078D4; /* Azure Blue */
--color-primary-dark: #004578; /* Dark Blue */
--color-success: #107C10; /* Green (7.4:1) */
--color-warning: #CA5010; /* Orange (4.5:1) */
--color-error: #D13438; /* Red (5.9:1) */
--color-info: #0078D4; /* Blue (4.5:1) */
/* Text colors */
--color-text-primary: #323130; /* Dark Gray (12.6:1) */
--color-text-secondary: #605E5C; /* Medium Gray (7.2:1) */
--color-text-disabled: #A19F9D; /* Light Gray (3.0:1) */
/* Background colors */
--color-bg-light: #FFFFFF;
--color-bg-gray: #F3F2F1;
--color-bg-dark: #201F1E;
}
/* Never rely on color alone */
.status-indicator {
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.status-indicator::before {
content: '';
width: 12px;
height: 12px;
border-radius: 50%;
}
.status-success {
color: var(--color-success);
}
.status-success::before {
background: var(--color-success);
/* Add pattern for color-blind users */
background-image:
repeating-linear-gradient(45deg,
transparent,
transparent 2px,
rgba(255,255,255,0.3) 2px,
rgba(255,255,255,0.3) 4px);
}
.status-error {
color: var(--color-error);
}
.status-error::before {
background: var(--color-error);
/* Different pattern for errors */
background-image:
repeating-linear-gradient(-45deg,
transparent,
transparent 2px,
rgba(255,255,255,0.3) 2px,
rgba(255,255,255,0.3) 4px);
}
Text and Typography¶
Font Size and Scaling¶
/* Scalable typography */
html {
/* Base font size - users can scale */
font-size: 16px;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto',
'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans',
'Helvetica Neue', sans-serif;
line-height: 1.5;
}
/* Use relative units */
.demo-title {
font-size: 2rem; /* 32px at default */
line-height: 1.2;
}
.demo-content {
font-size: 1rem; /* 16px at default */
line-height: 1.6;
}
.demo-caption {
font-size: 0.875rem; /* 14px at default */
line-height: 1.4;
}
/* Support text zoom up to 200% */
@media (min-width: 1920px) {
html {
font-size: 18px;
}
}
/* Ensure content reflows at 320px viewport */
@media (max-width: 320px) {
.demo-container {
max-width: 100%;
overflow-x: auto;
}
}
Readable Content¶
/* Maximum line length for readability */
.demo-description,
.demo-instructions {
max-width: 75ch; /* Optimal reading width */
margin-left: auto;
margin-right: auto;
}
/* Adequate spacing */
p {
margin-bottom: 1em;
}
h2, h3, h4 {
margin-top: 1.5em;
margin-bottom: 0.75em;
}
/* Clear visual hierarchy */
h2 {
font-size: 1.75rem;
font-weight: 600;
}
h3 {
font-size: 1.5rem;
font-weight: 600;
}
h4 {
font-size: 1.25rem;
font-weight: 600;
}
🖱️ Interactive Elements¶
Buttons and Controls¶
<!-- Accessible button examples -->
<button
type="button"
class="btn-primary"
aria-label="Run Spark query demonstration"
aria-describedby="run-query-help"
>
<span aria-hidden="true">▶</span>
Run Query
</button>
<p id="run-query-help" class="sr-only">
Execute the PySpark query against sample dataset
</p>
<!-- Toggle button with state -->
<button
type="button"
role="switch"
aria-checked="false"
aria-label="Enable auto-refresh"
id="auto-refresh-toggle"
class="toggle-button"
>
<span class="toggle-label">Auto-refresh</span>
<span class="toggle-state" aria-hidden="true">Off</span>
</button>
<script>
// Toggle button behavior
const toggleBtn = document.getElementById('auto-refresh-toggle');
toggleBtn.addEventListener('click', function() {
const isChecked = this.getAttribute('aria-checked') === 'true';
this.setAttribute('aria-checked', !isChecked);
this.querySelector('.toggle-state').textContent = isChecked ? 'Off' : 'On';
announcer.announce(`Auto-refresh ${isChecked ? 'disabled' : 'enabled'}`);
});
</script>
Form Controls¶
<!-- Accessible form example -->
<form class="demo-config-form" aria-labelledby="config-form-title">
<h3 id="config-form-title">Configure Spark Pool</h3>
<!-- Text input -->
<div class="form-group">
<label for="pool-name">
Pool Name
<span class="required" aria-label="required">*</span>
</label>
<input
type="text"
id="pool-name"
name="poolName"
required
aria-required="true"
aria-describedby="pool-name-help pool-name-error"
aria-invalid="false"
/>
<p id="pool-name-help" class="help-text">
Enter a unique name for the Spark pool (3-15 characters)
</p>
<p id="pool-name-error" class="error-text" role="alert" aria-live="assertive">
<!-- Error message appears here -->
</p>
</div>
<!-- Select dropdown -->
<div class="form-group">
<label for="node-size">Node Size</label>
<select
id="node-size"
name="nodeSize"
aria-describedby="node-size-help"
>
<option value="">Select node size</option>
<option value="small">Small (4 vCores, 32 GB RAM)</option>
<option value="medium">Medium (8 vCores, 64 GB RAM)</option>
<option value="large">Large (16 vCores, 128 GB RAM)</option>
</select>
<p id="node-size-help" class="help-text">
Choose node size based on workload requirements
</p>
</div>
<!-- Radio group -->
<fieldset>
<legend>Auto-scaling</legend>
<div class="radio-group">
<input
type="radio"
id="autoscale-enabled"
name="autoscale"
value="enabled"
checked
/>
<label for="autoscale-enabled">Enabled</label>
</div>
<div class="radio-group">
<input
type="radio"
id="autoscale-disabled"
name="autoscale"
value="disabled"
/>
<label for="autoscale-disabled">Disabled</label>
</div>
</fieldset>
<!-- Checkbox -->
<div class="form-group">
<input
type="checkbox"
id="enable-cache"
name="enableCache"
/>
<label for="enable-cache">
Enable result caching for improved performance
</label>
</div>
<!-- Submit button -->
<button type="submit" class="btn-primary">
Create Spark Pool
</button>
</form>
Modal Dialogs¶
// Accessible modal implementation
class AccessibleModal {
constructor(modalId) {
this.modal = document.getElementById(modalId);
this.openButton = document.querySelector(`[data-modal="${modalId}"]`);
this.closeButton = this.modal.querySelector('.modal-close');
this.previousFocus = null;
this.setup();
}
setup() {
this.openButton.addEventListener('click', () => this.open());
this.closeButton.addEventListener('click', () => this.close());
// Close on escape
this.modal.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
this.close();
}
});
// Close on backdrop click
this.modal.addEventListener('click', (e) => {
if (e.target === this.modal) {
this.close();
}
});
}
open() {
// Store current focus
this.previousFocus = document.activeElement;
// Show modal
this.modal.style.display = 'block';
this.modal.setAttribute('aria-hidden', 'false');
// Trap focus
const focusableElements = this.modal.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
if (focusableElements.length > 0) {
focusableElements[0].focus();
}
// Prevent body scroll
document.body.style.overflow = 'hidden';
// Announce to screen readers
announcer.announce('Dialog opened');
}
close() {
// Hide modal
this.modal.style.display = 'none';
this.modal.setAttribute('aria-hidden', 'true');
// Restore focus
if (this.previousFocus) {
this.previousFocus.focus();
}
// Restore body scroll
document.body.style.overflow = '';
// Announce to screen readers
announcer.announce('Dialog closed');
}
}
📱 Responsive Design¶
Touch Targets¶
/* Minimum touch target size: 44x44 pixels */
.interactive-button,
.demo-control {
min-width: 44px;
min-height: 44px;
padding: 0.75rem 1.5rem;
}
/* Adequate spacing between touch targets */
.button-group .interactive-button {
margin: 0.5rem;
}
/* Larger targets for critical actions */
.primary-action {
min-width: 48px;
min-height: 48px;
padding: 1rem 2rem;
}
Responsive Breakpoints¶
/* Mobile-first responsive design */
.demo-container {
padding: 1rem;
}
/* Tablet */
@media (min-width: 768px) {
.demo-container {
padding: 2rem;
}
.demo-layout {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2rem;
}
}
/* Desktop */
@media (min-width: 1024px) {
.demo-container {
padding: 3rem;
max-width: 1400px;
margin: 0 auto;
}
.demo-layout {
grid-template-columns: 1fr 2fr;
}
}
/* Support orientation changes */
@media (orientation: portrait) {
.demo-visualization {
max-height: 60vh;
}
}
@media (orientation: landscape) {
.demo-visualization {
max-height: 80vh;
}
}
🧪 Testing Requirements¶
Manual Testing Checklist¶
- Keyboard Navigation
- All interactive elements accessible via Tab
- Logical tab order
- Visible focus indicators
- Skip links available
-
No keyboard traps
-
Screen Reader Testing
- Test with NVDA (Windows)
- Test with JAWS (Windows)
- Test with VoiceOver (macOS/iOS)
- Test with TalkBack (Android)
- All images have alt text
- Form labels properly associated
-
Live regions announce changes
-
Visual Testing
- Minimum contrast ratios met
- Text scalable to 200%
- Content reflows at 320px
- Color not sole indicator
-
High contrast mode support
-
Motor Impairment Testing
- Touch targets ≥44x44px
- No time-sensitive interactions
- Ample time for interactions
- Can cancel accidental actions
Automated Testing¶
// Automated accessibility testing with axe-core
const { AxePuppeteer } = require('@axe-core/puppeteer');
const puppeteer = require('puppeteer');
async function testAccessibility(url) {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto(url);
await page.setBypassCSP(true);
const results = await new AxePuppeteer(page).analyze();
console.log(`Accessibility Violations: ${results.violations.length}`);
results.violations.forEach(violation => {
console.log(`\n[${violation.impact}] ${violation.id}`);
console.log(violation.description);
console.log(`Help: ${violation.helpUrl}`);
violation.nodes.forEach(node => {
console.log(` - ${node.html}`);
});
});
await browser.close();
return results.violations.length === 0;
}
// Test all interactive demos
const demoUrls = [
'http://localhost:8000/demos/cost-calculator',
'http://localhost:8000/demos/query-playground',
'http://localhost:8000/demos/pipeline-builder'
];
(async () => {
for (const url of demoUrls) {
console.log(`\nTesting: ${url}`);
const passed = await testAccessibility(url);
console.log(`Result: ${passed ? 'PASSED' : 'FAILED'}`);
}
})();
📚 Resources¶
WCAG 2.1 Guidelines¶
Testing Tools¶
- Automated Testing
- axe DevTools
- WAVE
- Lighthouse
-
Screen Readers
- NVDA (Windows)
- JAWS (Windows)
- VoiceOver (macOS/iOS - built-in)
-
TalkBack (Android - built-in)
-
Color Contrast
- WebAIM Contrast Checker
- Colour Contrast Analyser
- Accessible Colors
Microsoft Resources¶
💬 Feedback¶
Your feedback helps improve accessibility. Please report issues:
- Accessibility Barriers: Report Barrier
- Screen Reader Issues: Report Issue
- Suggestions: Share Ideas
Last Updated: January 2025 | WCAG Version: 2.1 Level AA