skills/hack23/cia/accessibility-wcag-patterns

accessibility-wcag-patterns

SKILL.md

Accessibility WCAG Patterns

Purpose

This skill provides guidance for implementing WCAG 2.1 Level AA accessibility standards in the CIA platform, ensuring political transparency data is accessible to all users including those with disabilities. Covers ARIA attributes, keyboard navigation, screen reader optimization, and automated accessibility testing.

When to Use

✅ Use this skill when:

  • Designing new UI components
  • Implementing forms and interactive elements
  • Creating data visualizations and charts
  • Building responsive layouts
  • Reviewing UI for accessibility compliance
  • Fixing accessibility violations
  • Writing automated accessibility tests
  • Conducting accessibility audits

❌ Don't use this skill for:

  • General UI design patterns (use vaadin-component-design)
  • Performance optimization (use code-quality-checks)
  • Security (use input-validation, secure-code-review)
  • Backend implementation (use spring-framework-patterns)

Patterns & Examples

WCAG 2.1 Compliance Checklist

Level A (Must Have):

  • 1.1.1: Non-text content has text alternatives
  • 2.1.1: All functionality available via keyboard
  • 2.4.1: Bypass blocks (skip navigation)
  • 3.1.1: Language of page identified
  • 4.1.2: Name, role, value for UI components

Level AA (Target):

  • 1.4.3: Contrast ratio at least 4.5:1 for normal text
  • 1.4.5: Images of text avoided (use actual text)
  • 2.4.6: Headings and labels describe topic/purpose
  • 2.4.7: Visible focus indicator
  • 3.2.4: Consistent identification of components

Vaadin Accessibility Patterns

// Accessible form with proper labels and ARIA
@Route("politician/edit")
public class PoliticianEditView extends VerticalLayout {
    
    public PoliticianEditView() {
        // Page title for screen readers
        getElement().setAttribute("role", "main");
        getElement().setAttribute("aria-label", "Edit Politician Profile");
        
        H1 title = new H1("Edit Politician Profile");
        add(title);
        
        // Accessible form fields
        TextField nameField = new TextField("Full Name");
        nameField.setRequired(true);
        nameField.setErrorMessage("Name is required");
        nameField.setAriaLabel("Politician full name");
        
        TextArea bioField = new TextArea("Biography");
        bioField.setMaxLength(1000);
        bioField.setHelperText("Maximum 1000 characters");
        bioField.setAriaLabel("Politician biography");
        
        // Accessible select with clear labels
        Select<String> partySelect = new Select<>();
        partySelect.setLabel("Political Party");
        partySelect.setItems("Social Democrats", "Moderates", "Sweden Democrats");
        partySelect.setRequiredIndicatorVisible(true);
        
        // Accessible buttons with clear labels
        Button saveButton = new Button("Save Changes");
        saveButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
        saveButton.setAriaLabel("Save politician profile changes");
        saveButton.setIcon(new Icon(VaadinIcon.CHECK));
        
        Button cancelButton = new Button("Cancel");
        cancelButton.setAriaLabel("Cancel editing and return to profile");
        cancelButton.setIcon(new Icon(VaadinIcon.CLOSE));
        
        // Accessible button group
        HorizontalLayout buttons = new HorizontalLayout(saveButton, cancelButton);
        buttons.getElement().setAttribute("role", "group");
        buttons.getElement().setAttribute("aria-label", "Form actions");
        
        add(nameField, bioField, partySelect, buttons);
    }
}

Accessible Data Table

// Accessible grid with ARIA labels
Grid<Politician> grid = new Grid<>(Politician.class, false);
grid.setAriaLabel("Politicians List");

// Column headers with sort indicators
grid.addColumn(Politician::getName)
    .setHeader("Name")
    .setSortable(true)
    .setAriaLabel("Politician name, sortable");

grid.addColumn(Politician::getParty)
    .setHeader("Party")
    .setSortable(true);

grid.addColumn(politician -> {
    Span badge = new Span(String.format("%.0f", politician.getRiskScore()));
    badge.getElement().getThemeList().add(
        politician.getRiskScore() > 70 ? "badge error" : "badge success"
    );
    // ARIA label for screen readers
    badge.getElement().setAttribute("aria-label", 
        String.format("Risk score %d out of 100", 
            Math.round(politician.getRiskScore())));
    return badge;
})
.setHeader("Risk Score")
.setAriaLabel("Risk assessment score");

// Keyboard navigation
grid.setSelectionMode(Grid.SelectionMode.SINGLE);
grid.addFocusListener(e -> {
    // Announce selection to screen readers
    Notification.show("Use arrow keys to navigate, Enter to select");
});

Color Contrast Compliance

/* WCAG AA Contrast Ratios */
:root {
  /* Text on white background (4.5:1 minimum) */
  --text-primary: #212121;        /* 16.1:1 - Excellent */
  --text-secondary: #616161;      /* 5.9:1 - Good */
  
  /* Text on colored backgrounds */
  --risk-high-bg: #c62828;        /* Red */
  --risk-high-text: #ffffff;      /* 6.3:1 - Pass */
  
  --risk-medium-bg: #f57c00;      /* Orange */
  --risk-medium-text: #000000;    /* 6.7:1 - Pass */
  
  --risk-low-bg: #2e7d32;         /* Green */
  --risk-low-text: #ffffff;       /* 4.5:1 - Pass */
  
  /* Interactive elements (focus indicators) */
  --focus-outline: 3px solid #1976d2;
  --focus-outline-offset: 2px;
}

