A design tool's handoff mode. `onSelect` fills the panel on the right with the CSS and the Tailwind classes of whatever layer is selected, and it swaps as you move.
@keyframeui
registry/demos/redline-handoff-demo.tsx
"use client";
import * as React from "react";
import { Redline, type RedlineSelection } from "@/components/ui/redline";
const features = ["Unlimited projects", "Version history", "Priority support"];
type Layer = { name: string; size: string; css: [string, string][]; classes: string[] };
let paint: CanvasRenderingContext2D | null | undefined;
/** Computed colors come back as oklch from Tailwind; handoff wants hex. */
function hex(color: string) {
paint ??= document.createElement("canvas").getContext("2d", { willReadFrequently: true });
if (!paint) return color;
paint.clearRect(0, 0, 1, 1);
paint.fillStyle = color;
paint.fillRect(0, 0, 1, 1);
const [r, g, b, a] = paint.getImageData(0, 0, 1, 1).data;
if (a === 0) return null;
const pair = (n: number) => n.toString(16).padStart(2, "0");
return `#${pair(r)}${pair(g)}${pair(b)}${a < 255 ? pair(a) : ""}`.toUpperCase();
}
const px = (value: number) => (value ? `${value}px` : "0");
/** The CSS a developer needs for one layer, from its measurements and computed style. */
function layerOf(selection: RedlineSelection): Layer {
const { element, width, height, padding, gap, text } = selection;
const cs = getComputedStyle(element);
const css: [string, string][] = [
["width", `${width}px`],
["height", `${height}px`],
];
if (gap) {
css.push(["display", cs.display]);
if (cs.display.endsWith("flex") && cs.flexDirection !== "row") {
css.push(["flex-direction", cs.flexDirection]);
}
if (cs.alignItems !== "normal" && cs.alignItems !== "stretch") css.push(["align-items", cs.alignItems]);
const both = gap.row === gap.column ? [gap.row] : [gap.row, gap.column];
if (gap.row || gap.column) css.push(["gap", both.map(px).join(" ")]);
}
const { top, right, bottom, left } = padding;
if (top || right || bottom || left) {
const sides =
top === bottom && left === right
? top === left
? [top]
: [top, right]
: [top, right, bottom, left];
css.push(["padding", sides.map(px).join(" ")]);
}
if (parseFloat(cs.borderTopLeftRadius)) css.push(["border-radius", cs.borderTopLeftRadius]);
const background = hex(cs.backgroundColor);
if (background) css.push(["background", background]);
if (text) {
const leading = text.lineHeight ? `/${text.lineHeight}px` : "";
css.push(["font", `${text.weight} ${text.size}px${leading} ${text.font}`]);
if (text.letterSpacing) css.push(["letter-spacing", `${Math.round(text.letterSpacing * 10) / 1000}em`]);
css.push(["color", text.color]);
}
return {
name: selection.name ?? element.tagName.toLowerCase(),
size: `${width} × ${height}`,
css,
classes: (element.getAttribute("class") ?? "").split(/\s+/).filter(Boolean),
};
}
export function RedlineHandoffDemo() {
const [layer, setLayer] = React.useState<Layer | null>(null);
const [copied, setCopied] = React.useState(false);
React.useEffect(() => {
if (!copied) return;
const timer = window.setTimeout(() => setCopied(false), 1400);
return () => window.clearTimeout(timer);
}, [copied]);
const copy = () => {
if (!layer) return;
const text = layer.css.map(([property, value]) => `${property}: ${value};`).join("\n");
navigator.clipboard?.writeText(text).then(() => setCopied(true), () => {});
};
return (
<div className="w-full max-w-4xl overflow-hidden rounded-2xl border border-zinc-200 bg-white text-zinc-900 shadow-sm dark:border-zinc-800 dark:bg-zinc-950 dark:text-zinc-50">
<div className="flex h-11 items-center justify-between gap-4 border-b border-zinc-200 px-4 text-xs dark:border-zinc-800">
<p className="truncate text-zinc-500">
Pricing <span className="px-1 text-zinc-300 dark:text-zinc-700">/</span>
<span className="font-medium text-zinc-900 dark:text-zinc-100">Pro plan</span>
</p>
<div className="flex shrink-0 rounded-md bg-zinc-100 p-0.5 font-medium dark:bg-zinc-900">
<span className="px-2.5 py-1 text-zinc-500">Design</span>
<span className="rounded-[5px] bg-white px-2.5 py-1 shadow-sm dark:bg-zinc-800">Dev Mode</span>
</div>
</div>
<div className="grid md:grid-cols-[minmax(0,1fr)_18rem]">
<Redline
tour={["Plan", "Price", "Features", "Button"]}
selector="[data-redline]"
onSelect={(selection) => selection && setLayer(layerOf(selection))}
aria-label="Pro plan card"
className="flex items-center justify-center bg-zinc-100 bg-[radial-gradient(circle,rgb(0_0_0/0.07)_1px,transparent_1px)] bg-size-[16px_16px] px-6 py-12 dark:bg-zinc-900/60 dark:bg-[radial-gradient(circle,rgb(255_255_255/0.06)_1px,transparent_1px)]"
>
<div
data-redline="Plan"
className="flex w-64 flex-col gap-6 rounded-2xl bg-white p-6 shadow-sm ring-1 ring-zinc-950/5 dark:bg-zinc-950 dark:ring-white/10"
>
<div data-redline="Header" className="flex flex-col gap-2">
<span
data-redline="Badge"
className="w-fit rounded-full bg-violet-100 px-2 py-0.5 text-xs font-medium text-violet-700 dark:bg-violet-500/15 dark:text-violet-300"
>
Pro
</span>
<p data-redline="Price" className="text-4xl/10 font-semibold tracking-tight">
$24
<span className="text-sm font-normal tracking-normal text-zinc-500"> / month</span>
</p>
<p data-redline="Blurb" className="text-sm/5 text-zinc-500 dark:text-zinc-400">
For teams that ship every week.
</p>
</div>
<ul data-redline="Features" className="flex flex-col gap-2.5 text-sm/5">
{features.map((feature) => (
<li key={feature} data-redline={feature} className="flex items-center gap-2">
<svg
viewBox="0 0 16 16"
aria-hidden
className="size-4 fill-none stroke-violet-600 stroke-2 dark:stroke-violet-400"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="m3.5 8.5 3 3 6-7" />
</svg>
{feature}
</li>
))}
</ul>
<button
data-redline="Button"
type="button"
className="h-11 rounded-[10px] bg-zinc-900 px-4 text-sm font-medium text-white dark:bg-zinc-50 dark:text-zinc-900"
>
Start free trial
</button>
</div>
</Redline>
<aside className="flex min-h-80 flex-col border-t border-zinc-200 text-xs md:border-t-0 md:border-l dark:border-zinc-800">
{layer ? (
<>
<div className="flex items-baseline justify-between gap-3 border-b border-zinc-200 px-4 py-3 dark:border-zinc-800">
<p className="truncate text-sm font-medium">{layer.name}</p>
<p className="shrink-0 font-mono text-zinc-500 tabular-nums">{layer.size}</p>
</div>
<div className="border-b border-zinc-200 px-4 py-3 dark:border-zinc-800">
<div className="flex items-center justify-between">
<p className="font-medium text-zinc-500">CSS</p>
<button
type="button"
onClick={copy}
className="rounded px-1.5 py-0.5 font-medium text-zinc-500 hover:bg-zinc-100 hover:text-zinc-900 focus-visible:ring-2 focus-visible:ring-zinc-400 focus-visible:outline-none dark:hover:bg-zinc-900 dark:hover:text-zinc-100"
>
{copied ? "Copied" : "Copy"}
</button>
</div>
<pre className="mt-2 overflow-x-auto font-mono text-[12px]/5">
{layer.css.map(([property, value]) => (
<div key={property}>
<span className="text-sky-700 dark:text-sky-300">{property}</span>
<span className="text-zinc-400">: </span>
<span>{value}</span>
<span className="text-zinc-400">;</span>
</div>
))}
</pre>
</div>
<div className="px-4 py-3">
<p className="font-medium text-zinc-500">Tailwind</p>
<div className="mt-2 flex flex-wrap gap-1">
{layer.classes.map((name) => (
<code
key={name}
className="rounded bg-zinc-100 px-1.5 py-0.5 font-mono text-[11px] text-zinc-700 dark:bg-zinc-900 dark:text-zinc-300"
>
{name}
</code>
))}
</div>
</div>
</>
) : (
<p className="m-auto px-6 text-center text-zinc-500">
Point at a layer to see its CSS and Tailwind classes.
</p>
)}
</aside>
</div>
</div>
);
}
Live preview, examples and props: https://keyframeui.com/components/redline#redline-handoff-demo

















