Components

JSON-UI includes 20 built-in components for building complete application interfaces from Rust handlers.

Component Overview

CategoryComponents
DisplayCard, Table, Badge, Alert, Separator, DescriptionList, Text, Button
FormForm, Input, Select, Checkbox, Switch
NavigationTabs, Breadcrumb, Pagination
FeedbackProgress, Avatar, Skeleton
LayoutModal

Every component is wrapped in a ComponentNode that provides a unique key, an optional action binding, and optional visibility rules:

#![allow(unused)]
fn main() {
use ferro::*;

ComponentNode {
    key: "my-card".to_string(),
    component: Component::Card(CardProps { /* ... */ }),
    action: None,
    visibility: None,
}
}

Shared Types

These enums are used across multiple components.

Size

Controls sizing for Button, Avatar, and other components.

ValueSerialized
Size::Xs"xs"
Size::Sm"sm"
Size::Default"default"
Size::Lg"lg"

ButtonVariant

Visual styles for the Button component (aligned to shadcn/ui).

ValueSerializedUse Case
ButtonVariant::Default"default"Primary actions
ButtonVariant::Secondary"secondary"Secondary actions
ButtonVariant::Destructive"destructive"Delete, remove
ButtonVariant::Outline"outline"Bordered style
ButtonVariant::Ghost"ghost"Minimal style
ButtonVariant::Link"link"Link appearance

AlertVariant

Visual styles for the Alert component.

ValueSerialized
AlertVariant::Info"info"
AlertVariant::Success"success"
AlertVariant::Warning"warning"
AlertVariant::Error"error"

BadgeVariant

Visual styles for the Badge component (aligned to shadcn/ui).

ValueSerialized
BadgeVariant::Default"default"
BadgeVariant::Secondary"secondary"
BadgeVariant::Destructive"destructive"
BadgeVariant::Outline"outline"

ColumnFormat

Display format for Table columns and DescriptionList items.

ValueSerialized
ColumnFormat::Date"date"
ColumnFormat::DateTime"date_time"
ColumnFormat::Currency"currency"
ColumnFormat::Boolean"boolean"

TextElement

Semantic HTML element for the Text component.

ValueSerializedHTML
TextElement::P"p"<p>
TextElement::H1"h1"<h1>
TextElement::H2"h2"<h2>
TextElement::H3"h3"<h3>
TextElement::Span"span"<span>

Display Components

Card

Container with title, optional description, nested children, and footer.

PropTypeRequiredDefaultDescription
titleStringYes-Card title
descriptionOption<String>NoNoneDescription below the title
childrenVec<ComponentNode>No[]Nested components in the card body
footerVec<ComponentNode>No[]Components in the card footer
#![allow(unused)]
fn main() {
use ferro::*;

ComponentNode {
    key: "user-card".to_string(),
    component: Component::Card(CardProps {
        title: "User Details".to_string(),
        description: Some("Account information".to_string()),
        children: vec![
            ComponentNode {
                key: "name".to_string(),
                component: Component::Text(TextProps {
                    content: "Alice Johnson".to_string(),
                    element: TextElement::H3,
                }),
                action: None,
                visibility: None,
            },
        ],
        footer: vec![
            ComponentNode {
                key: "edit-btn".to_string(),
                component: Component::Button(ButtonProps {
                    label: "Edit".to_string(),
                    variant: ButtonVariant::Outline,
                    size: Size::Default,
                    disabled: None,
                    icon: None,
                    icon_position: None,
                }),
                action: Some(Action::get("users.edit")),
                visibility: None,
            },
        ],
    }),
    action: None,
    visibility: None,
}
}

Table

Data table with column definitions, row actions, and sorting support. Rows are loaded from handler data via data_path.

PropTypeRequiredDefaultDescription
columnsVec<Column>Yes-Column definitions
data_pathStringYes-Path to the row data array (e.g., "/data/users")
row_actionsOption<Vec<Action>>NoNoneActions available per row
empty_messageOption<String>NoNoneMessage when no data
sortableOption<bool>NoNoneEnable column sorting
sort_columnOption<String>NoNoneCurrently sorted column key
sort_directionOption<SortDirection>NoNoneSort direction: asc or desc

