<script lang="ts">
import * as Select from "$lib/components/ui/select/index.js";
const fruits = [
{ value: "apple", label: "Apple" },
{ value: "banana", label: "Banana" },
{ value: "blueberry", label: "Blueberry" },
{ value: "grapes", label: "Grapes" },
{ value: "pineapple", label: "Pineapple" }
];
let value = $state("");
const triggerContent = $derived(
fruits.find((f) => f.value === value)?.label ?? "Select a fruit"
);
</script>
<Select.Root type="single" name="favoriteFruit" bind:value>
<Select.Trigger class="w-[180px]">
{triggerContent}
</Select.Trigger>
<Select.Content>
<Select.Group>
<Select.Label>Fruits</Select.Label>
{#each fruits as fruit (fruit.value)}
<Select.Item
value={fruit.value}
label={fruit.label}
disabled={fruit.value === "grapes"}
>
{fruit.label}
</Select.Item>
{/each}
</Select.Group>
</Select.Content>
</Select.Root> Installation
pnpm dlx shadcn-svelte@latest add select Install bits-ui:
pnpm add bits-ui -D Copy and paste the following code into your project.
import Root from './select.svelte';
import Group from './select-group.svelte';
import Label from './select-label.svelte';
import Item from './select-item.svelte';
import Content from './select-content.svelte';
import Trigger from './select-trigger.svelte';
import Separator from './select-separator.svelte';
import ScrollDownButton from './select-scroll-down-button.svelte';
import ScrollUpButton from './select-scroll-up-button.svelte';
import GroupHeading from './select-group-heading.svelte';
import Portal from './select-portal.svelte';
export {
Root,
Group,
Label,
Item,
Content,
Trigger,
Separator,
ScrollDownButton,
ScrollUpButton,
GroupHeading,
Portal,
//
Root as Select,
Group as SelectGroup,
Label as SelectLabel,
Item as SelectItem,
Content as SelectContent,
Trigger as SelectTrigger,
Separator as SelectSeparator,
ScrollDownButton as SelectScrollDownButton,
ScrollUpButton as SelectScrollUpButton,
GroupHeading as SelectGroupHeading,
Portal as SelectPortal
};
<script lang="ts">
import { Select as SelectPrimitive } from 'bits-ui';
import SelectPortal from './select-portal.svelte';
import SelectScrollUpButton from './select-scroll-up-button.svelte';
import SelectScrollDownButton from './select-scroll-down-button.svelte';
import { cn, type WithoutChild } from '$UTILS$.js';
import type { ComponentProps } from 'svelte';
import type { WithoutChildrenOrChild } from '$UTILS$.js';
let {
ref = $bindable(null),
class: className,
sideOffset = 4,
portalProps,
children,
preventScroll = true,
...restProps
}: WithoutChild<SelectPrimitive.ContentProps> & {
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof SelectPortal>>;
} = $props();
</script>
<SelectPortal {...portalProps}>
<SelectPrimitive.Content
bind:ref
{sideOffset}
{preventScroll}
data-slot="select-content"
class={cn(
'border border-zinc-800 bg-zinc-950 text-zinc-50 data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-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 min-w-36 rounded-none shadow-none duration-100 data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-end]:slide-in-from-left-2 relative isolate z-50 overflow-x-hidden overflow-y-auto',
className
)}
{...restProps}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
class={cn(
'h-(--bits-select-anchor-height) w-full min-w-(--bits-select-anchor-width) scroll-my-1'
)}
>
{@render children?.()}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPortal>
<script lang="ts">
import { Select as SelectPrimitive } from 'bits-ui';
import { cn } from '$UTILS$.js';
import type { ComponentProps } from 'svelte';
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: ComponentProps<typeof SelectPrimitive.GroupHeading> = $props();
</script>
<SelectPrimitive.GroupHeading
bind:ref
data-slot="select-group-heading"
class={cn(
'px-2 py-1.5 font-mono text-[0.625rem] font-semibold tracking-widest text-zinc-500 uppercase',
className
)}
{...restProps}
>
{@render children?.()}
</SelectPrimitive.GroupHeading>
<script lang="ts">
import { Select as SelectPrimitive } from 'bits-ui';
import { cn } from '$UTILS$.js';
let {
ref = $bindable(null),
class: className,
...restProps
}: SelectPrimitive.GroupProps = $props();
</script>
<SelectPrimitive.Group
bind:ref
data-slot="select-group"
class={cn('scroll-my-1.5 p-1.5 text-zinc-300', className)}
{...restProps}
/>
<script lang="ts">
import { Select as SelectPrimitive } from 'bits-ui';
import { cn, type WithoutChild } from '$UTILS$.js';
import CheckIcon from 'phosphor-svelte/lib/Check';
let {
ref = $bindable(null),
class: className,
value,
label,
children: childrenProp,
...restProps
}: WithoutChild<SelectPrimitive.ItemProps> = $props();
</script>
<SelectPrimitive.Item
bind:ref
{value}
data-slot="select-item"
class={cn(
"focus:bg-[#b9d765]/18 focus:text-zinc-50 not-data-[variant=destructive]:focus:**:text-zinc-50 gap-2.5 rounded-none py-2 pr-8 pl-3 font-mono text-xs font-medium tracking-wide text-zinc-300 [&_svg:not([class*='size-'])]:size-3.5 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2 data-highlighted:bg-[#b9d765]/18 data-highlighted:text-zinc-50 relative flex w-full cursor-default items-center outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className
)}
{...restProps}
>
{#snippet children({ selected, highlighted })}
<span class="absolute end-2 flex size-3.5 items-center justify-center text-[#d0e891]">
{#if selected}
<CheckIcon class="cn-select-item-indicator-icon" />
{/if}
</span>
<span class="flex flex-1 gap-2 shrink-0 whitespace-nowrap">
{#if childrenProp}
{@render childrenProp({ selected, highlighted })}
{:else}
{label || value}
{/if}
</span>
{/snippet}
</SelectPrimitive.Item>
<script lang="ts">
import { cn, type WithElementRef } from '$UTILS$.js';
import type { HTMLAttributes } from 'svelte/elements';
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {} = $props();
</script>
<div
bind:this={ref}
data-slot="select-label"
class={cn(
'border-b border-zinc-900 px-3 py-2 font-mono text-[0.625rem] font-semibold tracking-widest text-zinc-500 uppercase',
className
)}
{...restProps}
>
{@render children?.()}
</div>
<script lang="ts">
import { Select as SelectPrimitive } from 'bits-ui';
let { ...restProps }: SelectPrimitive.PortalProps = $props();
</script>
<SelectPrimitive.Portal {...restProps} />
<script lang="ts">
import { Select as SelectPrimitive } from 'bits-ui';
import { cn, type WithoutChildrenOrChild } from '$UTILS$.js';
import CaretDownIcon from 'phosphor-svelte/lib/CaretDown';
let {
ref = $bindable(null),
class: className,
...restProps
}: WithoutChildrenOrChild<SelectPrimitive.ScrollDownButtonProps> = $props();
</script>
<SelectPrimitive.ScrollDownButton
bind:ref
data-slot="select-scroll-down-button"
class={cn(
"bg-zinc-950 text-zinc-500 z-10 flex cursor-default items-center justify-center border-t border-zinc-900 py-1 [&_svg:not([class*='size-'])]:size-3.5 bottom-0 w-full",
className
)}
{...restProps}
>
<CaretDownIcon />
</SelectPrimitive.ScrollDownButton>
<script lang="ts">
import { Select as SelectPrimitive } from 'bits-ui';
import { cn, type WithoutChildrenOrChild } from '$UTILS$.js';
import CaretUpIcon from 'phosphor-svelte/lib/CaretUp';
let {
ref = $bindable(null),
class: className,
...restProps
}: WithoutChildrenOrChild<SelectPrimitive.ScrollUpButtonProps> = $props();
</script>
<SelectPrimitive.ScrollUpButton
bind:ref
data-slot="select-scroll-up-button"
class={cn(
"bg-zinc-950 text-zinc-500 z-10 flex cursor-default items-center justify-center border-b border-zinc-900 py-1 [&_svg:not([class*='size-'])]:size-3.5 top-0 w-full",
className
)}
{...restProps}
>
<CaretUpIcon />
</SelectPrimitive.ScrollUpButton>
<script lang="ts">
import type { Separator as SeparatorPrimitive } from 'bits-ui';
import { Separator } from '$UI$/separator/index.js';
import { cn } from '$UTILS$.js';
let {
ref = $bindable(null),
class: className,
...restProps
}: SeparatorPrimitive.RootProps = $props();
</script>
<Separator
bind:ref
data-slot="select-separator"
class={cn('bg-zinc-800 -mx-1.5 my-1.5 h-px pointer-events-none', className)}
{...restProps}
/>
<script lang="ts">
import { Select as SelectPrimitive } from 'bits-ui';
import { cn, type WithoutChild } from '$UTILS$.js';
import CaretDownIcon from 'phosphor-svelte/lib/CaretDown';
let {
ref = $bindable(null),
class: className,
children,
size = 'default',
...restProps
}: WithoutChild<SelectPrimitive.TriggerProps> & {
size?: 'sm' | 'default';
} = $props();
</script>
<SelectPrimitive.Trigger
bind:ref
data-slot="select-trigger"
data-size={size}
class={cn(
"border-zinc-800 bg-zinc-900 text-zinc-50 data-placeholder:text-zinc-500 focus-visible:border-zinc-300 focus-visible:ring-2 focus-visible:ring-zinc-300/25 aria-invalid:border-destructive aria-invalid:ring-2 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 gap-1.5 rounded-none border px-3 py-2 text-sm transition-[color,border-color,box-shadow] data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:flex *:data-[slot=select-value]:gap-1.5 [&_svg:not([class*='size-'])]:size-3.5 flex w-fit items-center justify-between whitespace-nowrap outline-none disabled:cursor-not-allowed disabled:opacity-50 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center [&_svg]:pointer-events-none [&_svg]:shrink-0",
className
)}
{...restProps}
>
{@render children?.()}
<CaretDownIcon class="text-zinc-500 size-3.5 pointer-events-none" />
</SelectPrimitive.Trigger>
<script lang="ts">
import { Select as SelectPrimitive } from 'bits-ui';
let {
open = $bindable(false),
value = $bindable(),
...restProps
}: SelectPrimitive.RootProps = $props();
</script>
<SelectPrimitive.Root bind:open bind:value={value as never} {...restProps} />
Usage
<script lang="ts">
import * as Select from '$lib/components/ui/select/index.js';
</script> <Select.Root type="single">
<Select.Trigger class="w-[180px]"></Select.Trigger>
<Select.Content>
<Select.Item value="light">Light</Select.Item>
<Select.Item value="dark">Dark</Select.Item>
<Select.Item value="system">System</Select.Item>
</Select.Content>
</Select.Root> Examples
Scrollable
<script lang="ts">
import * as Select from "$lib/components/ui/select/index.js";
</script>
<Select.Root type="single">
<Select.Trigger class="w-[280px]">Select a timezone</Select.Trigger>
<Select.Content class="max-h-[300px]">
<Select.Group>
<Select.Label>North America</Select.Label>
<Select.Item value="est">Eastern Standard Time (EST)</Select.Item>
<Select.Item value="cst">Central Standard Time (CST)</Select.Item>
<Select.Item value="mst">Mountain Standard Time (MST)</Select.Item>
<Select.Item value="pst">Pacific Standard Time (PST)</Select.Item>
<Select.Item value="akst">Alaska Standard Time (AKST)</Select.Item>
<Select.Item value="hst">Hawaii Standard Time (HST)</Select.Item>
</Select.Group>
<Select.Group>
<Select.Label>Europe & Africa</Select.Label>
<Select.Item value="gmt">Greenwich Mean Time (GMT)</Select.Item>
<Select.Item value="cet">Central European Time (CET)</Select.Item>
<Select.Item value="eet">Eastern European Time (EET)</Select.Item>
<Select.Item value="west">Western European Summer Time (WEST)</Select.Item
>
<Select.Item value="cat">Central Africa Time (CAT)</Select.Item>
<Select.Item value="eat">East Africa Time (EAT)</Select.Item>
</Select.Group>
<Select.Group>
<Select.Label>Asia</Select.Label>
<Select.Item value="msk">Moscow Time (MSK)</Select.Item>
<Select.Item value="ist">India Standard Time (IST)</Select.Item>
<Select.Item value="cst_china">China Standard Time (CST)</Select.Item>
<Select.Item value="jst">Japan Standard Time (JST)</Select.Item>
<Select.Item value="kst">Korea Standard Time (KST)</Select.Item>
<Select.Item value="ist_indonesia"
>Indonesia Central Standard Time (WITA)</Select.Item
>
</Select.Group>
<Select.Group>
<Select.Label>Australia & Pacific</Select.Label>
<Select.Item value="awst"
>Australian Western Standard Time (AWST)</Select.Item
>
<Select.Item value="acst"
>Australian Central Standard Time (ACST)</Select.Item
>
<Select.Item value="aest"
>Australian Eastern Standard Time (AEST)</Select.Item
>
<Select.Item value="nzst">New Zealand Standard Time (NZST)</Select.Item>
<Select.Item value="fjt">Fiji Time (FJT)</Select.Item>
</Select.Group>
<Select.Group>
<Select.Label>South America</Select.Label>
<Select.Item value="art">Argentina Time (ART)</Select.Item>
<Select.Item value="bot">Bolivia Time (BOT)</Select.Item>
<Select.Item value="brt">Brasilia Time (BRT)</Select.Item>
<Select.Item value="clt">Chile Standard Time (CLT)</Select.Item>
</Select.Group>
</Select.Content>
</Select.Root>