AniUI Academy

Build accessible tabs

easyAccessibility+150 XP

Build a tab list with three tabs, each showing its own panel.

Clicking a tab activates it. The keyboard has to work too: with a tab active, ArrowRight/ArrowLeft move to the next/previous tab and activate it immediately, wrapping around at each end.

Examples

// Initial: [Overview]* Specs  Reviews
//   tabpanel renders "Overview content".

// Click Specs → aria-selected moves to Specs; panel = "Specs content".

// Focus Reviews, press ArrowRight
// → wraps to Overview.

// Focus Overview, press ArrowLeft
// → wraps to Reviews.

Automatic activation — arrow keys don't just move focus, they commit the tab.

function handleKeyDown(e) {
  if (e.key === "ArrowRight") {
    setActive((i) => (i + 1) % TABS.length);
  } else if (e.key === "ArrowLeft") {
    setActive((i) => (i - 1 + TABS.length) % TABS.length);
  }
}

`(i - 1 + N) % N` handles the negative wrap safely; plain `-1 % N` goes to -1 in JS.

What it has to do

  • Each tab is a `<button role="tab" aria-selected="true|false">`, and there are exactly three.
  • Exactly one `<div role="tabpanel">` is present at a time — the one for the active tab.
  • Clicking a tab makes it the active one.
  • With the last tab active, ArrowRight moves to the first tab. With the first tab active, ArrowLeft moves to the last.

Notes

  • ARIA has two patterns: automatic activation (arrows change tab) and manual activation (arrows move focus, Enter activates). This problem uses automatic — common for small tab sets.
  • Link each tab to its panel: `aria-controls="panel-X"` on the tab, `aria-labelledby="tab-X"` on the panel. The test doesn't check these, but production needs them.
  • Use roving tabindex (`tabindex=0` on the active tab, `-1` on the rest) so Tab moves users past the whole tab group — not through each tab individually.
  • Keep only one `<div role="tabpanel">` mounted — unmounting on switch resets any transient panel state (scroll position, form drafts). That's a feature, not a bug.
Asked atMicrosoftGoogleAtlassian

Stuck? The react course covers the ideas this problem is built on.

import { useState } from "react";

const TABS = [
  { key: "overview", label: "Overview", content: "Overview content" },
  { key: "specs", label: "Specs", content: "Specs content" },
  { key: "reviews", label: "Reviews", content: "Reviews content" },
];

export default function App() {
  const [active, setActive] = useState(0);

  function handleKeyDown(e) {
    // TODO: ArrowRight should move to the next tab (wrapping to the
    // first after the last), ArrowLeft to the previous (wrapping to
    // the last from the first) — and the newly reached tab should
    // become active, same as clicking it.
  }

  return (
    <div style={{ fontFamily: "sans-serif", padding: 16 }}>
      <div role="tablist" style={{ display: "flex", gap: 8 }}>
        {TABS.map((tab, index) => (
          <button
            key={tab.key}
            id={`tab-${tab.key}`}
            role="tab"
            aria-selected={index === active}
            onClick={() => setActive(index)}
            onKeyDown={handleKeyDown}
            style={{ padding: "8px 12px", fontWeight: index === active ? "bold" : "normal" }}
          >
            {tab.label}
          </button>
        ))}
      </div>
      <div id={`panel-${TABS[active].key}`} role="tabpanel" style={{ padding: 16, border: "1px solid #ddd" }}>
        {TABS[active].content}
      </div>
    </div>
  );
}

5 tests · +150 XP

Discussion

Sign in to post
Sign in to join the conversation.Sign in

No comments yet. Be the first to say something.