shshadcn-htmx

Components

Kbd

An inline keyboard-key badge. Renders a native <kbd> for a single key, or nests one <kbd> per key inside an outer one to render a shortcut like Ctrl + Shift + R. Zero JavaScript — it's a label, not a control.

Installation

1. Install via the shadcn CLI

npx shadcn@latest add http://localhost/r/kbd.json

2. Use it

components/ui/kbd.tsx
import { Kbd, KbdGroup } from "@/components/ui/kbd"

<Kbd>Esc</Kbd>
<KbdGroup keys={["Ctrl", "Shift", "R"]} />
<KbdGroup>
  <Kbd>⌘</Kbd> + <Kbd>K</Kbd>
</KbdGroup>
Or copy the source manually
components/ui/kbd.tsx
/** @jsxImportSource hono/jsx */
import type { Child, PropsWithChildren } from "hono/jsx"
import { cn, type ClassValue } from "@/registry/lib/cn"

// Kbd — shadcn-htmx, htmx v4 + Tailwind v4.
//
// Native element (source of truth):
//   repos/mdn/files/en-us/web/html/reference/elements/kbd/index.md
//     - <kbd> denotes textual user input from a keyboard / voice / other
//       text-entry device. Implicit ARIA role: "no corresponding role"
//       (it's phrasing content, announced as plain text). No interaction,
//       so this component ships ZERO JavaScript.
//     - MDN's "Representing keystrokes within an input" pattern nests a
//       <kbd> per key inside an outer <kbd> that represents the whole
//       shortcut. We follow that: <KbdGroup> is the outer <kbd>, each
//       <Kbd> is an inner key. A lone <Kbd> (no group) is still a valid
//       single-key <kbd>.
//
// Visual styling translated from shadcn/ui's Kbd (kept on-brand with our
// theme tokens, not copied verbatim):
//   repos/shadcn-ui/apps/v4/registry/new-york-v4/ui/kbd.tsx
//
// We use ONLY existing theme tokens (bg-muted / text-muted-foreground /
// border). select-none + pointer-events-none keep the key glyphs from being
// selected or clicked — they are a label, not a control.

const kbdBase =
  "pointer-events-none inline-flex h-5 w-fit min-w-5 shrink-0 select-none items-center justify-center gap-1 " +
  "rounded-sm border bg-muted px-1 font-sans text-xs font-medium text-muted-foreground " +
  "[&_svg:not([class*='size-'])]:size-3"

// The outer <kbd> that wraps a sequence of keys. MDN allows nesting <kbd>
// inside <kbd>; the group carries no background of its own so the inner
// keys read as discrete caps with separators (the "+" text) between them.
const kbdGroupBase = "inline-flex w-fit items-center gap-1 align-middle"

export function kbdClasses(opts?: { class?: ClassValue }): string {
  return cn(kbdBase, opts?.class)
}

export function kbdGroupClasses(opts?: { class?: ClassValue }): string {
  return cn(kbdGroupBase, opts?.class)
}

type KbdProps = PropsWithChildren<{
  class?: ClassValue
  id?: string
  // Accessible name override — useful for symbol-only keys (e.g. content
  // "⌘" with ariaLabel="Command"). See aria-label on MDN.
  ariaLabel?: string
  title?: string
  // htmx attrs / data-* / aria-* flow through via {...rest}.
}>

// A single key cap. Renders one <kbd>. Use inside <KbdGroup> for shortcuts.
export function Kbd(props: KbdProps) {
  const { children, class: className, id, ariaLabel, title, ...rest } = props
  return (
    <kbd
      id={id}
      data-slot="kbd"
      class={kbdClasses({ class: className })}
      aria-label={ariaLabel}
      title={title}
      {...rest}
    >
      {children}
    </kbd>
  )
}

type KbdGroupProps = PropsWithChildren<{
  class?: ClassValue
  id?: string
  // Convenience: render a shortcut from a list of key labels, joined by a
  // visible separator. Omit `keys` and pass children to compose manually.
  keys?: Child[]
  // Separator rendered between keys (default "+"). Set "" for none.
  separator?: Child
  // Accessible name for the whole shortcut, e.g. "Control Shift R". The
  // outer <kbd> is the labelled unit; per-key caps inherit no extra role.
  ariaLabel?: string
}>

