All components
Image Tab Panel
navigationUi-Layouts component.
responsive · 688px
Install
Same command in any shadcn project — React (Vite/CRA), Next.js, Remix, Astro, and more:
$
npx shadcn@latest add https://your-domain/r/image-tab1.jsonUsage
"use client";
import {
TabsProvider,
TabList,
TabItem,
TabHeader,
TabDes,
TabImageContainer,
TabImage,
} from "@/registry/ui-layouts/image-tabs";
const TABS = [
{
value: "design",
title: "Design Systems",
description:
"A design system is a collection of reusable components guided by clear standards that teams use to build consistent digital products faster.",
imageUrl: "https://picsum.photos/seed/design42/1200/800",
},
{
value: "motion",
title: "Motion & Animation",
description:
"Purposeful motion guides attention, communicates state, and adds delight — making interfaces feel alive without sacrificing performance.",
imageUrl: "https://picsum.photos/seed/motion17/1200/800",
},
{
value: "typography",
title: "Typography",
description:
"Type is the voice of your interface. Hierarchy, scale, and rhythm turn raw text into a reading experience that carries users through content effortlessly.",
imageUrl: "https://picsum.photos/seed/typo88/1200/800",
},
];
export default function Demo() {
return (
<div className="w-full h-full p-6 flex items-center justify-center">
<TabsProvider defaultValue="design" className="max-w-4xl w-full">
<div className="md:grid md:grid-cols-12 gap-4 items-start">
<TabList className="md:col-span-5">
{TABS.map((tab) => (
<TabItem key={tab.value} value={tab.value}>
<TabHeader value={tab.value}>{tab.title}</TabHeader>
<TabDes value={tab.value}>
<p className="dark:bg-white bg-[#F2F2F2] text-black p-3 text-sm">
{tab.description}
</p>
<img
src={tab.imageUrl}
alt={tab.title}
className="mb-2 max-w-full h-auto md:hidden block rounded-md object-cover"
width={1200}
height={800}
/>
</TabDes>
</TabItem>
))}
</TabList>
<TabImageContainer className="md:col-span-7">
{TABS.map((tab) => (
<TabImage key={tab.value} value={tab.value}>
<img
src={tab.imageUrl}
alt={tab.title}
className="w-full h-full object-cover rounded-md"
width={1200}
height={800}
/>
</TabImage>
))}
</TabImageContainer>
</div>
</TabsProvider>
</div>
);
}Component source
'use client';
import { useMediaQuery } from '@/hooks/use-media-query';
import { cn } from '@/lib/utils';
import { AnimatePresence, motion } from 'motion/react';
import React, { createContext, type ReactNode, useContext, useState } from 'react';
interface Tab {
id: string;
title: string;
description: string;
imageUrl: string;
}
interface TabsContextType {
activeTab: string;
setActiveTab: (id: string) => void;
isDesktop: boolean;
}
const TabsContext = createContext<TabsContextType | undefined>(undefined);
const useTabs = () => {
const context = useContext(TabsContext);
if (!context) {
throw new Error('Tabs components must be used within a TabsProvider');
}
return context;
};
export function TabsProvider({
children,
defaultValue,
className,
}: {
children: ReactNode;
defaultValue: string;
className?: string;
}) {
const [activeTab, setActiveTab] = useState(defaultValue);
const isDesktop = useMediaQuery('(min-width: 768px)');
return (
<TabsContext.Provider value={{ activeTab, setActiveTab, isDesktop }}>
<div className={cn('w-full h-full', className)}>{children}</div>
</TabsContext.Provider>
);
}
export function TabList({ children, className }: { children: ReactNode; className?: string }) {
return <div className={cn('rounded-xs h-fit', className)}>{children}</div>;
}
export function TabItem({ children, value }: { children: ReactNode; value: string }) {
const { activeTab, setActiveTab } = useTabs();
return (
<motion.div
className={`rounded-lg overflow-hidden mb-2 md:text-base text-sm ${
activeTab === value
? 'active border-2 dark:border-[#656fe2] border-[#F2F2F2] dark:bg-[#E0ECFB] bg-[#F2F2F2]'
: 'bg-transparent border-2 dark:hover:border-[#656fe2]'
}`}
onClick={() => setActiveTab(value)}
>
{children}
</motion.div>
);
}
export function TabHeader({ children, value }: { children: ReactNode; value: string }) {
const { activeTab } = useTabs();
return (
<h3
className={`p-4 md:text-base text-sm cursor-pointer transition-all font-semibold dark:text-white text-black dark:hover:bg-[#1e2a78] hover:bg-[#F2F2F2] dark:hover:text-white hover:text-black flex justify-between items-center ${
activeTab === value ? 'active dark:bg-[#1e2a78] bg-[#F2F2F2]' : 'dark:bg-[#11112b] bg-white'
}`}
>
{children}
</h3>
);
}
export function TabDes({ children, value }: { children: ReactNode; value: string }) {
const { activeTab } = useTabs();
return (
<AnimatePresence mode='sync'>
{activeTab === value && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{
duration: 0.3,
ease: 'easeInOut',
delay: 0.14,
}}
>
{children}
</motion.div>
)}
</AnimatePresence>
);
}
export function TabImageContainer({
children,
className,
}: {
children: ReactNode;
className?: string;
}) {
return (
<div className={cn('', className)}>
<AnimatePresence mode='popLayout'>{children}</AnimatePresence>
</div>
);
}
export function TabImage({ children, value }: { children: ReactNode; value: string }) {
const { activeTab, isDesktop } = useTabs();
if (activeTab !== value || !isDesktop) return null;
return (
<motion.div
initial={{ opacity: 0, overflow: 'hidden' }}
animate={{ opacity: 1, overflow: 'hidden' }}
exit={{ opacity: 0, overflow: 'hidden' }}
transition={{
duration: 0.4,
delay: 0.2,
}}
className='p-4 h-[400px] rounded-md overflow-hidden'
>
{children}
</motion.div>
);
}Source: Ui-Layouts