Styled keyboard key(s) with grouping support.

Preview
Style
Basic
Ctrl⌘KCtrl + B
Modifier Keys
C
KbdGroup
CtrlShiftP
Arrow Keys
With Icons
With Icons and Text
Left Voice Enabled
InputGroup
Space
Tooltip
Hover for tooltip — S
With samp
File
Grouped
src/demo/views/kbd.ts170 lines
import type { Html, HtmlBuilder } from 'foldkit/html'

import { Kbd } from '../../generated/registry/ui/kbd'
import {
  inputGroup,
  inputGroupAddon,
  inputGroupInput,
} from '../../generated/registry/ui/input-group'
import { button } from '../../generated/registry/ui/button'
import { icon } from '../../generated/registry/lib/icons'
import { Save, CircleDashed, ArrowLeft, ArrowRight } from 'lucide'

import { defineSlice } from '../slice'
import type { Message, Model } from '../assemble'

export const kbdView = (_model: Model, h: HtmlBuilder<Message>): Html =>
  h.div(
    [h.Class('flex w-full flex-col gap-8')],
    [
      h.div(
        [h.Class('flex w-full flex-col gap-2')],
        [
          h.div([h.Class('px-1 text-xs font-medium text-muted-foreground')], ['Basic']),
          h.div(
            [h.Class('flex items-center gap-2')],
            [
              Kbd<Message>({}, ['Ctrl'], h),
              Kbd<Message>({}, ['⌘K'], h),
              Kbd<Message>({}, ['Ctrl + B'], h),
            ],
          ),
        ],
      ),
      h.div(
        [h.Class('flex w-full flex-col gap-2')],
        [
          h.div([h.Class('px-1 text-xs font-medium text-muted-foreground')], ['Modifier Keys']),
          h.div(
            [h.Class('flex items-center gap-2')],
            [Kbd<Message>({}, ['⌘'], h), Kbd<Message>({}, ['C'], h)],
          ),
        ],
      ),
      h.div(
        [h.Class('flex w-full flex-col gap-2')],
        [
          h.div([h.Class('px-1 text-xs font-medium text-muted-foreground')], ['KbdGroup']),
          Kbd.group<Message>(
            {},
            [
              Kbd<Message>({}, ['Ctrl'], h),
              Kbd<Message>({}, ['Shift'], h),
              Kbd<Message>({}, ['P'], h),
            ],
            h,
          ),
        ],
      ),
      h.div(
        [h.Class('flex w-full flex-col gap-2')],
        [
          h.div([h.Class('px-1 text-xs font-medium text-muted-foreground')], ['Arrow Keys']),
          h.div(
            [h.Class('flex items-center gap-2')],
            [
              Kbd<Message>({}, ['↑'], h),
              Kbd<Message>({}, ['↓'], h),
              Kbd<Message>({}, ['←'], h),
              Kbd<Message>({}, ['→'], h),
            ],
          ),
        ],
      ),
      h.div(
        [h.Class('flex w-full flex-col gap-2')],
        [
          h.div([h.Class('px-1 text-xs font-medium text-muted-foreground')], ['With Icons']),
          Kbd.group<Message>(
            {},
            [
              Kbd<Message>({}, [icon(h, CircleDashed, 'size-3')], h),
              Kbd<Message>({}, [icon(h, ArrowLeft, 'size-3')], h),
              Kbd<Message>({}, [icon(h, ArrowRight, 'size-3')], h),
            ],
            h,
          ),
        ],
      ),
      h.div(
        [h.Class('flex w-full flex-col gap-2')],
        [
          h.div(
            [h.Class('px-1 text-xs font-medium text-muted-foreground')],
            ['With Icons and Text'],
          ),
          Kbd.group<Message>(
            {},
            [
              Kbd<Message>({}, [icon(h, ArrowLeft, 'size-3'), ' Left'], h),
              Kbd<Message>({}, [icon(h, CircleDashed, 'size-3'), ' Voice Enabled'], h),
            ],
            h,
          ),
        ],
      ),
      h.div(
        [h.Class('flex w-full flex-col gap-2')],
        [
          h.div([h.Class('px-1 text-xs font-medium text-muted-foreground')], ['InputGroup']),
          h.div(
            [h.Class('w-full max-w-sm')],
            [
              inputGroup(
                {},
                [
                  inputGroupInput({ id: 'kbd-input-group', placeholder: '' }, h),
                  inputGroupAddon({}, [Kbd<Message>({}, ['Space'], h)], h),
                ],
                h,
              ),
            ],
          ),
        ],
      ),
      h.div(
        [h.Class('flex w-full flex-col gap-2')],
        [
          h.div([h.Class('px-1 text-xs font-medium text-muted-foreground')], ['Tooltip']),
          h.div(
            [h.Class('flex items-center gap-2')],
            [
              button<Message>({ variant: 'outline', size: 'icon-sm' }, icon(h, Save, 'size-4'), h),
              h.span([h.Class('text-sm text-muted-foreground')], ['Hover for tooltip — S']),
            ],
          ),
        ],
      ),
      h.div(
        [h.Class('flex w-full flex-col gap-2')],
        [
          h.div([h.Class('px-1 text-xs font-medium text-muted-foreground')], ['With samp']),
          Kbd<Message>({}, [h.span([], ['File'])], h),
        ],
      ),
      h.div(
        [h.Class('flex w-full flex-col gap-2')],
        [
          h.div([h.Class('px-1 text-xs font-medium text-muted-foreground')], ['Grouped']),
          Kbd.group<Message>(
            {},
            [
              Kbd<Message>({}, ['⌘'], h),
              Kbd<Message>({}, ['⇧'], h),
              Kbd<Message>({}, ['⌥'], h),
              Kbd<Message>({}, ['⌃'], h),
            ],
            h,
          ),
        ],
      ),
    ],
  )

