Skip to main content
Casca
Theme

Casca Accessibility GuideLink to section

WCAG 2.1 AA ComplianceLink to section

Casca is designed with accessibility as a first-class requirement, targeting WCAG 2.1 Level AA compliance out of the box. This applies to both the read-only charts (visual + a .casca-data text alternative) and the interactive controls (slot grid, data table, range slider, stat card) and no-JS interaction primitives (toggle, switch, disclosure, filter), which are built on native HTML so they are keyboard- and screen-reader-accessible by default. See "Controls & Interactions Accessibility" below.

Core Accessibility FeaturesLink to section

1. Semantic HTML StructureLink to section

All charts use proper semantic HTML:

<!-- Proper figure/figcaption for charts -->
<figure class="casca casca-figure" role="img" aria-labelledby="chart-title">
  <figcaption>
    <div id="chart-title" class="casca-title">Monthly Revenue</div>
  </figcaption>
  <!-- Chart visualization -->
</figure>

Benefits:

2. Data Table AlternativeLink to section

Every chart should include an accessible data table for screen readers:

<table class="casca-data">
  <caption>Monthly Revenue Data</caption>
  <thead>
    <tr>
      <th>Month</th>
      <th>Revenue</th>
    </tr>
  </thead>
  <tbody>
    <tr><td>January</td><td>$50,000</td></tr>
    <tr><td>February</td><td>$65,000</td></tr>
  </tbody>
</table>

<div class="casca-bar" aria-hidden="true">
  <!-- Visual chart here -->
</div>

Key Points:

3. Keyboard NavigationLink to section

All interactive elements are fully keyboard accessible:

Focus Indicators:

:focus-visible {
  outline: 3px solid var(--casca-color-1);
  outline-offset: 3px;
}

Navigation:

Interactive Elements:

4. Color and ContrastLink to section

Meets WCAG AA contrast requirements:

Color Independence:

High Contrast Mode:

@media (prefers-contrast: more) {
  :root {
    --casca-line-stroke-width: 3px;
    --casca-bar-min-height: 4px;
    --casca-axis-width: 2px;
  }

  .casca-bar-value:hover,
  .casca-pie-chart:hover {
    outline: 3px solid;
    outline-offset: 2px;
  }
}

5. Reduced Motion SupportLink to section

Respects user motion preferences:

@media (prefers-reduced-motion: reduce) {
  .casca,
  .casca * {
    --casca-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    scroll-behavior: auto !important;
  }
}

What this does:

6. Screen Reader SupportLink to section

Tested with:

Best Practices:

<!-- Descriptive labels -->
<div class="casca-title">Monthly Cash Flow</div>
<div class="casca-subtitle">Income vs Expenses</div>

<!-- Live regions for dynamic updates -->
<div class="casca-live-region" aria-live="polite" aria-atomic="true">
  Chart updated: New data available
</div>

<!-- Descriptive data labels -->
<div class="casca-bar-value"
     data-label="January: $50,000"
     aria-label="January: $50,000">
</div>

7. Touch Target SizesLink to section

Minimum 44×44 CSS pixels for all interactive elements:

.casca-legend-nav label {
  min-width: var(--casca-size-6); /* 2rem = 32px */
  padding: var(--casca-size-1) var(--casca-size-2);
  /* Effective size: ~44×36px */
}

.casca-bar-value {
  min-height: var(--casca-bar-min-height);
  width: var(--casca-size-6); /* 32px */
  /* Clickable area enhanced via padding/margin */
}

8. Forced Colors ModeLink to section

Windows High Contrast Mode support:

@media (forced-colors: active) {
  .casca-bar-value,
  .casca-line polyline,
  .casca-pie-chart {
    forced-color-adjust: none;
    border: 1px solid canvastext;
  }

  .casca-line-point {
    fill: canvastext;
    stroke: canvas;
  }
}

Prose Extensions AccessibilityLink to section

Prose callouts, disclosures, definition lists, and heading anchors are scoped to .casca-prose and preserve native HTML semantics.

CalloutsLink to section

Use the explicit callout form when the intent is more specific than a note:

<div class="casca-prose-callout" data-callout="danger" role="note" aria-label="Danger">
  <p><strong>Do not delete the source data.</strong> This action cannot be reconstructed.</p>
</div>

The CSS uses data-callout only for visual treatment. Authors should add role="note" because <div> has no implicit role, and should add an aria-label matching the intent: Note, Tip, Warning, Danger, or Important. Localized label text is fine.

Auto-detected callouts use a bold-leading blockquote:

<blockquote>
  <p><strong>Note.</strong> This renders as a note callout.</p>
</blockquote>

The blockquote keeps its native semantics, so no additional ARIA is required. The auto-detected path always renders as a note. To opt out for a real quotation that happens to start with bold text, wrap the bold run so it is not the first child of the first paragraph:

<blockquote>
  <p><span><strong>Hello,</strong></span> she said.</p>
</blockquote>

