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
| Name | Type | Default | Description |
|---|---|---|---|
| class | String | "" | CSS class applied to the root wrapper element. |
| value | Option<String> | None | The initially selected value. |
| positioning | Positioning | BottomStart | Where to render the content relative to the trigger. |
| hide_delay | Duration | 200ms | How long to wait before unmounting the content after closing begins. |
| avoid_collisions | AvoidCollisions | Flip | How the overlay reacts when it would overflow the viewport. |
| on_value_change | Option<Callback<String>> | None | Callback fired when the selected value changes. |
Trigger
| Name | Type | Default | Description |
|---|---|---|---|
| class | String | "" | CSS class applied to the trigger button. Use data-[state=open]:... to style the open state. |
Value
| Name | Type | Default | Description |
|---|---|---|---|
| placeholder | String | "" | Text shown when no value is selected. |
Content
| Name | Type | Default | Description |
|---|---|---|---|
| class | String | "" | CSS class applied in both open and closed states. Add origin-[var(--biji-transform-origin)] to scale animations from the trigger direction. |
| show_class | String | "" | CSS class applied when the select is open. |
| hide_class | String | "" | CSS class applied while the select is closing. |
Item
| Name | Type | Default | Description |
|---|---|---|---|
| value | String | The value this item represents (stored in context on selection). | |
| label | Option<String> | value | Display text shown in the trigger when this item is selected. Defaults to value if not provided. |
| class | String | "" | CSS class applied to the item element. Use hover: and focus: to style the highlighted state. |
| disabled | bool | false | When true, the item cannot be selected. |
Data Attributes
| Attribute | Description |
|---|---|
| data-state | "open" or "closed" on Trigger. "checked" or "unchecked" on Item. |
| data-highlighted | Present 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-disabled | Present on Item when the item is disabled. |
Keyboard Navigation
| Key | Description |
|---|---|
| ArrowDown | Opens the select and focuses the first item; navigates to the next item when open. |
| ArrowUp | Opens the select and focuses the last item; navigates to the previous item when open. |
| Enter / Space | Selects the focused item and closes the dropdown. |
| Home | Moves focus to the first item. |
| End | Moves focus to the last item. |
| Escape | Closes the select without selecting and returns focus to the trigger. |
| Tab | Closes the select and moves focus to the next focusable element. |