Skip to content
Learn — Azure analytics reference library covering services, architecture patterns, tutorials, solutions, monitoring, DevOps

♿ 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.

Status: Active WCAG: 2.1 AA Priority: High

📋 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:

  1. Perceivable - Information must be presentable to users in ways they can perceive
  2. Operable - User interface components must be operable by all users
  3. Understandable - Information and operation must be understandable
  4. 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>
// 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

Microsoft Resources


💬 Feedback

Your feedback helps improve accessibility. Please report issues:


Last Updated: January 2025 | WCAG Version: 2.1 Level AA