Quickstart cardDesign system
Product

Quickstart card

Onboarding card with numbered steps, a masked API key, setup snippets, and copy actions.

npx context-ds add quickstart-card
In the product

Usage

Use it for

Dashboard onboarding steps for making the first API call.

Avoid

  • Showing the full key by default
  • Snippets that change the box's height

Dependencies: glyph-card, glide-tabs, copy-button, secret-field, scroll-area, tooltip, glyph-engine, lucide-react

Example

examples/quickstart-card.tsx
"use client";import { useState } from "react";import { CardLink } from "@/components/product/glyph-card";import { CopyPill, KeyField, QuickstartCard, QuickstartStep, SnippetBox } from "@/components/product/quickstart-card";const KEY = "ctx_live_9f2c41b7e0a84d6c3b5e";export function Product() {  const [method, setMethod] = useState<"mcp" | "cli">("mcp");  return (    <div className="h-[30rem] w-full max-w-md">      <QuickstartCard aside={<CardLink href="/preview/table-page/Block">Manage keys</CardLink>}>        <QuickstartStep number={1} title="Copy your API key">          <KeyField secret={KEY} name="Default" />        </QuickstartStep>        <QuickstartStep number={2} title="Onboard your agent">          <SnippetBox            label="Setup method"            value={method}            onValueChange={setMethod}            snippets={[              { value: "mcp", label: "MCP", code: "claude mcp add --transport http context-dev https://mcp.context.dev/mcp", note: "Sign in when the browser opens." },              { value: "cli", label: "CLI", code: "npm i -g @context-dev/cli && context login" },            ]}          />        </QuickstartStep>        <QuickstartStep number={3} title="Prompt" aside={<CopyPill value="Use the context-dev MCP server for live web data." label="Copy prompt" />}>          <p className="rounded-control border border-line px-3 py-2.5 text-body-sm leading-relaxed text-fg">Use the context-dev MCP server for live web data.</p>        </QuickstartStep>      </QuickstartCard>    </div>  );}

Source

