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