# ARIA State Variants

**SHOULD** · **ID:** `interactions-aria-variants` · **Category:** interactions
**Source:** [Tailwind](https://tailwindcss.com/docs)
**Interactive version:** https://ui-guides-agent-rules.netlify.app/principles/interactions-aria-variants

> SHOULD: Use aria-* variants (aria-selected:, aria-expanded:) to tie styles to ARIA state

Use aria-* variants for accessible interactive states

Using aria-* variants ensures your visual styles match the accessibility state. This prevents mismatches where something looks selected but isn't announced as selected.

## Bad — do not do this

`interactions-aria-variants-bad`

```tsx
import { useState } from 'react';

export function AriaVariantsBad() {
  const [selectedTab, setSelectedTab] = useState(0);
  // BAD: Separate state for visual styling, not using aria-* CSS variants
  const [activeStyles, setActiveStyles] = useState([true, false, false]);
  const tabs = ['Account', 'Security', 'Billing'];

  const handleClick = (index: number) => {
    setSelectedTab(index);
    // BAD: Manually syncing visual state - can drift from ARIA state
    setActiveStyles(tabs.map((_, i) => i === index));
  };

  return (
    <div className="w-full max-w-sm space-y-4">
      <div className="bg-card border border-border rounded-lg p-4">
        <h4 className="font-medium mb-3">Manual Class Toggling</h4>
        {/* BAD: Using JS state for styling instead of aria-selected: CSS variants */}
        <div className="flex gap-1 bg-muted p-1 rounded-lg">
          {tabs.map((tab, i) => (
            <button
              key={tab}
              onClick={() => handleClick(i)}
              className={`flex-1 px-3 py-1.5 text-sm rounded-md transition-colors ${
                activeStyles[i]
                  ? 'bg-background text-foreground shadow-sm'
                  : 'text-muted-foreground'
              }`}
            >
              {tab}
            </button>
          ))}
        </div>
        <div className="mt-4 p-3 bg-muted rounded text-sm">
          Content for {tabs[selectedTab]} tab
        </div>
        <div className="mt-3 bg-muted rounded p-2 font-mono text-xs">
          <code className="text-error">{`activeStyles[i] ? '...' : '...'`}</code>
          <p className="mt-1 text-error">No aria-selected, no role="tab"</p>
        </div>
      </div>
      <p className="text-xs text-error">
        Manual class toggling instead of using aria-selected: CSS variants
      </p>
    </div>
  );
}
```

## Good — do this

`interactions-aria-variants-good`

```tsx
import { useState } from 'react';

export function AriaVariantsGood() {
  const [selectedTab, setSelectedTab] = useState(0);
  const tabs = ['Account', 'Security', 'Billing'];

  return (
    <div className="w-full max-w-sm space-y-4">
      <div className="bg-card border border-border rounded-lg p-4">
        <h4 className="font-medium mb-3">ARIA-Selected Tabs</h4>
        <div role="tablist" className="flex gap-1 bg-muted p-1 rounded-lg">
          {tabs.map((tab, i) => (
            <button
              key={tab}
              role="tab"
              aria-selected={selectedTab === i}
              onClick={() => setSelectedTab(i)}
              className="flex-1 px-3 py-1.5 text-sm rounded-md transition-colors aria-selected:bg-background aria-selected:text-foreground aria-selected:shadow-sm text-muted-foreground"
            >
              {tab}
            </button>
          ))}
        </div>
        <div className="mt-4 p-3 bg-muted rounded text-sm">
          Content for {tabs[selectedTab]} tab
        </div>
        <div className="mt-3 bg-muted rounded p-2 font-mono text-xs">
          <code>aria-selected:bg-background</code>
        </div>
      </div>
      <p className="text-xs text-success">
        Visual state tied to ARIA attribute - always in sync
      </p>
    </div>
  );
}
```

## References

- [ARIA States](https://tailwindcss.com/docs/hover-focus-and-other-states#aria-states)
- [WAI-ARIA](https://www.w3.org/WAI/ARIA/apg/)
