Todos os componentes

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.

Abrir no catálogo

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

index.tsx
"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>
  );
}

Componentes parecidos