glasscn
componentcomponent-previewsource: ok · refreshed
Component Preview
A preview shell for component demos with switchable default and glass variants.
npx shadcn add "https://glasscn-components.vercel.app/r/component-preview.json"free
Where it goes
testimonial
Visual style
glass-blur
Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.
Dependencies
Bundle weight · mediumiconslucide-react
animationmotion
Registry dependencies
@glasscn/utils@glasscn/button@glasscn/card@glasscn/separator@glasscn/glass-button@glasscn/glass-card@glasscn/glass-variants
Files5 · source cached 10/8/2026, 9:23:39 AM
components/component-preview-tabs.tsx
"use client";
import { ChevronUpIcon } from "lucide-react";
import * as React from "react";
import { CopyButton } from "@/components/custom/copy-button";
import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
import { cn } from "@/lib/utils";
import { Card } from "./ui/card";
export type ComponentPreviewTabsProps = React.ComponentProps<"div"> & {
previewClassName?: string;
align?: "center" | "start" | "end";
hideCode?: boolean;
component: React.ReactNode;
code: string;
highlightedCode: string;
previewHighlightedCode: string;
};
export function ComponentPreviewTabs({
className,
previewClassName,
align = "center",
hideCode = false,
component,
code,
highlightedCode,
previewHighlightedCode,
}: ComponentPreviewTabsProps) {
const [codeVisible, setCodeVisible] = React.useState(false);
return (
<Card data-slot="component-preview" className={cn("mt-4 mb-12 gap-0 overflow-hidden py-0", className)}>
<div data-slot="preview" dir="ltr">
<div
data-align={align}
className={cn(
"preview relative flex min-h-72 w-full justify-center p-10 data-[align=center]:items-center data-[align=end]:items-end data-[align=start]:items-start",
previewClassName,
)}
>
{component}
</div>
</div>
{!hideCode && (
<>
<Separator />
<div data-slot="code" data-code-visible={codeVisible} className="group/code bg-background relative">
<div className="absolute top-4 right-4 z-10 flex items-center gap-2">
{codeVisible && (
<Button
type="button"
size="icon"
variant="outline"
className="text-foreground/80 hover:text-foreground h-7 w-7"
onClick={() => setCodeVisible(false)}
>
<ChevronUpIcon className="h-3.5 w-3.5" />
<span className="sr-only">Collapse code</span>
</Button>
)}
<CopyButton
text={code}
size="icon"
variant="outline"
className={cn(
"h-7 w-7 text-foreground/80 hover:text-foreground [&_svg]:h-3.5 [&_svg]:w-3.5",
!codeVisible && "bg-background/50",
)}
/>
</div>
{codeVisible ? (
<div className="[&_pre]:max-h-96" dangerouslySetInnerHTML={{ __html: highlightedCode }} />
) : (
<div className="relative">
<div
className="max-h-28 overflow-hidden [&_pre]:overflow-hidden"
dangerouslySetInnerHTML={{ __html: previewHighlightedCode }}
/>
<div className="from-background via-background/70 dark:via-background/80 absolute inset-0 flex items-end justify-center bg-linear-to-t to-transparent pb-4">
<Button type="button" size="sm" variant="outline" onClick={() => setCodeVisible(true)}>
View Code
</Button>
</div>
</div>
)}
</div>
</>
)}
</Card>
);
}
components/glass-component-preview-tabs.tsx
"use client";
import { ChevronUpIcon } from "lucide-react";
import * as React from "react";
import { CopyButton } from "@/components/custom/copy-button";
import { Card } from "@/components/ui/card";
import { GlassButton } from "@/components/ui/glasscn/glass-button";
import { GlassCard } from "@/components/ui/glasscn/glass-card";
import type { FrostGlassVariant } from "@/lib/glass-variants";
import { cn } from "@/lib/utils";
export type GlassComponentPreviewTabsProps = React.ComponentProps<"div"> & {
previewClassName?: string;
align?: "center" | "start" | "end";
hideCode?: boolean;
component: React.ReactNode;
code: string;
highlightedCode: string;
previewHighlightedCode: string;
previewGlassVariant?: FrostGlassVariant;
codeGlassVariant?: FrostGlassVariant;
};
export function GlassComponentPreviewTabs({
className,
previewClassName,
align = "center",
hideCode = false,
component,
code,
highlightedCode,
previewHighlightedCode,
codeGlassVariant = "frosted",
}: GlassComponentPreviewTabsProps) {
const [codeVisible, setCodeVisible] = React.useState(false);
const codePanel = (
<>
<div className="absolute top-4 right-4 z-10 flex items-center gap-2">
{codeVisible && (
<GlassButton
type="button"
size="icon"
variant="outline"
glassVariant="liquid-refract"
className="text-foreground/80 hover:text-foreground h-7 w-7"
onClick={() => setCodeVisible(false)}
>
<ChevronUpIcon className="h-3.5 w-3.5" />
<span className="sr-only">Collapse code</span>
</GlassButton>
)}
<CopyButton
text={code}
size="icon"
variant="outline"
className={cn(
"h-7 w-7 text-foreground/80 hover:text-foreground [&_svg]:h-3.5 [&_svg]:w-3.5",
!codeVisible && "bg-background/40",
)}
/>
</div>
{codeVisible ? (
<div className="[&_pre]:max-h-96" dangerouslySetInnerHTML={{ __html: highlightedCode }} />
) : (
<div className="relative">
<div
className="max-h-28 overflow-hidden [&_pre]:overflow-hidden"
dangerouslySetInnerHTML={{ __html: previewHighlightedCode }}
/>
<div className="absolute inset-0 flex items-end justify-center bg-linear-to-t from-white/55 via-white/25 to-transparent pb-4 dark:from-black/70 dark:via-black/35">
<GlassButton
type="button"
size="sm"
variant="outline"
glassVariant="liquid-refract"
onClick={() => setCodeVisible(true)}
>
View Code
</GlassButton>
</div>
</div>
)}
</>
);
return (
<div className={cn("mt-4 mb-12 flex flex-col gap-0", className)}>
<Card
data-slot="glass-component-preview"
variant="outline"
className="gap-0 overflow-hidden rounded-t-[28px] rounded-b-none bg-transparent py-0"
>
<div data-slot="preview" dir="ltr">
<div
data-align={align}
className={cn(
"preview relative flex min-h-72 w-full justify-center p-10 data-[align=center]:items-center data-[align=end]:items-end data-[align=start]:items-start",
previewClassName,
)}
>
{component}
</div>
</div>
</Card>
{!hideCode && (
<GlassCard
data-slot="glass-component-code"
glassVariant={codeGlassVariant}
className="group/code relative -mt-px gap-0 overflow-hidden rounded-t-none rounded-b-[28px] py-0"
>
{codePanel}
</GlassCard>
)}
</div>
);
}
components/custom/copy-button.tsx
"use client";
import { CheckIcon, CircleXIcon, CopyIcon } from "lucide-react";
import type { HTMLMotionProps, Variants } from "motion/react";
import { AnimatePresence, motion } from "motion/react";
import type { ComponentProps } from "react";
import { Button } from "@/components/ui/button";
import type { CopyState } from "@/hooks/use-copy-to-clipboard";
import { useCopyToClipboard } from "@/hooks/use-copy-to-clipboard";
export const motionIconVariants: Variants = {
initial: { opacity: 0, scale: 0.8, filter: "blur(2px)" },
animate: { opacity: 1, scale: 1, filter: "blur(0px)" },
exit: { opacity: 0, scale: 0.8 },
};
export const motionIconProps: HTMLMotionProps<"span"> = {
variants: motionIconVariants,
initial: "initial",
animate: "animate",
exit: "exit",
transition: { duration: 0.25, ease: "easeOut" },
};
export type CopyStateIconProps = {
state: CopyState;
idleIcon?: React.ReactNode;
doneIcon?: React.ReactNode;
errorIcon?: React.ReactNode;
};
export function CopyStateIcon({ state, idleIcon, doneIcon, errorIcon }: CopyStateIconProps) {
return (
<AnimatePresence mode="wait" initial={false}>
{state === "idle" ? (
<motion.span key="idle" {...motionIconProps}>
{idleIcon ?? <CopyIcon />}
</motion.span>
) : state === "done" ? (
<motion.span key="done" {...motionIconProps}>
{doneIcon ?? <CheckIcon strokeWidth={3} color="green" />}
</motion.span>
) : state === "error" ? (
<motion.span key="error" {...motionIconProps}>
{errorIcon ?? <CircleXIcon />}
</motion.span>
) : null}
</AnimatePresence>
);
}
export type CopyButtonProps = ComponentProps<typeof Button> & {
text: string | (() => string);
onCopySuccess?: (text: string) => void;
onCopyError?: (error: Error) => void;
} & Pick<CopyStateIconProps, "idleIcon" | "doneIcon" | "errorIcon">;
export function CopyButton({
size = "icon",
variant = "ghost",
children,
text,
idleIcon,
doneIcon,
errorIcon,
onClick,
onCopySuccess,
onCopyError,
...props
}: CopyButtonProps) {
const { state, copy } = useCopyToClipboard({ onCopySuccess, onCopyError });
return (
<Button
size={size}
variant={variant}
onClick={(e) => {
void copy(text);
onClick?.(e);
}}
aria-label="Copy"
{...props}
>
<CopyStateIcon state={state} idleIcon={idleIcon} doneIcon={doneIcon} errorIcon={errorIcon} />
{children}
</Button>
);
}
hooks/use-copy-to-clipboard.ts
import { useState } from "react";
export type CopyState = "idle" | "done" | "error";
type UseCopyToClipboardOptions = {
onCopySuccess?: (text: string) => void;
onCopyError?: (error: Error) => void;
timeout?: number;
};
export function useCopyToClipboard({ onCopySuccess, onCopyError, timeout = 2000 }: UseCopyToClipboardOptions = {}) {
const [state, setState] = useState<CopyState>("idle");
async function copy(text: string | (() => string)) {
const value = typeof text === "function" ? text() : text;
try {
await navigator.clipboard.writeText(value);
setState("done");
onCopySuccess?.(value);
} catch (err) {
setState("error");
onCopyError?.(err instanceof Error ? err : new Error(String(err)));
} finally {
setTimeout(() => setState("idle"), timeout);
}
}
return { state, copy };
}
lib/highlight-code.ts
import { createHash } from "node:crypto";
import { codeToHtml } from "shiki";
// Highlighting is deterministic for a given source string + language, so we can
// safely memoize the generated HTML in-process and avoid repeating the Shiki work
// on every request during local dev or while the server stays warm.
const highlightCache = new Map<string, string>();
export async function highlightCode(code: string, language: string = "tsx") {
// Use the content itself as the cache input so repeated component docs pages
// can reuse the exact same highlighted payload without caring which route asked
// for it.
const cacheKey = createHash("sha256").update(`${language}:${code}`).digest("hex");
const cached = highlightCache.get(cacheKey);
if (cached) {
return cached;
}
// Shiki runs fine on the server: it tokenizes the source code and returns an
// HTML string with theme variables and token spans already applied. That lets
// the page ship highlighted markup on first paint instead of waiting for a
// client-side effect to colorize plain text after hydration.
const html = await codeToHtml(code, {
lang: language,
themes: { dark: "github-dark", light: "github-light" },
transformers: [
{
pre(node) {
node.properties.class =
"shiki no-scrollbar min-w-0 overflow-x-auto overflow-y-auto overscroll-x-contain overscroll-y-auto p-4 pr-16 text-sm leading-6";
},
code(node) {
node.properties["data-line-numbers"] = "";
},
line(node) {
node.properties["data-line"] = "";
},
},
],
});
// The caller renders this HTML payload directly. We keep the wrapper classes
// here so every preview gets the same structure and scrolling behavior from one
// place instead of rebuilding that shape at each call site.
highlightCache.set(cacheKey, html);
return html;
}