From 088832aa39b8efd886f9d083adba586a24429ecc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sun, 10 May 2026 13:02:17 +0200 Subject: [PATCH] fix(admin): remove spurious scrollbar from feedback tab strip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Marc reported a stray "mini scroll-button" on the right side of the admin feedback inbox tab strip (Open / Acknowledged / Resolved / Archived). The four short tabs do not horizontally overflow the viewport, so the painted bar surprised him every time he opened the inbox. Root cause is a CSS overflow coupling: setting only `overflow-x: auto` silently forces `overflow-y` to `auto` too, because a single axis cannot be `visible` while the other clips. The strip is locked at `h-9` (36 CSS px) but its badge children (`py-0.5` + text-xs digits) ride 1-2 px taller than the strip on some glyph stacks, so the implicit `overflow-y: auto` paints a tiny vertical scrollbar on the right edge. Fix is a single class on `tabsListVariants`: `overflow-y-hidden` (plus `overflow-y-visible` in the vertical-orientation override so vertical tab columns are not affected). Horizontal swipe and keyboard-arrow scrolling still work — only the implicit y-axis bar is suppressed. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/components/ui/__tests__/tabs.test.tsx | 33 +++++++++++++++++++++++ src/components/ui/tabs.tsx | 12 ++++++++- 2 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 src/components/ui/__tests__/tabs.test.tsx diff --git a/src/components/ui/__tests__/tabs.test.tsx b/src/components/ui/__tests__/tabs.test.tsx new file mode 100644 index 000000000..1685e00df --- /dev/null +++ b/src/components/ui/__tests__/tabs.test.tsx @@ -0,0 +1,33 @@ +import { describe, expect, it } from "vitest"; +import { renderToStaticMarkup } from "react-dom/server"; + +import { Tabs, TabsList, TabsTrigger } from "../tabs"; + +describe("", () => { + it("clamps the y-axis so a fixed-height strip never paints a vertical scrollbar", () => { + // Marc reported a stray "mini scroll-button" on the right side of + // the feedback inbox tab strip (Open / Acknowledged / Resolved / + // Archived). Root cause: `overflow-x-auto` couples to `overflow-y` + // — once one axis is `auto`, the other can no longer be `visible`, + // so the browser silently flips it to `auto` too. Combined with + // the fixed `h-9` strip and the badges (`py-0.5` + text-xs) that + // ride 1-2 px taller than the strip on some glyph stacks, a + // tiny vertical scrollbar paints on the right edge. + // + // Locking `overflow-y-hidden` on the strip kills the painted bar + // without touching the horizontal swipe behaviour. + const html = renderToStaticMarkup( + + + A + B + + , + ); + const list = html.match(/]*data-slot="tabs-list"[^>]*>/); + expect(list).not.toBeNull(); + expect(list![0]).toMatch(/\boverflow-y-hidden\b/); + // Horizontal scroll behaviour is still intact for long strips. + expect(list![0]).toMatch(/\boverflow-x-auto\b/); + }); +}); diff --git a/src/components/ui/tabs.tsx b/src/components/ui/tabs.tsx index 70835b32f..16bef7550 100644 --- a/src/components/ui/tabs.tsx +++ b/src/components/ui/tabs.tsx @@ -34,8 +34,18 @@ function Tabs({ // `whitespace-nowrap` so labels never word-wrap mid-tab. Desktop // looks identical because the inline-flex shrink-wraps inside the // wider container. +// +// v1.4.19 A7 — `overflow-y-hidden` clamps the y-axis. Browsers couple +// the two overflow axes once one is `auto`: setting only +// `overflow-x: auto` silently flips `overflow-y` to `auto` as well. +// Combined with the fixed `h-9` strip and badge children that ride +// ~1 px taller than the strip on some glyph stacks, a tiny painted +// vertical scrollbar appeared on the right edge of short strips +// (e.g. the admin feedback `Open / Acknowledged / Resolved / +// Archived` tabs Marc reported in v1.4.19). Locking the y-axis hides +// that bar without affecting horizontal swipe behaviour. const tabsListVariants = cva( - "rounded-lg p-[3px] group-data-[orientation=horizontal]/tabs:h-9 data-[variant=line]:rounded-none group/tabs-list text-muted-foreground inline-flex w-fit max-w-full items-center justify-center overflow-x-auto touch-pan-x scrollbar-thin group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col group-data-[orientation=vertical]/tabs:overflow-x-visible", + "rounded-lg p-[3px] group-data-[orientation=horizontal]/tabs:h-9 data-[variant=line]:rounded-none group/tabs-list text-muted-foreground inline-flex w-fit max-w-full items-center justify-center overflow-x-auto overflow-y-hidden touch-pan-x scrollbar-thin group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col group-data-[orientation=vertical]/tabs:overflow-x-visible group-data-[orientation=vertical]/tabs:overflow-y-visible", { variants: { variant: {