From 8313d61b9e86cb82c9e49022913e1f27c789a06e Mon Sep 17 00:00:00 2001 From: varun-pahuja Date: Sat, 17 Oct 2026 19:55:58 +0530 Subject: [PATCH 1/2] fix: give the JWT decoder switch real tab semantics and name its fields The Decode/Encode switch looked like tabs but was two plain buttons, so screen reader users had no tab semantics and no arrow-key navigation. Follow the Base64 tool: a tablist with aria-label, role=tab on each button with aria-selected, aria-controls and a roving tabIndex, Arrow/Home/End handling with wraparound, and role=tabpanel with aria-labelledby on each panel. Also name the fields that had none: the token textarea, the Endpoint input (now tied to its label with htmlFor and id), the payload editor, the secret/key inputs and the JWKS URL. The decoding error block becomes role=alert, and the verification toggle gets aria-expanded and aria-controls. No className changes, so both themes look identical. --- app/(tools)/jwt-decoder/JwtDecoderClient.tsx | 81 +++++++++++++++++-- .../jwt-decoder/components/EncoderTab.tsx | 3 + .../jwt-decoder/components/VerifyPanel.tsx | 3 + 3 files changed, 80 insertions(+), 7 deletions(-) diff --git a/app/(tools)/jwt-decoder/JwtDecoderClient.tsx b/app/(tools)/jwt-decoder/JwtDecoderClient.tsx index b5b9e80..df90c73 100644 --- a/app/(tools)/jwt-decoder/JwtDecoderClient.tsx +++ b/app/(tools)/jwt-decoder/JwtDecoderClient.tsx @@ -37,8 +37,12 @@ import { cn } from "@/lib/cn"; type TabId = "decode" | "encode"; +const TAB_IDS: TabId[] = ["decode", "encode"]; + export function JwtDecoderClient() { const [tab, setTab] = useState("decode"); + const tablistRef = useRef(null); + const [token, setToken] = useState(""); const [error, setError] = useState(null); const [decoded, setDecoded] = useState(null); @@ -97,6 +101,18 @@ export function JwtDecoderClient() { push("Could not copy link", "error"); } }, [token, push]); + function onTabKeyDown(e: React.KeyboardEvent, index: number) { + let next = index; + if (e.key === "ArrowRight") next = (index + 1) % TAB_IDS.length; + else if (e.key === "ArrowLeft") next = (index - 1 + TAB_IDS.length) % TAB_IDS.length; + else if (e.key === "Home") next = 0; + else if (e.key === "End") next = TAB_IDS.length - 1; + else return; + e.preventDefault(); + setTab(TAB_IDS[next]!); + const btns = tablistRef.current?.querySelectorAll('[role="tab"]'); + btns?.[next]?.focus(); + } const downloadJson = useCallback(() => { if (!decoded) return; @@ -141,12 +157,29 @@ export function JwtDecoderClient() { return ( <>
-
- setTab("decode")}> +
+ setTab("decode")} + onKeyDown={(e) => onTabKeyDown(e, 0)} + > Decode - setTab("encode")}> + setTab("encode")} + onKeyDown={(e) => onTabKeyDown(e, 1)} + > Encode @@ -155,7 +188,12 @@ export function JwtDecoderClient() { {tab === "decode" && ( <> -
+
@@ -215,6 +253,7 @@ export function JwtDecoderClient() {