Keyboard

A two-tone mechanical keyboard with 3D press animations and physical key highlighting.

Keyboard renders a 60% mechanical keyboard with chunky two-tone keycaps and smooth press animations. Keys press down as you type on your physical keyboard, and clicking a key calls onKeyPress with its value.

Preview

Installation

$ npx shadcn@latest add https://spark-ui-olive.vercel.app/r/keyboard.json

Usage

import { Button } from "@/components/ui/button";
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from "@/components/ui/dialog";
import { Keyboard } from "@/components/keyboard";
import { Keyboard as KeyboardIcon, Volume2, VolumeX } from "lucide-react";
import { useState } from "react";
"use client";

export default function KeyboardDemo() {
  const [soundOn, setSoundOn] = useState(true);

  return (
    <div className="flex w-full justify-center p-4">
      <Dialog>
        <DialogTrigger asChild>
          <Button variant="outline">
            <KeyboardIcon className="mr-2 size-4" aria-hidden />
            Open keyboard
          </Button>
        </DialogTrigger>
        <DialogContent
          className={
            "bottom-0 left-0 top-auto max-w-none translate-x-0 translate-y-0 rounded-none border-x-0 border-b-0 sm:rounded-none " +
            "data-[state=open]:zoom-in-100 data-[state=open]:slide-in-from-bottom-10 " +
            "data-[state=closed]:zoom-out-100 data-[state=closed]:slide-out-to-bottom-10"
          }
        >
          <div className="mx-auto flex w-full max-w-4xl items-start justify-between gap-4">
            <DialogHeader className="text-left">
              <DialogTitle>Keyboard</DialogTitle>
              <DialogDescription>
                Type on your physical keyboard to see the keys press down.
              </DialogDescription>
            </DialogHeader>
            <button
              type="button"
              aria-label={soundOn ? "Mute key sounds" : "Unmute key sounds"}
              aria-pressed={!soundOn}
              onClick={() => setSoundOn((s) => !s)}
              className="inline-flex shrink-0 items-center gap-1.5 rounded-full border border-border bg-muted/50 px-3 py-1.5 text-xs text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
            >
              {soundOn ? (
                <Volume2 className="size-3" aria-hidden />
              ) : (
                <VolumeX className="size-3" aria-hidden />
              )}
              {soundOn ? "sound on" : "sound off"}
            </button>
          </div>
          <Keyboard className="mx-auto" sound={soundOn} />
          <p className="mx-auto w-full max-w-4xl text-center text-xs text-muted-foreground sm:text-sm">
            The preview card is too narrow for a full-width keyboard, so it
            opens down here where the board fits at its real size.
          </p>
        </DialogContent>
      </Dialog>
    </div>
  );
}

Examples

Dynamic Island

Pair the keyboard with a Dynamic Island keystroke feed via onKeyPress — each key blurs in, lingers for a moment, and blurs away.

Properties

PropTypeDefaultDescription
onKeyPress(key: string) => voidCalled when an on-screen key is clicked. Printable keys pass a single character ("a", "1", " "); the rest pass their name ("Backspace", "Enter", "Shift", …).
listenbooleantrueHighlight keys while the user types on their physical keyboard. Physical presses only animate — they do not call onKeyPress.
soundbooleantruePlay a mechanical click on every key press. The sound is synthesized with the Web Audio API — no audio files ship with the component.
classNamestringAdditional classes for the keyboard container.

Accessibility

  • Every key is a real <button>, so the component is fully keyboard operable with visible focus-visible rings.
  • Icon-only keys (, space) carry aria-labels.
  • Press animations are disabled under prefers-reduced-motion; pressed keys still change appearance.