/* Visible focus indicators */
button:focus,
a:focus,
input:focus,
select:focus,
textarea:focus {
  outline: var(--focus-outline);
  outline-offset: var(--focus-outline-offset);
}

/* Skip navigation link */
.skip-link {
  position: absolute;
  top: -40px;
  left: 0;
  background: #000;
  color: #fff;
  padding: 8px;
  text-decoration: none;
  z-index: 100;
}

.skip-link:focus {
  top: 0;
}

Screen Reader Optimization

<!-- Landmark regions -->
<header role="banner">
  <h1>Citizen Intelligence Agency</h1>
  <nav role="navigation" aria-label="Main navigation">
    <!-- Navigation items -->
  </nav>
</header>

<main role="main" aria-labelledby="page-title">
  <h1 id="page-title">Politician Risk Dashboard</h1>
  
  <!-- Live region for dynamic updates -->
  <div aria-live="polite" aria-atomic="true" class="sr-only">
    <!-- Announcements for screen readers -->
  </div>
  
  <!-- Search form -->
  <form role="search" aria-label="Search politicians">
    <label for="search-input">Search by name or party</label>
    <input 
      id="search-input" 
      type="search" 
      aria-describedby="search-help"
    />
    <span id="search-help" class="helper-text">
      Enter at least 3 characters to search
    </span>
  </form>
  
  <!-- Data visualization -->
  <figure role="img" aria-labelledby="chart-title">
    <h2 id="chart-title">Risk Score Distribution</h2>
    <canvas id="risk-chart" aria-describedby="chart-desc"></canvas>
    <figcaption id="chart-desc">
      Bar chart showing risk score distribution across 349 politicians.
      Highest concentration in 40-60 range.
    </figcaption>
  </figure>
</main>

<footer role="contentinfo">
  <!-- Footer content -->
</footer>

Keyboard Navigation Patterns

// Custom keyboard navigation for complex widgets
class AccessibleRiskSlider {
  constructor(element) {
    this.element = element;
    this.value = 50;
    this.min = 0;
    this.max = 100;
    
    // ARIA attributes
    element.setAttribute('role', 'slider');
    element.setAttribute('aria-valuemin', this.min);
    element.setAttribute('aria-valuemax', this.max);
    element.setAttribute('aria-valuenow', this.value);
    element.setAttribute('aria-label', 'Filter by risk score');
    element.setAttribute('tabindex', '0');
    
    // Keyboard event handlers
    element.addEventListener('keydown', this.handleKeyDown.bind(this));
  }
  
  handleKeyDown(event) {
    let newValue = this.value;
    
    switch(event.key) {
      case 'ArrowRight':
      case 'ArrowUp':
        newValue = Math.min(this.value + 1, this.max);
        event.preventDefault();
        break;
      case 'ArrowLeft':
      case 'ArrowDown':
        newValue = Math.max(this.value - 1, this.min);
        event.preventDefault();
        break;
      case 'Home':
        newValue = this.min;
        event.preventDefault();
        break;
      case 'End':
        newValue = this.max;
        event.preventDefault();
        break;
      case 'PageUp':
        newValue = Math.min(this.value + 10, this.max);
        event.preventDefault();
        break;
      case 'PageDown':
        newValue = Math.max(this.value - 10, this.min);
        event.preventDefault();
        break;
    }
    
    if (newValue !== this.value) {
      this.setValue(newValue);
    }
  }
  
  setValue(value) {
    this.value = value;
    this.element.setAttribute('aria-valuenow', value);
    // Update visual representation
    this.updateVisual();
    // Trigger change event
    this.element.dispatchEvent(new CustomEvent('change', { detail: value }));
  }
}

ISMS Compliance Mapping

ISO 27001:2022 Annex A Controls

A.7.1 - Physical and environmental security

  • Digital accessibility ensures equal access to information

A.5.1 - Policies for information security

  • Accessibility policy as part of information security

GDPR Considerations

Article 9 - Processing of special categories

  • Accessibility features must not discriminate
  • Equal access to personal data for data subjects with disabilities

Legal Requirements

EU Web Accessibility Directive

  • Public sector websites must meet WCAG 2.1 Level AA
  • Accessibility statements required
  • Compliance monitoring and reporting

Hack23 ISMS Policy References

References

Standards & Guidelines

Testing Tools

Remember

  • Accessibility is not optional: WCAG 2.1 AA is the baseline
  • Test with real users: Include users with disabilities in testing
  • Keyboard-first design: All functionality via keyboard
  • Semantic HTML: Use proper elements for structure
  • Progressive enhancement: Core content accessible without JavaScript
  • Automated testing: Integrate accessibility checks in CI/CD
Weekly Installs
2
Repository
hack23/cia
GitHub Stars
212
First Seen
10 days ago
Installed on
amp2
cline2
opencode2
cursor2
kimi-cli2
codex2