Badge
Displays a badge or a component that looks like a badge.
Variants
Badges come in multiple style variants.
Default
Secondary
Destructive
Outline
Success
Warning
Status Indicators
Badges are commonly used to indicate status.
ActivePendingExpiredDraft
With Icons
Badges can include icons for additional context.
PremiumVerifiedBlocked
Notification Counts
Badges can display notification counts.
3
12
99+
In Context
Badges used alongside other content.
JD
John Doe
AS
Alice Smith
BJ
Bob Johnson
Custom Styling
Badges can be customized with additional classes.
RoundedBluePurplePinkDashed
FreeBadge
Small status and category labels.
Install with the Proa CLI
$ proa ui add badgeInstalls 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
//! Badge recipe - type-safe styling system for Badge component
//!
//! Provides variant styles and base styling for the Badge component.
use crate::tw_join;
use proa_core::shared::attr_value::AttrValue;
use proa_core::{DataLoader, WebContext, WriteBuf, WriteError};
/// Base styles for the badge
mod base {
use super::*;
pub const BADGE_BASE: &str = tw_join!(
// Layout
"inline-flex",
"items-center",
// Sizing
"rounded-md",
"px-2.5",
Badge source
Browse and copy the reviewed source included with this Free component.
badge.rsOpen raw ↗
// SPDX-FileCopyrightText: 2026 Proa Labs, Inc.
// SPDX-License-Identifier: MIT OR Apache-2.0
//! Badge component implementation
//!
//! Renders a small status indicator or label with variant styling.
use super::recipe::{BadgeRecipeProps, BadgeVariant, BADGE_RECIPE};
use proa_core::{WebContext, WebRenderSync, WriteBuf};
use proa_macros::html_sync;
/// Badge component - displays a small status indicator or label
pub struct Badge<C = ()> {
/// Style variant
pub variant: BadgeVariant,
/// Additional CSS classes
pub class: Option<&'static str>,
/// The badge content (text or other elements)
pub children: Option<C>,
}
impl Badge<()> {
pub const fn new() -> Self {
Self {
variant: BadgeVariant::Default,
class: None,
children: None,
}
}
}
impl<C> Default for Badge<C> {
fn default() -> Self {
Self {
variant: BadgeVariant::Default,
class: None,
children: None,
}
}
}
impl<B: WriteBuf, C: WebRenderSync<L, B>, L: ::proa_core::DataLoader> WebRenderSync<L, B>
for Badge<C>
{
fn render(
self,
cx: &mut WebContext<L, B>,
) -> Result<(), proa_core::shared::write_buf::WriteError> {
let style = BADGE_RECIPE.resolve(BadgeRecipeProps {
variant: Some(self.variant),
});
html_sync! {
<span
data-badge
data-slot="badge"
data-variant={style.variant().as_str()}
class={proa_macros::text!("{} {}", style, self.class)}
>
{self.children}
</span>
}
.render(cx)
}
}
#[cfg(test)]
mod tests {
use super::*;
use proa_core::ctx::Ctx;
use proa_core::text::text;
#[test]
fn badge_renders_with_data_attributes() {
let mut out = Vec::new();
let mut cx = Ctx::with_buffer(&mut out);
Badge {
variant: BadgeVariant::Default,
class: None,
children: Some(text("New")),
}
.render(&mut cx)
.unwrap();
let html = String::from_utf8(out).unwrap();
assert!(html.contains("data-badge"));
assert!(html.contains("data-slot=\"badge\""));
assert!(html.contains("data-variant=\"default\""));
assert!(html.contains("New"));
}
#[test]
fn badge_renders_different_variants() {
for variant in [
BadgeVariant::Default,
BadgeVariant::Secondary,
BadgeVariant::Destructive,
BadgeVariant::Outline,
BadgeVariant::Success,
BadgeVariant::Warning,
] {
let mut out = Vec::new();
let mut cx = Ctx::with_buffer(&mut out);
Badge {
variant,
class: None,
children: Some(text("Test")),
}
.render(&mut cx)
.unwrap();
let html = String::from_utf8(out).unwrap();
assert!(html.contains(&format!("data-variant=\"{}\"", variant.as_str())));
}
}
#[test]
fn badge_accepts_custom_class() {
let mut out = Vec::new();
let mut cx = Ctx::with_buffer(&mut out);
Badge {
variant: BadgeVariant::Default,
class: Some("custom-class"),
children: Some(text("Custom")),
}
.render(&mut cx)
.unwrap();
let html = String::from_utf8(out).unwrap();
assert!(html.contains("custom-class"));
}
#[test]
fn badge_has_base_classes() {
let mut out = Vec::new();
let mut cx = Ctx::with_buffer(&mut out);
Badge {
variant: BadgeVariant::Default,
class: None,
children: Some(text("Badge")),
}
.render(&mut cx)
.unwrap();
let html = String::from_utf8(out).unwrap();
assert!(html.contains("inline-flex"));
assert!(html.contains("rounded-md"));
assert!(html.contains("text-xs"));
}
}
md.rsOpen raw ↗
// SPDX-FileCopyrightText: 2026 Proa Labs, Inc.
// SPDX-License-Identifier: MIT OR Apache-2.0
use super::Badge;
use proa_core::WebContext;
use proa_core::{MdCodeContent, MdRenderSync, WriteBuf, WriteError};
impl<Loader: ::proa_core::DataLoader, B: WriteBuf, C: MdCodeContent> MdRenderSync<Loader, B>
for Badge<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),
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn badge_renders_inline_code() {
let mut buf = Vec::new();
let mut cx = WebContext::with_buffer(&mut buf);
Badge {
children: Some("Active"),
..Default::default()
}
.render_md(&mut cx)
.unwrap();
assert_eq!(String::from_utf8(buf).unwrap(), "`Active`");
}
#[test]
fn badge_uses_an_adaptive_delimiter_for_runtime_backticks() {
let mut buf = Vec::new();
let mut cx = WebContext::with_buffer(&mut buf);
Badge {
children: Some(String::from("`active``now`")),
..Default::default()
}
.render_md(&mut cx)
.unwrap();
assert_eq!(String::from_utf8(buf).unwrap(), "``` `active``now` ```");
}
}
mod.rsOpen raw ↗
// SPDX-FileCopyrightText: 2026 Proa Labs, Inc.
// SPDX-License-Identifier: MIT OR Apache-2.0
#![allow(clippy::module_inception)]
//! Badge component - displays a small status indicator or label
//!
//! The Badge component is used for highlighting status, categories, or counts.
//! It supports multiple variants for different visual styles.
mod badge;
mod md;
mod recipe;
// Re-export badge types
pub use badge::Badge;
// Re-export recipe types
pub use recipe::{
badge_classes, badge_recipe, BadgeRecipe, BadgeRecipeProps, BadgeStyle, BadgeVariant,
BADGE_RECIPE,
};
recipe.rsOpen raw ↗
// SPDX-FileCopyrightText: 2026 Proa Labs, Inc.
// SPDX-License-Identifier: MIT OR Apache-2.0
//! Badge recipe - type-safe styling system for Badge component
//!
//! Provides variant styles and base styling for the Badge component.
use crate::tw_join;
use proa_core::shared::attr_value::AttrValue;
use proa_core::{DataLoader, WebContext, WriteBuf, WriteError};
/// Base styles for the badge
mod base {
use super::*;
pub const BADGE_BASE: &str = tw_join!(
// Layout
"inline-flex",
"items-center",
// Sizing
"rounded-md",
"px-2.5",
"py-0.5",
// Typography
"text-xs",
"font-medium",
// Transitions
"transition-colors",
// Focus
"focus:outline-hidden",
"focus:ring-2",
"focus:ring-ring",
"focus:ring-offset-2"
);
}
/// Variant styles for the badge
mod variants {
use super::*;
pub const BADGE_DEFAULT: &str = tw_join!(
"bg-primary",
"text-primary-foreground",
"shadow",
"hover:bg-primary/80"
);
pub const BADGE_SECONDARY: &str = tw_join!(
"bg-secondary",
"text-secondary-foreground",
"hover:bg-secondary/80"
);
pub const BADGE_DESTRUCTIVE: &str = tw_join!(
"bg-destructive",
"text-destructive-foreground",
"shadow",
"hover:bg-destructive/80"
);
pub const BADGE_OUTLINE: &str =
tw_join!("border", "border-input", "text-foreground", "bg-background");
pub const BADGE_SUCCESS: &str = tw_join!(
"bg-green-600",
"text-white",
"shadow",
"hover:bg-green-600/80"
);
pub const BADGE_WARNING: &str = tw_join!(
"bg-yellow-500",
"text-white",
"shadow",
"hover:bg-yellow-500/80"
);
}
/// Variant options for the badge
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, proa_macros::ProaVariant)]
#[variant(name = "variant")]
pub enum BadgeVariant {
/// Primary badge style (default)
#[default]
Default,
/// Secondary/neutral style
Secondary,
/// Destructive/error style
Destructive,
/// Outline/bordered style
Outline,
/// Success/positive style
Success,
/// Warning style
Warning,
}
impl BadgeVariant {
/// Get the Tailwind classes for this variant
pub const fn classes(self) -> &'static str {
match self {
BadgeVariant::Default => variants::BADGE_DEFAULT,
BadgeVariant::Secondary => variants::BADGE_SECONDARY,
BadgeVariant::Destructive => variants::BADGE_DESTRUCTIVE,
BadgeVariant::Outline => variants::BADGE_OUTLINE,
BadgeVariant::Success => variants::BADGE_SUCCESS,
BadgeVariant::Warning => variants::BADGE_WARNING,
}
}
/// Get the data attribute value
pub const fn as_str(self) -> &'static str {
match self {
BadgeVariant::Default => "default",
BadgeVariant::Secondary => "secondary",
BadgeVariant::Destructive => "destructive",
BadgeVariant::Outline => "outline",
BadgeVariant::Success => "success",
BadgeVariant::Warning => "warning",
}
}
}
/// Optional inputs accepted by [`BadgeRecipe::resolve`].
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct BadgeRecipeProps {
pub variant: Option<BadgeVariant>,
}
/// Fully typed badge recipe with compile-time defaults.
#[derive(Debug, Clone, Copy)]
pub struct BadgeRecipe {
base: &'static str,
variant_default: BadgeVariant,
}
/// Global badge recipe instance
pub const BADGE_RECIPE: BadgeRecipe = BadgeRecipe {
base: base::BADGE_BASE,
variant_default: BadgeVariant::Default,
};
impl BadgeRecipe {
pub const fn resolve(&'static self, props: BadgeRecipeProps) -> BadgeStyle {
let variant = match props.variant {
Some(variant) => variant,
None => self.variant_default,
};
BadgeStyle {
recipe: self,
variant,
}
}
}
/// Fully resolved, zero-allocation badge style.
#[must_use]
#[derive(Debug, Clone, Copy)]
pub struct BadgeStyle {
recipe: &'static BadgeRecipe,
variant: BadgeVariant,
}
impl BadgeStyle {
pub const fn variant(self) -> BadgeVariant {
self.variant
}
}
impl AttrValue for BadgeStyle {
#[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.base.as_bytes())?;
cx.out.extend_static(b" ")?;
cx.out.extend_static(self.variant.classes().as_bytes())
}
}
/// Get the global badge recipe
pub const fn badge_recipe() -> &'static BadgeRecipe {
&BADGE_RECIPE
}
/// Compatibility resolver for callers of the former helper.
pub const fn badge_classes(variant: BadgeVariant) -> BadgeStyle {
BADGE_RECIPE.resolve(BadgeRecipeProps {
variant: Some(variant),
})
}
impl crate::variant_spec::ComponentSpec for BadgeRecipe {
const NAME: &'static str = "Badge";
fn base_classes(&self) -> &'static str {
self.base
}
fn variant_metadata(&self) -> Vec<crate::variant_spec::VariantMetadata> {
vec![crate::variant_spec::variant_metadata::<BadgeVariant>()]
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn badge_recipe_generates_default() {
let style = badge_recipe().resolve(BadgeRecipeProps::default());
assert_eq!(style.variant(), BadgeVariant::Default);
assert!(badge_recipe().base.contains("inline-flex"));
assert!(style.variant().classes().contains("bg-primary"));
assert!(badge_recipe().base.contains("rounded-md"));
}
#[test]
fn badge_recipe_supports_variants() {
for (variant, expected) in [
(BadgeVariant::Secondary, "bg-secondary"),
(BadgeVariant::Destructive, "bg-destructive"),
(BadgeVariant::Outline, "border"),
] {
let style = badge_recipe().resolve(BadgeRecipeProps {
variant: Some(variant),
});
assert_eq!(style.variant(), variant);
assert!(style.variant().classes().contains(expected));
}
}
#[test]
fn badge_variants_have_correct_classes() {
assert!(BadgeVariant::Default.classes().contains("bg-primary"));
assert!(BadgeVariant::Secondary.classes().contains("bg-secondary"));
assert!(BadgeVariant::Destructive
.classes()
.contains("bg-destructive"));
assert!(BadgeVariant::Outline.classes().contains("border"));
assert!(BadgeVariant::Success.classes().contains("bg-green-600"));
assert!(BadgeVariant::Warning.classes().contains("bg-yellow-500"));
}
}