

# Select

Custom select component.

# Installation

Copy the following code into your app directory.


### CLI

```bash
sapphireui add component select
```

### Manual Installation

```python
"""Custom select component."""

from typing import Any, Literal

from reflex.components.component import Component, ComponentNamespace
from reflex.components.core.foreach import foreach
from reflex.event import EventHandler, passthrough_event_spec
from reflex.utils.imports import ImportVar
from reflex.vars.base import Var

from .button import button
from ..base_ui import PACKAGE_NAME, BaseUIComponent
from ...icons.hugeicon import hi
from ...icons.others import select_arrow
from ...utils.twmerge import cn

LiteralSelectSize = Literal["xs", "sm", "md", "lg", "xl"]
LiteralAlign = Literal["start", "center", "end"]
LiteralSide = Literal["bottom", "inline-end", "inline-start", "left", "right", "top"]
LiteralPosition = Literal["absolute", "fixed"]
LiteralOrientation = Literal["horizontal", "vertical"]


class ClassNames:
    """Class names for select components."""

    TRIGGER = "border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
    VALUE = "flex-1 text-left cursor-default"
    ICON = "flex size-4 text-secondary-10 group-data-[disabled]/trigger:text-current"
    POPUP = "bg-popover/80 text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border border-input shadow-md"
    ITEM = "focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 px-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2"
    ITEM_INDICATOR = ""
    ITEM_TEXT = "text-start"
    GROUP = "p-1"
    GROUP_LABEL = "text-muted-foreground px-2 py-1.5 text-xs"
    SEPARATOR = "bg-border pointer-events-none -mx-1 my-1 h-px"
    ARROW = "data-[side=bottom]:top-[-8px] data-[side=left]:right-[-13px] data-[side=left]:rotate-90 data-[side=right]:left-[-13px] data-[side=right]:-rotate-90 data-[side=top]:bottom-[-8px] data-[side=top]:rotate-180"
    POSITIONER = "outline-none"
    SCROLL_ARROW_UP = "top-0 z-[1] flex h-4 w-full cursor-default items-center justify-center rounded-ui-md bg-secondary-1 text-center text-xs before:absolute before:top-[-100%] before:left-0 before:h-full before:w-full before:content-[''] data-[direction=down]:bottom-0 data-[direction=down]:before:bottom-[-100%]"
    SCROLL_ARROW_DOWN = "bottom-0 z-[1] flex h-4 w-full cursor-default items-center justify-center rounded-ui-md bg-secondary-1 text-center text-xs before:absolute before:top-[-100%] before:left-0 before:h-full before:w-full before:content-[''] data-[direction=down]:bottom-0 data-[direction=down]:before:bottom-[-100%]"


class SelectBaseComponent(BaseUIComponent):
    """Base component for select components."""

    library = f"{PACKAGE_NAME}/select"

    @property
    def import_var(self):
        """Return the import variable for the select component."""
        return ImportVar(tag="Select", package_path="", install=False)


class SelectRoot(SelectBaseComponent):
    """Groups all parts of the select. Doesn't render its own HTML element."""

    tag = "Select.Root"

    # Identifies the field when a form is submitted.
    name: Var[str]

    # The uncontrolled value of the select when it's initially rendered.
    # To render a controlled select, use the `value` prop instead.
    default_value: Var[Any]

    # The value of the select
    value: Var[Any]

    # Callback fired when the value of the select changes. Use when controlled.
    on_value_change: EventHandler[passthrough_event_spec(str)]

    # Whether the select popup is initially open.
    # To render a controlled select popup, use the `open` prop instead.
    default_open: Var[bool]

    # Whether the select popup is currently open
    open: Var[bool]

    # Event handler called when the select popup is opened or closed
    on_open_change: EventHandler[passthrough_event_spec(bool)]

    # A ref to imperative actions.
    # When specified, the select will not be unmounted when closed.
    # Instead, the `unmount` function must be called to unmount the select manually.
    # Useful when the select's animation is controlled by an external library.
    actions_ref: Var[str]

    # Custom comparison logic used to determine if a select item value matches the current selected value.
    # Useful when item values are objects without matching referentially.
    # Defaults to `Object.is` comparison.
    is_item_equal_to_value: Var[Any]

    # When the item values are objects, this function converts the object value to a string representation for display in the trigger.
    # If the shape of the object is `{ value, label }`, the label will be used automatically without needing to specify this prop.
    item_to_string_label: Var[Any]

    # When the item values are objects, this function converts the object value to a string representation for form submission.
    # If the shape of the object is `{ value, label }`, the value will be used automatically without needing to specify this prop.
    item_to_string_value: Var[Any]

    # Data structure of the items rendered in the select popup.
    # When specified, `<Select.Value>` renders the label of the selected item instead of the raw value.
    items: Var[Any]

    # Determines if the select enters a modal state when open.
    # - True: user interaction is limited to the select: document page scroll is locked and pointer interactions on outside elements are disabled.
    # - False: user interaction with the rest of the document is allowed. Defaults to True.
    modal: Var[bool]

    # Whether multiple items can be selected. Defaults to False.
    multiple: Var[bool]

    # Event handler called after any animations complete when the select popup is opened or closed
    on_open_change_complete: EventHandler[passthrough_event_spec(bool)]

    # Whether the component should ignore user interaction. Defaults to False.
    disabled: Var[bool]

    # Whether the user should be unable to choose a different option from the select popup. Defaults to False.
    read_only: Var[bool]

    # Whether the user must choose a value before submitting a form. Defaults to False.
    required: Var[bool]

    # A ref to access the hidden input element.
    input_ref: Var[Any]

    @classmethod
    def create(cls, *children, **props) -> BaseUIComponent:
        """Create the select root component."""
        props["data-slot"] = "select"
        return super().create(*children, **props)


class SelectTrigger(SelectBaseComponent):
    """A button that opens the select menu."""

    tag = "Select.Trigger"

    # Whether the component should ignore user interaction. Defaults to False.
    disabled: Var[bool]

    # The render prop
    render_: Var[Component]

    @classmethod
    def create(cls, *children, **props) -> BaseUIComponent:
        """Create the select trigger component."""
        props["data-slot"] = "select-trigger"
        cls.set_class_name(ClassNames.TRIGGER, props)
        return super().create(*children, **props)


class SelectValue(SelectBaseComponent):
    """Text label of the currently selected item."""

    tag = "Select.Value"

    # The render prop
    render_: Var[Component]

    @classmethod
    def create(cls, *children, **props) -> BaseUIComponent:
        """Create the dialog trigger component."""
        props["data-slot"] = "select-value"
        cls.set_class_name(ClassNames.VALUE, props)
        return super().create(*children, **props)


class SelectBackdrop(SelectBaseComponent):
    """An overlay displayed beneath the menu popup."""

    tag = "Select.Backdrop"

    # The render prop
    render_: Var[Component]


class SelectPortal(SelectBaseComponent):
    """A portal element that moves the popup to a different part of the DOM.
    By default, the portal element is appended to <body>.
    """

    tag = "Select.Portal"

    # A parent element to render the portal element into.
    container: Var[str]


class SelectPositioner(SelectBaseComponent):
    """Positions the select menu popup."""

    tag = "Select.Positioner"

    # How to align the popup relative to the specified side. Defaults to "center".
    align: Var[LiteralAlign]

    # Additional offset along the alignment axis in pixels. Defaults to 0.
    align_offset: Var[int]

    # Which side of the anchor element to align the popup against. May automatically change to avoid collisions. Defaults to "bottom".
    side: Var[LiteralSide]

    # Minimum distance to maintain between the arrow and the edges of the popup.
    # Use it to prevent the arrow element from hanging out of the rounded corners of a popup. Defaults to 5.
    arrow_padding: Var[int]

    # Additional space to maintain from the edge of the collision boundary. Defaults to 5.
    collision_padding: Var[int | list[int]]

    # Whether to maintain the popup in the viewport after the anchor element was scrolled out of view. Defaults to False.
    sticky: Var[bool]

    # Determines which CSS position property to use. Defaults to "absolute".
    position_method: Var[LiteralPosition]

    # Whether the positioner overlaps the trigger so the selected item's text is aligned with the trigger's value text. This only applies to mouse input and is automatically disabled if there is not enough space. Defaults to False.
    align_item_with_trigger: Var[bool] = Var.create(False)

    # Whether the popup tracks any layout shift of its positioning anchor. Defaults to True.
    track_anchor: Var[bool]

    # Distance between the anchor and the popup in pixels. Defaults to 0.
    side_offset: Var[int]

    # Determines how to handle collisions when positioning the popup.
    collision_avoidance: Var[str]

    # The render prop
    render_: Var[Component]

    @classmethod
    def create(cls, *children, **props) -> BaseUIComponent:
        """Create the dialog trigger component."""
        props["data-slot"] = "select-positioner"
        props.setdefault("side_offset", 4)
        cls.set_class_name(ClassNames.POSITIONER, props)
        return super().create(*children, **props)


class SelectPopup(SelectBaseComponent):
    """A container for the select items."""

    tag = "Select.Popup"

    # The render prop
    render_: Var[Component]

    @classmethod
    def create(cls, *children, **props) -> BaseUIComponent:
        """Create the dialog trigger component."""
        props["data-slot"] = "select-popup"
        cls.set_class_name(ClassNames.POPUP, props)
        return super().create(*children, **props)


class SelectItem(SelectBaseComponent):
    """An individual option in the select menu."""

    tag = "Select.Item"

    # Overrides the text label to use on the trigger when this item is selected and when the item is matched during keyboard text navigation.
    label: Var[str]

    # A unique value that identifies this select item.
    value: Var[Any]

    # Whether the component should ignore user interaction.
    disabled: Var[bool]

    # The render prop
    render_: Var[Component]

    @classmethod
    def create(cls, *children, **props) -> BaseUIComponent:
        """Create the dialog trigger component."""
        props["data-slot"] = "select-item"
        cls.set_class_name(ClassNames.ITEM, props)
        return super().create(*children, **props)


class SelectItemText(SelectBaseComponent):
    """A text label of the select item."""

    tag = "Select.ItemText"

    # The render prop
    render_: Var[Component]

    @classmethod
    def create(cls, *children, **props) -> BaseUIComponent:
        """Create the dialog trigger component."""
        props["data-slot"] = "select-item-text"
        cls.set_class_name(ClassNames.ITEM_TEXT, props)
        return super().create(*children, **props)


class SelectItemIndicator(SelectBaseComponent):
    """Indicates whether the select item is selected."""

    tag = "Select.ItemIndicator"

    # Whether to keep the HTML element in the DOM when the item is not selected. Defaults to False.
    keep_mounted: Var[bool]

    # The render prop
    render_: Var[Component]

    @classmethod
    def create(cls, *children, **props) -> BaseUIComponent:
        """Create the dialog trigger component."""
        props["data-slot"] = "select-item-indicator"
        cls.set_class_name(ClassNames.ITEM_INDICATOR, props)
        return super().create(*children, **props)


class SelectGroup(SelectBaseComponent):
    """Groups related select items with the corresponding label."""

    tag = "Select.Group"

    # The render prop
    render_: Var[Component]

    @classmethod
    def create(cls, *children, **props) -> BaseUIComponent:
        """Create the dialog trigger component."""
        props["data-slot"] = "select-group"
        cls.set_class_name(ClassNames.GROUP, props)
        return super().create(*children, **props)


class SelectGroupLabel(SelectBaseComponent):
    """An accessible label that is automatically associated with its parent group."""

    tag = "Select.GroupLabel"

    # The render prop
    render_: Var[Component]

    @classmethod
    def create(cls, *children, **props) -> BaseUIComponent:
        """Create the dialog trigger component."""
        props["data-slot"] = "select-group-label"
        cls.set_class_name(ClassNames.GROUP_LABEL, props)
        return super().create(*children, **props)


class SelectSeparator(SelectBaseComponent):
    """A separator element accessible to screen readers."""

    tag = "Select.Separator"

    # The orientation of the separator.
    orientation: Var[LiteralOrientation] = Var.create("horizontal")

    # The render prop
    render_: Var[Component]

    @classmethod
    def create(cls, *children, **props) -> BaseUIComponent:
        """Create the dialog trigger component."""
        props["data-slot"] = "select-separator"
        cls.set_class_name(ClassNames.SEPARATOR, props)
        return super().create(*children, **props)


class SelectIcon(SelectBaseComponent):
    """An icon that indicates that the trigger button opens a select menu."""

    tag = "Select.Icon"

    # The render prop
    render_: Var[Component]

    @classmethod
    def create(cls, *children, **props) -> BaseUIComponent:
        """Create the dialog trigger component."""
        props["data-slot"] = "select-icon"
        cls.set_class_name(ClassNames.ICON, props)
        return super().create(*children, **props)


class SelectArrow(SelectBaseComponent):
    """Displays an element positioned against the select menu anchor."""

    tag = "Select.Arrow"

    # The render prop
    render_: Var[Component]

    @classmethod
    def create(cls, *children, **props) -> BaseUIComponent:
        """Create the dialog trigger component."""
        props["data-slot"] = "select-arrow"
        cls.set_class_name(ClassNames.ARROW, props)
        return super().create(*children, **props)


class SelectScrollUpArrow(SelectBaseComponent):
    """An element that scrolls the select menu up when hovered."""

    tag = "Select.ScrollUpArrow"

    # Whether to keep the component mounted when not visible. Defaults to False.
    keep_mounted: Var[bool]

    # The render prop
    render_: Var[Component]

    @classmethod
    def create(cls, *children, **props) -> BaseUIComponent:
        """Create the dialog trigger component."""
        props["data-slot"] = "select-scroll-up-arrow"
        cls.set_class_name(ClassNames.SCROLL_ARROW_UP, props)
        return super().create(*children, **props)


class SelectScrollDownArrow(SelectBaseComponent):
    """An element that scrolls the select menu down when hovered."""

    tag = "Select.ScrollDownArrow"

    # Whether to keep the component mounted when not visible. Defaults to False.
    keep_mounted: Var[bool]

    # The render prop
    render_: Var[Component]

    @classmethod
    def create(cls, *children, **props) -> BaseUIComponent:
        """Create the dialog trigger component."""
        props["data-slot"] = "select-scroll-down-arrow"
        cls.set_class_name(ClassNames.SCROLL_ARROW_DOWN, props)
        return super().create(*children, **props)


class HighLevelSelect(SelectRoot):
    """High level wrapper for the Select component."""

    # The list of items to display in the select dropdown
    items: Var[list[str]]

    # The placeholder text to display when no item is selected
    placeholder: Var[str]

    # The size of the select component. Defaults to "md".
    size: Var[LiteralSelectSize]

    # Props for different component parts
    _trigger_props = {"placeholder", "size"}
    _items_props = {"items"}
    _positioner_props = {
        "align",
        "align_offset",
        "side",
        "arrow_padding",
        "collision_padding",
        "sticky",
        "position_method",
        "align_item_with_trigger",
        "track_anchor",
        "side_offset",
        "collision_avoidance",
    }
    _portal_props = {"container"}

    @classmethod
    def create(cls, *children, **props) -> BaseUIComponent:
        """Create a select component.

        Args:
            *children: Additional children to include in the select.
            **props: Additional properties to apply to the select component.

        Returns:
            The select component.
        """
        # Extract props for different parts
        trigger_props = {k: props.pop(k) for k in cls._trigger_props & props.keys()}
        items_props = {k: props.pop(k) for k in cls._items_props & props.keys()}
        positioner_props = {
            k: props.pop(k) for k in cls._positioner_props & props.keys()
        }
        portal_props = {k: props.pop(k) for k in cls._portal_props & props.keys()}

        # Get extracted values with defaults
        size = trigger_props.get("size", "md")
        items = items_props.get("items", [])

        # Create the items children
        if isinstance(items, Var):
            items_children = foreach(
                items,
                lambda item: SelectItem.create(
                    render_=button(
                        SelectItemText.create(item),
                        SelectItemIndicator.create(
                            hi(
                                "Tick02Icon",
                                class_name="size-4",
                            ),
                        ),
                        variant="ghost",
                        size=size,
                        type="button",
                        class_name=ClassNames.ITEM,
                        disabled=props.get("disabled", False),
                    ),
                    value=item,
                    key=item,
                ),
            )
        else:
            items_children = [
                SelectItem.create(
                    render_=button(
                        SelectItemText.create(item),
                        SelectItemIndicator.create(
                            hi(
                                "Tick02Icon",
                                class_name="size-4",
                            ),
                        ),
                        variant="ghost",
                        size=size,
                        type="button",
                        class_name=ClassNames.ITEM,
                    ),
                    value=item,
                    key=item,
                )
                for item in items
            ]

        return SelectRoot.create(
            SelectTrigger.create(
                render_=button(
                    SelectValue.create(),
                    SelectIcon.create(
                        select_arrow(class_name="size-4 text-secondary-9")
                    ),
                    variant="outline",
                    size=size,
                    type="button",
                    class_name=ClassNames.TRIGGER,
                    disabled=props.get("disabled", False),
                ),
            ),
            SelectPortal.create(
                SelectPositioner.create(
                    SelectPopup.create(
                        items_children,
                        class_name=cn(
                            ClassNames.POPUP,
                            "",
                            "rounded-radius",
                        ),
                    ),
                    **positioner_props,
                ),
                **portal_props,
            ),
            *children,
            **props,
        )


class Select(ComponentNamespace):
    """Namespace for Select components."""

    root = staticmethod(SelectRoot.create)
    trigger = staticmethod(SelectTrigger.create)
    value = staticmethod(SelectValue.create)
    icon = staticmethod(SelectIcon.create)
    backdrop = staticmethod(SelectBackdrop.create)
    portal = staticmethod(SelectPortal.create)
    positioner = staticmethod(SelectPositioner.create)
    popup = staticmethod(SelectPopup.create)
    arrow = staticmethod(SelectArrow.create)
    scroll_up_arrow = staticmethod(SelectScrollUpArrow.create)
    scroll_down_arrow = staticmethod(SelectScrollDownArrow.create)
    item = staticmethod(SelectItem.create)
    item_text = staticmethod(SelectItemText.create)
    item_indicator = staticmethod(SelectItemIndicator.create)
    group = staticmethod(SelectGroup.create)
    group_label = staticmethod(SelectGroupLabel.create)
    separator = staticmethod(SelectSeparator.create)
    class_names = ClassNames
    __call__ = staticmethod(HighLevelSelect.create)


select = Select()
```


# Usage

Make sure to correctly set your imports relative to the component.

```python
from components.base_ui.select import select
```

# Example


```python
def select_example():
    """A basic select example."""

    return select.root(
        select.trigger(
            select.value(),
            hi(
                "Add01Icon",
                class_name="size-4 transition-transform duration-50 ease-in-out group-aria-[expanded=true]:rotate-45",
            ),
            class_name="w-[180px] flex items-center justify-between group",
        ),
        select.portal(
            select.positioner(
                select.popup(
                    select.group(
                        select.group_label("Fruit"),
                        *[
                            select.item(
                                select.item_text(fruit.capitalize()),
                                select.item_indicator(
                                    hi("Tick02Icon", class_name="size-4")
                                ),
                                value=fruit.capitalize(),
                                class_name="w-full flex flex-row items-center justify-between",
                            )
                            for fruit in [
                                "apple",
                                "banana",
                                "orange",
                                "grape",
                                "blueberry",
                                "pineapple",
                            ]
                        ],
                    ),
                    class_name="w-[180px]",
                ),
                side_offset=4,
            ),
        ),
        name="example_select",
        default_value="Select a fruit",
    )
```