components/product/quickstart-card.tsx
"use client";import * as React from "react";import { Check, Copy, Eye, EyeOff } from "lucide-react";import { GlyphCard } from "@/components/product/glyph-card";import { GlideTabs } from "@/components/product/glide-tabs";import { useCopy } from "@/components/ui/copy-button";import { maskSecret } from "@/components/ui/secret-field";import { ScrollArea } from "@/components/ui/scroll-area";import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";import type { GlyphPairName } from "@/components/visuals/glyph/glyph-art";import { cn } from "@/lib/utils";// The overview's quickstart: the shortest way from signing up to a working call, as numbered steps in a glyph card// (copy the key, connect the agent, paste one prompt, explore the rest). The steps scroll inside the card when they// run longer than the space beside them. Built from KeyField, SnippetBox and CopyPill.function QuickstartCard({ title = "Quickstart", glyph = "apiKeys", aside, className, children }: { title?: React.ReactNode; glyph?: GlyphPairName; aside?: React.ReactNode; className?: string; children: React.ReactNode }) {  return (    <GlyphCard glyph={glyph} title={title} aside={aside} className={cn("h-full min-h-0", className)}>      {/* It reaches 4px past the card's content on each side, so the ring a hovered control draws isn't clipped. */}      <div className="-mx-1 flex min-h-0 flex-1 flex-col border-t border-line-subtle">        <ScrollArea className="flex flex-col gap-4 px-1 pt-3 pb-1">{children}</ScrollArea>      </div>    </GlyphCard>  );}/** One numbered step: the number in a small outlined square, the title, then the step's control. */function QuickstartStep({ number, title, aside, children }: { number: number; title: React.ReactNode; aside?: React.ReactNode; children: React.ReactNode }) {  return (    <section data-slot="quickstart-step" className="flex flex-col gap-2">      <div className="flex min-h-7 items-center gap-2">        <span className="grid size-5 shrink-0 place-items-center rounded-tag border border-line text-axis text-fg-muted tabular-nums">{number}</span>        <h4 className="flex-1 text-body-sm font-normal tracking-normal">{title}</h4>        {aside}      </div>      {children}    </section>  );}// Each icon and word shares a cell with its pair, so they cross-fade in place (with a little blur to blend them) and// the pill never changes width.const SWAP = "col-start-1 row-start-1 transition-[opacity,scale,filter] duration-200 ease-out motion-reduce:transition-none";const SWAP_HIDDEN = "scale-50 opacity-0 blur-[2px]";/** A bordered copy pill that turns green with a check once copied; `compact` keeps the icon alone. */function CopyPill({ value, label, compact = false }: { value: string; label: string; compact?: boolean }) {  const { hasCopied, copy } = useCopy();  return (    <button      type="button"      onClick={() => copy(value)}      aria-label={hasCopied ? "Copied" : label}      className={cn(        "flex h-7 shrink-0 items-center gap-1.5 rounded-tag border bg-surface text-caption outline-none transition-[color,border-color,scale] duration-150 ease-out focus-visible:ring-2 focus-visible:ring-focus-ring active:scale-[0.96] motion-reduce:active:scale-100",        compact ? "px-2" : "px-2.5",        hasCopied ? "border-success/40 text-success" : "border-line text-fg-muted hover:border-line-strong hover:text-fg",      )}    >      <span aria-hidden className="grid size-3.5 place-items-center">        <Copy className={cn(SWAP, "size-3.5", hasCopied && SWAP_HIDDEN)} strokeWidth={1.5} />        <Check className={cn(SWAP, "size-3.5", !hasCopied && SWAP_HIDDEN)} strokeWidth={1.75} />      </span>      {!compact && (        <span aria-hidden className="grid justify-items-center">          <span className={cn(SWAP, hasCopied && "opacity-0 blur-[2px]")}>Copy</span>          <span className={cn(SWAP, !hasCopied && "opacity-0 blur-[2px]")}>Copied</span>        </span>      )}    </button>  );}/** The API key, masked to its first characters, with show and a copy pill; revealing it never changes the width. */function KeyField({ secret, name = "key", visibleChars = 14 }: { secret: string; name?: string; visibleChars?: number }) {  const [revealed, setRevealed] = React.useState(false);  return (    <div data-slot="key-field" className="flex h-10 items-center gap-1 rounded-control border border-line pr-1 pl-3">      <code className="relative min-w-0 flex-1 font-mono text-caption text-fg-muted">        <span className={cn("block truncate", revealed && "invisible")} aria-hidden={revealed}>          {maskSecret(secret, visibleChars)}        </span>        {revealed && <span className="absolute inset-0 truncate">{secret}</span>}      </code>      <Tooltip>        <TooltipTrigger asChild>          <button            type="button"            onClick={() => setRevealed((r) => !r)}            aria-label={revealed ? `Hide ${name}` : `Show ${name}`}            className="grid size-7 shrink-0 place-items-center rounded-tag text-fg-subtle outline-none transition-colors duration-150 hover:bg-wash-control hover:text-fg focus-visible:ring-2 focus-visible:ring-focus-ring"          >            {revealed ? <EyeOff className="size-3.5" strokeWidth={1.5} /> : <Eye className="size-3.5" strokeWidth={1.5} />}          </button>        </TooltipTrigger>        <TooltipContent side="top">{revealed ? "Hide" : "Show"}</TooltipContent>      </Tooltip>      <CopyPill value={secret} label={`Copy ${name}`} />    </div>  );}export type Snippet<T extends string> = { value: T; label: string; code: string; copy?: string; note?: React.ReactNode };/** * Ways to do one thing as tabs over one snippet, its copy pill and anything else after them. Every snippet shares one * cell, so the box keeps the tallest one's height and switching never moves what's around it; the picked one fades and * settles in (300ms) while the others fade out faster (150ms). */function SnippetBox<T extends string>({ label, snippets, value, onValueChange, aside }: { label: string; snippets: Snippet<T>[]; value: T; onValueChange: (value: T) => void; aside?: React.ReactNode }) {  const shown = snippets.find((s) => s.value === value) ?? snippets[0];  return (    <div data-slot="snippet-box" className="rounded-control border border-line">      <div className="flex h-10 items-center gap-2 border-b border-line-subtle pr-1 pl-1.5">        <GlideTabs role="tab" className="[&>button]:px-3" label={label} value={shown.value} onValueChange={onValueChange} options={snippets.map((s) => ({ value: s.value, label: s.label }))} />        <span className="ml-auto flex items-center gap-1.5">          <CopyPill value={shown.copy ?? shown.code} label={`Copy ${label.toLowerCase()} snippet`} compact />          {aside}        </span>      </div>      <div className="grid overflow-hidden rounded-b-snippet bg-neutral-5">        {snippets.map((s) => {          const isShown = s.value === shown.value;          return (            <div              key={s.value}              aria-hidden={!isShown || undefined}              inert={!isShown || undefined}              className={cn(                "col-start-1 row-start-1 flex flex-col transition-[opacity,translate,filter] ease-out motion-reduce:transition-none",                isShown ? "translate-y-0 opacity-100 blur-none duration-300" : "pointer-events-none translate-y-1 opacity-0 blur-[2px] duration-150",              )}            >              <pre className="px-3 py-2.5 font-mono text-axis leading-snippet wrap-anywhere whitespace-pre-wrap text-fg-muted">{s.code}</pre>              {s.note && <p className="border-t border-line-subtle px-3 py-2 text-caption text-fg-subtle">{s.note}</p>}            </div>          );        })}      </div>    </div>  );}export { CopyPill, KeyField, QuickstartCard, QuickstartStep, SnippetBox };

Reference: context-dashboard c0ddf7a src/components/overview/quickstart-card.tsx; src/components/overview/setup-parts.tsx.