Callout meaning must be present in text, not color alone. Keep a visible bold lead-in such as Warning. or Important. inside the body. In forced-colors mode Casca drops tinted backgrounds to system colors, keeps the leading accent stripe, and preserves the text signal. In print, callouts stay visible and use print-color-adjust: exact for the tinted background where printers allow it.

Details and Definition ListsLink to section

<details> and <summary> keep their native disclosure behavior. Enter and Space toggle the disclosure, the summary remains the focusable control, and the summary text remains the accessible label. Casca hides the browser marker with list-style: none plus ::-webkit-details-marker and draws a decorative marker that rotates. Reduced-motion users get the state change without animation.

Definition lists use plain <dl>, <dt>, and <dd>. Styling changes weight, font, spacing, and indent only; the term and definition association remains in the HTML.

Heading AnchorsLink to section

The SSG emits heading anchors as real links:

<a class="casca-prose-anchor" href="#callouts"><span class="casca-prose-anchor-label">Link to section</span></a>

The .casca-prose-anchor-label span supplies the accessible name. It is visually hidden with the same clip-rect pattern as .sr-only, plus clip-path: inset(50%), and remains available to assistive technology. The visible # glyph is decorative CSS ::after content.

Heading anchors are in the normal Tab order. They reveal on heading hover, anchor hover, :focus-visible, and the :focus fallback. On touch-only devices, @media (hover: none) reveals anchors at rest so the permalink affordance is discoverable without hover. In forced-colors mode anchors are always visible and use LinkText. In print, the anchors are hidden because the glyph is interactive-only.

<casca-layout> AccessibilityLink to section

<casca-layout> uses Declarative Shadow DOM for the chrome and light-DOM slots for consumer content. Focus order follows the slot positions in the shadow template, not the order of slotted nodes in the light DOM. In sequence, the template places the skip link first, then brand, nav, theme picker, landing hero when rendered, main content, and footer. Authors should not rely on light DOM source order to define keyboard order.

Landmark roles come from native element semantics. The shadow template uses <header>, <nav>, <main>, and <footer> without explicit role= attributes. Slotted content is exposed in the composed accessibility tree inside those landmarks.

The skip link has visible text, Skip to main content, which is its accessible name. Its href targets #casca-layout-main in light DOM. The consumer's main-slot wrapper must carry both id="casca-layout-main" and tabindex="-1" so activation transfers focus without inserting the wrapper into sequential Tab order.

Slotted content keeps its own accessibility contract. Nav items remain real links, the theme picker remains a native form with radios and a submit button, the main slot should use semantic content such as <article class="casca-prose"> for prose pages, and footer content should use real text rather than decorative CSS-only labels.

Chart-Specific AccessibilityLink to section

Bar ChartsLink to section

<!-- Accessible bar chart pattern -->
<figure class="casca casca-figure" role="img" aria-labelledby="bar-title">
  <figcaption>
    <div id="bar-title" class="casca-title">Revenue by Quarter</div>
  </figcaption>

  <table class="casca-data">
    <!-- Data table for screen readers -->
  </table>

  <div class="casca-bar" aria-hidden="true">
    <div class="casca-bar-group">
      <div class="casca-bar-item">
        <div class="casca-bar-value"
             data-label="Q1: $50k"
             role="img"
             aria-label="Quarter 1: $50,000">
        </div>
      </div>
    </div>
  </div>
</figure>

Diverging Bars (Cash Flow)Link to section

<!-- Clear positive/negative indication -->
<div class="casca-bar-value"
     data-direction="positive"
     data-label="+$15k"
     aria-label="Income: Positive $15,000">
</div>

<div class="casca-bar-value"
     data-direction="negative"
     data-label="-$8k"
     aria-label="Expenses: Negative $8,000">
</div>

Paginated Legends (CSS-only)Link to section

<!-- Fully keyboard accessible pagination -->
<div class="casca-legend-paginated">
  <!-- Hidden but accessible radio buttons -->
  <input type="radio"
         name="legend-pagination"
         id="legend-page-1"
         checked
         aria-label="View legend page 1">

  <!-- Visible navigation -->
  <div class="casca-legend-nav" role="tablist">
    <label for="legend-page-1" role="tab" aria-selected="true">1</label>
    <label for="legend-page-2" role="tab" aria-selected="false">2</label>
  </div>
</div>

Keyboard Flow:

  1. Tab to pagination controls
  2. Arrow keys navigate between page buttons
  3. Enter/Space activates page
  4. Focus remains on navigation
<!-- Chart as button with clear focus -->
<button class="casca casca-figure casca-interactive"
        aria-label="View detailed sales report">
  <div class="casca-title">Monthly Sales</div>
  <div class="casca-bar" aria-hidden="true">
    <!-- Visual chart -->
  </div>
</button>

<!-- Focus state automatically applied -->
button.casca:focus-visible {
  outline: 3px solid var(--casca-color-1);
  outline-offset: 3px;
}

All read-only chart typesLink to section