Column defines a table column:

FieldTypeRequiredDescription
keyStringYesData field key matching the row object
labelStringYesColumn header text
formatOption<ColumnFormat>NoDisplay format (Date, DateTime, Currency, Boolean)
#![allow(unused)]
fn main() {
use ferro::*;

ComponentNode {
    key: "users-table".to_string(),
    component: Component::Table(TableProps {
        columns: vec![
            Column {
                key: "name".to_string(),
                label: "Name".to_string(),
                format: None,
            },
            Column {
                key: "email".to_string(),
                label: "Email".to_string(),
                format: None,
            },
            Column {
                key: "created_at".to_string(),
                label: "Created".to_string(),
                format: Some(ColumnFormat::Date),
            },
        ],
        data_path: "/data/users".to_string(),
        row_actions: Some(vec![
            Action::get("users.edit"),
            Action::delete("users.destroy")
                .confirm_danger("Delete this user?"),
        ]),
        empty_message: Some("No users found.".to_string()),
        sortable: Some(true),
        sort_column: None,
        sort_direction: None,
    }),
    action: None,
    visibility: None,
}
}

The data_path points to an array in the handler data. Each object in the array maps its keys to column key fields.

Badge

Small label with variant-based styling.

PropTypeRequiredDefaultDescription
labelStringYes-Badge text
variantBadgeVariantNoDefaultVisual style
#![allow(unused)]
fn main() {
use ferro::*;

ComponentNode {
    key: "status".to_string(),
    component: Component::Badge(BadgeProps {
        label: "Active".to_string(),
        variant: BadgeVariant::Default,
    }),
    action: None,
    visibility: None,
}
}

Alert

Alert message with variant-based styling and optional title.

PropTypeRequiredDefaultDescription
messageStringYes-Alert message content
variantAlertVariantNoInfoVisual style
titleOption<String>NoNoneAlert title
#![allow(unused)]
fn main() {
use ferro::*;

ComponentNode {
    key: "warning".to_string(),
    component: Component::Alert(AlertProps {
        message: "Your trial expires in 3 days.".to_string(),
        variant: AlertVariant::Warning,
        title: Some("Trial Ending".to_string()),
    }),
    action: None,
    visibility: None,
}
}

Separator

Visual divider between content sections.

PropTypeRequiredDefaultDescription
orientationOption<Orientation>NoHorizontalDirection: horizontal or vertical
#![allow(unused)]
fn main() {
use ferro::*;

ComponentNode {
    key: "divider".to_string(),
    component: Component::Separator(SeparatorProps {
        orientation: None, // defaults to horizontal
    }),
    action: None,
    visibility: None,
}
}

DescriptionList

Key-value pairs displayed as a description list. Reuses ColumnFormat for value formatting.

PropTypeRequiredDefaultDescription
itemsVec<DescriptionItem>Yes-Key-value items
columnsOption<u8>NoNoneNumber of columns for layout

DescriptionItem defines a key-value pair:

FieldTypeRequiredDescription
labelStringYesItem label
valueStringYesItem value
formatOption<ColumnFormat>NoDisplay format
#![allow(unused)]
fn main() {
use ferro::*;

ComponentNode {
    key: "user-info".to_string(),
    component: Component::DescriptionList(DescriptionListProps {
        items: vec![
            DescriptionItem {
                label: "Name".to_string(),
                value: "Alice Johnson".to_string(),
                format: None,
            },
            DescriptionItem {
                label: "Joined".to_string(),
                value: "2026-01-15".to_string(),
                format: Some(ColumnFormat::Date),
            },
            DescriptionItem {
                label: "Active".to_string(),
                value: "true".to_string(),
                format: Some(ColumnFormat::Boolean),
            },
        ],
        columns: Some(2),
    }),
    action: None,
    visibility: None,
}
}

