caliper uidocs

Feature tabs block

Block

A short feature tour: features beside one stage, with a thin rule that fills and moves on. It waits while hovered, focused or off screen; choosing a feature or Pause stops it.

Installs components/ui/feature-tabs-block.tsxDepends on lucide-reactMotion honours reduced motion
pnpm dlx shadcn@latest add https://caliperui.site/r/feature-tabs-block.json

How Halden closes the month

Every account reconciled before lunch

Bank feeds, rules and approvals in one place, so the close is a review rather than a rebuild.

halden.app/reconcile
Mercury ••44102,184 matched$412,880.12
Stripe payouts96 matched$88,214.50
Amex ••100241 to review$9,120.07

When to use #

Use it for

  • The middle of a landing page with three to five features that each have a screen.
  • Replacing a carousel that hides content behind arrows.

Skip it when

  • Features without a picture of their own; use the feature rows.
  • More than five features; the tour gets long.

Installation #

terminal
pnpm dlx shadcn@latest add https://caliperui.site/r/feature-tabs-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 { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { FeatureTabsBlock } from "@/components/ui/feature-tabs-block"
import { Sparkline } from "@/components/ui/sparkline"
import { Switch } from "@/components/ui/switch"

function MiniLedger({ rows }: { rows: [string, string, string][] }) {
  return (
    <table className="w-full text-left text-[13px]">
      <tbody>
        {rows.map(([a, b, c]) => (
          <tr key={a} className="border-b border-border last:border-0">
            <td className="py-2.5 font-medium">{a}</td>
            <td className="py-2.5 text-muted-foreground">{b}</td>
            <td className="py-2.5 text-right tabular-nums">{c}</td>
          </tr>
        ))}
      </tbody>
    </table>
  );
}

export function FeatureTabsBlockDemo() {
  return (
    <FeatureTabsBlock
      eyebrow="How Halden closes the month"
      title="Every account reconciled before lunch"
      lead="Bank feeds, rules and approvals in one place, so the close is a review rather than a rebuild."
      features={[
        {
          title: "Bank feeds that match themselves",
          body: "Transactions arrive overnight and match to invoices and bills by amount, date and payee. You review the few that don't.",
          stageTitle: "halden.app/reconcile",
          stage: <MiniLedger rows={[["Mercury ••4410", "2,184 matched", "$412,880.12"], ["Stripe payouts", "96 matched", "$88,214.50"], ["Amex ••1002", "41 to review", "$9,120.07"]]} />,
        },
        {
          title: "Rules you write once",
          body: "Categorise by payee, amount or memo. Rules show how many past transactions they would have caught before you save them.",
          stageTitle: "halden.app/rules",
          stage: (
            <div className="grid gap-3 text-[13px]">
              {[["Payee contains “AWS”", "Software · 312 past matches", true], ["Amount over $10,000", "Needs approval · 8 past matches", true], ["Memo has “refund”", "Refunds · 44 past matches", false]].map(([a, b, on]) => (
                <div key={a as string} className="flex items-center justify-between gap-3 rounded-control border border-border px-3 py-2.5">
                  <span className="grid"><span className="font-medium">{a}</span><span className="text-muted-foreground">{b}</span></span>
                  <Switch aria-label={`Rule ${a}`} defaultChecked={on as boolean} />
                </div>
              ))}
            </div>
          ),
        },
        {
          title: "Approvals with the receipt attached",
          body: "Large payments wait for the right person. They see the receipt, the rule that flagged it and the vendor's history.",
          stageTitle: "halden.app/approvals",
          stage: (
            <div className="grid gap-3 text-[13px]">
              <div className="flex items-center justify-between"><span className="font-medium">Ostra Freight · invoice 2209</span><Badge variant="warning">Needs approval</Badge></div>
              <p className="text-muted-foreground">$18,240.00 · flagged by “Amount over $10,000” · paid 11 times before, always on time.</p>
              <div className="flex gap-2"><Button size="sm">Approve</Button><Button size="sm" variant="secondary">Ask a question</Button></div>
            </div>
          ),
        },
        {
          title: "A close you can read",
          body: "One page shows what changed since last month and why, ready to send to the board.",
          stageTitle: "halden.app/close/september",
          stage: (
            <div className="grid gap-2 text-[13px]">
              <div className="flex items-baseline justify-between"><span className="text-muted-foreground">Cash</span><span className="text-xl font-medium tabular-nums">$1.24M</span></div>
              <Sparkline label="Cash over 12 months" values={[0.82, 0.88, 0.85, 0.94, 0.99, 1.02, 0.98, 1.08, 1.12, 1.15, 1.19, 1.24]} />
              <p className="text-muted-foreground">Up 4% on August, mostly two annual renewals paid early.</p>
            </div>
          ),
        },
      ]}
    />
  );
}

API reference #

FeatureTabsBlock

component
PropTypeDefaultDescription
eyebrowstring—
title *string—
leadstring—
features *readonly FeatureTab[]—
intervalnumber6000Milliseconds per feature while the tour plays.
classNamestring—

Accessibility #

It exposes tablist, tab and tabpanel semantics, with aria-orientation, aria-label, aria-selected, aria-controls, aria-hidden and aria-pressed 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 checks prefers-reduced-motion itself and settles immediately instead of travelling. Every state is still reachable.