Select
Collapsible menu allowing users to select an option from a predefined list.
Select
The Select component presents a collapsible menu of options, enabling users to choose a single value from a predetermined set. When closed, it displays the currently selected value or a welcoming placeholder; when triggered, it opens a floating listbox styled with Hashti’s serene architectural geometry.
Built on React Aria Components’ Select, it provides full typeahead searching, keyboard navigation, popover collision repositioning, and screen reader announcements.
Import
import { Select, ListBoxItem, ListBoxSection, SectionHeader } from "@hashti/react";
Sub-parts for advanced composition:
import { Select, FieldLayout, Popover, ListBox } from "@hashti/react";
Examples
Standard Select
A basic select dropdown using static <ListBoxItem> children:
import { Select, ListBoxItem } from "@hashti/react";
export function BasicSelectDemo() {
return (
<div className="max-w-xs">
<Select label="Architectural Style" placeholder="Choose a style">
<ListBoxItem id="isfahani">Isfahani Style (سبک اصفهانی)</ListBoxItem>
<ListBoxItem id="razi">Razi Style (سبک رازی)</ListBoxItem>
<ListBoxItem id="azari">Azari Style (سبک آذری)</ListBoxItem>
<ListBoxItem id="khorasani">Khorasani Style (سبک خراسانی)</ListBoxItem>
</Select>
</div>
);
}
Dynamic Items Collection
Render options dynamically from an array of objects:
import { useState } from "react";
import { Select, ListBoxItem } from "@hashti/react";
import type { Key } from "react-aria-components";
interface City {
id: string;
name: string;
province: string;
}
const CITIES: City[] = [
{ id: "isfahan", name: "Isfahan", province: "Isfahan Province" },
{ id: "shiraz", name: "Shiraz", province: "Fars Province" },
{ id: "yazd", name: "Yazd", province: "Yazd Province" },
{ id: "kashan", name: "Kashan", province: "Isfahan Province" },
{ id: "tabriz", name: "Tabriz", province: "East Azerbaijan" },
];
export function DynamicSelectDemo() {
const [selectedCity, setSelectedCity] = useState<Key>("isfahan");
return (
<div className="max-w-xs flex flex-col gap-2">
<Select
label="Historical Center"
items={CITIES}
selectedKey={selectedCity}
onSelectionChange={setSelectedCity}
description="Select your target preservation region."
>
{(city) => (
<ListBoxItem id={city.id} textValue={city.name}>
<div className="flex flex-col">
<span className="font-medium">{city.name}</span>
<span className="text-xs text-gray-500">{city.province}</span>
</div>
</ListBoxItem>
)}
</Select>
</div>
);
}
Disabled Options & Disabled State
You can disable specific individual options using disabledKeys, or disable the entire select component with isDisabled:
import { Select, ListBoxItem } from "@hashti/react";
export function DisabledSelectDemo() {
return (
<div className="flex flex-col gap-4 max-w-xs">
{/* Individual items disabled */}
<Select label="Room Type" disabledKeys={["penthouse"]} placeholder="Select room">
<ListBoxItem id="standard">Standard Garden View</ListBoxItem>
<ListBoxItem id="suite">Courtyard Suite</ListBoxItem>
<ListBoxItem id="penthouse">Rooftop Pavilion (Sold Out)</ListBoxItem>
</Select>
{/* Whole component disabled */}
<Select label="Booking Season" isDisabled defaultSelectedKey="spring">
<ListBoxItem id="spring">Spring Nowruz (Bookings Closed)</ListBoxItem>
<ListBoxItem id="autumn">Autumn</ListBoxItem>
</Select>
</div>
);
}
Validation Error State
When validation fails, pass isInvalid and an errorMessage:
import { Select, ListBoxItem } from "@hashti/react";
export function InvalidSelectDemo() {
return (
<div className="max-w-xs">
<Select
label="Vault Material"
placeholder="Choose material..."
isInvalid
errorMessage="Selecting a structural vault material is required to proceed."
>
<ListBoxItem id="adobe">Sun-dried Adobe</ListBoxItem>
<ListBoxItem id="fired-brick">Fired Kiln Brick</ListBoxItem>
<ListBoxItem id="glazed-tile">Glazed Ceramic Tile</ListBoxItem>
</Select>
</div>
);
}
Multi-Selection with Chips, Badges, or Text
Set selectionMode="multiple" to enable picking several options. The multipleDisplay prop controls how the selected values are displayed in the closed trigger:
'chips'(default): Interactive tags with individual remove buttons (×).'badge': Displays the first selected item along with a count pill badge (e.g.+2).'text': Comma-separated plain text summary.
import { useState } from "react";
import { Select, ListBoxItem } from "@hashti/react";
import type { Selection } from "react-aria-components";
export function MultiSelectDemo() {
const [selected, setSelected] = useState<Selection>(new Set(["react", "astro"]));
return (
<div className="max-w-md space-y-4">
{/* Chips display (default) */}
<Select
label="Technologies (Chips)"
selectionMode="multiple"
multipleDisplay="chips"
selectedKeys={selected}
onSelectionChange={setSelected}
>
<ListBoxItem id="react">React 19</ListBoxItem>
<ListBoxItem id="astro">Astro</ListBoxItem>
<ListBoxItem id="tailwind">Tailwind CSS v4</ListBoxItem>
<ListBoxItem id="bun">Bun Runtime</ListBoxItem>
</Select>
{/* Badge display */}
<Select
label="Technologies (Badge)"
selectionMode="multiple"
multipleDisplay="badge"
selectedKeys={selected}
onSelectionChange={setSelected}
>
<ListBoxItem id="react">React 19</ListBoxItem>
<ListBoxItem id="astro">Astro</ListBoxItem>
<ListBoxItem id="tailwind">Tailwind CSS v4</ListBoxItem>
<ListBoxItem id="bun">Bun Runtime</ListBoxItem>
</Select>
</div>
);
}
Grouped Sections
Use <ListBoxSection> and <SectionHeader> to organize items into clear categorical groupings:
import { Select, ListBoxItem, ListBoxSection, SectionHeader } from "@hashti/react";
export function GroupedSelectDemo() {
return (
<div className="max-w-xs">
<Select label="Spatial Elements" placeholder="Select an architectural feature">
<ListBoxSection>
<SectionHeader className="font-semibold text-xs text-gray-400 px-3 py-1">
Transitional Spaces
</SectionHeader>
<ListBoxItem id="hashti">Hashti (Octagonal Vestibule)</ListBoxItem>
<ListBoxItem id="dalan">Dalan (Connecting Corridor)</ListBoxItem>
<ListBoxItem id="sar-dar">Sar-dar (Entrance Portal)</ListBoxItem>
</ListBoxSection>
<ListBoxSection>
<SectionHeader className="font-semibold text-xs text-gray-400 px-3 py-1">
Interior Sanctuaries
</SectionHeader>
<ListBoxItem id="iwan">Iwan (Vaulted Hall)</ListBoxItem>
<ListBoxItem id="howz-khaneh">Howz-Khaneh (Pool Room)</ListBoxItem>
<ListBoxItem id="panj-dari">Panj-Dari (Five-Door Living Room)</ListBoxItem>
</ListBoxSection>
</Select>
</div>
);
}
Props Reference
| Prop | Type | Default | Description |
|---|---|---|---|
selectionMode | 'single' | 'multiple' | 'single' | Selection mode allowing single or multiple option choices. |
multipleDisplay | 'chips' | 'badge' | 'text' | 'chips' | Display format for multi-selected items in the trigger. |
selectedKey | Key | — | The currently selected option ID (controlled). |
defaultSelectedKey | Key | — | The initial selected option ID (uncontrolled). |
onSelectionChange | (key: Key) => void | — | Handler invoked when an option is selected. |
items | Iterable<T> | — | Dynamic item collection array or iterable. |
children | ReactNode | ((item: T) => ReactNode) | — | Options to render within the select dropdown. |
placeholder | string | — | Prompt text shown when no item has been selected. |
label | ReactNode | — | Accessible label for the select element. |
description | ReactNode | — | Guidance text rendered with the field. |
errorMessage | ReactNode | — | Error text rendered when isInvalid is true. |
size | 'sm' | 'md' | 'lg' | 'md' | Height and scale of the trigger button. |
isDisabled | boolean | false | Whether the select dropdown is disabled. |
isInvalid | boolean | false | Marks the select as invalid and activates error styling. |
isRequired | boolean | false | Indicates that an option selection is required. |
disabledKeys | Iterable<Key> | — | Keys of specific options that should be disabled. |
renderValue | (items: T[]) => ReactNode | — | Custom renderer for the closed trigger display value. |
Accessibility
Select complies with the WAI-ARIA combobox specification:
- Opening & Closing:
- Pressing Space, Enter, or ↓ opens the popup listbox.
- Pressing Esc closes the listbox and restores focus to the select trigger button.
- Keyboard Navigation:
- ↑ and ↓ navigate between options.
- Home moves focus to the first item; End jumps to the last item.
- Typeahead Letter Jumping: Typing letters (e.g. “I”, “S”, “H”) immediately jumps focus to the next matching item.
- Focus Management:
- Focus is never lost; closing returns focus directly to the trigger button.
- While open,
aria-activedescendantis tracked on the focused item, ensuring screen readers announce options accurately.
- ARIA Attributes:
- The trigger has
role="combobox",aria-haspopup="listbox",aria-expanded="false|true", andaria-labelledbyreferencing the label.
- The trigger has
Keyboard Interactions
| Key | Action |
|---|---|
| Space / Enter / ↓ | Opens the dropdown listbox and focuses the active item. |
| ↑ / ↓ | Navigates between items in the listbox. |
| Home / End | Moves focus to the first or last available option. |
| Esc | Closes the dropdown listbox and restores focus to the trigger. |
| Tab | Closes the listbox and moves focus to the next interactive page element. |
WAI-ARIA & Data Attributes
| Attribute | Target | Description |
|---|---|---|
[data-hovered] | Trigger, Item | Present when the pointer hovers over the element. |
[data-focused] | Trigger, Item | Present when the element holds keyboard or pointer focus. |
[data-pressed] | Trigger, Item | Active while the element is actively pressed. |
[data-selected] | Item | Present on currently selected listbox options. |
[data-disabled] | Trigger, Item | Present when the element is disabled from interaction. |