Tabs source

Inspect and copy the reviewed source included with this Free component.

Free source · 5 files

recipe.rs

// SPDX-FileCopyrightText: 2026 Proa Labs, Inc.
// SPDX-License-Identifier: MIT OR Apache-2.0

//! Tabs recipe — typed root orientation and named slot classes.
//!
//! The root resolves orientation once. Descendant slots inherit its visual
//! direction through the semantic `group/tabs` root and never resolve an
//! independent orientation style.

use crate::tw_join;
use proa_core::shared::attr_value::AttrValue;
use proa_core::{DataLoader, WebContext, WriteBuf, WriteError};

mod base {
    use super::*;

    pub const TABS_ROOT: &str = "group/tabs";

    pub const TABS_LIST: &str = tw_join!(
        "inline-flex",
        "h-9",
        "items-center",
        "justify-center",
        "rounded-lg",
        "bg-muted",
        "p-1",
        "text-muted-foreground",
        "group-data-[orientation=vertical]/tabs:h-auto",
        "group-data-[orientation=vertical]/tabs:flex-col",
        "group-data-[orientation=vertical]/tabs:items-stretch",
        "group-data-[orientation=vertical]/tabs:justify-start"
    );

    pub const TABS_TRIGGER: &str = tw_join!(
        "inline-flex",
        "items-center",
        "justify-center",
        "whitespace-nowrap",
        "rounded-md",
        "px-3",
        "py-1",
        "text-sm",
        "font-medium",
        "ring-offset-background",
        "transition-all",
        "focus-visible:outline-hidden",
        "focus-visible:ring-2",
        "focus-visible:ring-ring",
        "focus-visible:ring-offset-2",
        "disabled:pointer-events-none",
        "disabled:opacity-50",
        "data-[state=active]:bg-background",
        "data-[state=active]:text-foreground",
        "data-[state=active]:shadow",
        "data-[active=true]:bg-background",
        "data-[active=true]:text-foreground",
        "data-[active=true]:shadow",
        "group-data-[orientation=vertical]/tabs:w-full",
        "group-data-[orientation=vertical]/tabs:justify-start"
    );

    pub const TABS_CONTENT: &str = tw_join!(
        "mt-2",
        "ring-offset-background",
        "focus-visible:outline-hidden",
        "focus-visible:ring-2",
        "focus-visible:ring-ring",
        "focus-visible:ring-offset-2",
        "group-data-[orientation=vertical]/tabs:mt-0",
        "group-data-[orientation=vertical]/tabs:min-w-0",
        "group-data-[orientation=vertical]/tabs:flex-1"
    );
}

/// Visual and keyboard orientation owned by the Tabs root.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, proa_macros::ProaVariant)]
#[variant(name = "orientation")]
pub enum TabsOrientation {
    #[default]
    Horizontal,
    Vertical,
}

impl TabsOrientation {
    pub const fn as_str(self) -> &'static str {
        match self {
            Self::Horizontal => "horizontal",
            Self::Vertical => "vertical",
        }
    }

    pub const fn classes(self) -> &'static str {
        match self {
            Self::Horizontal => "",
            Self::Vertical => tw_join!("flex", "w-full", "items-start", "gap-4"),
        }
    }
}

/// Optional author inputs accepted by [`TabsRecipe::resolve`].
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct TabsRecipeProps {
    pub orientation: Option<TabsOrientation>,
}

/// Static named classes for every Tabs slot.
#[derive(Debug, Clone, Copy)]
pub struct TabsRecipe {
    pub root: &'static str,
    pub list: &'static str,
    pub trigger: &'static str,
    pub content: &'static str,
    orientation_default: TabsOrientation,
}

impl TabsRecipe {
    /// Resolve the root's effective orientation without allocating class
    /// storage for it or any descendant.
    pub const fn resolve(&'static self, props: TabsRecipeProps) -> TabsStyle {
        let orientation = match props.orientation {
            Some(orientation) => orientation,
            None => self.orientation_default,
        };

        TabsStyle {
            recipe: self,
            orientation,
        }
    }
}

/// Fully resolved, streamable Tabs root style.
#[must_use]
#[derive(Debug, Clone, Copy)]
pub struct TabsStyle {
    recipe: &'static TabsRecipe,
    orientation: TabsOrientation,
}

impl TabsStyle {
    pub const fn orientation(self) -> TabsOrientation {
        self.orientation
    }
}

impl AttrValue for TabsStyle {
    #[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())?;
        let orientation = self.orientation.classes();
        if !orientation.is_empty() {
            cx.out.extend_static(b" ")?;
            cx.out.extend_static(orientation.as_bytes())?;
        }
        Ok(())
    }
}

