caliper uidocs

Showcase block

Block

The product one job at a time: tabs beside a single stage, each view swapped in place with one line on what it does.

Installs components/ui/showcase-block.tsxDepends on lucide-reactMotion none in component source
pnpm dlx shadcn@latest add https://caliperui.site/r/showcase-block.json

The product

One close, three jobs

Halden does the matching, hands you what it could not decide, and keeps the record your auditors want.

halden.app/close/october
October close412 of 418 matched
Mercury · Stripe payout · INV-2291rule: payouts$18,240.00Matched
AWS · Invoice 0923vendor history$4,112.37Matched
Gusto · Payroll · 31 Octamount differs by $12.40$61,904.12Review
Figma · Annual · Org planrule: SaaS$2,880.00Matched
Brex · Card settlementstatement$9,377.80Matched

When to use #

Use it for

  • Right after the hero, when the product has three to five jobs worth seeing.
  • You have real screens or fragments for each job.

Skip it when

  • One screen is enough; put it in the hero.
  • Features without a view; use feature rows.

Installation #

terminal
pnpm dlx shadcn@latest add https://caliperui.site/r/showcase-block.json

Adds its 4 files to components/ui/, each with its own stylesheet, and installs their packages. No account needed. The first part also adds Caliper UI’s tokens; import them once as in Installation.

Usage #

This is the code behind the preview above.

example.tsx
import { type ReactNode } from "react"
import { ShowcaseBlock } from "@/components/ui/showcase-block"

function Frame({ children }: { children: ReactNode }) {
  return <div className="w-full overflow-hidden rounded-[14px] shadow-[0_0_0_1px_var(--border)]">{children}</div>;
}

function Ledger() {
  const rows = [
    ["Mercury", "Stripe payout · INV-2291", "$18,240.00", "Matched", "rule: payouts"],
    ["AWS", "Invoice 0923", "$4,112.37", "Matched", "vendor history"],
    ["Gusto", "Payroll · 31 Oct", "$61,904.12", "Review", "amount differs by $12.40"],
    ["Figma", "Annual · Org plan", "$2,880.00", "Matched", "rule: SaaS"],
    ["Brex", "Card settlement", "$9,377.80", "Matched", "statement"],
  ];
  return (
    <div className="grid gap-4 text-[13px]">
      <div className="grid gap-2">
        <span className="text-[var(--k-muted)]">October close</span>
        <span data-slot="block-title" data-size="3" className="tabular-nums">412 of 418 matched</span>
        <div className="h-1.5 w-full max-w-[260px] overflow-hidden rounded-full bg-[var(--k-sunken)]" aria-hidden="true"><i className="block h-full w-[98.5%] rounded-full bg-[var(--k-hi)]" /></div>
      </div>
      <div className="grid">
        {rows.map(([vendor, memo, amount, state, why]) => (
          <div key={memo} className="grid grid-cols-[minmax(0,1fr)_auto] items-center gap-x-4 gap-y-0.5 border-t border-[var(--k-line)] py-2.5 tabular-nums @[560px]:grid-cols-[minmax(0,1fr)_minmax(0,1fr)_auto_88px]">
            <span className="truncate font-medium">{vendor} <span className="font-normal text-[var(--k-muted)]">· {memo}</span></span>
            <span className="truncate text-[var(--k-muted)] @max-[559px]:hidden">{why}</span>
            <span className="text-right">{amount}</span>
            <span className={`justify-self-end rounded-full px-2 py-0.5 text-xs @max-[559px]:hidden ${state === "Review" ? "bg-[var(--k-hi-soft)] text-[var(--k-hi)]" : "text-[var(--k-muted)]"}`}>{state}</span>
          </div>
        ))}
      </div>
    </div>
  );
}

