From e66544832f902fd40a4fcc3b07d8d40b843ab1f0 Mon Sep 17 00:00:00 2001 From: jacobvjk Date: Mon, 26 Oct 2026 11:33:05 +0100 Subject: [PATCH] feat(tabs): make the pathway detail tabs read as clickable The resting tabs were plain text with no border or background, so they read as labels rather than controls, and Tailwind v4's preflight left them with the arrow cursor. * cursor-pointer, and a light neutral-50 background on hover, so a resting tab responds like a control. * The selected tab is semibold. Each tab reserves its semibold width with a hidden copy of its label, so switching tabs doesn't nudge the others sideways; the copy is aria-hidden, keeping one accessible name. The look at rest is unchanged. The UI kit's tab list is updated to match. Co-Authored-By: Claude Opus 5.5 --- docs/design/tpr-ui-kit.html | 16 +++++++++++----- src/components/Tabs.test.tsx | 9 +++++++++ src/components/Tabs.tsx | 18 ++++++++++++++---- 3 files changed, 34 insertions(+), 9 deletions(-) diff --git a/docs/design/tpr-ui-kit.html b/docs/design/tpr-ui-kit.html index c18594cd..cad5bc77 100644 --- a/docs/design/tpr-ui-kit.html +++ b/docs/design/tpr-ui-kit.html @@ -1080,6 +1080,7 @@ background: none; border: none; border-bottom: 2px solid transparent; + border-radius: 6px 6px 0 0; margin-bottom: -1px; padding: 10px 16px; font-size: 14px; @@ -1091,10 +1092,13 @@ .tablist button:hover { color: var(--color-bluespruce); border-bottom-color: var(--color-neutral-300); + background: var(--color-neutral-50); } .tablist button[aria-selected="true"] { color: var(--color-bluespruce); border-bottom-color: var(--color-bluespruce); + background: none; + font-weight: 600; } .tablist button.focus-demo { box-shadow: @@ -4065,13 +4069,15 @@

Tabs v2

Tab list — selected / resting / keyboard focus

Tabs.tsx — Tabs + TabPanel + useActiveTab

- px-4 py-2.5 text-sm font-mediumborder-b-2px-4 py-2.5 text-smborder-b-2 rounded-t-md cursor-pointerselected: border-bluespruce text-bluespruceresting: text-rmigray-600selected: font-semibold border-bluespruce + text-bluespruceresting: font-medium text-rmigray-600hover:text-bluespruce hover:border-neutral-300hover:text-bluespruce hover:border-neutral-300 + hover:bg-neutral-50focus-visible:ring-energy
diff --git a/src/components/Tabs.test.tsx b/src/components/Tabs.test.tsx index 78d73c8d..7b688416 100644 --- a/src/components/Tabs.test.tsx +++ b/src/components/Tabs.test.tsx @@ -50,6 +50,15 @@ describe("Tabs", () => { expect(screen.getAllByRole("tab")).toHaveLength(3); }); + it("names each tab by its label alone", () => { + renderHarness(); + // Each tab also holds a hidden copy of its label to reserve the semibold + // width; it must not be read out a second time ("One One"). + for (const { label } of TABS) { + expect(screen.getByRole("tab", { name: label })).toBeInTheDocument(); + } + }); + it("defaults to the first tab and shows only its panel", () => { renderHarness(); expect(screen.getByRole("tab", { name: "One" })).toHaveAttribute( diff --git a/src/components/Tabs.tsx b/src/components/Tabs.tsx index 81231867..9313535b 100644 --- a/src/components/Tabs.tsx +++ b/src/components/Tabs.tsx @@ -114,13 +114,23 @@ export const Tabs: React.FC = ({ tabIndex={selected ? 0 : -1} onClick={() => onChange(t.id)} className={clsx( - "px-4 py-2.5 text-sm font-medium border-b-2 transition-colors duration-150 focus:outline-none focus-visible:ring-2 focus-visible:ring-energy focus-visible:ring-offset-1", + "px-4 py-2.5 text-sm border-b-2 rounded-t-md cursor-pointer transition-colors duration-150 focus:outline-none focus-visible:ring-2 focus-visible:ring-energy focus-visible:ring-offset-1", selected - ? "border-bluespruce text-bluespruce" - : "border-transparent text-rmigray-600 hover:text-bluespruce hover:border-neutral-300", + ? "font-semibold border-bluespruce text-bluespruce" + : "font-medium border-transparent text-rmigray-600 hover:text-bluespruce hover:border-neutral-300 hover:bg-neutral-50", )} > - {t.label} + + {t.label} + {/* Reserves the semibold width, so selecting a tab doesn't + nudge the tabs after it sideways. */} + + ); })}