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

javascript
const { Pool } = require('pg');
 
const pool = new Pool({
  connectionString: process.env.DATABASE_URL,

Per-request Postgres connections in Express

connection-pooling middleware transactions
Intermediate 8 steps
javascript
const express = require('express');
const router = express.Router();
const db = require('../db');
 

Building a paginated orders page in Express

pagination routing sql-queries
Intermediate 7 steps
javascript
import { Suspense } from 'react';
import { searchProducts } from '@/lib/products';
import SearchInput from './search-input';
 

Streaming search results in a Next.js Server Component

server-components suspense streaming
Intermediate 8 steps
javascript
import { useState } from 'react';
 
function StarRating({ value = 0, max = 5, onChange, size = 24 }) {
  const [hovered, setHovered] = useState(null);

Building an accessible star rating in React

controlled-component accessibility state-management
Intermediate 8 steps
javascript
import { notFound } from 'next/navigation'
import { Suspense } from 'react'
import { getPostBySlug, getRelatedPosts } from '@/lib/posts'
import { RelatedPosts } from '@/components/related-posts'

Building a dynamic blog post page in Next.js

server-components dynamic-routing metadata
Intermediate 8 steps
javascript
const express = require('express');
const { createProxyMiddleware, fixRequestBody } = require('http-proxy-middleware');
 
const router = express.Router();

Building an API gateway proxy in Express

reverse-proxy middleware api-gateway
Intermediate 6 steps

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