# API Keys

> A list of API keys with the last time each was used, hold to revoke, and a Create key dialog that shows the new secret once with a copy button.

A shadcn/ui block from the [@sureui](https://sureui.com) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/sureui/api-keys-01).

- Page, with a live preview: https://shadcn.exchange/r/sureui/api-keys-01
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/sureui/api-keys-01-light-8c998cdd25252d93.png
- Registry item: https://sureui.com/r/api-keys-01.json
- Categories: block, settings
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://sureui.com/r/api-keys-01.json
```

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

## Dependencies

- npm packages: `lucide-react`
- Registry items: `@sureui/confirm-button`, `button`, `card`, `dialog`, `input`, `label`

## Files

### `components/blocks/api-keys-01/api-keys.tsx`

```tsx
"use client"

import * as React from "react"

import { cn } from "@/lib/utils"
import {
  Card,
  CardAction,
  CardContent,
  CardDescription,
  CardHeader,
  CardTitle,
} from "@/components/ui/card"
import { ConfirmButton } from "@/components/ui/sureui/confirm-button"

import { CreateKeyDialog } from "./create-key-dialog"

type ApiKey = {
  id: string
  name: string
  prefix: string
  last4: string
  lastUsed?: string
  revoked?: boolean
}

type ApiKeysProps = {
  initialKeys?: ApiKey[]
  className?: string
}

const sampleKeys: ApiKey[] = [
  {
    id: "key_1",
    name: "Production",
    prefix: "sk_live_",
    last4: "4f2a",
    lastUsed: "2 min ago",
  },
  {
    id: "key_2",
    name: "Billing worker",
    prefix: "sk_live_",
    last4: "9c1e",
    lastUsed: "1 hour ago",
  },
  {
    id: "key_3",
    name: "Staging",
    prefix: "sk_test_",
    last4: "71b3",
    lastUsed: "3 days ago",
  },
  {
    id: "key_4",
    name: "Old CI runner",
    prefix: "sk_test_",
    last4: "0d88",
    lastUsed: "4 months ago",
  },
]

const alphabet =
  "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"

function randomSecret(length: number) {
  const bytes = crypto.getRandomValues(new Uint8Array(length))
  return Array.from(bytes, (byte) => alphabet[byte % alphabet.length]).join("")
}

function request() {
  return Promise.resolve()
}

function ApiKeys({ initialKeys = sampleKeys, className }: ApiKeysProps) {
  const [keys, setKeys] = React.useState(initialKeys)

  async function createKey(name: string) {
    await request()
    const prefix = "sk_live_"
    const secret = `${prefix}${randomSecret(32)}`
    setKeys((prev) => [
      {
        id: `key_${Date.now()}`,
        name,
        prefix,
        last4: secret.slice(-4),
      },
      ...prev,
    ])
    return { name, secret }
  }

  async function revokeKey(id: string) {
    await request()
    setKeys((prev) =>
      prev.map((key) => (key.id === id ? { ...key, revoked: true } : key))
    )
  }

  return (
    <Card className={cn("w-full", className)}>
      <CardHeader>
        <CardTitle>API keys</CardTitle>
        <CardDescription>
          A revoked key stops working right away.
        </CardDescription>
        <CardAction>
          <CreateKeyDialog onCreate={createKey} />
        </CardAction>
      </CardHeader>
      <CardContent>
        <ul aria-label="API keys" className="divide-y">
          {keys.map((key) => (
            <li
              key={key.id}
              data-state={key.revoked ? "revoked" : "active"}
              className="flex items-center gap-3 py-3 first:pt-0 last:pb-0"
            >
              <div className="grid min-w-0 flex-1 gap-0.5">
                <span
                  className={cn(
                    "truncate font-medium",
                    key.revoked && "text-muted-foreground"
                  )}
                >
                  {key.name}
                </span>
                <span className="truncate font-mono text-xs text-muted-foreground">
                  {key.prefix}••••{key.last4}
                </span>
                <span className="truncate text-xs text-muted-foreground">
                  {key.revoked
                    ? "Revoked"
                    : key.lastUsed
                      ? `Used ${key.lastUsed}`
                      : "Never used"}
                </span>
              </div>
              {!key.revoked && (
                <ConfirmButton
                  gesture="hold"
                  variant="destructive"
                  size="sm"
                  aria-label={`Hold to revoke ${key.name}`}
                  className="shrink-0"
                  onConfirm={() => revokeKey(key.id)}
                >
                  Hold to revoke
                </ConfirmButton>
              )}
            </li>
          ))}
        </ul>
      </CardContent>
    </Card>
  )
}

export { ApiKeys, type ApiKey, type ApiKeysProps }
```

### `components/blocks/api-keys-01/create-key-dialog.tsx`

```tsx
"use client"

import * as React from "react"
import { CheckIcon, CopyIcon } from "lucide-react"

import { Button } from "@/components/ui/button"
import {
  Dialog,
  DialogClose,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from "@/components/ui/dialog"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"

type CreatedKey = {
  name: string
  secret: string
}

type CreateKeyDialogProps = {
  onCreate: (name: string) => Promise<CreatedKey>
}

function CreateKeyDialog({ onCreate }: CreateKeyDialogProps) {
  const [open, setOpen] = React.useState(false)
  const [name, setName] = React.useState("")
  const [pending, setPending] = React.useState(false)
  const [created, setCreated] = React.useState<CreatedKey | null>(null)
  const [copied, setCopied] = React.useState(false)
  const nameId = React.useId()
  const secretId = React.useId()

  React.useEffect(() => {
    if (!copied) return
    const timer = setTimeout(() => setCopied(false), 2000)
    return () => clearTimeout(timer)
  }, [copied])

  function reset() {
    setName("")
    setCreated(null)
    setCopied(false)
  }

  async function create(event: React.FormEvent<HTMLFormElement>) {
    event.preventDefault()
    if (!name.trim() || pending) return
    setPending(true)
    try {
      setCreated(await onCreate(name.trim()))
    } finally {
      setPending(false)
    }
  }

  async function copy() {
    if (!created) return
    await navigator.clipboard.writeText(created.secret)
    setCopied(true)
  }

  return (
    <Dialog
      open={open}
      disablePointerDismissal={created !== null || pending}
      onOpenChange={(next) => {
        if (pending) return
        setOpen(next)
      }}
      onOpenChangeComplete={(next) => {
        if (!next) reset()
      }}
    >
      <DialogTrigger render={<Button size="sm" />}>Create key</DialogTrigger>
      <DialogContent showCloseButton={created === null}>
        {created ? (
          <>
            <DialogHeader>
              <DialogTitle>Copy your new key</DialogTitle>
              <DialogDescription>
                You won&apos;t see the secret for {created.name} again. Store it
                somewhere safe.
              </DialogDescription>
            </DialogHeader>
            <div className="grid gap-2">
              <Label htmlFor={secretId}>Secret key</Label>
              <div className="flex gap-2">
                <Input
                  id={secretId}
                  readOnly
                  value={created.secret}
                  className="font-mono text-xs"
                  onFocus={(event) => event.currentTarget.select()}
                />
                <Button
                  variant="outline"
                  size="icon"
                  aria-label={copied ? "Copied" : "Copy secret key"}
                  onClick={copy}
                >
                  {copied ? <CheckIcon /> : <CopyIcon />}
                </Button>
              </div>
            </div>
            <DialogFooter>
              <DialogClose render={<Button />}>Done</DialogClose>
            </DialogFooter>
          </>
        ) : (
          <form onSubmit={create} className="grid gap-4">
            <DialogHeader>
              <DialogTitle>Create API key</DialogTitle>
              <DialogDescription>
                Name it after the service that will use it.
              </DialogDescription>
            </DialogHeader>
            <div className="grid gap-2">
              <Label htmlFor={nameId}>Name</Label>
              <Input
                id={nameId}
                value={name}
                placeholder="Billing worker"
                autoComplete="off"
                readOnly={pending}
                onChange={(event) => setName(event.target.value)}
              />
            </div>
            <DialogFooter>
              <DialogClose
                render={<Button variant="outline" />}
                disabled={pending}
              >
                Cancel
              </DialogClose>
              <Button type="submit" disabled={!name.trim() || pending}>
                {pending ? "Creating…" : "Create key"}
              </Button>
            </DialogFooter>
          </form>
        )}
      </DialogContent>
    </Dialog>
  )
}

export { CreateKeyDialog, type CreateKeyDialogProps, type CreatedKey }
```
