Explore
Golden Hour
Into the Wild
Deep Blue
Stellar Drift
Installation
npx shadcn@latest add https://tentui.com/r/animated-view.json
Usage
import { AnimatedView } from "@/components/animated-view";
import { Camera } from "lucide-react";
const items = [
{
id: "1",
title: "Cinematic Horizons",
subtitle: "Photography",
badge: "#209",
image: "https://example.com/image.jpg",
icon: Camera,
},
];
export default function Page() {
return <AnimatedView items={items} title="My Collection" />;
}Props
AnimatedView
| Prop | Type | Default | Description |
|---|---|---|---|
items | AnimatedViewItem[] | — | Array of items to display. |
title | string | "My Collection" | Heading rendered above the view toggle. |
defaultView | "list" | "grid" | "list" | The view mode rendered on first mount. |
className | string | — | Extra classes forwarded to the outer wrapper. |
AnimatedViewItem
| Field | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Unique key used by React and Motion for layout tracking. |
title | string | Yes | Primary text label. |
subtitle | string | Yes | Secondary text shown beneath the title. |
image | string | Yes | URL of the item's thumbnail image. |
badge | string | No | Short label rendered as a star badge (e.g. "#209"). |
icon | React.ElementType | No | Lucide (or any size-prop icon) shown next to subtitle. |
How the animation works
The switcher uses Motion's layout prop on every element in the tree, from the outer grid wrapper down to the image and text nodes. When view changes, React re-renders the list with new class names (flex-col vs. grid-cols-2) and Motion automatically interpolates each element's position and size using a spring — no manual x/y tweening needed.
The tab indicator is a separate motion.div that shares layoutId="animated-view-active-tab". Motion keeps that element alive across re-renders and slides it under whichever tab is active, giving the pill-follows-cursor feel.
Text metadata fades in and out via AnimatePresence with a short blur transition so the content change feels intentional rather than abrupt during the layout shift.
Patterns
Custom default view
Start in grid mode when the content is image-heavy.
<AnimatedView items={items} defaultView="grid" />Items without icons or badges
Both icon and badge are optional — omit them for a cleaner look.
const items = [
{ id: "1", title: "Report Q1", subtitle: "PDF", image: "/thumb.png" },
];Multiple instances on one page
Each AnimatedView manages its own view state independently. The tab indicator uses a stable layoutId ("animated-view-active-tab") scoped to each LayoutGroup, so multiple instances on the same page will not interfere with each other.