Biji UI

Select

An accessible custom select that displays a list of options in an anchor-positioned overlay.

Nothing selected yet

Installation

biji-ui = { version = "0.5.0", features = ["select"] }

Usage

use leptos::prelude::*;
use biji_ui::components::select;

// Reusable class constants
const TRIGGER_CLS: &str =
    "flex h-10 w-48 items-center justify-between rounded-md border border-input \
     bg-background px-3 py-2 text-sm ring-offset-background \
     focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring \
     focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 \
     data-[state=open]:ring-2 data-[state=open]:ring-ring";

const CONTENT_CLS: &str =
    "z-50 min-w-[8rem] overflow-hidden rounded-md border border-border \
     bg-background text-foreground shadow-md text-sm py-1 \
     transition origin-[var(--biji-transform-origin)]";

const ITEM_CLS: &str =
    "relative flex w-full cursor-default select-none items-center justify-between \
     rounded-sm px-3 py-1.5 text-sm outline-none \
     hover:bg-accent hover:text-accent-foreground \
     focus:bg-accent focus:text-accent-foreground \
     data-[disabled]:pointer-events-none data-[disabled]:opacity-50";

#[component]
pub fn MySelect() -> impl IntoView {
    view! {
        <select::Root>
            <select::Trigger class=TRIGGER_CLS>
                <select::Value placeholder="Select a fruit..." />
                <svg class="h-4 w-4 opacity-50 shrink-0" viewBox="0 0 24 24" fill="none"
                    stroke="currentColor" stroke-width="2">
                    <path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7"/>
                </svg>
            </select::Trigger>
            <select::Content
                class=CONTENT_CLS
                show_class="opacity-100 scale-100 duration-150 ease-out"
                hide_class="opacity-0 scale-95 duration-100 ease-in"
            >
                <select::Item value="apple" class=ITEM_CLS>
                    <select::ItemText>"Apple"</select::ItemText>
                    <select::ItemIndicator>
                        <svg class="h-4 w-4" viewBox="0 0 24 24" fill="none"
                            stroke="currentColor" stroke-width="2.5">
                            <path stroke-linecap="round" stroke-linejoin="round"
                                d="M5 13l4 4L19 7"/>
                        </svg>
                    </select::ItemIndicator>
                </select::Item>
                <select::Item value="banana" class=ITEM_CLS>
                    <select::ItemText>"Banana"</select::ItemText>
                    <select::ItemIndicator>
                        <svg class="h-4 w-4" viewBox="0 0 24 24" fill="none"
                            stroke="currentColor" stroke-width="2.5">
                            <path stroke-linecap="round" stroke-linejoin="round"
                                d="M5 13l4 4L19 7"/>
                        </svg>
                    </select::ItemIndicator>
                </select::Item>
                <select::Item value="cherry" class=ITEM_CLS>
                    <select::ItemText>"Cherry"</select::ItemText>
                    <select::ItemIndicator>
                        <svg class="h-4 w-4" viewBox="0 0 24 24" fill="none"
                            stroke="currentColor" stroke-width="2.5">
                            <path stroke-linecap="round" stroke-linejoin="round"
                                d="M5 13l4 4L19 7"/>
                        </svg>
                    </select::ItemIndicator>
                </select::Item>
            </select::Content>
        </select::Root>
    }
}

RootWith

Use RootWith to access SelectState inline via the let: binding. The state is Copy and safe to pass as a prop.

Nothing selected

use leptos::prelude::*;
use biji_ui::components::select;

#[component]
pub fn MySelect() -> impl IntoView {
    view! {
        <select::RootWith let:s>
            <p class="text-sm text-muted-foreground">
                {move || {
                    if s.open.get() {
                        "Dropdown is open".to_string()
                    } else {
                        s.value.get()
                            .map(|v| format!("Selected: {v}"))
                            .unwrap_or_else(|| "Nothing selected".to_string())
                    }
                }}
            </p>
            <select::Trigger class="...">
                <select::Value placeholder="Select a fruit..." />
            </select::Trigger>
            <select::Content class="..." show_class="..." hide_class="...">
                <select::Item value="apple" label="Apple" class="...">
                    <select::ItemText>"Apple"</select::ItemText>
                </select::Item>
            </select::Content>
        </select::RootWith>
    }
}

API Reference

Root / RootWith

NameTypeDefaultDescription
classString""CSS class applied to the root wrapper element.
valueOption<String>NoneThe initially selected value.
positioningPositioningBottomStartWhere to render the content relative to the trigger.
hide_delayDuration200msHow long to wait before unmounting the content after closing begins.
avoid_collisionsAvoidCollisionsFlipHow the overlay reacts when it would overflow the viewport.
on_value_changeOption<Callback<String>>NoneCallback fired when the selected value changes.

Trigger

NameTypeDefaultDescription
classString""CSS class applied to the trigger button. Use data-[state=open]:... to style the open state.

Value

NameTypeDefaultDescription
placeholderString""Text shown when no value is selected.

Content

NameTypeDefaultDescription
classString""CSS class applied in both open and closed states. Add origin-[var(--biji-transform-origin)] to scale animations from the trigger direction.
show_classString""CSS class applied when the select is open.
hide_classString""CSS class applied while the select is closing.

Item

NameTypeDefaultDescription
valueString The value this item represents (stored in context on selection).
labelOption<String>valueDisplay text shown in the trigger when this item is selected. Defaults to value if not provided.
classString""CSS class applied to the item element. Use hover: and focus: to style the highlighted state.
disabledboolfalseWhen true, the item cannot be selected.

Data Attributes

AttributeDescription
data-state"open" or "closed" on Trigger. "checked" or "unchecked" on Item.
data-highlightedPresent on Item when it has keyboard focus or is hovered. Style the focused state with hover: and focus: Tailwind classes on the item directly.
data-disabledPresent on Item when the item is disabled.

Keyboard Navigation

KeyDescription
ArrowDownOpens the select and focuses the first item; navigates to the next item when open.
ArrowUpOpens the select and focuses the last item; navigates to the previous item when open.
Enter / SpaceSelects the focused item and closes the dropdown.
HomeMoves focus to the first item.
EndMoves focus to the last item.
EscapeCloses the select without selecting and returns focus to the trigger.
TabCloses the select and moves focus to the next focusable element.