function Exceptions() {
  return (
    <div className="grid gap-3 text-[13px]">
      <div className="flex items-baseline justify-between gap-3"><span data-slot="block-title" data-size="3">6 lines need you</span><span className="text-[var(--k-muted)]">sorted by amount</span></div>
      {[["Gusto · Payroll", "$61,904.12", "Ledger says $61,891.72", "Employer NI adjustment, Oct"], ["Stripe · Refund", "$1,280.00", "No matching credit note", "Refund for INV-2244, 2 Oct"]].map(([name, amount, issue, guess]) => (
        <div key={name} className="grid gap-2 rounded-[12px] p-4 shadow-[0_0_0_1px_var(--k-line)]">
          <div className="flex justify-between gap-3 tabular-nums"><b className="font-medium">{name}</b><span>{amount}</span></div>
          <span className="text-[var(--k-muted)]">{issue}</span>
          <span className="flex flex-wrap items-center gap-2"><span className="rounded-full bg-[var(--k-hi-soft)] px-2 py-0.5 text-xs text-[var(--k-hi)]">Likely</span>{guess}</span>
        </div>
      ))}
    </div>
  );
}

function AuditExport() {
  return (
    <div className="grid gap-3 text-[13px]">
      <span data-slot="block-title" data-size="3">October audit trail</span>
      <div className="grid gap-0 font-mono text-[12px] tabular-nums">
        {[["09:12:04", "matched", "STRIPE-PO-2291", "rule: payouts"], ["09:12:05", "matched", "AWS-0923", "vendor history"], ["10:40:18", "edited", "GUSTO-1031", "ana.reyes"], ["10:41:02", "approved", "GUSTO-1031", "lena.brandt"]].map((row) => (
          <div key={row.join()} className="grid grid-cols-[72px_72px_minmax(0,1fr)] gap-3 border-t border-[var(--k-line)] py-2 @[560px]:grid-cols-[80px_80px_minmax(0,1fr)_120px]">
            <span className="text-[var(--k-muted)]">{row[0]}</span><span className={row[1] === "approved" ? "text-[var(--k-hi)]" : undefined}>{row[1]}</span><span className="truncate">{row[2]}</span><span className="text-[var(--k-muted)] @max-[559px]:hidden">{row[3]}</span>
          </div>
        ))}
      </div>
      <span className="text-[var(--k-muted)]">Exports as one CSV your auditors can sample · 4,182 rows</span>
    </div>
  );
}

const showcases: Parameters<typeof ShowcaseBlock>[0] = { eyebrow: "The product", title: "One close, three jobs", lead: "Halden does the matching, hands you what it could not decide, and keeps the record your auditors want.", items: [
    { id: "match", label: "Match", body: "Every bank line is matched to the ledger as it lands, with the rule that matched it.", media: <Ledger />, stageTitle: "halden.app/close/october" },
    { id: "review", label: "Review", body: "What is left arrives with the likeliest explanation, so a review takes minutes.", media: <Exceptions />, stageTitle: "halden.app/close/october/review" },
    { id: "export", label: "Export", body: "Every match, edit and approval exports as one file auditors can sample.", media: <AuditExport />, stageTitle: "halden.app/audit/october.csv" },
  ] };

export function ShowcaseBlockDemo() {
  return <Frame><ShowcaseBlock {...showcases} /></Frame>;
}

API reference #

ShowcaseBlock

component
PropTypeDefault
eyebrowstring—
title *string—
leadstring—
items *readonly ShowcaseItem[]—
defaultValuestring—
layout"beside" | "stacked""beside"
classNamestring—

Accessibility #

It exposes tablist, tab and tabpanel semantics, with aria-label, aria-orientation, aria-selected, aria-controls, aria-hidden and aria-labelledby kept in step with state.

KeyBehaviour
↓Moves to the next item or value.
→Moves to the next item or value.
↑Moves to the previous item or value.
←Moves to the previous item or value.
HomeJumps to the first item or the minimum.
EndJumps to the last item or the maximum.

Motion #

It declares no animation in its component source. Any transition comes from its stylesheet, which uses the duration tokens that collapse to instant under prefers-reduced-motion.