Text

Renders text content with semantic HTML element selection.

PropTypeRequiredDefaultDescription
contentStringYes-Text content
elementTextElementNoPHTML element: p, h1, h2, h3, span
#![allow(unused)]
fn main() {
use ferro::*;

ComponentNode {
    key: "heading".to_string(),
    component: Component::Text(TextProps {
        content: "Welcome to the dashboard".to_string(),
        element: TextElement::H1,
    }),
    action: None,
    visibility: None,
}
}

Button

Interactive button with visual variants, sizing, and optional icon.

PropTypeRequiredDefaultDescription
labelStringYes-Button label text
variantButtonVariantNoDefaultVisual style
sizeSizeNoDefaultButton size
disabledOption<bool>NoNoneWhether the button is disabled
iconOption<String>NoNoneIcon name
icon_positionOption<IconPosition>NoLeftIcon placement: left or right

Buttons are typically combined with an action on the ComponentNode to bind click behavior:

#![allow(unused)]
fn main() {
use ferro::*;

ComponentNode {
    key: "save-btn".to_string(),
    component: Component::Button(ButtonProps {
        label: "Save Changes".to_string(),
        variant: ButtonVariant::Default,
        size: Size::Default,
        disabled: None,
        icon: Some("save".to_string()),
        icon_position: Some(IconPosition::Left),
    }),
    action: Some(Action::new("users.update")),
    visibility: None,
}
}

Form Components

Form

Form container with action binding and field components. The action defines the submit endpoint.

PropTypeRequiredDefaultDescription
actionActionYes-Action to execute on form submit
fieldsVec<ComponentNode>Yes-Form field components (Input, Select, Checkbox, etc.)
methodOption<HttpMethod>NoNoneHTTP method override (GET, POST, PUT, PATCH, DELETE)
#![allow(unused)]
fn main() {
use ferro::*;

ComponentNode {
    key: "create-form".to_string(),
    component: Component::Form(FormProps {
        action: Action::new("users.store"),
        fields: vec![
            ComponentNode {
                key: "name-input".to_string(),
                component: Component::Input(InputProps {
                    field: "name".to_string(),
                    label: "Name".to_string(),
                    input_type: InputType::Text,
                    placeholder: Some("Enter name".to_string()),
                    required: Some(true),
                    disabled: None,
                    error: None,
                    description: None,
                    default_value: None,
                    data_path: None,
                }),
                action: None,
                visibility: None,
            },
            ComponentNode {
                key: "email-input".to_string(),
                component: Component::Input(InputProps {
                    field: "email".to_string(),
                    label: "Email".to_string(),
                    input_type: InputType::Email,
                    placeholder: Some("user@example.com".to_string()),
                    required: Some(true),
                    disabled: None,
                    error: None,
                    description: None,
                    default_value: None,
                    data_path: None,
                }),
                action: None,
                visibility: None,
            },
        ],
        method: None,
    }),
    action: None,
    visibility: None,
}
}

Input

Text input field with type variants, validation error display, and data binding.

PropTypeRequiredDefaultDescription
fieldStringYes-Form field name for data binding
labelStringYes-Input label text
input_typeInputTypeNoTextInput type
placeholderOption<String>NoNonePlaceholder text
requiredOption<bool>NoNoneWhether the field is required
disabledOption<bool>NoNoneWhether the field is disabled
errorOption<String>NoNoneValidation error message
descriptionOption<String>NoNoneHelp text below the input
default_valueOption<String>NoNonePre-filled value
data_pathOption<String>NoNoneData path for pre-filling from handler data (e.g., "/data/user/name")
stepOption<String>NoNoneHTML step attribute for number inputs (e.g., "any", "0.01"). Controls valid increment granularity.

InputType variants:

