javascript 59 lines · 8 steps

URL-driven tabs with React Router

A tab component that stores the active tab in the URL query string instead of local state, making selections shareable and bookmarkable.

Explained by highlit
1import { useSearchParams } from "react-router-dom";
2 
3const TABS = [
4 { id: "overview", label: "Overview" },
5 { id: "activity", label: "Activity" },
6 { id: "settings", label: "Settings" },
7];
8 
9const DEFAULT_TAB = TABS[0].id;
10 
11export function ProjectTabs({ children }) {
12 const [searchParams, setSearchParams] = useSearchParams();
13 
14 const requested = searchParams.get("tab");
15 const activeTab = TABS.some((t) => t.id === requested) ? requested : DEFAULT_TAB;
16 
17 const selectTab = (tabId) => {
18 setSearchParams(
19 (prev) => {
20 const next = new URLSearchParams(prev);
21 if (tabId === DEFAULT_TAB) {
22 next.delete("tab");
23 } else {
24 next.set("tab", tabId);
25 }
26 return next;
27 },
28 { replace: true }
29 );
30 };
31 
32 return (
33 <div className="project-tabs">
34 <div role="tablist" aria-label="Project sections">
35 {TABS.map((tab) => (
36 <button
37 key={tab.id}
38 role="tab"
39 id={`tab-${tab.id}`}
40 aria-selected={tab.id === activeTab}
41 aria-controls={`panel-${tab.id}`}
42 tabIndex={tab.id === activeTab ? 0 : -1}
43 className={tab.id === activeTab ? "tab is-active" : "tab"}
44 onClick={() => selectTab(tab.id)}
45 >
46 {tab.label}
47 </button>
48 ))}
49 </div>
50 <div
51 role="tabpanel"
52 id={`panel-${activeTab}`}
53 aria-labelledby={`tab-${activeTab}`}
54 >
55 {children[activeTab]}
56 </div>
57 </div>
58 );
59}
01 / 01
STEP 01

Walkthrough

Space play ←→ step click any line
Three takeaways
  1. 1Storing UI state in the URL makes views shareable, bookmarkable, and survivable across reloads.
  2. 2Validating the raw query value against a known list guards against arbitrary or stale input.
  3. 3Wiring ARIA roles and tabIndex to the derived active tab keeps keyboard and screen-reader behavior correct for free.

Related explainers

Share this explainer

Here's the card — post it anywhere.

URL-driven tabs with React Router — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code