Select
A dropdown component that allows users to select one or more options from a list.
import { createListCollection } from "@ark-ui/solid";
import { Index } from "solid-js";
import { Select, SelectLabel, SelectTrigger, SelectContent, SelectItem } from "~/components/select";
const frameworks = createListCollection({
items: [
{ label: "React", value: "react" },
{ label: "Solid.js", value: "solid" },
{ label: "Vue", value: "vue" },
{ label: "Svelte", value: "svelte" },
],
});
export default function SelectBasicDemo() {
return (
<div>
<Select collection={frameworks}>
<SelectLabel>Framework</SelectLabel>
<SelectTrigger placeholder="Select a framework" />
<SelectContent>
<Index each={frameworks.items}>
{(item) => <SelectItem item={item()}>{item().label}</SelectItem>}
</Index>
</SelectContent>
</Select>
</div>
);
}Installation
CLI
Run the following command to add the component to your project:
npx @ark-preset/cli@latest add selectManual
Create the recipe file at src/components/recipes/select.ts:
import { tv, type VariantProps } from "tailwind-variants";
export const selectVariants = tv({
slots: {
root: "grid gap-1.5 w-full",
control:
"flex h-8 w-full items-center justify-between rounded-md border border-input bg-background text-sm ring-offset-background focus-within:outline-none focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2",
trigger:
"flex flex-1 items-center justify-between self-stretch px-2.5 [&[data-state=open]>svg]:rotate-180",
valueText: "text-base md:text-sm data-[placeholder-shown]:text-muted-foreground",
indicator: "size-4 transition-transform text-muted-foreground",
clearTrigger:
"size-4 text-muted-foreground hover:text-foreground transition-colors pointer-events-auto",
searchInput:
"mx-1 mb-1.5 flex h-8 items-center rounded-md border border-input bg-background px-2.5 py-1.5 text-base md:text-sm outline-none placeholder:text-muted-foreground focus:ring-2 focus:ring-ring",
positioner: "z-50",
content:
"z-50 min-w-[8rem] w-[var(--reference-width)] rounded-md border border-border bg-background p-1 shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
item: "relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground",
itemText: "flex-1",
itemIndicator: "absolute right-2 flex size-4 items-center justify-center",
},
variants: {
error: {
true: {
control: "border-destructive focus-within:ring-destructive",
},
},
},
defaultVariants: {
error: false,
},
});
export type SelectVariants = VariantProps<typeof selectVariants>;Create the component directory and files.
src/components/select/select.base.tsx:
import { Select as ArkSelect } from "@ark-ui/solid/select";
import { splitProps, type Component } from "solid-js";
import { selectVariants, labelVariants } from "../recipes/select";
const styles = selectVariants();
type SelectRootProps = ArkSelect.RootProps<{ label: string; value: string }> & {
error?: boolean;
};
const Root: Component<SelectRootProps> = (props) => {
const [local, others] = splitProps(props, ["class", "error"]);
const localStyles = () => selectVariants({ error: !!local.error });
return <ArkSelect.Root class={localStyles().root({ class: local.class })} {...others} />;
};
type SelectRootProviderProps = ArkSelect.RootProviderProps<{ label: string; value: string }>;
const RootProvider: Component<SelectRootProviderProps> = (props) => {
const [local, others] = splitProps(props, ["class"]);
return <ArkSelect.RootProvider class={styles.root({ class: local.class })} {...others} />;
};
const Label: Component<ArkSelect.LabelProps & { error?: boolean }> = (props) => {
const [local, others] = splitProps(props, ["class", "error"]);
return (
<ArkSelect.Label
class={labelVariants({ class: local.class, error: local.error })}
{...others}
/>
);
};
const Trigger: Component<ArkSelect.TriggerProps> = (props) => {
const [local, others] = splitProps(props, ["class"]);
return <ArkSelect.Trigger class={styles.trigger({ class: local.class })} {...others} />;
};
const ValueText: Component<ArkSelect.ValueTextProps> = (props) => {
const [local, others] = splitProps(props, ["class"]);
return <ArkSelect.ValueText class={styles.valueText({ class: local.class })} {...others} />;
};
const Positioner: Component<ArkSelect.PositionerProps> = (props) => {
const [local, others] = splitProps(props, ["class"]);
return <ArkSelect.Positioner class={styles.positioner({ class: local.class })} {...others} />;
};
const Item: Component<ArkSelect.ItemProps> = (props) => {
const [local, others] = splitProps(props, ["class"]);
return <ArkSelect.Item class={styles.item({ class: local.class })} {...others} />;
};
const ItemText: Component<ArkSelect.ItemTextProps> = (props) => {
const [local, others] = splitProps(props, ["class"]);
return <ArkSelect.ItemText class={styles.itemText({ class: local.class })} {...others} />;
};
const ItemIndicator: Component<ArkSelect.ItemIndicatorProps> = (props) => {
const [local, others] = splitProps(props, ["class"]);
return (
<ArkSelect.ItemIndicator class={styles.itemIndicator({ class: local.class })} {...others} />
);
};
const Control: Component<ArkSelect.ControlProps> = (props) => {
const [local, others] = splitProps(props, ["class"]);
return <ArkSelect.Control class={styles.control({ class: local.class })} {...others} />;
};
const ClearTrigger: Component<ArkSelect.ClearTriggerProps> = (props) => {
const [local, others] = splitProps(props, ["class"]);
return <ArkSelect.ClearTrigger class={styles.clearTrigger({ class: local.class })} {...others} />;
};
const Indicator: Component<ArkSelect.IndicatorProps> = (props) => {
const [local, others] = splitProps(props, ["class"]);
return <ArkSelect.Indicator class={styles.indicator({ class: local.class })} {...others} />;
};
const Content: Component<ArkSelect.ContentProps> = (props) => {
const [local, others] = splitProps(props, ["class"]);
return <ArkSelect.Content class={styles.content({ class: local.class })} {...others} />;
};
const HiddenSelect = ArkSelect.HiddenSelect;
const ItemGroup = ArkSelect.ItemGroup;
const ItemGroupLabel = ArkSelect.ItemGroupLabel;
const List = ArkSelect.List;
export const Select = {
Root,
RootProvider,
Label,
Trigger,
ValueText,
Positioner,
Item,
ItemText,
ItemIndicator,
Control,
Indicator,
Content,
ClearTrigger,
HiddenSelect,
ItemGroup,
ItemGroupLabel,
List,
};src/components/select/index.tsx:
import { Select as SelectBase } from "./select.base";
import type { Select as ArkSelect, SelectOpenChangeDetails } from "@ark-ui/solid/select";
import { Portal } from "solid-js/web";
import {
Show,
createSignal,
createContext,
useContext,
type Accessor,
splitProps,
mergeProps,
type Component,
} from "solid-js";
import { selectVariants } from "../recipes/select";
import { ScrollArea } from "../scroll-area";
// ── Inline SVG Icons ──
function XIcon() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="size-4"
>
<path d="M18 6 6 18" />
<path d="m6 6 12 12" />
</svg>
);
}
function ChevronDownIcon() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="size-4"
>
<path d="m6 9 6 6 6-6" />
</svg>
);
}
function CheckIcon() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="size-4"
>
<path d="M20 6 9 17l-5-5" />
</svg>
);
}
const styles = selectVariants();
type SearchableContextValue = {
searchable: boolean;
searchValue: Accessor<string>;
onSearch: (value: string) => void;
};
const SelectSearchableContext = createContext<SearchableContextValue>();
const useSelectSearchable = () => useContext(SelectSearchableContext);
type SelectProps = ArkSelect.RootProps<{ label: string; value: string }> & {
error?: boolean;
searchable?: boolean;
onSearch?: (value: string) => void;
};
const Select: Component<SelectProps> = (props) => {
const [local, rest] = splitProps(props, [
"searchable",
"onSearch",
"onOpenChange",
"class",
"error",
]);
const [positioningProp, others] = splitProps(rest, ["positioning"]);
const [searchValue, setSearchValue] = createSignal("");
const handleSearch = (value: string) => {
setSearchValue(value);
local.onSearch?.(value);
};
const handleOpenChange = (details: SelectOpenChangeDetails) => {
if (details.open && local.searchable) {
setSearchValue("");
local.onSearch?.("");
}
local.onOpenChange?.(details);
};
return (
<SelectSearchableContext.Provider
value={{
searchable: !!local.searchable,
searchValue,
onSearch: handleSearch,
}}
>
<SelectBase.Root
class={local.class}
error={local.error}
onOpenChange={handleOpenChange}
positioning={mergeProps(
{ placement: "bottom", sameWidth: true } as const,
positioningProp.positioning,
)}
{...others}
/>
</SelectSearchableContext.Provider>
);
};
const SelectLabel = SelectBase.Label;
type SelectTriggerProps = ArkSelect.TriggerProps & {
placeholder?: string;
};
const SelectTrigger: Component<SelectTriggerProps> = (props) => {
const [local, others] = splitProps(props, ["class", "placeholder"]);
return (
<SelectBase.Control class={local.class}>
<SelectBase.Trigger {...others}>
<SelectBase.ValueText placeholder={local.placeholder ?? "Select..."} />
<SelectBase.Indicator>
<ChevronDownIcon />
</SelectBase.Indicator>
</SelectBase.Trigger>
<SelectBase.ClearTrigger class="absolute right-13">
<XIcon />
</SelectBase.ClearTrigger>
</SelectBase.Control>
);
};
const SelectContent: Component<ArkSelect.ContentProps> = (props) => {
const [local, others] = splitProps(props, ["class", "children"]);
const ctx = useSelectSearchable();
return (
<Portal>
<SelectBase.Positioner>
<SelectBase.Content class={local.class} {...others}>
<Show when={ctx?.searchable}>
<input
type="text"
value={ctx!.searchValue()}
onInput={(e) => ctx!.onSearch(e.currentTarget.value)}
placeholder="Search..."
class={styles.searchInput()}
onPointerDown={(e) => e.stopPropagation()}
/>
</Show>
<ScrollArea class="max-h-60" orientation="vertical">
{local.children}
</ScrollArea>
</SelectBase.Content>
</SelectBase.Positioner>
</Portal>
);
};
const SelectItem: Component<ArkSelect.ItemProps> = (props) => {
const [local, others] = splitProps(props, ["children"]);
return (
<SelectBase.Item {...others}>
{typeof local.children === "string" ? (
<SelectBase.ItemText>{local.children}</SelectBase.ItemText>
) : (
local.children
)}
<SelectBase.ItemIndicator>
<CheckIcon />
</SelectBase.ItemIndicator>
</SelectBase.Item>
);
};
const SelectRootProvider = SelectBase.RootProvider;
export {
Select,
SelectLabel,
SelectTrigger,
SelectContent,
SelectItem,
SelectRootProvider,
SelectBase,
};
export { selectVariants, type SelectVariants } from "../recipes/select";Note: Make sure your project has the Tailwind CSS theme variables set up (
--background,--foreground,--ring,--border, etc.) or override the utility classes to match your design system.
Dependencies: This component imports shared recipes or sub-components from other packages. Make sure the following are also installed: scroll-area.
Usage
Basic Usage
import { createListCollection } from "@ark-ui/solid";
import { Index } from "solid-js";
import { Select, SelectLabel, SelectTrigger, SelectContent, SelectItem } from "~/components/select";
const frameworks = createListCollection({
items: [
{ label: "React", value: "react" },
{ label: "Solid.js", value: "solid" },
{ label: "Vue", value: "vue" },
{ label: "Svelte", value: "svelte" },
],
});
export default function SelectBasicDemo() {
return (
<div>
<Select collection={frameworks}>
<SelectLabel>Framework</SelectLabel>
<SelectTrigger placeholder="Select a framework" />
<SelectContent>
<Index each={frameworks.items}>
{(item) => <SelectItem item={item()}>{item().label}</SelectItem>}
</Index>
</SelectContent>
</Select>
</div>
);
}Multiple Selection
Use the multiple prop to allow selecting multiple options. The SelectTrigger composite includes a clear button to deselect all selections.
import { createListCollection } from "@ark-ui/solid";
import { Index } from "solid-js";
import { Select, SelectLabel, SelectContent, SelectItem, SelectTrigger } from "~/components/select";
const frameworks = createListCollection({
items: [
{ label: "React", value: "react" },
{ label: "Solid.js", value: "solid" },
{ label: "Vue", value: "vue" },
{ label: "Svelte", value: "svelte" },
],
});
export default function SelectMultipleDemo() {
return (
<div>
<Select collection={frameworks} multiple>
<SelectLabel>Framework</SelectLabel>
<SelectTrigger placeholder="Select a framework" />
<SelectContent>
<Index each={frameworks.items}>
{(item) => <SelectItem item={item()}>{item().label}</SelectItem>}
</Index>
</SelectContent>
</Select>
</div>
);
}Searchable Selection
Use the searchable prop to enable a search input inside the dropdown. Pass onSearch to filter the collection as the user types.
import { useFilter, useListCollection } from "@ark-ui/solid";
import { Index } from "solid-js";
import { Select, SelectLabel, SelectTrigger, SelectContent, SelectItem } from "~/components/select";
export default function SelectSearchableDemo() {
const filterFn = useFilter({ sensitivity: "base" });
const { collection, filter } = useListCollection({
initialItems: [
{ label: "React", value: "react" },
{ label: "Solid.js", value: "solid" },
{ label: "Vue", value: "vue" },
{ label: "Svelte", value: "svelte" },
{ label: "Angular", value: "angular" },
{ label: "Ember", value: "ember" },
{ label: "Lit", value: "lit" },
{ label: "Preact", value: "preact" },
],
filter: filterFn().contains,
});
return (
<div>
<Select collection={collection()} searchable onSearch={(value) => filter(value)}>
<SelectLabel>Framework</SelectLabel>
<SelectTrigger placeholder="Select a framework" />
<SelectContent>
<Index each={collection().items}>
{(item) => <SelectItem item={item()}>{item().label}</SelectItem>}
</Index>
</SelectContent>
</Select>
</div>
);
}The searchable prop works with both single and multiple selection modes.
Composite Exports
The SelectTrigger, SelectContent, and SelectItem components are composite wrappers from the barrel entry point that include inline SVG icons and Portal. SelectTrigger also includes a clear button when used with multiple selection. Import them alongside Select:
import { Select, SelectLabel, SelectTrigger, SelectContent, SelectItem } from "~/components/select";Advanced Usage
When you need more control, import raw primitive parts directly from the base file:
import { Select } from "~/components/select/select.base";Or import SelectBase (the raw parts namespace) from the barrel entry point:
import { SelectBase } from "~/components/select";Root Provider
Use SelectRootProvider when you need to access the select state outside of the component tree. This pattern uses the useSelect hook from Ark UI to create a shared context that both the select and external elements can reference.
import { useSelect, createListCollection } from "@ark-ui/solid/select";
import { Index } from "solid-js";
import {
SelectRootProvider,
SelectLabel,
SelectTrigger,
SelectContent,
SelectItem,
} from "~/components/select";
const frameworks = createListCollection({
items: [
{ label: "React", value: "react" },
{ label: "Solid.js", value: "solid" },
{ label: "Vue", value: "vue" },
{ label: "Svelte", value: "svelte" },
],
});
export default function SelectRootProviderDemo() {
const select = useSelect({ collection: frameworks, defaultValue: ["solid"] });
return (
<div class="space-y-4">
<output class="block text-sm text-muted-foreground">
Value: {JSON.stringify(select().value)}
</output>
<SelectRootProvider value={select}>
<SelectLabel>Framework</SelectLabel>
<SelectTrigger placeholder="Select a framework" />
<SelectContent>
<Index each={frameworks.items}>
{(item) => <SelectItem item={item()}>{item().label}</SelectItem>}
</Index>
</SelectContent>
</SelectRootProvider>
</div>
);
}The key difference:
Select— manages its own state internally. Use for simple, self-contained selects.SelectRootProvider— accepts a pre-created select context viauseSelect. Use when you need to read or control the select state from outside the component tree.
Error State
Use the error prop on SelectBase.Root to show an error state. The structured Select component delegates its root styling to SelectBase.Root, so the error prop requires using the base component directly:
import { createListCollection } from "@ark-ui/solid";
import { Index } from "solid-js";
import {
SelectBase,
SelectLabel,
SelectTrigger,
SelectContent,
SelectItem,
} from "~/components/select";
const items = createListCollection({
items: [
{ label: "React", value: "react" },
{ label: "Solid.js", value: "solid" },
],
});
export default function SelectErrorDemo() {
return (
<div>
<SelectBase.Root collection={items} error>
<SelectLabel>Framework</SelectLabel>
<SelectTrigger placeholder="Select..." />
<SelectContent>
<Index each={items.items}>
{(item) => <SelectItem item={item()}>{item().label}</SelectItem>}
</Index>
</SelectContent>
</SelectBase.Root>
</div>
);
}API Reference
See the Ark UI Select documentation.