A segmented control presents a small set of mutually exclusive choices and keeps exactly one choice selected. Build the control in React with one selected index so its highlight, accessible state, and status text never disagree.
Implement the default App component in App.tsx. It takes no props and renders the fixed Day, Week, and Month segments.
Day has the selected class and aria-selected={true}, while the status reads Selected: Day.Week moves both the visual highlight and aria-selected={true} to Week, then changes the status to Selected: Week.Month after Week leaves only Month selected.useState(0) and derive every visible result from it.selected; do not store three independent booleans.role="tablist", role="tab", and aria-selected attributes already present in the starter.styles.css.Use one React state value as the source for the visual selection, accessible selection, and status text.
The three buttons describe one choice, so they must not manage themselves independently. If the highlight moves but aria-selected or the label does not, the control tells two different stories. One selected index keeps those outputs synchronized.
Treat selected as the answer to one question: which array position is active? A click changes that answer with setSelected(i). React then renders the class, ARIA value, and label from the new index.
function select(event: React.MouseEvent<HTMLButtonElement>) {
document.querySelector('.selected')?.classList.remove('selected');
event.currentTarget.classList.add('selected');
}
This moves the colored class, but React still has no selected value. The status stays on Day, and aria-selected can disagree with what the user sees.
import { useState } from 'react';
import './styles.css';
const SEGMENTS = ['Day', 'Week', 'Month'];
export default function App() {
const [selected, setSelected] = useState(0);
return (
<main className="container">
<h1>Segmented Control</h1>
<div className="segmented" role="tablist" aria-label="Time range">
{SEGMENTS.map((label, i) => (
<button
key={label}
type="button"
role="tab"
className={i === selected ? 'segment selected' : 'segment'}
aria-selected={i === selected}
onClick={() => setSelected(i)}
>
{label}
</button>
))}
</div>
<p className="status">Selected: {SEGMENTS[selected]}</p>
</main>
);
}
useState(0) makes Day the initial choice. Every button compares its own index with selected, so the same boolean controls its class and aria-selected. The status looks up the matching label instead of storing a second value that could drift.
The first render has selected = 0. Clicking Week calls setSelected(1). On the next render, only index 1 passes the comparison, so Week gains the highlight and true ARIA state while Day loses both; the status reads SEGMENTS[1].
role="tab" does not replace a button; retain the native <button> element.tabIndex for the full tablist keyboard pattern.value and onChange props to make the control parent-controlled.This version stores the selected index with a reducer. The rendered tablist, highlight, and status remain identical.
import { useReducer } from 'react';
import './styles.css';
const SEGMENTS = ['Day', 'Week', 'Month'];
export default function App() {
const [selected, select] = useReducer(
(_current: number, next: number) => next,
0,
);
return (
<main className="container">
<h1>Segmented Control</h1>
<div className="segmented" role="tablist" aria-label="Time range">
{SEGMENTS.map((label, index) => (
<button
key={label}
type="button"
role="tab"
className={index === selected ? 'segment selected' : 'segment'}
aria-selected={index === selected}
onClick={() => select(index)}
>
{label}
</button>
))}
</div>
<p className="status">Selected: {SEGMENTS[selected]}</p>
</main>
);
}Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.
A segmented control presents a small set of mutually exclusive choices and keeps exactly one choice selected. Build the control in React with one selected index so its highlight, accessible state, and status text never disagree.
Implement the default App component in App.tsx. It takes no props and renders the fixed Day, Week, and Month segments.
Day has the selected class and aria-selected={true}, while the status reads Selected: Day.Week moves both the visual highlight and aria-selected={true} to Week, then changes the status to Selected: Week.Month after Week leaves only Month selected.useState(0) and derive every visible result from it.selected; do not store three independent booleans.role="tablist", role="tab", and aria-selected attributes already present in the starter.styles.css.Use one React state value as the source for the visual selection, accessible selection, and status text.
The three buttons describe one choice, so they must not manage themselves independently. If the highlight moves but aria-selected or the label does not, the control tells two different stories. One selected index keeps those outputs synchronized.
Treat selected as the answer to one question: which array position is active? A click changes that answer with setSelected(i). React then renders the class, ARIA value, and label from the new index.
function select(event: React.MouseEvent<HTMLButtonElement>) {
document.querySelector('.selected')?.classList.remove('selected');
event.currentTarget.classList.add('selected');
}
This moves the colored class, but React still has no selected value. The status stays on Day, and aria-selected can disagree with what the user sees.
import { useState } from 'react';
import './styles.css';
const SEGMENTS = ['Day', 'Week', 'Month'];
export default function App() {
const [selected, setSelected] = useState(0);
return (
<main className="container">
<h1>Segmented Control</h1>
<div className="segmented" role="tablist" aria-label="Time range">
{SEGMENTS.map((label, i) => (
<button
key={label}
type="button"
role="tab"
className={i === selected ? 'segment selected' : 'segment'}
aria-selected={i === selected}
onClick={() => setSelected(i)}
>
{label}
</button>
))}
</div>
<p className="status">Selected: {SEGMENTS[selected]}</p>
</main>
);
}
useState(0) makes Day the initial choice. Every button compares its own index with selected, so the same boolean controls its class and aria-selected. The status looks up the matching label instead of storing a second value that could drift.
The first render has selected = 0. Clicking Week calls setSelected(1). On the next render, only index 1 passes the comparison, so Week gains the highlight and true ARIA state while Day loses both; the status reads SEGMENTS[1].
role="tab" does not replace a button; retain the native <button> element.tabIndex for the full tablist keyboard pattern.value and onChange props to make the control parent-controlled.This version stores the selected index with a reducer. The rendered tablist, highlight, and status remain identical.
import { useReducer } from 'react';
import './styles.css';
const SEGMENTS = ['Day', 'Week', 'Month'];
export default function App() {
const [selected, select] = useReducer(
(_current: number, next: number) => next,
0,
);
return (
<main className="container">
<h1>Segmented Control</h1>
<div className="segmented" role="tablist" aria-label="Time range">
{SEGMENTS.map((label, index) => (
<button
key={label}
type="button"
role="tab"
className={index === selected ? 'segment selected' : 'segment'}
aria-selected={index === selected}
onClick={() => select(index)}
>
{label}
</button>
))}
</div>
<p className="status">Selected: {SEGMENTS[selected]}</p>
</main>
);
}Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.