Input Group

A component that combines inputs with addons like icons, text, or buttons.

input
  • Rust/UI Icons - CopyCopy Demo
Rust/UI Icons - Search
Rust/UI Icons - Mail
Rust/UI Icons - CreditCard
Rust/UI Icons - Check
Rust/UI Icons - StarRust/UI Icons - Info
use icons::{Check, CreditCard, Info, Mail, Search, Star};
use leptos::prelude::*;

use crate::components::ui::input_group::{InputGroup, InputGroupAddon, InputGroupAddonAlign, InputGroupInput};

#[component]
pub fn DemoInputGroup() -> impl IntoView {
    view! {
        <div class="grid gap-6 w-full max-w-sm">
            <InputGroup>
                <InputGroupInput attr:placeholder="Search..." />
                <InputGroupAddon>
                    <Search />
                </InputGroupAddon>
            </InputGroup>

            <InputGroup>
                <InputGroupInput attr:placeholder="Enter your email" />
                <InputGroupAddon>
                    <Mail />
                </InputGroupAddon>
            </InputGroup>

            <InputGroup>
                <InputGroupInput attr:placeholder="Card number" />
                <InputGroupAddon>
                    <CreditCard />
                </InputGroupAddon>
                <InputGroupAddon align=InputGroupAddonAlign::InlineEnd>
                    <Check />
                </InputGroupAddon>
            </InputGroup>

            <InputGroup>
                <InputGroupInput attr:placeholder="Card number" />
                <InputGroupAddon align=InputGroupAddonAlign::InlineEnd>
                    <Star />
                    <Info />
                </InputGroupAddon>
            </InputGroup>
        </div>
    }
}

Installation

You can run either of the following commands:

# cargo install ui-cli --force
ui add demo_input_group
ui add input_group

Update the imports to match your project setup.

Copy and paste the following code into your project:

components/ui/input_group.rs

use leptos::prelude::*;
use leptos_ui::{clx, variants};
use tw_merge::{TwClass, TwVariant, tw_merge};

use crate::registry::ui::input::Input;
use crate::registry::ui::textarea::Textarea;

mod components {
    use super::*;
    clx! {InputGroupText, span, "text-muted-foreground flex items-center gap-2 text-sm [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4"}
}

pub use components::*;

/* ========================================================== */
/*                     ✨ FUNCTIONS ✨                        */
/* ========================================================== */

#[component]
pub fn InputGroup(#[prop(into, optional)] class: String, children: Children) -> impl IntoView {
    let merged_class = tw_merge!(
        "group/input-group border-input dark:bg-input/30 relative flex w-full items-center rounded-md border shadow-xs transition-[color,box-shadow] outline-none h-9 min-w-0 has-[>textarea]:h-auto has-[>[data-align=inline-start]]:[&>input]:pl-2 has-[>[data-align=inline-end]]:[&>input]:pr-2 has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3 has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot=input-group-control]:focus-visible]:ring-[3px] has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[[data-slot][aria-invalid=true]]:border-destructive dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40",
        class
    );

    view! {
        <div data-name="InputGroup" data-slot="input-group" role="group" class=merged_class>
            {children()}
        </div>
    }
}

#[component]
pub fn InputGroupAddon(
    #[prop(default = InputGroupAddonAlign::default())] align: InputGroupAddonAlign,
    #[prop(into, optional)] class: String,
    children: Children,
) -> impl IntoView {
    let addon = InputGroupAddonClass { align };
    let merged_class = addon.with_class(class);

    let align_attr = match align {
        InputGroupAddonAlign::InlineStart => "inline-start",
        InputGroupAddonAlign::InlineEnd => "inline-end",
        InputGroupAddonAlign::BlockStart => "block-start",
        InputGroupAddonAlign::BlockEnd => "block-end",
    };

    view! {
        <div
            data-name="InputGroupAddon"
            data-slot="input-group-addon"
            data-align=align_attr
            role="group"
            class=merged_class
        >
            {children()}
        </div>
    }
}

#[derive(TwClass, Default)]
#[tw(
    class = "text-muted-foreground flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium select-none [&>svg:not([class*='size-'])]:size-4 [&>kbd]:rounded-[calc(var(--radius)-5px)] group-data-[disabled=true]/input-group:opacity-50"
)]
struct InputGroupAddonClass {
    align: InputGroupAddonAlign,
}