export const slice = defineSlice({
  fields: {},
  init: {},
  messages: [],
  handlers: () => ({}),
})

This code is shown verbatim — it’s the exact file used for this preview. Much of the surrounding wiring (slice, fields, messages, handlers) belongs to this site’s demo harness; you only need the view and the state that matters for your own app. Adapt what you need.

Installation

Add this component to your project:

pnpm dlx shadcn@latest add @foldcn/kbd

Source

The component ships as plain source — no build step, no wrapper. Copy it and make it yours.

registry/default/ui/kbd.ts34 lines
import type { Html, HtmlBuilder } from 'foldkit/html'

type Child = Html | string

import { cn } from '@/lib/utils'

export const kbdClass =
  'bg-muted text-muted-foreground in-data-[slot=tooltip-content]:bg-background/20 in-data-[slot=tooltip-content]:text-background dark:in-data-[slot=tooltip-content]:bg-background/10 h-5 w-fit min-w-5 gap-1 rounded-sm px-1 font-sans text-xs font-medium [&_svg:not([class*=\'size-\'])]:size-3 pointer-events-none inline-flex items-center justify-center select-none'

export const kbdGroupClass = 'gap-1 inline-flex items-center'

type StyleConfig = Readonly<{ className?: string }>

const kbdContainer = <M>(
  config: StyleConfig,
  children: ReadonlyArray<Child>,
  h: HtmlBuilder<M>,
): Html =>
  h.kbd([h.Class(cn(kbdClass, config.className)), h.DataAttribute('slot', 'kbd')], children)

const kbdGroup = <M>(
  config: StyleConfig,
  children: ReadonlyArray<Child>,
  h: HtmlBuilder<M>,
): Html =>
  h.kbd(
    [h.Class(cn(kbdGroupClass, config.className)), h.DataAttribute('slot', 'kbd-group')],
    children,
  )

/** Styled keyboard key(s). `Kbd.group` clusters several keys. Mirrors the
 *  shadcn v4 `kbd.tsx`. */
export const Kbd = Object.assign(kbdContainer, { group: kbdGroup })