Luca Perullo
Components

Component

LiveNew

Preview Tabs

Tabs minimali per alternare anteprima e codice. Underline animato, switch tastiera-friendly, zero deps radix. Server-fallback: se code è omesso, mostra solo Anteprima.

TabsClientA11y
Open in ClaudeSorgente

Anteprima01

Anteprima non ancora pubblicata. Per ora puoi copiare il prompt qui sotto e rigenerare il componente con il tuo brand.

Note Niente radix-ui, niente headless deps. role=tablist, aria-selected, aria-controls, useId per stabilità SSR. L'underline è uno span absolute -bottom-px h-0.5 bg-fg.

Prompt LLM02

Incolla in Claude o ChatGPT per generare la tua variante. Include il contesto del brand, i token e i vincoli del progetto.

Prompt · preview-tabs
Open in Claude
Sei un senior frontend engineer. Stai lavorando su un sito Next.js 16 + React 19 + Tailwind v4 in italiano, look chanhdai-inspired: colonna stretta 672px, Geist Sans + Geist Mono, hairline 1px, divisori a stripe diagonale, palette zinc.

Token CSS disponibili: --bg, --bg-alt, --fg, --fg-muted, --fg-soft, --border, --border-strong, --accent. Usa SEMPRE queste variabili tramite le utility tailwind generate (bg-bg, text-fg-muted, border-border, ecc.). Helper "cn" da "@/lib/utils". Niente librerie UI extra: solo lucide-react e tailwind-merge.

Genera un client component <PreviewTabs>: tabs Preview/Code, zero dipendenze radix.
Props:
- preview: ReactNode
- code?: ReactNode — quando assente, render solo "Anteprima"
- defaultTab?: "preview" | "code"
- aside?: ReactNode — slot a destra del tablist
- className?: string

Comportamento: useState per active tab, useId per stabilità id, role=tablist, role=tab, aria-selected, aria-controls. Switch via click; se vuoi va bene anche senza arrow-key navigation in v1.

Layout: container rounded-[9px] border. Header: bg-bg-alt, due button mono uppercase tracking. Tab attivo: text-fg + underline (span absolute -bottom-px h-0.5 bg-fg). Inattivo: text-fg-muted hover:text-fg. Pannello preview: grid-dots backdrop, min-h-[180px], padding generoso. Pannello code: nessun padding extra (lo gestisce CodeBlock).

Constraints: "use client", accessibilità minima ma corretta (tablist + role=tab), niente animazioni FLIP.

Output: file completo .tsx.

Codice03

Sorgente live di src/components/preview-tabs.tsx. Letto a request-time dal repo — sempre allineato al sito.

tsxsrc/components/preview-tabs.tsx
"use client";

import { useId, useState, type ReactNode } from "react";
import { cn } from "@/lib/utils";

export type TabKey = "preview" | "code";

export type PreviewTabsProps = {
    /** Live preview content. */
    preview: ReactNode;
    /** Code panel content (typically a CodeBlock). */
    code?: ReactNode;
    defaultTab?: TabKey;
    /** Optional aside rendered to the right of the tab list (e.g. a small mono caption). */
    aside?: ReactNode;
    /** When true, the preview escapes the 480px content cap and fills the available width. */
    bleed?: boolean;
    className?: string;
};

const TABS: { key: TabKey; label: string }[] = [
    { key: "preview", label: "Anteprima" },
    { key: "code", label: "Codice" },
];

export function PreviewTabs({
    preview,
    code,
    defaultTab = "preview",
    aside,
    bleed = false,
    className,
}: PreviewTabsProps) {
    const [active, setActive] = useState<TabKey>(defaultTab);
    const id = useId();
    const items = code ? TABS : TABS.filter((t) => t.key === "preview");

    return (
        <div className={cn("overflow-hidden rounded-[9px] border border-border bg-bg", className)}>
            <div
                role="tablist"
                aria-orientation="horizontal"
                className="flex items-center justify-between gap-3 border-b border-border bg-bg-alt pl-1 pr-3"
            >
                <div className="flex">
                    {items.map((tab) => {
                        const isActive = tab.key === active;
                        return (
                            <button
                                key={tab.key}
                                id={`${id}-tab-${tab.key}`}
                                role="tab"
                                type="button"
                                aria-selected={isActive}
                                aria-controls={`${id}-panel-${tab.key}`}
                                onClick={() => setActive(tab.key)}
                                className={cn(
                                    "relative px-3 py-2 font-mono text-[11px] uppercase tracking-[0.06em] transition-colors",
                                    isActive
                                        ? "text-fg"
                                        : "text-fg-muted hover:text-fg",
                                )}
                            >
                                {tab.label}
                                <span
                                    aria-hidden
                                    className={cn(
                                        "absolute inset-x-2 -bottom-px h-0.5 rounded-none transition-colors",
                                        isActive ? "bg-fg" : "bg-transparent",
                                    )}
                                />
                            </button>
                        );
                    })}
                </div>
                {aside ? (
                    <span className="font-mono text-[10px] uppercase tracking-[0.08em] text-fg-soft">
                        {aside}
                    </span>
                ) : null}
            </div>

            {items.map((tab) => {
                const isActive = tab.key === active;
                return (
                    <div
                        key={tab.key}
                        id={`${id}-panel-${tab.key}`}
                        role="tabpanel"
                        aria-labelledby={`${id}-tab-${tab.key}`}
                        hidden={!isActive}
                    >
                        {tab.key === "preview" ? (
                            <div
                                className={cn(
                                    "grid-dots flex min-h-[180px] w-full items-center justify-center",
                                    bleed ? "p-0" : "px-5 py-8 sm:px-8",
                                )}
                            >
                                <div
                                    className={cn(
                                        "w-full",
                                        !bleed && "max-w-[480px]",
                                    )}
                                >
                                    {preview}
                                </div>
                            </div>
                        ) : (
                            <div>{code}</div>
                        )}
                    </div>
                );
            })}
        </div>
    );
}

Uso tipico04

tsx
<PreviewTabs preview={<MyComp />} code={<CodeBlock code={src} />} />

Dipendenze05

npm
  • tailwind-merge
  • clsx
Interno
  • @/lib/utils#cn

Ti è servito? Dimmelo, oppure proponi il prossimo componente.