/// Global, zero-initialization named-slot Tabs recipe.
pub const TABS_RECIPE: TabsRecipe = TabsRecipe {
    root: base::TABS_ROOT,
    list: base::TABS_LIST,
    trigger: base::TABS_TRIGGER,
    content: base::TABS_CONTENT,
    orientation_default: TabsOrientation::Horizontal,
};

pub const fn tabs_recipe() -> &'static TabsRecipe {
    &TABS_RECIPE
}

impl crate::variant_spec::ComponentSpec for TabsRecipe {
    const NAME: &'static str = "Tabs";

    fn base_classes(&self) -> &'static str {
        self.root
    }

    fn variant_metadata(&self) -> Vec<crate::variant_spec::VariantMetadata> {
        vec![crate::variant_spec::variant_metadata::<TabsOrientation>()]
    }

    fn anatomy(&self) -> Option<crate::Anatomy> {
        Some(super::TABS_ANATOMY)
    }
}

/// Compatibility accessors for the former standalone slot helpers.
pub const fn tabs_list_classes() -> &'static str {
    TABS_RECIPE.list
}

pub const fn tabs_trigger_classes() -> &'static str {
    TABS_RECIPE.trigger
}

pub const fn tabs_content_classes() -> &'static str {
    TABS_RECIPE.content
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn recipe_exposes_named_slots() {
        let recipe = tabs_recipe();
        assert!(recipe.root.contains("group/tabs"));
        assert!(recipe.list.contains("inline-flex"));
        assert!(recipe.trigger.contains("transition-all"));
        assert!(recipe.content.contains("ring-offset-background"));
    }

    #[test]
    fn recipe_resolves_defaults_and_overrides() {
        let default = tabs_recipe().resolve(TabsRecipeProps::default());
        assert_eq!(default.orientation(), TabsOrientation::Horizontal);

        let vertical = tabs_recipe().resolve(TabsRecipeProps {
            orientation: Some(TabsOrientation::Vertical),
        });
        assert_eq!(vertical.orientation(), TabsOrientation::Vertical);
        assert!(vertical.orientation().classes().contains("items-start"));
    }

    #[test]
    fn vertical_descendant_layout_is_owned_by_root_orientation() {
        assert!(TABS_RECIPE
            .list
            .contains("group-data-[orientation=vertical]/tabs:h-auto"));
        assert!(TABS_RECIPE
            .list
            .contains("group-data-[orientation=vertical]/tabs:flex-col"));
        assert!(TABS_RECIPE
            .trigger
            .contains("group-data-[orientation=vertical]/tabs:w-full"));
        assert!(TABS_RECIPE
            .content
            .contains("group-data-[orientation=vertical]/tabs:mt-0"));
        assert!(!TABS_RECIPE
            .list
            .split_ascii_whitespace()
            .any(|class| class.starts_with("data-[orientation=")));
    }

    #[test]
    fn controlled_and_static_active_states_share_trigger_styling() {
        assert!(TABS_RECIPE
            .trigger
            .contains("data-[state=active]:bg-background"));
        assert!(TABS_RECIPE
            .trigger
            .contains("data-[active=true]:bg-background"));
    }

    #[test]
    fn tabs_orientation_as_str() {
        assert_eq!(TabsOrientation::Horizontal.as_str(), "horizontal");
        assert_eq!(TabsOrientation::Vertical.as_str(), "vertical");
    }

    #[test]
    fn component_metadata_uses_root_axis_and_canonical_anatomy() {
        use crate::variant_spec::ComponentSpec;

        let variants = TABS_RECIPE.variant_metadata();
        assert_eq!(variants.len(), 1);
        assert_eq!(variants[0].name, "orientation");
        assert_eq!(TABS_RECIPE.anatomy(), Some(super::super::TABS_ANATOMY));
    }
}

Copy this source with the Free Proa UI components it imports; those component files remain available from their own source pages.

Licensed under MIT OR Apache-2.0. Third-party notices remain attached to the installed payload.

Open Markdown
FreeTabs

Tabbed content regions.

Install with the Proa CLI
proa ui add tabs

Installs 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

//! Tabs recipe — typed root orientation and named slot classes.
//!
//! The root resolves orientation once. Descendant slots inherit its visual
//! direction through the semantic `group/tabs` root and never resolve an
//! independent orientation style.

use crate::tw_join;
use proa_core::shared::attr_value::AttrValue;
use proa_core::{DataLoader, WebContext, WriteBuf, WriteError};

mod base {
    use super::*;

    pub const TABS_ROOT: &str = "group/tabs";

    pub const TABS_LIST: &str = tw_join!(
        "inline-flex",
        "h-9",
        "items-center",

Search

Type at least 2 characters

Customize

Make it yours

Palette

Radius

Overview
Components
Settings

Build your own

Themed Proa UI surface

Preview
Components52
RecipesTyped
customer-portal