#[derive(TwVariant)]
pub enum InputGroupAddonAlign {
    #[tw(default, class = "order-first pl-3 has-[>button]:ml-[-0.45rem] has-[>kbd]:ml-[-0.35rem]")]
    InlineStart,
    #[tw(class = "order-last pr-3 has-[>button]:mr-[-0.45rem] has-[>kbd]:mr-[-0.35rem]")]
    InlineEnd,
    #[tw(class = "order-first w-full justify-start px-3 pt-3 [.border-b]:pb-3 group-has-[>input]/input-group:pt-2.5")]
    BlockStart,
    #[tw(class = "order-last w-full justify-start px-3 pb-3 [.border-t]:pt-3 group-has-[>input]/input-group:pb-2.5")]
    BlockEnd,
}

variants! {
    InputGroupButton {
        base: "text-sm shadow-none flex gap-2 items-center",
        variants: {
            variant: {
                Ghost: "",
            },
            size: {
                Xs: "h-6 gap-1 px-2 rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-3.5 has-[>svg]:px-2",
                Sm: "h-8 px-2.5 gap-1.5 rounded-md has-[>svg]:px-2.5",
                IconXs: "size-6 rounded-[calc(var(--radius)-5px)] p-0 has-[>svg]:p-0",
                IconSm: "size-8 p-0 has-[>svg]:p-0",
            }
        },
        component: {
            element: button
        }
    }
}

#[component]
pub fn InputGroupInput(#[prop(into, optional)] class: String) -> impl IntoView {
    let merged_class = tw_merge!(
        "flex-1 rounded-none border-0 bg-transparent shadow-none focus-visible:ring-0 dark:bg-transparent",
        class
    );

    view! { <Input attr:data-slot="input-group-control" class=merged_class /> }
}

#[component]
pub fn InputGroupTextarea(#[prop(into, optional)] class: String) -> impl IntoView {
    let merged_class = tw_merge!(
        "flex-1 resize-none rounded-none border-0 bg-transparent py-3 shadow-none focus-visible:ring-0 dark:bg-transparent",
        class
    );

    view! { <Textarea class=merged_class attr:data-slot="input-group-control" /> }
}

Update the imports to match your project setup.

Usage

use icons::Search;

use crate::components::ui::input_group::{InputGroup, InputGroupAddon, InputGroupInput};
<InputGroup>
    <InputGroupInput attr:placeholder="Search..." />
    <InputGroupAddon>
        <Search />
    </InputGroupAddon>
</InputGroup>

Examples

Input Group Text

  • Rust/UI Icons - CopyCopy Demo
$
USD
https://
.com
@company.com
use leptos::prelude::*;

use crate::components::ui::input_group::{
    InputGroup, InputGroupAddon, InputGroupAddonAlign, InputGroupInput, InputGroupText,
};

#[component]
pub fn DemoInputGroupText() -> impl IntoView {
    view! {
        <div class="grid gap-6 w-full max-w-sm">
            <InputGroup>
                <InputGroupAddon>
                    <InputGroupText>$</InputGroupText>
                </InputGroupAddon>
                <InputGroupInput attr:placeholder="0.00" />
                <InputGroupAddon align=InputGroupAddonAlign::InlineEnd>
                    <InputGroupText>USD</InputGroupText>
                </InputGroupAddon>
            </InputGroup>

            <InputGroup>
                <InputGroupAddon>
                    <InputGroupText>{"https://"}</InputGroupText>
                </InputGroupAddon>
                <InputGroupInput attr:placeholder="example.com" class="!pl-0.5" />
                <InputGroupAddon align=InputGroupAddonAlign::InlineEnd>
                    <InputGroupText>{".com"}</InputGroupText>
                </InputGroupAddon>
            </InputGroup>

            <InputGroup>
                <InputGroupInput attr:placeholder="Enter your username" />
                <InputGroupAddon align=InputGroupAddonAlign::InlineEnd>
                    <InputGroupText>@company.com</InputGroupText>
                </InputGroupAddon>
            </InputGroup>
        </div>
    }
}