ValueSerialized
InputType::Text"text"
InputType::Email"email"
InputType::Password"password"
InputType::Number"number"
InputType::Textarea"textarea"
InputType::Hidden"hidden"
InputType::Date"date"
InputType::Time"time"
InputType::Url"url"
InputType::Tel"tel"
InputType::Search"search"
#![allow(unused)]
fn main() {
use ferro::*;

ComponentNode {
    key: "email-input".to_string(),
    component: Component::Input(InputProps {
        field: "email".to_string(),
        label: "Email Address".to_string(),
        input_type: InputType::Email,
        placeholder: Some("user@example.com".to_string()),
        required: Some(true),
        disabled: None,
        error: None,
        description: Some("Your work email".to_string()),
        default_value: None,
        data_path: Some("/data/user/email".to_string()),
        step: None,
    }),
    action: None,
    visibility: None,
}
}

Select

Dropdown select field with options, validation error, and data binding.

PropTypeRequiredDefaultDescription
fieldStringYes-Form field name for data binding
labelStringYes-Select label text
optionsVec<SelectOption>Yes-Options list
placeholderOption<String>NoNonePlaceholder text
requiredOption<bool>NoNoneWhether the field is required
disabledOption<bool>NoNoneWhether the field is disabled
errorOption<String>NoNoneValidation error message
descriptionOption<String>NoNoneHelp text below the select
default_valueOption<String>NoNonePre-selected value
data_pathOption<String>NoNoneData path for pre-filling from handler data

SelectOption defines a value-label pair:

FieldTypeRequiredDescription
valueStringYesOption value submitted with the form
labelStringYesDisplay text shown to the user
#![allow(unused)]
fn main() {
use ferro::*;

ComponentNode {
    key: "role-select".to_string(),
    component: Component::Select(SelectProps {
        field: "role".to_string(),
        label: "Role".to_string(),
        options: vec![
            SelectOption {
                value: "admin".to_string(),
                label: "Administrator".to_string(),
            },
            SelectOption {
                value: "editor".to_string(),
                label: "Editor".to_string(),
            },
            SelectOption {
                value: "viewer".to_string(),
                label: "Viewer".to_string(),
            },
        ],
        placeholder: Some("Select a role".to_string()),
        required: Some(true),
        disabled: None,
        error: None,
        description: None,
        default_value: None,
        data_path: Some("/data/user/role".to_string()),
    }),
    action: None,
    visibility: None,
}
}

Checkbox

Boolean checkbox field with label, description, and data binding.

PropTypeRequiredDefaultDescription
fieldStringYes-Form field name for data binding
labelStringYes-Checkbox label text
descriptionOption<String>NoNoneHelp text below the checkbox
checkedOption<bool>NoNoneDefault checked state
data_pathOption<String>NoNoneData path for pre-filling from handler data
requiredOption<bool>NoNoneWhether the field is required
disabledOption<bool>NoNoneWhether the field is disabled
errorOption<String>NoNoneValidation error message
#![allow(unused)]
fn main() {
use ferro::*;

ComponentNode {
    key: "terms-checkbox".to_string(),
    component: Component::Checkbox(CheckboxProps {
        field: "terms".to_string(),
        label: "Accept Terms of Service".to_string(),
        description: Some("You must accept to continue.".to_string()),
        checked: None,
        data_path: None,
        required: Some(true),
        disabled: None,
        error: None,
    }),
    action: None,
    visibility: None,
}
}

Switch

Toggle switch -- a visual alternative to Checkbox with identical props. The frontend renderer handles the visual difference.

PropTypeRequiredDefaultDescription
fieldStringYes-Form field name for data binding
labelStringYes-Switch label text
descriptionOption<String>NoNoneHelp text below the switch
checkedOption<bool>NoNoneDefault checked state
data_pathOption<String>NoNoneData path for pre-filling from handler data
requiredOption<bool>NoNoneWhether the field is required
disabledOption<bool>NoNoneWhether the field is disabled
errorOption<String>NoNoneValidation error message
#![allow(unused)]
fn main() {
use ferro::*;

ComponentNode {
    key: "notifications-switch".to_string(),
    component: Component::Switch(SwitchProps {
        field: "notifications".to_string(),
        label: "Enable Notifications".to_string(),
        description: Some("Receive email notifications".to_string()),
        checked: Some(true),
        data_path: Some("/data/user/notifications_enabled".to_string()),
        required: None,
        disabled: None,
        error: None,
    }),
    action: None,
    visibility: None,
}
}

