# Redline: Dev Handoff

> 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.

A shadcn/ui block from the [@keyframeui](https://keyframeui.com) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/keyframeui/redline-handoff-demo).

- Page, with a live preview: https://shadcn.exchange/r/keyframeui/redline-handoff-demo
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/keyframeui/redline-handoff-demo-light-7aea6b1e6c706c80.png
- Registry item: https://keyframeui.com/r/redline-handoff-demo.json
- Categories: inspector, hover, design
- Listed: 2026-10-14, source last changed 2026-10-15

## Install

```bash
npx shadcn@latest add https://keyframeui.com/r/redline-handoff-demo.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- Registry items: `https://keyframeui.com/r/redline.json`

## Notes from the registry

Live preview, examples and props: https://keyframeui.com/components/redline#redline-handoff-demo

## Files

### `registry/demos/redline-handoff-demo.tsx`

```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>
  );
}
```
