Accessibility

Pindoba is built with accessibility as a core requirement. Every component targets WCAG 2.1 Level AA by default. AAA-compliant themes are planned for the future.

Our Philosophy

Accessibility is more important than making components look stunning. A beautiful component that some users can’t operate is a broken component. That said, accessible and beautiful are not mutually exclusive — we work hard to balance both.

When there’s a conflict, accessibility wins:

  • Contrast ratios always meet the minimum threshold, even if a lighter shade would “look nicer”
  • Focus indicators are always visible, even when they break a clean visual design
  • Interactive areas are always large enough to tap
  • Screen reader announcements are always clear

Contrast & Color

AA Contrast Examples (Current)

Primary action (button, badge) bg: primary.sunken color: primary.text.contrast AA Pass
Body text on surface bg: neutral.surface color: neutral.text.bold AA Pass
Danger / error state bg: danger.sunken color: danger.text.contrast.bold AA Pass

Focus Indicators

---
import { css } from "@pindoba/styled-system/css";
import Badge from "@pindoba/astro-badge";
import Button from "@pindoba/astro-button";
import Input from "@pindoba/astro-input";

const containerStyle = css({
  display: "flex",
  flexDirection: "column",
  gap: "lg",
});

const sectionStyle = css.raw({
  backgroundColor: "neutral.surface",
  borderRadius: "lg",
  padding: "lg",
  border: "1px solid",
  borderColor: "neutral.border.muted",
});

const sectionTitleStyle = css.raw({
  fontSize: "md",
  fontWeight: "bold",
  marginBottom: "md",
  color: "neutral.text.bold",
});

const gridStyle = css.raw({
  display: "grid",
  gridTemplateColumns: "repeat(auto-fit, minmax(200px, 1fr))",
  gap: "md",
});

const cardStyle = css.raw({
  padding: "md",
  borderRadius: "md",
  display: "flex",
  flexDirection: "column",
  alignItems: "flex-start",
  gap: "xs",
});

const labelStyle = css.raw({
  fontSize: "xs",
  fontFamily: "mono",
});
---

<div class={containerStyle}>
  <div class={css(sectionStyle)}>
    <h3 class={css(sectionTitleStyle)}>AA Contrast Examples (Current)</h3>
    <div class={css(gridStyle)}>
      <div
        class={css(cardStyle, {
          backgroundColor: "primary.sunken",
          color: "primary.text.contrast",
        })}
      >
        <span>Primary action (button, badge)</span>
        <span class={css(labelStyle)}>bg: primary.sunken</span>
        <span class={css(labelStyle)}>color: primary.text.contrast</span>
        <Badge feedback="success">AA Pass</Badge>
      </div>

      <div
        class={css(cardStyle, {
          backgroundColor: "neutral.surface",
          color: "neutral.text.bold",
          borderStyle: "solid",
          borderWidth: "1px",
          borderColor: "neutral.border.muted",
        })}
      >
        <span>Body text on surface</span>
        <span class={css(labelStyle, { color: "neutral.text" })}
          >bg: neutral.surface</span
        >
        <span class={css(labelStyle, { color: "neutral.text" })}
          >color: neutral.text.bold</span
        >
        <Badge feedback="success">AA Pass</Badge>
      </div>

      <div
        class={css(cardStyle, {
          backgroundColor: "danger.sunken",
          color: "danger.text.contrast.bold",
        })}
      >
        <span>Danger / error state</span>
        <span class={css(labelStyle)}>bg: danger.sunken</span>
        <span class={css(labelStyle)}>color: danger.text.contrast.bold</span>
        <Badge feedback="success">AA Pass</Badge>
      </div>
    </div>
  </div>

  <div class={css(sectionStyle)}>
    <h3 class={css(sectionTitleStyle)}>Focus Indicators</h3>
    <div
      class={css({
        display: "flex",
        gap: "lg",
        flexWrap: "wrap",
        alignItems: "center",
      })}
    >
      <Button>Tab to focus me</Button>
      <Input placeholder="Focus this input" />
    </div>
  </div>
</div>

Semantic color tokens include .text.contrast variants that guarantee legible text on solid backgrounds:

import { css } from "@pindoba/styled-system/css";

<button className={css({
  backgroundColor: "primary.sunken",
  color: "primary.text.contrast",
})}>
  Guaranteed readable text
</button>

Color is never the only indicator. Always pair it with text, icons, or patterns:

// Color + icon + descriptive text
<span className={css({
  color: "danger.text.bold",
  display: "flex",
  alignItems: "center",
  gap: "xs",
})}>
  <ErrorIcon aria-hidden="true" />
  Error: your session has expired. Please sign in again.
</span>

Focus Management

All interactive components use :focus-visible for keyboard-only focus rings, powered by the pindobaOutline utility and the focusVisibleWithin condition:

<button className={css({
  _focusVisible: { pdbO: true },
})}>
  Tab to focus me
</button>

The focusVisibleWithin condition (&:has(:focus-visible, [data-focus-visible])) allows parent elements to react when any child receives keyboard focus.

Keyboard Navigation

Every Pindoba component is fully operable with a keyboard:

  • Buttons: Enter and Space to activate
  • Dialogs: Escape to close, focus is trapped inside
  • Tabs: Arrow keys to navigate between tabs
  • Checkboxes/Radios: Space to toggle, Arrow keys to navigate groups
  • Dropdowns: Arrow keys to navigate, Enter to select, Escape to close

Reduced Motion

Components respect the user’s motion preferences via the _motionReduce condition:

<div className={css({
  pdbT: "300ms",
  _motionReduce: { transition: "none" },
})}>
  Animated content
</div>

Current & Future Compliance

Current: AA (Default Theme)

  • Text contrast: 4.5:1 minimum for normal text, 3:1 for large text
  • UI component contrast: 3:1 minimum for interactive elements
  • Focus indicators: Visible and meet contrast requirements
  • Keyboard access: All interactive content is operable via keyboard

Future: AAA Themes

Planned themes with enhanced accessibility:

  • High contrast — increased ratios meeting AAA (7:1 for normal text, 4.5:1 for large text)
  • High visibility focus — bolder, more prominent focus indicators
  • Reduced transparency — solid alternatives where alpha colors are used today

These themes will be opt-in and can be combined with light or dark mode.

Best Practices

For Component Authors

  1. Use native HTML elements (<button>, <input>, <select>) instead of <div> with click handlers
  2. Include visible text labels — icon-only buttons need aria-label
  3. Test with keyboard navigation before considering a component complete
  4. Use .text.contrast tokens for text on colored backgrounds
  5. Respect prefers-reduced-motion for all animations

For Application Developers

  1. Provide meaningful page titles and heading hierarchy
  2. Include skip navigation links for content-heavy pages
  3. Ensure forms have visible labels and clear error messages
  4. Test with a screen reader periodically
  5. Don’t remove focus outlines — if the default style doesn’t fit, replace it with a visible alternative