Tabs

Tabbed content with multiple panels. Each tab contains its own set of child components.

PropTypeRequiredDefaultDescription
default_tabStringYes-Value of the initially active tab
tabsVec<Tab>Yes-Tab definitions

Tab defines a tab panel:

FieldTypeRequiredDescription
valueStringYesTab identifier (matches default_tab)
labelStringYesTab label text
childrenVec<ComponentNode>NoComponents displayed when the tab is active
#![allow(unused)]
fn main() {
use ferro::*;

ComponentNode {
    key: "settings-tabs".to_string(),
    component: Component::Tabs(TabsProps {
        default_tab: "general".to_string(),
        tabs: vec![
            Tab {
                value: "general".to_string(),
                label: "General".to_string(),
                children: vec![
                    ComponentNode {
                        key: "name-input".to_string(),
                        component: Component::Input(InputProps {
                            field: "name".to_string(),
                            label: "Name".to_string(),
                            input_type: InputType::Text,
                            placeholder: None,
                            required: None,
                            disabled: None,
                            error: None,
                            description: None,
                            default_value: None,
                            data_path: None,
                        }),
                        action: None,
                        visibility: None,
                    },
                ],
            },
            Tab {
                value: "security".to_string(),
                label: "Security".to_string(),
                children: vec![
                    ComponentNode {
                        key: "password-input".to_string(),
                        component: Component::Input(InputProps {
                            field: "password".to_string(),
                            label: "Password".to_string(),
                            input_type: InputType::Password,
                            placeholder: None,
                            required: None,
                            disabled: None,
                            error: None,
                            description: None,
                            default_value: None,
                            data_path: None,
                        }),
                        action: None,
                        visibility: None,
                    },
                ],
            },
        ],
    }),
    action: None,
    visibility: None,
}
}

Pagination

Page navigation for paginated data. Computes page count from total and per_page.

PropTypeRequiredDefaultDescription
current_pageu32Yes-Current page number
per_pageu32Yes-Items per page
totalu32Yes-Total number of items
base_urlOption<String>NoNoneBase URL for page links
#![allow(unused)]
fn main() {
use ferro::*;

ComponentNode {
    key: "users-pagination".to_string(),
    component: Component::Pagination(PaginationProps {
        current_page: 1,
        per_page: 25,
        total: 150,
        base_url: Some("/users".to_string()),
    }),
    action: None,
    visibility: None,
}
}

Navigation breadcrumb trail. The last item typically has no URL (current page).

PropTypeRequiredDefaultDescription
itemsVec<BreadcrumbItem>Yes-Breadcrumb items

BreadcrumbItem defines a breadcrumb entry:

FieldTypeRequiredDescription
labelStringYesBreadcrumb text
urlOption<String>NoLink URL (omit for the current page)
#![allow(unused)]
fn main() {
use ferro::*;

ComponentNode {
    key: "breadcrumbs".to_string(),
    component: Component::Breadcrumb(BreadcrumbProps {
        items: vec![
            BreadcrumbItem {
                label: "Home".to_string(),
                url: Some("/".to_string()),
            },
            BreadcrumbItem {
                label: "Users".to_string(),
                url: Some("/users".to_string()),
            },
            BreadcrumbItem {
                label: "Edit User".to_string(),
                url: None,
            },
        ],
    }),
    action: None,
    visibility: None,
}
}

Feedback Components

Progress

Progress bar with percentage value.

PropTypeRequiredDefaultDescription
valueu8Yes-Percentage value (0-100)
maxOption<u8>NoNoneMaximum value
labelOption<String>NoNoneLabel text above the bar
#![allow(unused)]
fn main() {
use ferro::*;

ComponentNode {
    key: "upload-progress".to_string(),
    component: Component::Progress(ProgressProps {
        value: 75,
        max: Some(100),
        label: Some("Uploading...".to_string()),
    }),
    action: None,
    visibility: None,
}
}

