Alert
Displays a callout for important information to attract user attention.
Variants
Alerts come in Default and Destructive variants.
This is a default alert.
This is a destructive alert.
With Title and Description
Alerts can include a title and description for structured content.
Heads up!
You can add components to your app using the CLI.
Error
Your session has expired. Please log in again.
Informational
An alert showing an informational message.
Did you know?
Proa renders UI components on the server in microseconds.
FreeAlert
Status messaging and inline notices.
Install with the Proa CLI
$ proa ui add alertInstalls 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
//! Alert recipe - type-safe styling system for Alert component
//!
//! Provides variant styles and base styling for the Alert component.
use crate::tw_join;
use proa_core::shared::attr_value::AttrValue;
use proa_core::{DataLoader, WebContext, WriteBuf, WriteError};
/// Base styles for the alert
mod base {
use super::*;
pub const ALERT_BASE: &str = tw_join!(
// Layout
"relative",
"w-full",
// Shape
"rounded-lg",
"border",
Alert source
Browse and copy the reviewed source included with this Free component.
alert.rsOpen raw ↗
// SPDX-FileCopyrightText: 2026 Proa Labs, Inc.
// SPDX-License-Identifier: MIT OR Apache-2.0
//! Alert component implementation
//!
//! Renders a callout for user attention with variant styling.
use super::recipe::{AlertRecipeProps, AlertVariant, ALERT_RECIPE};
use proa_core::{WebContext, WebRenderSync, WriteBuf};
use proa_macros::html_sync;
/// Alert component - displays a callout for user attention
pub struct Alert<C = ()> {
/// Style variant
pub variant: AlertVariant,
/// Additional CSS classes
pub class: Option<&'static str>,
/// The alert content (title, description, etc.)
pub children: Option<C>,
}
impl Alert<()> {
pub const fn new() -> Self {
Self {
variant: AlertVariant::Default,
class: None,
children: None,
}
}
}
impl<C> Default for Alert<C> {
fn default() -> Self {
Self {
variant: AlertVariant::Default,
class: None,
children: None,
}
}
}
impl<B: WriteBuf, C: WebRenderSync<L, B>, L: ::proa_core::DataLoader> WebRenderSync<L, B>
for Alert<C>
{
fn render(
self,
cx: &mut WebContext<L, B>,
) -> Result<(), proa_core::shared::write_buf::WriteError> {
let style = ALERT_RECIPE.resolve(AlertRecipeProps {
variant: Some(self.variant),
});
html_sync! {
<div
data-alert
data-slot="alert"
data-variant={style.variant().as_str()}
role="alert"
class={proa_macros::text!("{} {}", style, self.class)}
>
{self.children}
</div>
}
.render(cx)
}
}
/// AlertTitle component - renders the title within an Alert
pub struct AlertTitle<C = ()> {
/// Additional CSS classes
pub class: Option<&'static str>,
/// The title content
pub children: Option<C>,
}
impl AlertTitle<()> {
pub const fn new() -> Self {
Self {
class: None,
children: None,
}
}
}
impl<C> Default for AlertTitle<C> {
fn default() -> Self {
Self {
class: None,
children: None,
}
}
}
impl<B: WriteBuf, C: WebRenderSync<L, B>, L: ::proa_core::DataLoader> WebRenderSync<L, B>
for AlertTitle<C>
{
fn render(
self,
cx: &mut WebContext<L, B>,
) -> Result<(), proa_core::shared::write_buf::WriteError> {
html_sync! {
<h5
data-alert-title
data-slot="alert-title"
class={proa_macros::text!("{} {}", ALERT_RECIPE.title, self.class)}
>
{self.children}
</h5>
}
.render(cx)
}
}
/// AlertDescription component - renders the description within an Alert
pub struct AlertDescription<C = ()> {
/// Additional CSS classes
pub class: Option<&'static str>,
/// The description content
pub children: Option<C>,
}
impl AlertDescription<()> {
pub const fn new() -> Self {
Self {
class: None,
children: None,
}
}
}
impl<C> Default for AlertDescription<C> {
fn default() -> Self {
Self {
class: None,
children: None,
}
}
}
impl<B: WriteBuf, C: WebRenderSync<L, B>, L: ::proa_core::DataLoader> WebRenderSync<L, B>
for AlertDescription<C>
{
fn render(
self,
cx: &mut WebContext<L, B>,
) -> Result<(), proa_core::shared::write_buf::WriteError> {
html_sync! {
<div
data-alert-description
data-slot="alert-description"
class={proa_macros::text!("{} {}", ALERT_RECIPE.description, self.class)}
>
{self.children}
</div>
}
.render(cx)
}
}
#[cfg(test)]
mod tests {
use super::*;
use proa_core::ctx::Ctx;
use proa_core::text::text;
#[test]
fn alert_renders_with_data_attributes() {
let mut out = Vec::new();
let mut cx = Ctx::with_buffer(&mut out);
Alert {
variant: AlertVariant::Default,
class: None,
children: Some(text("Something happened")),
}
.render(&mut cx)
.unwrap();
let html = String::from_utf8(out).unwrap();
assert!(html.contains("data-alert"));
assert!(html.contains("data-slot=\"alert\""));
assert!(html.contains("data-variant=\"default\""));
assert!(html.contains("role=\"alert\""));
assert!(html.contains("Something happened"));
}
#[test]
fn alert_renders_destructive_variant() {
let mut out = Vec::new();
let mut cx = Ctx::with_buffer(&mut out);
Alert {
variant: AlertVariant::Destructive,
class: None,
children: Some(text("Error occurred")),
}
.render(&mut cx)
.unwrap();
let html = String::from_utf8(out).unwrap();
assert!(html.contains("data-variant=\"destructive\""));
assert!(html.contains("Error occurred"));
}
#[test]
fn alert_accepts_custom_class() {
let mut out = Vec::new();
let mut cx = Ctx::with_buffer(&mut out);
Alert {
variant: AlertVariant::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 alert_has_base_classes() {
let mut out = Vec::new();
let mut cx = Ctx::with_buffer(&mut out);
Alert {
variant: AlertVariant::Default,
class: None,
children: Some(text("Alert")),
}
.render(&mut cx)
.unwrap();
let html = String::from_utf8(out).unwrap();
assert!(html.contains("relative"));
assert!(html.contains("rounded-lg"));
assert!(html.contains("text-sm"));
}
#[test]
fn alert_title_renders_correctly() {
let mut out = Vec::new();
let mut cx = Ctx::with_buffer(&mut out);
AlertTitle {
class: None,
children: Some(text("Title")),
}
.render(&mut cx)
.unwrap();
let html = String::from_utf8(out).unwrap();
assert!(html.contains("data-alert-title"));
assert!(html.contains("data-slot=\"alert-title\""));
assert!(html.contains("<h5"));
assert!(html.contains("Title"));
assert!(html.contains("font-medium"));
}
#[test]
fn alert_description_renders_correctly() {
let mut out = Vec::new();
let mut cx = Ctx::with_buffer(&mut out);
AlertDescription {
class: None,
children: Some(text("Description text")),
}
.render(&mut cx)
.unwrap();
let html = String::from_utf8(out).unwrap();
assert!(html.contains("data-alert-description"));
assert!(html.contains("data-slot=\"alert-description\""));
assert!(html.contains("Description text"));
assert!(html.contains("text-sm"));
}
#[test]
fn alert_title_accepts_custom_class() {
let mut out = Vec::new();
let mut cx = Ctx::with_buffer(&mut out);
AlertTitle {
class: Some("my-title"),
children: Some(text("Custom Title")),
}
.render(&mut cx)
.unwrap();
let html = String::from_utf8(out).unwrap();
assert!(html.contains("my-title"));
}
#[test]
fn alert_description_accepts_custom_class() {
let mut out = Vec::new();
let mut cx = Ctx::with_buffer(&mut out);
AlertDescription {
class: Some("my-desc"),
children: Some(text("Custom Desc")),
}
.render(&mut cx)
.unwrap();
let html = String::from_utf8(out).unwrap();
assert!(html.contains("my-desc"));
}
}
md.rsOpen raw ↗
// SPDX-FileCopyrightText: 2026 Proa Labs, Inc.
// SPDX-License-Identifier: MIT OR Apache-2.0
use super::{Alert, AlertDescription, AlertTitle, AlertVariant};
use crate::components::md_support::block_boundary;
use proa_core::WebContext;
use proa_core::{MdRenderSync, WriteBuf, WriteError};
use proa_macros::md_sync;
/// Alert renders as a blockquote with a bold prefix indicating the variant.
impl<Loader: ::proa_core::DataLoader, B: WriteBuf, C: MdRenderSync<Loader, B>>
MdRenderSync<Loader, B> for Alert<C>
{
fn render_md(self, cx: &mut WebContext<Loader, B>) -> Result<(), WriteError> {
block_boundary(cx)?;
match self.variant {
AlertVariant::Default => md_sync! { "> **Note:** " }.render_md(cx)?,
AlertVariant::Destructive => md_sync! { "> **Warning:** " }.render_md(cx)?,
}
let previous_inline = cx.md_set_inline(true);
let result = self.children.render_md(cx);
cx.md_set_inline(previous_inline);
result?;
md_sync! { "" "\n" }.render_md(cx)?;
block_boundary(cx)
}
}
/// AlertTitle renders its children as inline text (consumed by Alert's blockquote).
impl<Loader: ::proa_core::DataLoader, B: WriteBuf, C: MdRenderSync<Loader, B>>
MdRenderSync<Loader, B> for AlertTitle<C>
{
fn render_md(self, cx: &mut WebContext<Loader, B>) -> Result<(), WriteError> {
self.children.render_md(cx)
}
}
/// AlertDescription renders its children as inline text.
impl<Loader: ::proa_core::DataLoader, B: WriteBuf, C: MdRenderSync<Loader, B>>
MdRenderSync<Loader, B> for AlertDescription<C>
{
fn render_md(self, cx: &mut WebContext<Loader, B>) -> Result<(), WriteError> {
if !cx.md_inline() {
md_sync! { " " }.render_md(cx)?;
}
self.children.render_md(cx)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn alert_default_renders_blockquote() {
let mut buf = Vec::new();
let mut cx = WebContext::with_buffer(&mut buf);
Alert {
variant: AlertVariant::Default,
class: None,
children: Some((
AlertTitle {
class: None,
children: Some("Heads up!"),
},
AlertDescription {
class: None,
children: Some("Something to know about."),
},
)),
}
.render_md(&mut cx)
.unwrap();
let output = String::from_utf8(buf).unwrap();
assert!(output.contains("> **Note:**"), "output: {output}");
assert!(output.contains("Heads up\\!"), "output: {output}");
assert!(
output.contains("Something to know about\\."),
"output: {output}"
);
}
#[test]
fn alert_destructive() {
let mut buf = Vec::new();
let mut cx = WebContext::with_buffer(&mut buf);
Alert {
variant: AlertVariant::Destructive,
class: None,
children: Some(AlertTitle {
class: None,
children: Some("Error occurred"),
}),
}
.render_md(&mut cx)
.unwrap();
let output = String::from_utf8(buf).unwrap();
assert!(output.contains("> **Warning:**"), "output: {output}");
}
#[test]
fn alert_composes_after_inline_text() {
let mut buf = Vec::new();
let mut cx = WebContext::with_buffer(&mut buf);
md_sync! { "intro prose" }.render_md(&mut cx).unwrap();
Alert {
variant: AlertVariant::Default,
class: None,
children: Some(AlertTitle {
class: None,
children: Some("Heads up!"),
}),
}
.render_md(&mut cx)
.unwrap();
let output = String::from_utf8(buf).unwrap();
assert!(
output.contains("intro prose\n\n> **Note:**"),
"blockquote must start at a block boundary, got:\n{output}"
);
}
}
mod.rsOpen raw ↗
// SPDX-FileCopyrightText: 2026 Proa Labs, Inc.
// SPDX-License-Identifier: MIT OR Apache-2.0
#![allow(clippy::module_inception)]
//! Alert component - displays a callout for user attention
//!
//! Supports Default and Destructive variants.
mod alert;
mod md;
mod recipe;
pub use alert::{Alert, AlertDescription, AlertTitle};
pub use recipe::{
alert_classes, alert_recipe, AlertRecipe, AlertRecipeProps, AlertStyle, AlertVariant,
ALERT_RECIPE,
};
recipe.rsOpen raw ↗
// SPDX-FileCopyrightText: 2026 Proa Labs, Inc.
// SPDX-License-Identifier: MIT OR Apache-2.0
//! Alert recipe - type-safe styling system for Alert component
//!
//! Provides variant styles and base styling for the Alert component.
use crate::tw_join;
use proa_core::shared::attr_value::AttrValue;
use proa_core::{DataLoader, WebContext, WriteBuf, WriteError};
/// Base styles for the alert
mod base {
use super::*;
pub const ALERT_BASE: &str = tw_join!(
// Layout
"relative",
"w-full",
// Shape
"rounded-lg",
"border",
// Spacing
"px-4",
"py-3",
// Typography
"text-sm",
// SVG styling
"[&>svg]:size-4",
"[&>svg]:text-current"
);
}
/// Variant styles for the alert
mod variants {
use super::*;
pub const ALERT_DEFAULT: &str = tw_join!("bg-card", "text-card-foreground");
pub const ALERT_DESTRUCTIVE: &str = tw_join!(
"text-destructive",
"bg-destructive/5",
"border-destructive/20"
);
}
/// Variant options for the alert
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, proa_macros::ProaVariant)]
#[variant(name = "variant")]
pub enum AlertVariant {
/// Default alert style
#[default]
Default,
/// Destructive/error alert style
Destructive,
}
impl AlertVariant {
/// Get the Tailwind classes for this variant
pub const fn classes(self) -> &'static str {
match self {
AlertVariant::Default => variants::ALERT_DEFAULT,
AlertVariant::Destructive => variants::ALERT_DESTRUCTIVE,
}
}
/// Get the data attribute value
pub const fn as_str(self) -> &'static str {
match self {
AlertVariant::Default => "default",
AlertVariant::Destructive => "destructive",
}
}
}
/// Optional root inputs accepted by [`AlertRecipe::resolve`].
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct AlertRecipeProps {
pub variant: Option<AlertVariant>,
}
/// Static named classes for every Alert slot.
#[derive(Debug, Clone, Copy)]
pub struct AlertRecipe {
pub root: &'static str,
pub title: &'static str,
pub description: &'static str,
variant_default: AlertVariant,
}
impl AlertRecipe {
pub const fn resolve(&'static self, props: AlertRecipeProps) -> AlertStyle {
let variant = match props.variant {
Some(variant) => variant,
None => self.variant_default,
};
AlertStyle {
recipe: self,
variant,
}
}
}
/// Fully resolved, streamable Alert root style.
#[must_use]
#[derive(Debug, Clone, Copy)]
pub struct AlertStyle {
recipe: &'static AlertRecipe,
variant: AlertVariant,
}
impl AlertStyle {
pub const fn variant(self) -> AlertVariant {
self.variant
}
}
impl AttrValue for AlertStyle {
#[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())?;
cx.out.extend_static(b" ")?;
cx.out.extend_static(self.variant.classes().as_bytes())
}
}
/// AlertTitle slot classes.
pub const ALERT_TITLE_CLASSES: &str =
tw_join!("mb-1", "font-medium", "leading-none", "tracking-tight");
/// AlertDescription slot classes.
pub const ALERT_DESCRIPTION_CLASSES: &str = tw_join!("text-sm", "[&_p]:leading-relaxed");
/// Global alert recipe instance.
pub const ALERT_RECIPE: AlertRecipe = AlertRecipe {
root: base::ALERT_BASE,
title: ALERT_TITLE_CLASSES,
description: ALERT_DESCRIPTION_CLASSES,
variant_default: AlertVariant::Default,
};
/// Get the global alert recipe
pub const fn alert_recipe() -> &'static AlertRecipe {
&ALERT_RECIPE
}
/// Compatibility resolver for callers of the former helper.
pub const fn alert_classes(variant: AlertVariant) -> AlertStyle {
ALERT_RECIPE.resolve(AlertRecipeProps {
variant: Some(variant),
})
}
impl crate::variant_spec::ComponentSpec for AlertRecipe {
const NAME: &'static str = "Alert";
fn base_classes(&self) -> &'static str {
self.root
}
fn variant_metadata(&self) -> Vec<crate::variant_spec::VariantMetadata> {
vec![crate::variant_spec::variant_metadata::<AlertVariant>()]
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn alert_recipe_generates_default() {
let style = alert_recipe().resolve(AlertRecipeProps::default());
assert!(alert_recipe().root.contains("relative"));
assert!(alert_recipe().root.contains("rounded-lg"));
assert_eq!(style.variant(), AlertVariant::Default);
assert!(style.variant().classes().contains("bg-card"));
}
#[test]
fn alert_recipe_supports_variants() {
let style = alert_recipe().resolve(AlertRecipeProps {
variant: Some(AlertVariant::Destructive),
});
assert_eq!(style.variant(), AlertVariant::Destructive);
assert!(style.variant().classes().contains("text-destructive"));
}
#[test]
fn alert_variants_have_correct_classes() {
assert!(AlertVariant::Default.classes().contains("bg-card"));
assert!(AlertVariant::Destructive
.classes()
.contains("text-destructive"));
}
#[test]
fn recipe_exposes_named_slots() {
assert!(ALERT_RECIPE.title.contains("font-medium"));
assert!(ALERT_RECIPE.description.contains("leading-relaxed"));
}
}