Every read-only chart (bar, line, area, pie/donut, progress/gauge, heatmap, scatter, waterfall, radar, candlestick) uses the same pattern: the visual is aria-hidden="true" and a .casca-data table (or equivalent semantic text) carries the data for assistive tech. None of them rely on color alone: each slice/series/point also carries a data-label / text label, and dark-mode + forced-colors paths preserve contrast. When adding a chart, pair the visual with a data-table alternative. Do not ship the visual as the only data source.

For server-rendered output, the Integration Guide shows the static no-JS recipe (figure + .casca-data table → static file) and a security contract: keep inline style numeric (server-computed), and HTML-escape every string in text/quoted attributes, so a label can't become CSS/markup injection.

Axis Labels (.casca-axis)Link to section

The axis-label primitive renders ticks and titles as real DOM text, not CSS pseudo-content, so they are selectable and exposed to assistive tech. They label the visual but are not a substitute for the .casca-data table, which remains the source of truth.

Two placement rules keep the labels accessible:

Controls & Interactions AccessibilityLink to section

Unlike the read-only charts, the controls below are native, interactive HTML (real <input>, <table>, <details>), so they are keyboard-operable and screen-reader-announced by default. There is no separate aria-hidden visual. Everything works with JavaScript disabled; state changes ride native form controls and a server round-trip.

Slot Grid (.casca-slot-grid)Link to section

Data Table (.casca-table)Link to section

Analytics Block (.casca-analytics)Link to section

Range / Value Slider (.casca-range)Link to section

KPI / Stat Card (.casca-stat)Link to section

No-JS Interaction Primitives (@layer casca.interactions)Link to section

Toggle, switch, disclosure, and filter are driven by native checkboxes, radios, and <details>, all keyboard-operable and announced natively.

Layout Shell Accessibility (casca-extended-layout)Link to section

The shell primitives (card, card-grid, toolbar, anchor-nav) are structural and add no ARIA scaffolding of their own. They rely on correct host markup:

Testing ChecklistLink to section

Automated TestingLink to section

Manual TestingLink to section

Keyboard Navigation:

Screen Readers:

Visual:

Motion:

Common Accessibility PatternsLink to section

1. Descriptive TitlesLink to section

<!-- Good: Specific, informative -->
<div class="casca-title">Monthly Revenue: January - June 2025</div>

<!-- Bad: Vague, uninformative -->
<div class="casca-title">Chart</div>

2. Data Table StructureLink to section

<!-- Good: Clear headers and scope -->
<table class="casca-data">
  <caption>Quarterly Revenue Breakdown</caption>
  <thead>
    <tr>
      <th scope="col">Quarter</th>
      <th scope="col">Revenue</th>
      <th scope="col">Growth</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th scope="row">Q1 2025</th>
      <td>$150,000</td>
      <td>+15%</td>
    </tr>
  </tbody>
</table>

3. Error StatesLink to section

<!-- Accessible error indication -->
<div class="casca casca-figure" role="alert">
  <div class="casca-chip" data-variant="error">
    ⚠ Data unavailable
  </div>
</div>

4. Loading StatesLink to section

<!-- Accessible loading indicator -->
<div class="casca casca-figure" aria-busy="true" aria-live="polite">
  <div class="casca-title">Loading chart data...</div>
  <div class="casca-progress" data-indeterminate>
    <div class="casca-progress-bar"></div>
  </div>
</div>

ResourcesLink to section

Server-Rendered AccessibilityLink to section

Go TemplatesLink to section

Go Template Pattern:

type ChartData struct {
    ID          string
    Title       string
    Description string // For aria-describedby
    DataTable   template.HTML
}
{{define "accessible-chart"}}
<figure class="casca casca-figure"
        role="img"
        aria-labelledby="chart-{{.ID}}-title"
        {{if .Description}}aria-describedby="chart-{{.ID}}-desc"{{end}}>
  <figcaption>
    <div id="chart-{{.ID}}-title" class="casca-title">{{.Title}}</div>
    {{if .Description}}
    <div id="chart-{{.ID}}-desc" class="casca-subtitle">{{.Description}}</div>
    {{end}}
  </figcaption>

  {{{.DataTable}}}

  <div class="casca-bar" aria-hidden="true">
    <!-- Visual chart -->
  </div>
</figure>
{{end}}

htmx Fragment SwapsLink to section

Maintain accessibility across updates:

<!-- Announce updates to screen readers -->
<div id="chart-container"
     hx-get="/api/charts/sales"
     hx-swap="innerHTML"
     hx-on::after-swap="announceUpdate()"
     role="region"
     aria-live="polite">
  <!-- Chart content -->
</div>

<script>
function announceUpdate() {
  // Optional JS enhancement for announcements
  const sr = document.createElement('div');
  sr.setAttribute('role', 'status');
  sr.setAttribute('aria-live', 'polite');
  sr.textContent = 'Chart updated with new data';
  document.body.appendChild(sr);
  setTimeout(() => sr.remove(), 1000);
}
</script>

Note: Above JS is progressive enhancement. Chart remains fully accessible without it.

SupportLink to section

For accessibility issues or questions: