# Pricing Dock: Why Switch

> Us against the two tools teams switch from, with logo cards instead of prices: each mark shrinks in beside its name as the cards dock. On a phone, the plan pills pick whose column the table shows, or swipe the table sideways.

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

- Page, with a live preview: https://shadcn.exchange/r/keyframeui/pricing-dock-compare-demo
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/keyframeui/pricing-dock-compare-demo-light-a87bc025baea2e94.png
- Registry item: https://keyframeui.com/r/pricing-dock-compare-demo.json
- Categories: pricing, scroll, table
- Listed: 2026-10-15, source last changed 2026-10-15

## Install

```bash
npx shadcn@latest add https://keyframeui.com/r/pricing-dock-compare-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/pricing-dock.json`

## Notes from the registry

Live preview, examples and props: https://keyframeui.com/components/pricing-dock#pricing-dock-compare-demo

## Files

### `registry/demos/pricing-dock-compare-demo.tsx`

```tsx
"use client";

import * as React from "react";

import { PricingDock, type PricingDockGroup, type PricingDockPlan } from "@/components/ui/pricing-dock";

function Mark({ className, children }: { className: string; children: React.ReactNode }) {
  return (
    <span className={`flex size-10 items-center justify-center rounded-[11px] ${className}`} aria-hidden>
      {children}
    </span>
  );
}

const plans: PricingDockPlan[] = [
  {
    id: "us",
    name: "Northlight",
    logo: (
      <Mark className="bg-blue-600 text-white dark:bg-sky-400 dark:text-sky-950">
        <svg viewBox="0 0 20 20" fill="currentColor" className="size-5">
          <path d="M10 2.5 12.2 8l5.3 2-5.3 2L10 17.5 7.8 12l-5.3-2 5.3-2L10 2.5Z" />
        </svg>
      </Mark>
    ),
    pitch: "Reviews that read the whole repo, not just the diff.",
    highlights: ["First review in under a minute", "Learns your team's conventions", "Fixes as one-click suggestions", "Cloud or self-hosted"],
    featured: true,
    badge: "Free migration",
    cta: { label: "Switch in 5 minutes", href: "#" },
  },
  {
    id: "a",
    name: "Competitor A",
    logo: (
      <Mark className="bg-zinc-200 text-zinc-500 dark:bg-zinc-800 dark:text-zinc-400">
        <span className="font-mono text-sm font-semibold">A</span>
      </Mark>
    ),
    pitch: "Rule-based linting with an AI summary on top.",
    highlights: ["First review in 5–10 minutes", "Fixed rule sets", "Comments only, no fixes", "Cloud only"],
    cta: { label: "Moving from A", href: "#" },
  },
  {
    id: "b",
    name: "Competitor B",
    logo: (
      <Mark className="bg-zinc-200 text-zinc-500 dark:bg-zinc-800 dark:text-zinc-400">
        <span className="font-mono text-sm font-semibold">B</span>
      </Mark>
    ),
    pitch: "A general chat assistant wired into pull requests.",
    highlights: ["First review in 2–4 minutes", "Prompted per repository", "Suggestions in a chat thread", "Cloud only"],
    cta: { label: "Moving from B", href: "#" },
  },
];

const groups: PricingDockGroup[] = [
  {
    label: "Speed",
    rows: [
      { feature: "Time to first review", values: { us: "Under 1 min", a: "5–10 min", b: "2–4 min" } },
      { feature: "Large pull requests", values: { us: "1,000+ files", a: "Up to 200", b: "Up to 300" } },
      { feature: "Re-reviews only what changed", values: { us: true, a: false, b: true } },
    ],
  },
  {
    label: "Review quality",
    rows: [
      {
        feature: "Reads the whole repository",
        hint: "Follows imports, call sites and tests outside the diff before it comments.",
        values: { us: true, a: false, b: "Open files" },
      },
      { feature: "Learns team conventions", values: { us: true, a: false, b: "By prompt" } },
      { feature: "One-click fixes", values: { us: true, a: false, b: true } },
      {
        feature: "False-positive rate",
        hint: "Comments dismissed without a change, across 40 open-source repos we reviewed in September 2026.",
        values: { us: "3%", a: "18%", b: "9%" },
      },
      { feature: "Writes missing tests", values: { us: true, a: false, b: false } },
    ],
  },
  {
    label: "Workflow",
    rows: [
      { feature: "GitHub, GitLab, Bitbucket", values: { us: true, a: "GitHub only", b: "GitHub, GitLab" } },
      { feature: "Slack and Linear", values: { us: true, a: "Slack", b: false } },
      { feature: "Custom review rules", values: { us: true, a: true, b: false } },
      { feature: "CLI and pre-commit hook", values: { us: true, a: false, b: false } },
    ],
  },
  {
    label: "Security",
    rows: [
      { feature: "Self-hosting", values: { us: true, a: false, b: false } },
      { feature: "Zero data retention", values: { us: true, a: false, b: "Enterprise" } },
      { feature: "SOC 2 Type II", values: { us: true, a: true, b: true } },
      { feature: "SSO and SCIM", values: { us: true, a: "Enterprise", b: true } },
      { feature: "Never trains on your code", values: { us: true, a: "Opt-out", b: true } },
    ],
  },
  {
    label: "Price",
    rows: [
      { feature: "Per seat, per month", values: { us: "$19", a: "$29", b: "$24" } },
      { feature: "Free for open source", values: { us: true, a: false, b: true } },
      { feature: "Migration help", values: { us: "Free, hands-on", a: false, b: false } },
    ],
  },
];

export function PricingDockCompareDemo() {
  const scrollRef = React.useRef<HTMLDivElement>(null);

  return (
    <div className="relative isolate w-full overflow-hidden rounded-xl border border-zinc-200 bg-zinc-50 dark:border-zinc-800 dark:bg-[#08090c]">
      <div ref={scrollRef} className="h-[var(--demo-height,44rem)] overflow-y-auto overscroll-contain">
        <div className="mx-auto max-w-5xl px-4 sm:px-6">
          <header className="pt-14 pb-10 sm:pt-20">
            <p className="font-mono text-[11px] tracking-widest text-blue-600 uppercase dark:text-sky-400">Compare</p>
            <h2 className="mt-3 max-w-lg text-3xl/tight font-semibold tracking-tight text-balance text-zinc-900 sm:text-4xl/tight dark:text-zinc-50">
              Why teams switch to Northlight
            </h2>
            <p className="mt-3 max-w-md text-sm/6 text-pretty text-zinc-500">
              The two tools teams most often move from, side by side. Hover a column to follow it, or click a name once
              it docks to see its card again.
            </p>
          </header>

          <PricingDock
            containerRef={scrollRef}
            plans={plans}
            groups={groups}
            intro="Checked against each tool's public docs and pricing in October 2026."
            compareLabel="Jump to the comparison"
            className="[--dock-accent:var(--color-blue-600)] [--dock-accent-foreground:white] [--dock-page:var(--color-zinc-50)] [--dock-surface:white] dark:[--dock-accent:var(--color-sky-400)] dark:[--dock-accent-foreground:var(--color-zinc-950)] dark:[--dock-page:#08090c] dark:[--dock-surface:var(--color-zinc-900)]"
          />

          <footer className="flex flex-col items-center py-24 text-center">
            <h3 className="text-xl font-semibold tracking-tight text-zinc-900 dark:text-zinc-50">
              Bring your rules with you
            </h3>
            <p className="mt-2 max-w-sm text-sm/6 text-pretty text-zinc-500">
              We import your existing review rules and ignore lists, so day one feels like day one hundred.
            </p>
            <a
              href="#"
              className="mt-5 rounded-full bg-blue-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-500 dark:bg-sky-400 dark:text-sky-950"
            >
              Start the migration
            </a>
          </footer>
        </div>
      </div>
    </div>
  );
}
```
