Components
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.
Tabs Client A11y
Anteprima 01 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 LLM 02 Incolla in Claude o ChatGPT per generare la tua variante. Include il contesto del brand, i token e i vincoli del progetto.
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.
Codice 03 Sorgente live di src/components/preview-tabs.tsx. Letto a request-time dal repo — sempre allineato al sito.
tsx src/components/preview-tabs.tsx
Copiato Copy Copiato "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 tipico 04 <PreviewTabs preview={<MyComp />} code={<CodeBlock code={src} />} />
Dipendenze 05
Ti è servito? Dimmelo, oppure proponi il prossimo componente.