Label
Renders an accessible label associated with controls.
Basic
A simple label with text.
With Checkbox
Labels work seamlessly with form controls using the for attribute.
With Input
Labels are commonly used with text inputs.
Disabled State
Labels automatically style when their associated input is disabled (using the peer-disabled utility).
Custom Styling
Labels can be customized with additional classes.
Card Style with Checkbox
Labels can wrap checkboxes to create interactive card-style selections using has-[:checked] selectors.
Form Example
A complete form using labels.
FreeLabel
Accessible form labels.
Install with the Proa CLI
$ proa ui add labelInstalls the reviewed component source, dependencies, shared support files, and required legal notices.
CLI and registry setup →mod.rs
// SPDX-FileCopyrightText: 2026 Proa Labs, Inc.
// SPDX-License-Identifier: (MIT OR Apache-2.0) AND MIT
//! Label component - accessible form label
//!
//! A simple styled label component that renders as a native HTML `<label>` element.
//! Based on shadcn/ui's Label component.
mod md;
use crate::tw_join;
use proa_core::{ClassList, WebContext, WebRenderSync, WriteBuf};
use proa_macros::html_sync;
/// Base styles for the label - matches shadcn/ui exactly
/// "flex items-center gap-2 text-sm leading-none font-medium select-none
/// group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50
/// peer-disabled:cursor-not-allowed peer-disabled:opacity-50"
const LABEL_BASE: &str = tw_join!(
"flex",
"items-center",
"gap-2",
"text-sm",
"leading-none",
"font-medium",
Label source
Browse and copy the reviewed source included with this Free component.
md.rsOpen raw ↗
// SPDX-FileCopyrightText: 2026 Proa Labs, Inc.
// SPDX-License-Identifier: MIT OR Apache-2.0
use super::Label;
use proa_core::WebContext;
use proa_core::{MdRenderSync, WriteBuf, WriteError};
/// Label renders as bold text.
impl<Loader: ::proa_core::DataLoader, B: WriteBuf> MdRenderSync<Loader, B> for Label {
fn render_md(self, cx: &mut WebContext<Loader, B>) -> Result<(), WriteError> {
cx.md_bold(self.text)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn label_renders_bold() {
let mut buf = Vec::new();
let mut cx = WebContext::with_buffer(&mut buf);
Label::with_text("Email").render_md(&mut cx).unwrap();
assert_eq!(String::from_utf8(buf).unwrap(), "**Email**");
}
}
mod.rsOpen raw ↗
// SPDX-FileCopyrightText: 2026 Proa Labs, Inc.
// SPDX-License-Identifier: (MIT OR Apache-2.0) AND MIT
//! Label component - accessible form label
//!
//! A simple styled label component that renders as a native HTML `<label>` element.
//! Based on shadcn/ui's Label component.
mod md;
use crate::tw_join;
use proa_core::{ClassList, WebContext, WebRenderSync, WriteBuf};
use proa_macros::html_sync;
/// Base styles for the label - matches shadcn/ui exactly
/// "flex items-center gap-2 text-sm leading-none font-medium select-none
/// group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50
/// peer-disabled:cursor-not-allowed peer-disabled:opacity-50"
const LABEL_BASE: &str = tw_join!(
"flex",
"items-center",
"gap-2",
"text-sm",
"leading-none",
"font-medium",
"text-foreground",
"select-none",
"group-data-[disabled=true]:pointer-events-none",
"group-data-[disabled=true]:opacity-50",
"peer-disabled:cursor-not-allowed",
"peer-disabled:opacity-50"
);
/// Type-safe Label component.
///
/// Renders an accessible label associated with form controls.
/// Uses the native HTML `<label>` element for proper accessibility.
///
/// # Example
///
/// ```ignore
/// use proa_ui::Label;
/// use proa_macros::html_sync;
///
/// // Basic usage with for attribute
/// html_sync! {
/// <div class="flex items-center gap-2">
/// <input type="checkbox" id="terms" />
/// {Label {
/// for_id: Some("terms"),
/// text: "Accept terms and conditions",
/// ..Label::new()
/// }}
/// </div>
/// }.render(cx);
///
/// // With custom class
/// html_sync! {
/// {Label {
/// for_id: Some("email"),
/// text: "Email address",
/// class: Some("text-red-500"),
/// ..Label::new()
/// }}
/// }.render(cx);
/// ```
pub struct Label {
/// The id of the form element this label is for
pub for_id: Option<&'static str>,
/// The text content of the label
pub text: &'static str,
/// Additional CSS classes
pub class: Option<&'static str>,
/// Custom id for the label element
pub id: Option<&'static str>,
}
impl Label {
/// Create a new label with default settings
pub const fn new() -> Self {
Self {
for_id: None,
text: "",
class: None,
id: None,
}
}
/// Create a new label with text
pub const fn with_text(text: &'static str) -> Self {
Self {
for_id: None,
text,
class: None,
id: None,
}
}
}
impl Default for Label {
fn default() -> Self {
Self::new()
}
}
/// Get the label classes
#[inline(always)]
pub fn label_classes() -> ClassList<2> {
ClassList::new().add(LABEL_BASE)
}
impl<B: WriteBuf, L: ::proa_core::DataLoader> WebRenderSync<L, B> for Label {
#[inline(always)]
fn render(
self,
cx: &mut WebContext<L, B>,
) -> Result<(), proa_core::shared::write_buf::WriteError> {
let classes = label_classes().add_opt(self.class);
html_sync! {
<label data-label class={classes} for={self.for_id} id={self.id} data-slot="label">
{self.text}
</label>
}
.render(cx)
}
}
pub use Label as LabelComponent;
#[cfg(test)]
mod tests {
use super::*;
use proa_core::ctx::Ctx;
#[test]
fn label_renders_with_defaults() {
let mut out = Vec::new();
let mut cx = Ctx::with_buffer(&mut out);
Label {
text: "Email",
..Label::new()
}
.render(&mut cx)
.unwrap();
let html = String::from_utf8(out).unwrap();
assert!(html.contains("<label"));
assert!(html.contains("Email"));
assert!(html.contains("data-slot=\"label\""));
assert!(html.contains("text-sm"));
assert!(html.contains("font-medium"));
}
#[test]
fn label_renders_with_for_attribute() {
let mut out = Vec::new();
let mut cx = Ctx::with_buffer(&mut out);
Label {
for_id: Some("my-input"),
text: "Username",
..Label::new()
}
.render(&mut cx)
.unwrap();
let html = String::from_utf8(out).unwrap();
assert!(html.contains("for=\"my-input\""));
assert!(html.contains("Username"));
}
#[test]
fn label_renders_with_custom_class() {
let mut out = Vec::new();
let mut cx = Ctx::with_buffer(&mut out);
Label {
text: "Custom",
class: Some("text-red-500"),
..Label::new()
}
.render(&mut cx)
.unwrap();
let html = String::from_utf8(out).unwrap();
assert!(html.contains("text-red-500"));
}
#[test]
fn label_renders_with_id() {
let mut out = Vec::new();
let mut cx = Ctx::with_buffer(&mut out);
Label {
id: Some("my-label"),
text: "Test",
..Label::new()
}
.render(&mut cx)
.unwrap();
let html = String::from_utf8(out).unwrap();
assert!(html.contains("id=\"my-label\""));
}
#[test]
fn label_with_text_constructor() {
let mut out = Vec::new();
let mut cx = Ctx::with_buffer(&mut out);
Label::with_text("Quick label").render(&mut cx).unwrap();
let html = String::from_utf8(out).unwrap();
assert!(html.contains("Quick label"));
}
#[test]
fn label_size_is_reasonable() {
assert!(std::mem::size_of::<Label>() < 128);
const _LABEL: Label = Label::new();
}
}