Separator
Visually or semantically separates content.
Horizontal
A horizontal separator divides content vertically.
Content above the separator.
Content below the separator.
Vertical
A vertical separator divides content horizontally within a flex row.
BlogDocsSource
In Context
Separators used in realistic layouts.
Proa UI
A Rust-first library with Free and Pro components.
BlogDocsSource
Profile Settings
Manage your account details
Notifications
Configure notification preferences
Security
Update password and security options
FreeSeparator
Visual dividers.
Install with the Proa CLI
$ proa ui add separatorInstalls 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
//! Separator recipe - type-safe styling system for Separator component
//!
//! Provides orientation-based styling for the Separator component.
use crate::tw_join;
use proa_core::shared::attr_value::AttrValue;
use proa_core::{DataLoader, WebContext, WriteBuf, WriteError};
/// Base styles for the separator
mod base {
use super::*;
pub const SEPARATOR_BASE: &str = tw_join!("shrink-0", "bg-border");
}
/// Orientation styles for the separator
mod orientations {
use super::*;
Separator 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::Separator;
use crate::components::md_support::block_boundary;
use proa_core::WebContext;
use proa_core::{MdRenderSync, WriteBuf, WriteError};
impl<Loader: ::proa_core::DataLoader, B: WriteBuf> MdRenderSync<Loader, B> for Separator {
fn render_md(self, cx: &mut WebContext<Loader, B>) -> Result<(), WriteError> {
// `---` directly under a paragraph line would turn that line into a
// setext heading; a full blank-line boundary avoids it.
block_boundary(cx)?;
cx.md_hr()?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn separator_renders_hr() {
let mut buf = Vec::new();
let mut cx = WebContext::with_buffer(&mut buf);
Separator::new().render_md(&mut cx).unwrap();
assert_eq!(String::from_utf8(buf).unwrap(), "---\n\n");
}
#[test]
fn separator_composes_after_inline_text() {
let mut buf = Vec::new();
let mut cx = WebContext::with_buffer(&mut buf);
cx.write_md_prepared_static(b"intro prose").unwrap();
Separator::new().render_md(&mut cx).unwrap();
assert_eq!(String::from_utf8(buf).unwrap(), "intro prose\n\n---\n\n");
}
}
mod.rsOpen raw ↗
// SPDX-FileCopyrightText: 2026 Proa Labs, Inc.
// SPDX-License-Identifier: MIT OR Apache-2.0
#![allow(clippy::module_inception)]
//! Separator component - a visual divider between content sections
//!
//! The Separator component renders a horizontal or vertical line to visually
//! divide content. It supports decorative mode (default) for purely visual
//! separators and semantic mode for accessible content division.
mod md;
mod recipe;
mod separator;
// Re-export separator types
pub use separator::Separator;
// Re-export recipe types
pub use recipe::{
separator_classes, separator_recipe, SeparatorOrientation, SeparatorRecipe,
SeparatorRecipeProps, SeparatorStyle, SEPARATOR_RECIPE,
};
recipe.rsOpen raw ↗
// SPDX-FileCopyrightText: 2026 Proa Labs, Inc.
// SPDX-License-Identifier: MIT OR Apache-2.0
//! Separator recipe - type-safe styling system for Separator component
//!
//! Provides orientation-based styling for the Separator component.
use crate::tw_join;
use proa_core::shared::attr_value::AttrValue;
use proa_core::{DataLoader, WebContext, WriteBuf, WriteError};
/// Base styles for the separator
mod base {
use super::*;
pub const SEPARATOR_BASE: &str = tw_join!("shrink-0", "bg-border");
}
/// Orientation styles for the separator
mod orientations {
use super::*;
pub const SEPARATOR_HORIZONTAL: &str = tw_join!("h-px", "w-full");
pub const SEPARATOR_VERTICAL: &str = tw_join!("h-full", "w-px");
}
/// Orientation options for the separator
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, proa_macros::ProaVariant)]
#[variant(name = "orientation")]
pub enum SeparatorOrientation {
/// Horizontal separator (default)
#[default]
Horizontal,
/// Vertical separator
Vertical,
}
impl SeparatorOrientation {
/// Get the Tailwind classes for this orientation
pub const fn classes(self) -> &'static str {
match self {
SeparatorOrientation::Horizontal => orientations::SEPARATOR_HORIZONTAL,
SeparatorOrientation::Vertical => orientations::SEPARATOR_VERTICAL,
}
}
/// Get the data attribute value
pub const fn as_str(self) -> &'static str {
match self {
SeparatorOrientation::Horizontal => "horizontal",
SeparatorOrientation::Vertical => "vertical",
}
}
}
/// Optional inputs accepted by [`SeparatorRecipe::resolve`].
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct SeparatorRecipeProps {
pub orientation: Option<SeparatorOrientation>,
}
/// Compile-time Separator recipe.
#[derive(Debug, Clone, Copy)]
pub struct SeparatorRecipe {
root: &'static str,
orientation_default: SeparatorOrientation,
}
impl SeparatorRecipe {
pub const fn resolve(&'static self, props: SeparatorRecipeProps) -> SeparatorStyle {
let orientation = match props.orientation {
Some(orientation) => orientation,
None => self.orientation_default,
};
SeparatorStyle {
recipe: self,
orientation,
}
}
}
/// Fully resolved, streamable Separator style.
#[must_use]
#[derive(Debug, Clone, Copy)]
pub struct SeparatorStyle {
recipe: &'static SeparatorRecipe,
orientation: SeparatorOrientation,
}
impl SeparatorStyle {
pub const fn orientation(self) -> SeparatorOrientation {
self.orientation
}
}
impl AttrValue for SeparatorStyle {
#[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.orientation.classes().as_bytes())
}
}
pub const SEPARATOR_RECIPE: SeparatorRecipe = SeparatorRecipe {
root: base::SEPARATOR_BASE,
orientation_default: SeparatorOrientation::Horizontal,
};
pub const fn separator_recipe() -> &'static SeparatorRecipe {
&SEPARATOR_RECIPE
}
/// Compatibility resolver for callers of the former helper.
pub const fn separator_classes(orientation: SeparatorOrientation) -> SeparatorStyle {
SEPARATOR_RECIPE.resolve(SeparatorRecipeProps {
orientation: Some(orientation),
})
}
impl crate::variant_spec::ComponentSpec for SeparatorRecipe {
const NAME: &'static str = "Separator";
fn base_classes(&self) -> &'static str {
self.root
}
fn variant_metadata(&self) -> Vec<crate::variant_spec::VariantMetadata> {
vec![crate::variant_spec::variant_metadata::<SeparatorOrientation>()]
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn separator_horizontal_has_correct_classes() {
let base = base::SEPARATOR_BASE;
let orientation = SeparatorOrientation::Horizontal.classes();
assert!(base.contains("shrink-0"));
assert!(base.contains("bg-border"));
assert!(orientation.contains("h-px"));
assert!(orientation.contains("w-full"));
}
#[test]
fn separator_vertical_has_correct_classes() {
let base = base::SEPARATOR_BASE;
let orientation = SeparatorOrientation::Vertical.classes();
assert!(base.contains("shrink-0"));
assert!(base.contains("bg-border"));
assert!(orientation.contains("h-full"));
assert!(orientation.contains("w-px"));
}
#[test]
fn separator_orientation_as_str() {
assert_eq!(SeparatorOrientation::Horizontal.as_str(), "horizontal");
assert_eq!(SeparatorOrientation::Vertical.as_str(), "vertical");
}
#[test]
fn separator_orientation_default_is_horizontal() {
assert_eq!(
SeparatorOrientation::default(),
SeparatorOrientation::Horizontal
);
}
#[test]
fn recipe_resolves_defaults_and_overrides() {
let default = separator_recipe().resolve(SeparatorRecipeProps::default());
assert_eq!(default.orientation(), SeparatorOrientation::Horizontal);
let vertical = separator_recipe().resolve(SeparatorRecipeProps {
orientation: Some(SeparatorOrientation::Vertical),
});
assert_eq!(vertical.orientation(), SeparatorOrientation::Vertical);
assert!(vertical.orientation().classes().contains("w-px"));
}
}
separator.rsOpen raw ↗
// SPDX-FileCopyrightText: 2026 Proa Labs, Inc.
// SPDX-License-Identifier: MIT OR Apache-2.0
//! Separator component implementation
//!
//! Renders a visual divider between content sections.
use super::recipe::{SeparatorOrientation, SeparatorRecipeProps, SEPARATOR_RECIPE};
use proa_core::{WebContext, WebRenderSync, WriteBuf};
use proa_macros::html_sync;
/// Separator component - a visual divider between content sections
///
/// A pure CSS separator that supports horizontal and vertical orientations.
/// When `decorative` is true (default), sets `role="none"` to indicate
/// the separator is purely visual. When false, sets `role="separator"`
/// for semantic meaning.
///
/// # Example
///
/// ```ignore
/// use proa_ui::{Separator, SeparatorOrientation};
/// use proa_macros::html_sync;
///
/// html_sync! {
/// <div>
/// <p>"Above"</p>
/// {Separator::new()}
/// <p>"Below"</p>
/// </div>
/// }.render(cx);
/// ```
pub struct Separator {
/// Orientation of the separator
pub orientation: SeparatorOrientation,
/// Whether the separator is purely decorative (default: true)
///
/// If true, sets `role="none"`. If false, sets `role="separator"`.
pub decorative: bool,
/// Additional CSS classes
pub class: Option<&'static str>,
}
impl Separator {
/// Create a new separator with default settings
pub const fn new() -> Self {
Self {
orientation: SeparatorOrientation::Horizontal,
decorative: true,
class: None,
}
}
}
impl Default for Separator {
fn default() -> Self {
Self::new()
}
}
impl<B: WriteBuf, L: ::proa_core::DataLoader> WebRenderSync<L, B> for Separator {
#[inline(always)]
fn render(
self,
cx: &mut WebContext<L, B>,
) -> Result<(), proa_core::shared::write_buf::WriteError> {
let style = SEPARATOR_RECIPE.resolve(SeparatorRecipeProps {
orientation: Some(self.orientation),
});
let role = if self.decorative { "none" } else { "separator" };
html_sync! {
<div
data-separator
data-slot="separator"
data-orientation={style.orientation().as_str()}
role={role}
class={proa_macros::text!("{} {}", style, self.class)}
></div>
}
.render(cx)
}
}
#[cfg(test)]
mod tests {
use super::*;
use proa_core::ctx::Ctx;
#[test]
fn separator_renders_with_defaults() {
let mut out = Vec::new();
let mut cx = Ctx::with_buffer(&mut out);
Separator::new().render(&mut cx).unwrap();
let html = String::from_utf8(out).unwrap();
assert!(html.contains("data-separator"));
assert!(html.contains("data-slot=\"separator\""));
assert!(html.contains("data-orientation=\"horizontal\""));
assert!(html.contains("role=\"none\""));
assert!(html.contains("shrink-0"));
assert!(html.contains("bg-border"));
assert!(html.contains("h-px"));
assert!(html.contains("w-full"));
}
#[test]
fn separator_renders_vertical() {
let mut out = Vec::new();
let mut cx = Ctx::with_buffer(&mut out);
Separator {
orientation: SeparatorOrientation::Vertical,
..Separator::new()
}
.render(&mut cx)
.unwrap();
let html = String::from_utf8(out).unwrap();
assert!(html.contains("data-orientation=\"vertical\""));
assert!(html.contains("h-full"));
assert!(html.contains("w-px"));
}
#[test]
fn separator_decorative_false_sets_role_separator() {
let mut out = Vec::new();
let mut cx = Ctx::with_buffer(&mut out);
Separator {
decorative: false,
..Separator::new()
}
.render(&mut cx)
.unwrap();
let html = String::from_utf8(out).unwrap();
assert!(html.contains("role=\"separator\""));
}
#[test]
fn separator_accepts_custom_class() {
let mut out = Vec::new();
let mut cx = Ctx::with_buffer(&mut out);
Separator {
class: Some("my-4"),
..Separator::new()
}
.render(&mut cx)
.unwrap();
let html = String::from_utf8(out).unwrap();
assert!(html.contains("my-4"));
}
#[test]
fn separator_is_zero_alloc() {
assert!(std::mem::size_of::<Separator>() < 64);
assert!(!std::mem::needs_drop::<Separator>());
const _SEPARATOR: Separator = Separator::new();
}
}