Skip to content
edmi-ui

Kbd

Keyboard key cap; flat by default, elevation raises the key.

StackBlitz ↗StackBlitz ↗StackBlitz ↗
K⌘KEnterPress ⌘J to ask the agent
K⌘KEnter Press ⌘J to ask the agent
K ⌘ K Enter Press ⌘ J to ask the agent
Terminal window
npx shadcn@latest add @edmi-ui/kbd
Terminal window
pnpm dlx shadcn@latest add @edmi-ui/kbd
Terminal window
yarn dlx shadcn@latest add @edmi-ui/kbd
Terminal window
bunx shadcn@latest add @edmi-ui/kbd

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

Terminal window
npx shadcn-vue@latest add @edmi-ui/kbd
Terminal window
pnpm dlx shadcn-vue@latest add @edmi-ui/kbd
Terminal window
yarn dlx shadcn-vue@latest add @edmi-ui/kbd
Terminal window
bunx shadcn-vue@latest add @edmi-ui/kbd

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

Terminal window
npx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/kbd.json
Terminal window
pnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/kbd.json
Terminal window
yarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/kbd.json
Terminal window
bunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/kbd.json

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

import { Kbd, KbdGroup } from "@/components/ui/kbd";
<div className="flex flex-wrap items-center gap-6">
<Kbd>K</Kbd>
<KbdGroup>
<Kbd>⌘</Kbd>
<Kbd>K</Kbd>
</KbdGroup>
<Kbd>Enter</Kbd>
<span className="text-sm">
Press{" "}
<KbdGroup>
<Kbd>⌘</Kbd>
<Kbd>J</Kbd>
</KbdGroup>{" "}
to ask the agent
</span>
</div>
<script setup lang="ts">
import { Kbd, KbdGroup } from "@/components/ui/kbd";
</script>
<template>
<div class="flex flex-wrap items-center gap-6">
<Kbd>K</Kbd>
<KbdGroup>
<Kbd>⌘</Kbd>
<Kbd>K</Kbd>
</KbdGroup>
<Kbd>Enter</Kbd>
<span class="text-sm">
Press
<KbdGroup>
<Kbd>⌘</Kbd>
<Kbd>J</Kbd>
</KbdGroup>
to ask the agent
</span>
</div>
</template>
<script lang="ts">
import * as Kbd from "$lib/components/ui/kbd";
</script>
<div class="flex flex-wrap items-center gap-6">
<Kbd.Root>K</Kbd.Root>
<Kbd.Group>
<Kbd.Root>⌘</Kbd.Root>
<Kbd.Root>K</Kbd.Root>
</Kbd.Group>
<Kbd.Root>Enter</Kbd.Root>
<span class="text-sm">
Press
<Kbd.Group>
<Kbd.Root>⌘</Kbd.Root>
<Kbd.Root>J</Kbd.Root>
</Kbd.Group>
to ask the agent
</span>
</div>
registry/ui/kbd.tsx
"use client";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "cn";
import { type Elevation, useElevation } from "@/registry/edmi/ui/elevation";
const kbdVariants = cva(
"pointer-events-none inline-flex h-[22px] w-fit min-w-[22px] items-center justify-center gap-1 rounded-[5px] border border-input bg-muted px-1.5 font-mono text-[11.5px] font-medium text-muted-foreground select-none in-data-[slot=tooltip-content]:border-[color-mix(in_srgb,var(--background)_20%,var(--primary))] in-data-[slot=tooltip-content]:bg-[color-mix(in_srgb,var(--background)_10%,var(--primary))] in-data-[slot=tooltip-content]:text-background [&_svg:not([class*='size-'])]:size-3",
{
variants: {
// ✦ depth (v4); inside a tooltip the key stays flat
elevation: {
flat: "",
sunken: "",
raised:
"border-transparent [background-image:var(--r1-s-face)] [background-origin:border-box] shadow-btn-raised-neutral in-data-[slot=tooltip-content]:bg-none in-data-[slot=tooltip-content]:shadow-none",
floating:
"border-transparent [background-image:var(--fl-s-face)] [background-origin:border-box] shadow-btn-float-neutral in-data-[slot=tooltip-content]:bg-none in-data-[slot=tooltip-content]:shadow-none",
},
},
defaultVariants: { elevation: "flat" },
},
);
function Kbd({
className,
elevation,
...props
}: React.ComponentProps<"kbd"> &
Omit<VariantProps<typeof kbdVariants>, "elevation"> & {
/** ✦ depth: flat 0, raised +1, floating +2. */
elevation?: Elevation;
}) {
const level = useElevation(elevation, "handle");
return (
<kbd
data-slot="kbd"
className={cn(kbdVariants({ elevation: level }), className)}
{...props}
/>
);
}
function KbdGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<kbd
data-slot="kbd-group"
className={cn("inline-flex items-center gap-1", className)}
{...props}
/>
);
}
export { Kbd, KbdGroup, kbdVariants };
registry/ui/kbd/Kbd.vue
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/registry/edmi/lib/utils'
import { type Elevation, useElevation } from '@/registry/edmi/ui/elevation'
import { kbdVariants } from '.'
const props = withDefaults(defineProps<{
class?: HTMLAttributes['class']
/** ✦ depth: flat 0, raised +1, floating +2 */
elevation?: Elevation
}>(), { elevation: undefined })
const level = useElevation(() => props.elevation, 'handle')
</script>
<template>
<kbd
data-slot="kbd"
:class="cn(kbdVariants({ elevation: level }), props.class)"
>
<slot />
</kbd>
</template>
registry/ui/kbd/KbdGroup.vue
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/registry/edmi/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<kbd
data-slot="kbd-group"
:class="cn('inline-flex items-center gap-1', props.class)"
>
<slot />
</kbd>
</template>
registry/ui/kbd/index.ts
import type { VariantProps } from "class-variance-authority";
import { cva } from "class-variance-authority";
export { default as Kbd } from "./Kbd.vue";
export { default as KbdGroup } from "./KbdGroup.vue";
export const kbdVariants = cva(
"pointer-events-none inline-flex h-[22px] w-fit min-w-[22px] items-center justify-center gap-1 rounded-[5px] border border-input bg-muted px-1.5 font-mono text-[11.5px] font-medium text-muted-foreground select-none in-data-[slot=tooltip-content]:border-[color-mix(in_srgb,var(--background)_20%,var(--primary))] in-data-[slot=tooltip-content]:bg-[color-mix(in_srgb,var(--background)_10%,var(--primary))] in-data-[slot=tooltip-content]:text-background [&_svg:not([class*='size-'])]:size-3",
{
variants: {
// ✦ depth (v4); inside a tooltip the key stays flat
elevation: {
flat: "",
sunken: "",
raised:
"border-transparent [background-image:var(--r1-s-face)] [background-origin:border-box] shadow-btn-raised-neutral in-data-[slot=tooltip-content]:bg-none in-data-[slot=tooltip-content]:shadow-none",
floating:
"border-transparent [background-image:var(--fl-s-face)] [background-origin:border-box] shadow-btn-float-neutral in-data-[slot=tooltip-content]:bg-none in-data-[slot=tooltip-content]:shadow-none",
},
},
defaultVariants: { elevation: "flat" },
},
);
export type KbdVariants = VariantProps<typeof kbdVariants>;
src/lib/registry/ui/kbd/kbd.svelte
<script lang="ts" module>
import { tv, type VariantProps } from "tailwind-variants";
export const kbdVariants = tv({
base: "pointer-events-none inline-flex h-[22px] w-fit min-w-[22px] items-center justify-center gap-1 rounded-[5px] border border-input bg-muted px-1.5 font-mono text-[11.5px] font-medium text-muted-foreground select-none in-data-[slot=tooltip-content]:border-[color-mix(in_srgb,var(--background)_20%,var(--primary))] in-data-[slot=tooltip-content]:bg-[color-mix(in_srgb,var(--background)_10%,var(--primary))] in-data-[slot=tooltip-content]:text-background [&_svg:not([class*='size-'])]:size-3",
variants: {
// ✦ depth (v4); inside a tooltip the key stays flat
elevation: {
flat: "",
sunken: "",
raised:
"border-transparent [background-image:var(--r1-s-face)] [background-origin:border-box] shadow-btn-raised-neutral in-data-[slot=tooltip-content]:bg-none in-data-[slot=tooltip-content]:shadow-none",
floating:
"border-transparent [background-image:var(--fl-s-face)] [background-origin:border-box] shadow-btn-float-neutral in-data-[slot=tooltip-content]:bg-none in-data-[slot=tooltip-content]:shadow-none",
},
},
defaultVariants: { elevation: "flat" },
});
export type KbdVariants = VariantProps<typeof kbdVariants>;
</script>
<script lang="ts">
import { type Elevation, useElevation } from "$lib/registry/ui/elevation/index.js";
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
elevation = "auto",
...restProps
}: WithElementRef<HTMLAttributes<HTMLElement>> & {
/** ✦ depth: flat 0, raised +1, floating +2. */
elevation?: Elevation;
} = $props();
const level = useElevation(() => elevation, "handle");
</script>
<kbd
bind:this={ref}
data-slot="kbd"
class={cn(kbdVariants({ elevation: level.current }), className)}
{...restProps}
>
{@render children?.()}
</kbd>
src/lib/registry/ui/kbd/kbd-group.svelte
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLElement>> = $props();
</script>
<kbd
bind:this={ref}
data-slot="kbd-group"
class={cn("inline-flex items-center gap-1", className)}
{...restProps}
>
{@render children?.()}
</kbd>
src/lib/registry/ui/kbd/index.ts
import Root from "./kbd.svelte";
import Group from "./kbd-group.svelte";
export {
Group,
Group as KbdGroup,
Root,
//
Root as Kbd,
};

See the Elevation guide for the levels, roles and layered mode.

Set it on Kbd (not KbdGroup). raised (+1) bevels the key cap, floating (+2) adds a soft drop. Inside a tooltip the key stays flat.

StackBlitz ↗StackBlitz ↗StackBlitz ↗

Flat (0)

K⌘KEnter

Raised (+1)

K⌘KEnter

Floating (+2)

K⌘KEnter

Flat (0)

K⌘KEnter

Raised (+1)

K⌘KEnter

Floating (+2)

K⌘KEnter

Flat (0)

K ⌘ K Enter

Raised (+1)

K ⌘ K Enter

Floating (+2)

K ⌘ K Enter
Prop Type Default
className string none
elevation "auto" | "sunken" | "flat" | "raised" | "floating" "auto"

Parts: Kbd, KbdGroup.

  • Flat key cap (--muted, 1px border, mono); elevation adds the bevel. Adapts inside data-slot=tooltip-content.