From 0e2bb99f0251fc5246e31334f76fb339b8bd63e5 Mon Sep 17 00:00:00 2001 From: Alan Buscaglia Date: Fri, 5 Jun 2026 14:49:09 +0200 Subject: [PATCH] feat(ui): add Tabs content transition - Animate tab panels when switching active content - Preserve reduced-motion behavior - Cover shared Tabs motion with focused unit tests --- ui/components/shadcn/tabs/tabs.test.tsx | 35 +++++++++++++++++++++++++ ui/components/shadcn/tabs/tabs.tsx | 26 +++++++++++++----- 2 files changed, 55 insertions(+), 6 deletions(-) create mode 100644 ui/components/shadcn/tabs/tabs.test.tsx diff --git a/ui/components/shadcn/tabs/tabs.test.tsx b/ui/components/shadcn/tabs/tabs.test.tsx new file mode 100644 index 0000000000..ceffefed55 --- /dev/null +++ b/ui/components/shadcn/tabs/tabs.test.tsx @@ -0,0 +1,35 @@ +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { describe, expect, it } from "vitest"; + +import { Tabs, TabsContent, TabsList, TabsTrigger } from "./tabs"; + +describe("Tabs", () => { + it("animates tab content when switching between tabs", async () => { + // Given - A tabs group with two content panels + const user = userEvent.setup(); + render( + + + Overview + Events + + Overview content + Events content + , + ); + + // When - The user switches to another tab + await user.click(screen.getByRole("tab", { name: /events/i })); + + const eventsPanel = screen.getByText("Events content"); + + // Then - The newly active content keeps the motion-ready content element + expect(eventsPanel).toHaveAttribute("data-slot", "tabs-content"); + expect(eventsPanel).toHaveClass( + "will-change-transform", + "motion-reduce:transform-none", + ); + expect(eventsPanel).toHaveAttribute("data-state", "active"); + }); +}); diff --git a/ui/components/shadcn/tabs/tabs.tsx b/ui/components/shadcn/tabs/tabs.tsx index 92ca5575c8..e838aa3f23 100644 --- a/ui/components/shadcn/tabs/tabs.tsx +++ b/ui/components/shadcn/tabs/tabs.tsx @@ -1,6 +1,7 @@ "use client"; import * as TabsPrimitive from "@radix-ui/react-tabs"; +import { motion, useReducedMotion } from "framer-motion"; import type { ComponentProps, ReactNode } from "react"; import { @@ -30,7 +31,7 @@ const TRIGGER_STYLES = { * Content component styles */ const CONTENT_STYLES = - "mt-2 focus-visible:rounded-md focus-visible:outline-1 focus-visible:ring-[3px] focus-visible:border-ring focus-visible:outline-ring focus-visible:ring-ring/50" as const; + "mt-2 will-change-transform motion-reduce:transform-none focus-visible:rounded-md focus-visible:outline-1 focus-visible:ring-[3px] focus-visible:border-ring focus-visible:outline-ring focus-visible:ring-ring/50" as const; /** * Build trigger className by combining style parts @@ -122,14 +123,27 @@ function TabsTrigger({ function TabsContent({ className, + children, ...props }: ComponentProps) { + const shouldReduceMotion = useReducedMotion(); + return ( - + + + {children} + + ); }