Glass kbd
Five frosted keycaps: a plain frost, an info tint, a smoke tint, a crystal with inset highlights and a raised depth cap. Each renders a real kbd element and falls back to an opaque token surface where backdrop-filter is unsupported.
Installation
The styled layer is free and installs like any other ai2 component.
Run the following command
npx shadcn@latest add @ai2/kbd-glassDependencies, the @ai2/tokens theme and the component file are installed together.
Copy the source
components/ui/kbd-glass.tsx"use client"
import type * as React from "react"
import { cn } from "@/lib/utils"
import { glassDepth } from "@/components/ui/glass"
/* Glass kbd family: 5 frosted/semi-transparent keycaps. Color comes ONLY from tokens
(via an alpha utility or color-mix). Static - needs no motion. The surface derives
from glassDepth in @ai2/glass (AGENTS.md 4.5). Together with the badges, keycaps
are the smallest glass surfaces in the system (h-5..h-8). There is next to no
content behind a keycap to diffuse, so the scale sits at the very bottom end: sm
carries the body, md appears only on the two variations where diffusing makes
sense (Smoke, Depth). lg/xl are NEVER used. */
export type StyledSize = "sm" | "md" | "lg" | "xl"
const box: Record<StyledSize, string> = {
sm: "h-5 px-1.5 text-xs",
md: "h-6 px-2 text-xs",
lg: "h-7 px-2.5 text-sm",
xl: "h-8 px-3 text-sm",
}
const base =
"inline-flex w-fit shrink-0 select-none items-center justify-center gap-1 whitespace-nowrap rounded-md border font-mono font-medium leading-none [&_svg]:size-3 [&_svg]:shrink-0 [&_i]:text-xs [&_i]:leading-none"
type Props = React.ComponentProps<"kbd"> & { size?: StyledSize }
/* Frost: a plain frosted-glass cap. Depth = sm (4px). The lightest step is the default for a keycap: in a 24px box the "glass" feel comes not from the blur but from the library signature's inner top highlight - which is exactly what a keycap wants (light breaking at the top edge). */
export function FrostKbd({ className, size = "md", children, ...props }: Props) {
return (
<kbd
data-slot="styled-kbd"
className={cn(
base,
box[size],
glassDepth.sm,
"border-border/60 text-foreground",
className
)}
{...props}
>
{children ?? "Ctrl"}
</kbd>
)
}
/* Tint: an info-toned semi-transparent glass cap. Depth = sm (4px): what carries the colour is the surface, not the blur; more blur piles the hue onto itself and muddies it. info was chosen as the tone: primary sits on the neutral ramp, so a "tint" would stay colourless and look identical to Smoke; info carries a real colour. */
export function TintKbd({ className, size = "md", children, ...props }: Props) {
return (
<kbd
data-slot="styled-kbd"
data-tone="info"
className={cn(
base,
box[size],
glassDepth.sm,
"border-info/30 bg-info/15 text-info supports-[backdrop-filter]:bg-info/15",
className
)}
{...props}
>
{children ?? "Ctrl"}
</kbd>
)
}
/* Smoke: a neutral cap darkened with foreground. Depth = md (8px). Smoke means diffusion, so it sits one step above Frost/Crystal/Tint; but on a keycap this is also the ceiling. What separates it from Tint is the absence of hue. */
export function SmokeKbd({ className, size = "md", children, ...props }: Props) {
return (
<kbd
data-slot="styled-kbd"
className={cn(
base,
box[size],
glassDepth.md,
"border-foreground/10 bg-foreground/15 text-foreground supports-[backdrop-filter]:bg-foreground/15",
className
)}
{...props}
>
{children ?? "Ctrl"}
</kbd>
)
}
/* Crystal: clear glass. Depth = sm (4px), the least diffusing in the family - the
condition for being "clear" is being able to see what is behind. It used to draw
the top/bottom inset lines by hand; the lib signature already does exactly that,
and writing a second inset on top of it would have deleted the signature through
twMerge. */
export function CrystalKbd({ className, size = "md", children, ...props }: Props) {
return (
<kbd
data-slot="styled-kbd"
className={cn(
base,
box[size],
glassDepth.sm,
"border-[color-mix(in_oklab,var(--color-foreground)_20%,transparent)] text-foreground",
className
)}
{...props}
>
{children ?? "Ctrl"}
</kbd>
)
}
/* Depth: a glass body + a cast shadow, so the lid looks lifted off the surface.
Depth = md (8px), the deepest step in this file: this is the only surface that
rises, and a gap that genuinely needs diffusing forms underneath it. It does not
go up to lg - there is nothing for a 16px blur to show on a lid this size.
The shadow is now a drop-shadow (the filter group). The constraint the old note
identified still holds - box-shadow is a single twMerge group and shadow-lg would
silently delete the lib signature - but there is a solution now: drop-shadow is a
completely different CSS property, so Depth gets both the rising shadow and the
ai2 signature's top highlight.
The neutral black utility is deliberate: a foreground-based color-mix shadow
would turn into a WHITE halo in the dark theme (reading as a glow, not a
shadow). */
export function DepthKbd({ className, size = "md", children, ...props }: Props) {
return (
<kbd
data-slot="styled-kbd"
className={cn(
base,
box[size],
glassDepth.md,
"border-border/60 text-foreground drop-shadow-md",
className
)}
{...props}
>
{children ?? "Ctrl"}
</kbd>
)
}Manual installs skip the @ai2/tokens theme, so add the token CSS from the theming guide or the tone colors will be missing.
Variations
5 takes on the same idea. Each is its own export, and every one accepts a size prop (sm, md, lg, xl) aligned to the base Button scale.
Frost
A plain frosted keycap over the surface token.
import { FrostKbd } from "@/components/ui/kbd-glass"
<FrostKbd />Tint
An info-tinted translucent keycap.
import { TintKbd } from "@/components/ui/kbd-glass"
<TintKbd />Smoke
A darker foreground-tinted glass keycap.
import { SmokeKbd } from "@/components/ui/kbd-glass"
<SmokeKbd />Crystal
Glass with inset top and bottom highlight rules.
import { CrystalKbd } from "@/components/ui/kbd-glass"
<CrystalKbd />Depth
Glass with a drop shadow so the cap sits above the surface.
import { DepthKbd } from "@/components/ui/kbd-glass"
<DepthKbd />ai2 Glass kbd: 5 styled variations on the token system
The ai2 Glass kbd are a set of 5 decorative button variations from the styled layer of the @ai2 design system, built around frosted, translucent keycaps. They are free and MIT licensed, and every color comes from a semantic token, so they theme with the rest of ai2 in light and dark.
Motion runs on framer-motion: these are static, so no motion library work is required. The styled layer is opt-in, so the dependency only lands if you use it; the base components stay lean. Under reduced motion, there is no continuous motion to reduce.
What is in the ai2 Glass kbd?
5 exports in one file: Frost, Tint, Smoke, Crystal and Depth. Each renders a native button and takes a size prop (sm, md, lg, xl) aligned to the base Button. They are separate from the base Button on purpose: the base keeps its clean variant, tone and size axes, while the styled layer carries the effects.
You own the file. Copy the one category file and you have all 5 variations, with no runtime dependency on ai2 itself.
Why use it
- On-system by construction: Every color resolves to an ai2 semantic token, so the buttons follow your theme in light and dark with no extra work.
- Effect without the sprawl: The decorations live in a dedicated styled file, so the base Button keeps its clean, predictable API.
- Accessible and honest: Each renders a real button element, keeps a visible focus ring, and respects prefers-reduced-motion.
Features
- Token-driven color: No hardcoded hex or oklch; the look recolors with your theme tokens.
- framer-motion: these are static, so no motion library work is required.
- Reduced-motion aware: Under prefers-reduced-motion, there is no continuous motion to reduce.
- Size aligned to the base: Every variation takes sm, md, lg and xl matching the base Button height scale, so styled and base buttons line up in a row.
Production tips
- Use it for emphasis, not everywhere: Styled buttons draw the eye. Reserve them for the one action you want people to take on a screen, and use the base Button for the rest.
- Keep labels as verbs: The decoration adds weight, so a clear action label keeps the button scannable.
- Pick one variation per surface: The variations share a family; using two different ones in the same view competes for attention.
Works with the rest of ai2
The Glass kbd sit alongside the base Button and the rest of the @ai2 registry. They share the same token file, so a styled action next to a base button or a badge stays visually consistent in both modes.