Phototab
Phototab é um componente de navegação para React e Tailwind CSS, da biblioteca SmoothUI, com licença MIT. Copia e cola no teu projeto.
SmoothUI
MIT
navigation
O que é
Phototab é um componente de navegação para React e Tailwind CSS, da biblioteca SmoothUI, com licença MIT. Copia e cola no teu projeto.
Como usar
Abre o componente no catálogo, carrega em copiar, e cola no teu projeto. O prompt copiado leva o código e as regras para o teu agente o reproduzir sem alterar nada.
Cinco cópias grátis por mês. Sem cartão.
Precisa de
npm i motion @radix-ui/react-tabs
Licença
Este componente vem de SmoothUI e é redistribuído sob a licença MIT, que permite uso comercial. O aviso de copyright viaja com o código.
Phototab
"use client";
import {
Content as TabsContent,
List as TabsList,
Root as TabsRoot,
Trigger as TabsTrigger,
} from "@radix-ui/react-tabs";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import { useLayoutEffect, useRef, useState } from "react";
/**
* Tab definition for Phototab
*/
export interface PhototabTab {
/** Tab icon (ReactNode) */
icon: React.ReactNode;
/** Tab image (string: URL or import) */
image: string;
/** Tab label */
name: string;
}
export interface PhototabProps {
/** Class name for root */
className?: string;
/** Default selected tab name */
defaultTab?: string;
/** Height of the component in pixels */
height?: number;
/** Class name for image */
imageClassName?: string;
/** Class name for tab list */
tabListClassName?: string;
/** Array of tabs to display */
tabs: PhototabTab[];
/** Class name for tab trigger */
tabTriggerClassName?: string;
}
export default function Phototab({
tabs,
defaultTab,
height = 400,
className = "",
tabListClassName = "",
tabTriggerClassName = "",
imageClassName = "",
}: PhototabProps) {
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
const [bgStyle, setBgStyle] = useState<{
left: number;
top: number;
width: number;
height: number;
} | null>(null);
const triggersRef = useRef<(HTMLButtonElement | null)[]>([]);
const listRef = useRef<HTMLDivElement | null>(null);
const shouldReduceMotion = useReducedMotion();
useLayoutEffect(() => {
if (
hoveredIndex !== null &&
triggersRef.current[hoveredIndex] &&
listRef.current
) {
const trigger = triggersRef.current[hoveredIndex];
if (!trigger) {
return;
}
const listRect = listRef.current.getBoundingClientRect();
const triggerRect = trigger.getBoundingClientRect();
setBgStyle({
height: triggerRect.height,
left: triggerRect.left - listRect.left,
top: triggerRect.top - listRect.top,
width: triggerRect.width,
});
} else {
setBgStyle(null);
}
}, [hoveredIndex]);
return (
<TabsRoot
className={`group relative aspect-square w-auto overflow-hidden ${className}`}
defaultValue={defaultTab || (tabs[0]?.name ?? "")}
orientation="horizontal"
style={{ height: `${height}px` }}
>
<TabsList
aria-label="Phototab Tabs"
className={`absolute right-0 bottom-2 left-0 mx-auto flex w-40 -translate-y-10 flex-row items-center justify-between rounded-full bg-primary/40 px-3 py-2 font-medium text-sm ring ring-border/70 backdrop-blur-sm transition hover:text-foreground md:translate-y-20 md:group-hover:translate-y-0 ${tabListClassName}`}
ref={listRef}
style={{ pointerEvents: "auto" }}
>
<AnimatePresence>
{bgStyle ? (
<motion.span
animate={{
height: bgStyle.height,
left: bgStyle.left,
opacity: 1,
top: bgStyle.top,
width: bgStyle.width,
}}
className="absolute z-0 rounded-full bg-primary transition-colors"
exit={{ opacity: 0 }}
initial={{ opacity: 0 }}
layoutId="hoverBackground"
style={{ position: "absolute" }}
transition={
shouldReduceMotion
? { duration: 0 }
: {
damping: 40,
duration: 0.25,
stiffness: 400,
type: "spring" as const,
}
}
/>
) : null}
</AnimatePresence>
{tabs.map((tab, index) => (
<TabsTrigger
aria-label={tab.name}
className={`relative z-10 cursor-pointer rounded-full p-2 data-[state='active']:bg-background ${tabTriggerClassName}`}
key={tab.name}
onMouseEnter={() => {
setHoveredIndex(index);
}}
onMouseLeave={() => {
setHoveredIndex(null);
}}
ref={(el) => {
triggersRef.current[index] = el;
}}
value={tab.name}
>
<span className="relative z-10 rounded-full focus:outline-none">
{tab.icon}
<span className="sr-only">{tab.name}</span>
</span>
</TabsTrigger>
))}
</TabsList>
{tabs.map((tab) => (
<TabsContent className="h-full w-full" key={tab.name} value={tab.name}>
<img
alt={tab.name}
className={`h-full w-full rounded-2xl bg-primary object-cover ${imageClassName}`}
draggable={false}
height={height}
loading="lazy"
src={tab.image}
width={400}
/>
</TabsContent>
))}
</TabsRoot>
);
}