// The outer <kbd> wrapping a key sequence (MDN nested-kbd pattern). When
// `keys` is supplied it renders one <Kbd> per entry with a separator text
// node between them; otherwise it renders `children` verbatim so callers
// can mix keys, "+" text, and icons by hand.
export function KbdGroup(props: KbdGroupProps) {
  const {
    children,
    class: className,
    id,
    keys,
    separator = "+",
    ariaLabel,
    ...rest
  } = props

  let body: Child
  if (keys && keys.length > 0) {
    const parts: Child[] = []
    keys.forEach((k, i) => {
      if (i > 0 && separator !== "" && separator != null) {
        parts.push(
          <span aria-hidden="true" class="text-muted-foreground/70">
            {separator}
          </span>,
        )
      }
      parts.push(<Kbd>{k}</Kbd>)
    })
    body = parts
  } else {
    body = children
  }

  return (
    <kbd
      id={id}
      data-slot="kbd-group"
      class={kbdGroupClasses({ class: className })}
      aria-label={ariaLabel}
      {...rest}
    >
      {body}
    </kbd>
  )
}

1. Save the file

Copy kbd.html into templates/components/.

2. Use it

templates/components/kbd.html
{% from "components/kbd.html" import kbd, kbd_group %}

{{ kbd("Esc") }}
{{ kbd_group(["Ctrl", "Shift", "R"]) }}
View source
templates/components/kbd.html
{# Kbd macros — shadcn-htmx, htmx v4 + Tailwind v4.
   Mirrors registry/ui/kbd.tsx. Zero JavaScript — <kbd> is phrasing
   content with no interaction.

   Native element:
     repos/mdn/files/en-us/web/html/reference/elements/kbd/index.md
     (nested <kbd> per key inside an outer <kbd> = MDN keystroke pattern)

   Usage:
     {% from "components/kbd.html" import kbd, kbd_group %}
     {{ kbd("Esc") }}
     {{ kbd_group(["Ctrl", "Shift", "R"]) }}
     {% call kbd_group() %}<kbd ...>⌘</kbd> + <kbd ...>K</kbd>{% endcall %} #}

{%- set kbd_base -%}
pointer-events-none inline-flex h-5 w-fit min-w-5 shrink-0 select-none items-center justify-center gap-1 rounded-sm border bg-muted px-1 font-sans text-xs font-medium text-muted-foreground [&_svg:not([class*='size-'])]:size-3
{%- endset -%}

{%- set kbd_group_base -%}
inline-flex w-fit items-center gap-1 align-middle
{%- endset -%}

{# A single key cap → one <kbd>. #}
{% macro kbd(
    text,
    id=none,
    aria_label=none,
    title=none,
    extra_class="",
    **attrs
) %}
<kbd
  {%- if id %} id="{{ id }}"{% endif %}
  data-slot="kbd"
  class="{{ kbd_base }} {{ extra_class }}"
  {%- if aria_label %} aria-label="{{ aria_label }}"{% endif %}
  {%- if title %} title="{{ title }}"{% endif %}
  {%- for k, v in attrs.items() %} {{ k|replace('_', '-') }}="{{ v }}"{% endfor -%}
>{{ text }}</kbd>
{%- endmacro %}

{# The outer <kbd> wrapping a shortcut. Pass `keys` (a list) to render a
   key cap per entry joined by `separator`, or use {% call %} with a body. #}
{% macro kbd_group(
    keys=none,
    separator="+",
    id=none,
    aria_label=none,
    extra_class="",
    **attrs
) %}
<kbd
  {%- if id %} id="{{ id }}"{% endif %}
  data-slot="kbd-group"
  class="{{ kbd_group_base }} {{ extra_class }}"
  {%- if aria_label %} aria-label="{{ aria_label }}"{% endif %}
  {%- for k, v in attrs.items() %} {{ k|replace('_', '-') }}="{{ v }}"{% endfor -%}
>
  {%- if keys -%}
    {%- for key in keys -%}
      {%- if not loop.first and separator -%}<span aria-hidden="true" class="text-muted-foreground/70">{{ separator }}</span>{%- endif -%}
      <kbd data-slot="kbd" class="{{ kbd_base }}">{{ key }}</kbd>
    {%- endfor -%}
  {%- else -%}
    {{ caller() }}
  {%- endif -%}
</kbd>
{%- endmacro %}

1. Save the file

Add kbd.tmpl alongside your templates.

2. Use it

components/kbd.tmpl
{{template "kbd" (dict "Text" "Esc")}}
{{template "kbd-group" (dict "Keys" (list "Ctrl" "Shift" "R"))}}
View source
components/kbd.tmpl
{{/*
  Kbd templates — shadcn-htmx, htmx v4 + Tailwind v4.
  Mirrors registry/ui/kbd.tsx. Zero JavaScript — <kbd> is phrasing content.

  Native element:
    repos/mdn/files/en-us/web/html/reference/elements/kbd/index.md
    (nested <kbd> per key inside an outer <kbd> = MDN keystroke pattern)

  Usage:

      type KbdArgs struct {
          Text             string
          ID, AriaLabel    string
          Title            string
          Attrs            map[string]string
      }
      type KbdGroupArgs struct {
          Keys             []string // one cap per entry
          Separator        string   // default "+"
          ID, AriaLabel    string
          Body             template.HTML // used when Keys is empty
          Attrs            map[string]string
      }

      {{template "kbd" (dict "Text" "Esc")}}
      {{template "kbd-group" (dict "Keys" (list "Ctrl" "Shift" "R"))}}
*/}}

{{define "kbd"}}
{{- $base := "pointer-events-none inline-flex h-5 w-fit min-w-5 shrink-0 select-none items-center justify-center gap-1 rounded-sm border bg-muted px-1 font-sans text-xs font-medium text-muted-foreground [&_svg:not([class*='size-'])]:size-3" -}}
<kbd
  {{- if .ID}} id="{{.ID}}"{{end}}
  data-slot="kbd"
  class="{{$base}}"
  {{- if .AriaLabel}} aria-label="{{.AriaLabel}}"{{end}}
  {{- if .Title}} title="{{.Title}}"{{end}}
  {{- range $k, $v := .Attrs}} {{$k}}="{{$v}}"{{end -}}
>{{.Text}}</kbd>
{{end}}

{{define "kbd-group"}}
{{- $base := "pointer-events-none inline-flex h-5 w-fit min-w-5 shrink-0 select-none items-center justify-center gap-1 rounded-sm border bg-muted px-1 font-sans text-xs font-medium text-muted-foreground [&_svg:not([class*='size-'])]:size-3" -}}
{{- $groupBase := "inline-flex w-fit items-center gap-1 align-middle" -}}
{{- $sep := or .Separator "+" -}}
<kbd
  {{- if .ID}} id="{{.ID}}"{{end}}
  data-slot="kbd-group"
  class="{{$groupBase}}"
  {{- if .AriaLabel}} aria-label="{{.AriaLabel}}"{{end}}
  {{- range $k, $v := .Attrs}} {{$k}}="{{$v}}"{{end -}}
>
  {{- if .Keys -}}
    {{- range $i, $key := .Keys -}}
      {{- if and (gt $i 0) $sep -}}<span aria-hidden="true" class="text-muted-foreground/70">{{$sep}}</span>{{- end -}}
      <kbd data-slot="kbd" class="{{$base}}">{{$key}}</kbd>
    {{- end -}}
  {{- else -}}
    {{- htmlSafe .Body -}}
  {{- end -}}
</kbd>
{{end}}

1. Save the file

Drop kbd.ex into lib/my_app_web/components/.

2. Use it

lib/my_app_web/components/kbd.ex
<.kbd>Esc</.kbd>
<.kbd_group keys={["Ctrl", "Shift", "R"]} />
View source
lib/my_app_web/components/kbd.ex
defmodule ShadcnHtmx.Components.Kbd do
  @moduledoc """
  Kbd — shadcn-htmx, htmx v4 + Tailwind v4 for Phoenix.

  Mirrors registry/ui/kbd.tsx. Zero JavaScript — `<kbd>` is phrasing
  content with no interaction.

  Native element:
    repos/mdn/files/en-us/web/html/reference/elements/kbd/index.md
    (nested `<kbd>` per key inside an outer `<kbd>` = MDN keystroke pattern)

  `kbd/1` renders one key cap. `kbd_group/1` renders the outer `<kbd>`
  wrapping a shortcut — pass `keys` for an auto-joined sequence, or use the
  default slot to compose the body by hand.
  """

  use Phoenix.Component

  @base "pointer-events-none inline-flex h-5 w-fit min-w-5 shrink-0 select-none items-center justify-center gap-1 " <>
          "rounded-sm border bg-muted px-1 font-sans text-xs font-medium text-muted-foreground " <>
          "[&_svg:not([class*='size-'])]:size-3"

  @group_base "inline-flex w-fit items-center gap-1 align-middle"

  attr :id, :string, default: nil
  attr :aria_label, :string, default: nil
  attr :title, :string, default: nil
  attr :class, :string, default: nil
  attr :rest, :global

  slot :inner_block, required: true

  def kbd(assigns) do
    assigns = assign(assigns, :base_class, @base)

    ~H"""
    <kbd
      id={@id}
      data-slot="kbd"
      class={[@base_class, @class]}
      aria-label={@aria_label}
      title={@title}
      {@rest}
    >
      {render_slot(@inner_block)}
    </kbd>
    """
  end

  attr :keys, :list, default: nil
  attr :separator, :string, default: "+"
  attr :id, :string, default: nil
  attr :aria_label, :string, default: nil
  attr :class, :string, default: nil
  attr :rest, :global

  slot :inner_block

  def kbd_group(assigns) do
    assigns =
      assigns
      |> assign(:base_class, @base)
      |> assign(:group_class, @group_base)

    ~H"""
    <kbd
      id={@id}
      data-slot="kbd-group"
      class={[@group_class, @class]}
      aria-label={@aria_label}
      {@rest}
    >
      <%= if @keys && @keys != [] do %>
        <%= for {key, i} <- Enum.with_index(@keys) do %>
          <span :if={i > 0 && @separator != ""} aria-hidden="true" class="text-muted-foreground/70">{@separator}</span>
          <kbd data-slot="kbd" class={@base_class}>{key}</kbd>
        <% end %>
      <% else %>
        {render_slot(@inner_block)}
      <% end %>
    </kbd>
    """
  end
end

1. Save the file

Paste the markup; relies only on theme tokens.

2. Use it

snippets/kbd.html
<kbd data-slot="kbd"
     class="pointer-events-none inline-flex h-5 …">Esc</kbd>
View source
snippets/kbd.html
<!--
  shadcn-htmx — raw <kbd> snippets. Mirrors registry/ui/kbd.tsx.
  Zero JavaScript — <kbd> is phrasing content with no interaction.

  Native element:
    repos/mdn/files/en-us/web/html/reference/elements/kbd/index.md
    (nested <kbd> per key inside an outer <kbd> = MDN keystroke pattern)

  KEY CAP base (data-slot="kbd"):
    pointer-events-none inline-flex h-5 w-fit min-w-5 shrink-0 select-none
    items-center justify-center gap-1 rounded-sm border bg-muted px-1
    font-sans text-xs font-medium text-muted-foreground
    [&_svg:not([class*='size-'])]:size-3

  GROUP base (data-slot="kbd-group", the outer wrapping <kbd>):
    inline-flex w-fit items-center gap-1 align-middle
  Relies only on theme tokens; no script.
-->

<!-- Single key -->
<kbd data-slot="kbd"
  class="pointer-events-none inline-flex h-5 w-fit min-w-5 shrink-0 select-none items-center justify-center gap-1 rounded-sm border bg-muted px-1 font-sans text-xs font-medium text-muted-foreground [&_svg:not([class*='size-'])]:size-3">
  Esc
</kbd>

<!-- Shortcut: outer <kbd> groups inner key caps, "+" between them -->
<kbd data-slot="kbd-group" class="inline-flex w-fit items-center gap-1 align-middle">
  <kbd data-slot="kbd"
    class="pointer-events-none inline-flex h-5 w-fit min-w-5 shrink-0 select-none items-center justify-center gap-1 rounded-sm border bg-muted px-1 font-sans text-xs font-medium text-muted-foreground [&_svg:not([class*='size-'])]:size-3">Ctrl</kbd>
  <span aria-hidden="true" class="text-muted-foreground/70">+</span>
  <kbd data-slot="kbd"
    class="pointer-events-none inline-flex h-5 w-fit min-w-5 shrink-0 select-none items-center justify-center gap-1 rounded-sm border bg-muted px-1 font-sans text-xs font-medium text-muted-foreground [&_svg:not([class*='size-'])]:size-3">Shift</kbd>
  <span aria-hidden="true" class="text-muted-foreground/70">+</span>
  <kbd data-slot="kbd"
    class="pointer-events-none inline-flex h-5 w-fit min-w-5 shrink-0 select-none items-center justify-center gap-1 rounded-sm border bg-muted px-1 font-sans text-xs font-medium text-muted-foreground [&_svg:not([class*='size-'])]:size-3">R</kbd>
</kbd>

<!-- Symbol key with an accessible name (aria-label) for screen readers -->
<kbd data-slot="kbd" aria-label="Command"
  class="pointer-events-none inline-flex h-5 w-fit min-w-5 shrink-0 select-none items-center justify-center gap-1 rounded-sm border bg-muted px-1 font-sans text-xs font-medium text-muted-foreground [&_svg:not([class*='size-'])]:size-3">

</kbd>

Examples

Single key

A lone <Kbd> renders one native <kbd>. Symbol-only keys take an ariaLabel so screen readers announce a word, not a glyph.

<kbd> is phrasing content with no implicit ARIA role — it's read as plain text. For a symbol cap such as pass ariaLabel="Command" so assistive tech doesn't announce the raw glyph.

EscEnterTab
<Kbd>Esc</Kbd>
<Kbd>Enter</Kbd>
<Kbd>Tab</Kbd>
<Kbd ariaLabel="Command">⌘</Kbd>
<Kbd ariaLabel="Up arrow">↑</Kbd>
{{ kbd("Esc") }}
{{ kbd("Enter") }}
{{ kbd("Tab") }}
{{ kbd("⌘", aria_label="Command") }}
{{ kbd("↑", aria_label="Up arrow") }}
{{template "kbd" (dict "Text" "Esc")}}
{{template "kbd" (dict "Text" "Enter")}}
{{template "kbd" (dict "Text" "Tab")}}
{{template "kbd" (dict "Text" "⌘" "AriaLabel" "Command")}}
{{template "kbd" (dict "Text" "↑" "AriaLabel" "Up arrow")}}
<.kbd>Esc</.kbd>
<.kbd>Enter</.kbd>
<.kbd>Tab</.kbd>
<.kbd aria_label="Command"></.kbd>
<.kbd aria_label="Up arrow"></.kbd>
<div class="flex flex-wrap items-center justify-center gap-3">
  <kbd data-slot="kbd" class="pointer-events-none inline-flex h-5 w-fit min-w-5 shrink-0 select-none items-center justify-center gap-1 rounded-sm border bg-muted px-1 font-sans text-xs font-medium text-muted-foreground [&amp;_svg:not([class*=&#39;size-&#39;])]:size-3">Esc</kbd>
  <kbd data-slot="kbd" class="pointer-events-none inline-flex h-5 w-fit min-w-5 shrink-0 select-none items-center justify-center gap-1 rounded-sm border bg-muted px-1 font-sans text-xs font-medium text-muted-foreground [&amp;_svg:not([class*=&#39;size-&#39;])]:size-3">Enter</kbd>
  <kbd data-slot="kbd" class="pointer-events-none inline-flex h-5 w-fit min-w-5 shrink-0 select-none items-center justify-center gap-1 rounded-sm border bg-muted px-1 font-sans text-xs font-medium text-muted-foreground [&amp;_svg:not([class*=&#39;size-&#39;])]:size-3">Tab</kbd>
  <kbd data-slot="kbd" class="pointer-events-none inline-flex h-5 w-fit min-w-5 shrink-0 select-none items-center justify-center gap-1 rounded-sm border bg-muted px-1 font-sans text-xs font-medium text-muted-foreground [&amp;_svg:not([class*=&#39;size-&#39;])]:size-3" aria-label="Command">⌘</kbd>
  <kbd data-slot="kbd" class="pointer-events-none inline-flex h-5 w-fit min-w-5 shrink-0 select-none items-center justify-center gap-1 rounded-sm border bg-muted px-1 font-sans text-xs font-medium text-muted-foreground [&amp;_svg:not([class*=&#39;size-&#39;])]:size-3" aria-label="Up arrow">↑</kbd>
</div>

Further reading

Shortcut — nested keys

A shortcut is one outer <kbd> wrapping a <kbd> per key, with a "+" separator between them (MDN's keystroke pattern). Pass keys to build it, or compose by hand.

Per MDN, a multi-keystroke input nests each key in its own <kbd> inside an outer <kbd> that represents the whole input. The + separators are aria-hidden so the reading isn't cluttered with "plus".

CtrlShiftRKAltF4
<KbdGroup keys={["Ctrl", "Shift", "R"]} />
<KbdGroup keys={["⌘", "K"]} ariaLabel="Command K" />
<KbdGroup keys={["Alt", "F4"]} />
{{ kbd_group(["Ctrl", "Shift", "R"]) }}
{{ kbd_group(["⌘", "K"], aria_label="Command K") }}
{{ kbd_group(["Alt", "F4"]) }}
{{template "kbd-group" (dict "Keys" (list "Ctrl" "Shift" "R"))}}
{{template "kbd-group" (dict "Keys" (list "⌘" "K") "AriaLabel" "Command K")}}
{{template "kbd-group" (dict "Keys" (list "Alt" "F4"))}}
<.kbd_group keys={["Ctrl", "Shift", "R"]} />
<.kbd_group keys={["⌘", "K"]} aria_label="Command K" />
<.kbd_group keys={["Alt", "F4"]} />
<div class="flex flex-wrap items-center justify-center gap-4">
  <kbd data-slot="kbd-group" class="inline-flex w-fit items-center gap-1 align-middle">
    <kbd data-slot="kbd" class="pointer-events-none inline-flex h-5 w-fit min-w-5 shrink-0 select-none items-center justify-center gap-1 rounded-sm border bg-muted px-1 font-sans text-xs font-medium text-muted-foreground [&amp;_svg:not([class*=&#39;size-&#39;])]:size-3">Ctrl</kbd>
    <span aria-hidden="true" class="text-muted-foreground/70">+</span>
    <kbd data-slot="kbd" class="pointer-events-none inline-flex h-5 w-fit min-w-5 shrink-0 select-none items-center justify-center gap-1 rounded-sm border bg-muted px-1 font-sans text-xs font-medium text-muted-foreground [&amp;_svg:not([class*=&#39;size-&#39;])]:size-3">Shift</kbd>
    <span aria-hidden="true" class="text-muted-foreground/70">+</span>
    <kbd data-slot="kbd" class="pointer-events-none inline-flex h-5 w-fit min-w-5 shrink-0 select-none items-center justify-center gap-1 rounded-sm border bg-muted px-1 font-sans text-xs font-medium text-muted-foreground [&amp;_svg:not([class*=&#39;size-&#39;])]:size-3">R</kbd>
  </kbd>
  <kbd data-slot="kbd-group" class="inline-flex w-fit items-center gap-1 align-middle" aria-label="Command K">
    <kbd data-slot="kbd" class="pointer-events-none inline-flex h-5 w-fit min-w-5 shrink-0 select-none items-center justify-center gap-1 rounded-sm border bg-muted px-1 font-sans text-xs font-medium text-muted-foreground [&amp;_svg:not([class*=&#39;size-&#39;])]:size-3">⌘</kbd>
    <span aria-hidden="true" class="text-muted-foreground/70">+</span>
    <kbd data-slot="kbd" class="pointer-events-none inline-flex h-5 w-fit min-w-5 shrink-0 select-none items-center justify-center gap-1 rounded-sm border bg-muted px-1 font-sans text-xs font-medium text-muted-foreground [&amp;_svg:not([class*=&#39;size-&#39;])]:size-3">K</kbd>
  </kbd>
  <kbd data-slot="kbd-group" class="inline-flex w-fit items-center gap-1 align-middle">
    <kbd data-slot="kbd" class="pointer-events-none inline-flex h-5 w-fit min-w-5 shrink-0 select-none items-center justify-center gap-1 rounded-sm border bg-muted px-1 font-sans text-xs font-medium text-muted-foreground [&amp;_svg:not([class*=&#39;size-&#39;])]:size-3">Alt</kbd>
    <span aria-hidden="true" class="text-muted-foreground/70">+</span>
    <kbd data-slot="kbd" class="pointer-events-none inline-flex h-5 w-fit min-w-5 shrink-0 select-none items-center justify-center gap-1 rounded-sm border bg-muted px-1 font-sans text-xs font-medium text-muted-foreground [&amp;_svg:not([class*=&#39;size-&#39;])]:size-3">F4</kbd>
  </kbd>
</div>

Inline in prose

Kbd is phrasing content, so it sits inside running text, menu rows, and tooltips without breaking the line box.

Because <kbd> is inline phrasing content it flows with the surrounding sentence. The cap is select-none and pointer-events-none so copying the paragraph skips the glyphs and clicks fall through to whatever wraps it.

Press / to focus search, or CtrlK to open the command menu. Save with S.

<p>
  Press <Kbd>/</Kbd> to focus search, or{" "}
  <KbdGroup keys={["Ctrl", "K"]} /> to open the command menu.
  Save with <KbdGroup keys={["⌘", "S"]} ariaLabel="Command S" />.
</p>
<p>
  Press {{ kbd("/") }} to focus search, or
  {{ kbd_group(["Ctrl", "K"]) }} to open the command menu.
  Save with {{ kbd_group(["⌘", "S"], aria_label="Command S") }}.
</p>
<p>
  Press {{template "kbd" (dict "Text" "/")}} to focus search, or
  {{template "kbd-group" (dict "Keys" (list "Ctrl" "K"))}} to open the command menu.
  Save with {{template "kbd-group" (dict "Keys" (list "⌘" "S") "AriaLabel" "Command S")}}.
</p>
<p>
  Press <.kbd>/</.kbd> to focus search, or
  <.kbd_group keys={["Ctrl", "K"]} /> to open the command menu.
  Save with <.kbd_group keys={["⌘", "S"]} aria_label="Command S" />.
</p>
<div class="max-w-md text-sm text-muted-foreground">
  <p>
    Press
    <kbd data-slot="kbd" class="pointer-events-none inline-flex h-5 w-fit min-w-5 shrink-0 select-none items-center justify-center gap-1 rounded-sm border bg-muted px-1 font-sans text-xs font-medium text-muted-foreground [&amp;_svg:not([class*=&#39;size-&#39;])]:size-3">/</kbd>
    to focus search, or
    <kbd data-slot="kbd-group" class="inline-flex w-fit items-center gap-1 align-middle">
      <kbd data-slot="kbd" class="pointer-events-none inline-flex h-5 w-fit min-w-5 shrink-0 select-none items-center justify-center gap-1 rounded-sm border bg-muted px-1 font-sans text-xs font-medium text-muted-foreground [&amp;_svg:not([class*=&#39;size-&#39;])]:size-3">Ctrl</kbd>
      <span aria-hidden="true" class="text-muted-foreground/70">+</span>
      <kbd data-slot="kbd" class="pointer-events-none inline-flex h-5 w-fit min-w-5 shrink-0 select-none items-center justify-center gap-1 rounded-sm border bg-muted px-1 font-sans text-xs font-medium text-muted-foreground [&amp;_svg:not([class*=&#39;size-&#39;])]:size-3">K</kbd>
    </kbd>
    to open the command menu. Save with
    <kbd data-slot="kbd-group" class="inline-flex w-fit items-center gap-1 align-middle" aria-label="Command S">
      <kbd data-slot="kbd" class="pointer-events-none inline-flex h-5 w-fit min-w-5 shrink-0 select-none items-center justify-center gap-1 rounded-sm border bg-muted px-1 font-sans text-xs font-medium text-muted-foreground [&amp;_svg:not([class*=&#39;size-&#39;])]:size-3">⌘</kbd>
      <span aria-hidden="true" class="text-muted-foreground/70">+</span>
      <kbd data-slot="kbd" class="pointer-events-none inline-flex h-5 w-fit min-w-5 shrink-0 select-none items-center justify-center gap-1 rounded-sm border bg-muted px-1 font-sans text-xs font-medium text-muted-foreground [&amp;_svg:not([class*=&#39;size-&#39;])]:size-3">S</kbd>
    </kbd>
    .
  </p>
</div>

API Reference

<Kbd>

PropTypeDefaultDescription
keysChild[]
KbdGroup only. Render one key cap (a nested <kbd>) per entry, joined by separator. Omit and pass children to compose a shortcut by hand. Follows MDN's nested-<kbd> keystroke pattern.MDNRepresenting keystrokes within an input
separatorChild"+"
KbdGroup only. Node rendered between keys (aria-hidden so it is not announced). Set "" for no separator.
ariaLabelstring
Accessible name. Use on symbol-only caps (e.g. "⌘" with ariaLabel="Command") so AT announces a word, not the raw glyph; or on a KbdGroup to name the whole shortcut.MDNaria-label
titlestring
Kbd only. Native browser tooltip shown on hover.MDNtitle attribute
classstring
Extra Tailwind classes appended to the root element.
hx-*any
Any htmx attribute. Forwarded onto the underlying element.htmxAttribute reference