my/ui

Command Palette

Search for a command to run...

All components

Image Tab Panel

navigation

Ui-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.json

Usage

"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