Avatar

User avatar with image source, fallback text, and size variants.

PropTypeRequiredDefaultDescription
srcOption<String>NoNoneImage URL
altStringYes-Alt text (required for accessibility)
fallbackOption<String>NoNoneFallback initials when no image
sizeOption<Size>NoDefaultAvatar size: xs, sm, default, lg
#![allow(unused)]
fn main() {
use ferro::*;

ComponentNode {
    key: "user-avatar".to_string(),
    component: Component::Avatar(AvatarProps {
        src: Some("/images/alice.jpg".to_string()),
        alt: "Alice Johnson".to_string(),
        fallback: Some("AJ".to_string()),
        size: Some(Size::Lg),
    }),
    action: None,
    visibility: None,
}
}

Skeleton

Loading placeholder with configurable dimensions for content that is still loading.

PropTypeRequiredDefaultDescription
widthOption<String>NoNoneCSS width (e.g., "100%", "200px")
heightOption<String>NoNoneCSS height (e.g., "40px")
roundedOption<bool>NoNoneWhether to use rounded corners
#![allow(unused)]
fn main() {
use ferro::*;

ComponentNode {
    key: "loading-placeholder".to_string(),
    component: Component::Skeleton(SkeletonProps {
        width: Some("100%".to_string()),
        height: Some("40px".to_string()),
        rounded: Some(true),
    }),
    action: None,
    visibility: None,
}
}

Layout Components

Dialog overlay with title, content, footer, and trigger button.

PropTypeRequiredDefaultDescription
titleStringYes-Modal title
descriptionOption<String>NoNoneModal description
childrenVec<ComponentNode>No[]Content components inside the modal body
footerVec<ComponentNode>No[]Components in the modal footer
trigger_labelOption<String>NoNoneLabel for the trigger button
#![allow(unused)]
fn main() {
use ferro::*;

ComponentNode {
    key: "delete-modal".to_string(),
    component: Component::Modal(ModalProps {
        title: "Delete Item".to_string(),
        description: Some("This action cannot be undone.".to_string()),
        children: vec![
            ComponentNode {
                key: "confirm-text".to_string(),
                component: Component::Text(TextProps {
                    content: "Are you sure you want to delete this item?".to_string(),
                    element: TextElement::P,
                }),
                action: None,
                visibility: None,
            },
        ],
        footer: vec![
            ComponentNode {
                key: "cancel-btn".to_string(),
                component: Component::Button(ButtonProps {
                    label: "Cancel".to_string(),
                    variant: ButtonVariant::Outline,
                    size: Size::Default,
                    disabled: None,
                    icon: None,
                    icon_position: None,
                }),
                action: None,
                visibility: None,
            },
            ComponentNode {
                key: "delete-btn".to_string(),
                component: Component::Button(ButtonProps {
                    label: "Delete".to_string(),
                    variant: ButtonVariant::Destructive,
                    size: Size::Default,
                    disabled: None,
                    icon: None,
                    icon_position: None,
                }),
                action: Some(Action::delete("items.destroy")
                    .confirm_danger("Confirm deletion")),
                visibility: None,
            },
        ],
        trigger_label: Some("Delete".to_string()),
    }),
    action: None,
    visibility: None,
}
}

JSON Output

Every component tree serializes to JSON via serde. The Component enum uses serde's tagged representation, producing a "type" field that identifies the component:

{
  "key": "welcome-card",
  "type": "Card",
  "title": "Welcome",
  "description": "Your dashboard",
  "children": [
    {
      "key": "greeting",
      "type": "Text",
      "content": "Hello, Alice!",
      "element": "h2"
    }
  ],
  "footer": []
}

The ComponentNode fields (key, action, visibility) are flattened into the same JSON object alongside the component-specific props. This produces clean, predictable JSON that frontend renderers can consume directly.