Kbd
Displays keyboard input or keyboard shortcuts.
Basic
A single keyboard key.
KCtrlEscEnter
Keyboard Shortcut
Use KbdGroup to display keyboard shortcuts.
Ctrl+KCmd+Shift+P
Mac Symbols
Keyboard keys with Mac-style symbols.
⌘⇧⌥⌃↩
In Context
Keyboard shortcuts displayed inline with text.
Press Ctrl+C to copy
Press Ctrl+V to paste
Press Esc to close
Custom Styling
Kbd can be customized with additional classes.
EnterDelete
Accessibility
- Uses semantic <kbd> element for keyboard input
- pointer-events-none prevents accidental interaction
- select-none prevents text selection
- Consistent sizing ensures readability
FreeKbd
Keyboard shortcut display.
Install with the Proa CLI
$ proa ui add kbdInstalls the reviewed component source, dependencies, shared support files, and required legal notices.
CLI and registry setup →recipe.rs
// SPDX-FileCopyrightText: 2026 Proa Labs, Inc.
// SPDX-License-Identifier: MIT OR Apache-2.0
//! Kbd recipe — static named slots for keys and key groups.
use crate::tw_join;
use proa_core::shared::attr_value::AttrValue;
use proa_core::{DataLoader, WebContext, WriteBuf, WriteError};
const KBD_ROOT: &str = tw_join!(
"inline-flex",
"h-5",
"w-fit",
"min-w-5",
"items-center",
"justify-center",
"gap-1",
"rounded-xs",
"bg-muted",
"px-1",
"font-sans",
"text-xs",
Kbd source
Browse and copy the reviewed source included with this Free component.
kbd.rsOpen raw ↗
// SPDX-FileCopyrightText: 2026 Proa Labs, Inc.
// SPDX-License-Identifier: (MIT OR Apache-2.0) AND MIT
//! Kbd component implementation
//!
//! Displays keyboard input or keyboard shortcuts.
//! Based on shadcn/ui styling.
use super::recipe::{KbdRecipeProps, KBD_RECIPE};
use proa_core::{WebContext, WebRenderSync, WriteBuf};
use proa_macros::html_sync;
/// Kbd component - displays a keyboard key
///
/// # Example
///
/// ```ignore
/// use proa_ui::Kbd;
/// use proa_macros::html_sync;
///
/// html_sync! {
/// {Kbd {
/// children: Some("Ctrl"),
/// ..Default::default()
/// }}
/// }
/// ```
pub struct Kbd<C = ()> {
/// Additional classes
pub class: Option<&'static str>,
/// Key content (text like "Ctrl", "Esc" or symbols like "⌘")
pub children: Option<C>,
}
impl Kbd<()> {
pub const fn new() -> Self {
Self {
class: None,
children: None,
}
}
}
impl<C> Default for Kbd<C> {
fn default() -> Self {
Self {
class: None,
children: None,
}
}
}
impl<B: WriteBuf, C: WebRenderSync<L, B>, L: ::proa_core::DataLoader> WebRenderSync<L, B>
for Kbd<C>
{
fn render(
self,
cx: &mut WebContext<L, B>,
) -> Result<(), proa_core::shared::write_buf::WriteError> {
let style = KBD_RECIPE.resolve(KbdRecipeProps);
html_sync! {
<kbd data-kbd data-slot="kbd" class={proa_macros::text!("{} {}", style, self.class)}>
{self.children}
</kbd>
}
.render(cx)
}
}
/// KbdGroup component - groups multiple keyboard keys
///
/// # Example
///
/// ```ignore
/// use proa_ui::{Kbd, KbdGroup};
/// use proa_macros::html_sync;
///
/// html_sync! {
/// {KbdGroup {
/// children: Some(html_sync! {
/// {Kbd { children: Some("Ctrl"), ..Default::default() }}
/// "+"
/// {Kbd { children: Some("K"), ..Default::default() }}
/// }),
/// ..Default::default()
/// }}
/// }
/// ```
pub struct KbdGroup<C = ()> {
/// Additional classes
pub class: Option<&'static str>,
/// Group content (usually multiple Kbd components)
pub children: Option<C>,
}
impl<C> Default for KbdGroup<C> {
fn default() -> Self {
Self {
class: None,
children: None,
}
}
}
impl<B: WriteBuf, C: WebRenderSync<L, B>, L: ::proa_core::DataLoader> WebRenderSync<L, B>
for KbdGroup<C>
{
fn render(
self,
cx: &mut WebContext<L, B>,
) -> Result<(), proa_core::shared::write_buf::WriteError> {
html_sync! {
<kbd
data-slot="kbd-group"
class={proa_macros::text!("{} {}", KBD_RECIPE.group, self.class)}
>
{self.children}
</kbd>
}
.render(cx)
}
}
#[cfg(test)]
mod tests {
use super::*;
use proa_core::ctx::Ctx;
use proa_core::text::text;
#[test]
fn kbd_renders_with_data_slot() {
let mut out = Vec::new();
let mut cx = Ctx::with_buffer(&mut out);
Kbd {
children: Some(text("Ctrl")),
..Default::default()
}
.render(&mut cx)
.unwrap();
let html = String::from_utf8(out).unwrap();
assert!(html.contains("<kbd"));
assert!(html.contains("data-slot=\"kbd\""));
assert!(html.contains("Ctrl"));
}
#[test]
fn kbd_with_custom_class() {
let mut out = Vec::new();
let mut cx = Ctx::with_buffer(&mut out);
Kbd {
class: Some("custom-class"),
children: Some(text("K")),
}
.render(&mut cx)
.unwrap();
let html = String::from_utf8(out).unwrap();
assert!(html.contains("custom-class"));
}
#[test]
fn kbd_group_renders_with_data_slot() {
let mut out = Vec::new();
let mut cx = Ctx::with_buffer(&mut out);
KbdGroup {
children: Some(text("Ctrl + K")),
..Default::default()
}
.render(&mut cx)
.unwrap();
let html = String::from_utf8(out).unwrap();
assert!(html.contains("<kbd"));
assert!(html.contains("data-slot=\"kbd-group\""));
}
#[test]
fn kbd_group_accepts_custom_class() {
let mut out = Vec::new();
let mut cx = Ctx::with_buffer(&mut out);
KbdGroup {
class: Some("group-extra"),
children: Some(text("Ctrl + K")),
}
.render(&mut cx)
.unwrap();
let html = String::from_utf8(out).unwrap();
assert!(html.contains("group-extra"));
}
}
md.rsOpen raw ↗
// SPDX-FileCopyrightText: 2026 Proa Labs, Inc.
// SPDX-License-Identifier: MIT OR Apache-2.0
use super::{Kbd, KbdGroup};
use proa_core::WebContext;
use proa_core::{MdCodeContent, MdRenderSync, WriteBuf, WriteError};
/// Kbd renders as inline code.
impl<Loader: ::proa_core::DataLoader, B: WriteBuf, C: MdCodeContent> MdRenderSync<Loader, B>
for Kbd<C>
{
fn render_md(self, cx: &mut WebContext<Loader, B>) -> Result<(), WriteError> {
cx.write_md_inline_code(
self.children
.as_ref()
.map_or("", MdCodeContent::md_code_content),
)
}
}
/// KbdGroup just renders children (keys) separated by spaces.
impl<Loader: ::proa_core::DataLoader, B: WriteBuf, C: MdRenderSync<Loader, B>>
MdRenderSync<Loader, B> for KbdGroup<C>
{
fn render_md(self, cx: &mut WebContext<Loader, B>) -> Result<(), WriteError> {
self.children.render_md(cx)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn kbd_renders_code() {
let mut buf = Vec::new();
let mut cx = WebContext::with_buffer(&mut buf);
Kbd {
children: Some("Ctrl+C"),
..Default::default()
}
.render_md(&mut cx)
.unwrap();
assert_eq!(String::from_utf8(buf).unwrap(), "`Ctrl+C`");
}
#[test]
fn kbd_accepts_text_and_uses_a_payload_safe_delimiter() {
let mut buf = Vec::new();
let mut cx = WebContext::with_buffer(&mut buf);
Kbd {
children: Some(proa_core::text("press ` then ``")),
..Default::default()
}
.render_md(&mut cx)
.unwrap();
assert_eq!(String::from_utf8(buf).unwrap(), "``` press ` then `` ```");
}
}
mod.rsOpen raw ↗
// SPDX-FileCopyrightText: 2026 Proa Labs, Inc.
// SPDX-License-Identifier: (MIT OR Apache-2.0) AND MIT
//! Kbd component module
//!
//! Displays keyboard input or keyboard shortcuts.
//!
//! Based on shadcn/ui styling.
#[allow(clippy::module_inception)]
mod kbd;
mod md;
mod recipe;
pub use kbd::{Kbd, KbdGroup};
pub use recipe::{
kbd_base_classes, kbd_group_classes, kbd_recipe, KbdRecipe, KbdRecipeProps, KbdStyle,
KBD_RECIPE,
};
recipe.rsOpen raw ↗
// SPDX-FileCopyrightText: 2026 Proa Labs, Inc.
// SPDX-License-Identifier: MIT OR Apache-2.0
//! Kbd recipe — static named slots for keys and key groups.
use crate::tw_join;
use proa_core::shared::attr_value::AttrValue;
use proa_core::{DataLoader, WebContext, WriteBuf, WriteError};
const KBD_ROOT: &str = tw_join!(
"inline-flex",
"h-5",
"w-fit",
"min-w-5",
"items-center",
"justify-center",
"gap-1",
"rounded-xs",
"bg-muted",
"px-1",
"font-sans",
"text-xs",
"font-medium",
"text-muted-foreground",
"pointer-events-none",
"select-none"
);
const KBD_GROUP: &str = tw_join!("inline-flex", "items-center", "gap-1");
/// Optional inputs accepted by [`KbdRecipe::resolve`].
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct KbdRecipeProps;
/// Static named classes for Kbd and KbdGroup.
#[derive(Debug, Clone, Copy)]
pub struct KbdRecipe {
pub root: &'static str,
pub group: &'static str,
}
impl KbdRecipe {
pub const fn resolve(&'static self, _props: KbdRecipeProps) -> KbdStyle {
KbdStyle { recipe: self }
}
}
/// Fully resolved, streamable Kbd root style.
#[must_use]
#[derive(Debug, Clone, Copy)]
pub struct KbdStyle {
recipe: &'static KbdRecipe,
}
impl AttrValue for KbdStyle {
#[inline(always)]
fn should_render(&self) -> bool {
true
}
#[inline(always)]
fn render_attr_value<B: WriteBuf, L: DataLoader>(
&self,
cx: &mut WebContext<L, B>,
) -> Result<(), WriteError> {
cx.out.extend_static(self.recipe.root.as_bytes())
}
}
pub const KBD_RECIPE: KbdRecipe = KbdRecipe {
root: KBD_ROOT,
group: KBD_GROUP,
};
pub const fn kbd_recipe() -> &'static KbdRecipe {
&KBD_RECIPE
}
impl crate::variant_spec::ComponentSpec for KbdRecipe {
const NAME: &'static str = "Kbd";
fn base_classes(&self) -> &'static str {
self.root
}
fn variant_metadata(&self) -> Vec<crate::variant_spec::VariantMetadata> {
Vec::new()
}
}
/// Compatibility accessors for the former standalone helpers.
pub const fn kbd_base_classes() -> &'static str {
KBD_RECIPE.root
}
pub const fn kbd_group_classes() -> &'static str {
KBD_RECIPE.group
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn recipe_exposes_key_and_group_slots() {
let recipe = kbd_recipe();
assert!(recipe.root.contains("inline-flex"));
assert!(recipe.root.contains("bg-muted"));
assert!(recipe.root.contains("text-xs"));
assert!(recipe.group.contains("inline-flex"));
assert!(recipe.group.contains("gap-1"));
}
#[test]
fn recipe_is_const_and_copy() {
const STYLE: KbdStyle = KBD_RECIPE.resolve(KbdRecipeProps);
let copied = STYLE;
assert!(copied.recipe.root.contains("pointer-events-none"));
}
#[test]
fn recipe_metadata_describes_the_key_surface() {
let metadata = crate::variant_spec::component_metadata(kbd_recipe());
assert_eq!(metadata.name, "Kbd");
assert_eq!(metadata.base_classes, KBD_RECIPE.root);
assert!(metadata.variants.is_empty());
assert!(metadata.anatomy.is_none());
metadata.validate().unwrap();
}
}