Skip to content
portalbits
Home Docs GitHub(opens in a new tab)

Guides

Get startedFoundationsMotion

Components

All components

Buttons

ButtonIconButton

Inputs

InputSelectTextareaSlider

Selection

CheckboxToggleToggleGroupSwitchRadioGroup

Identity

AvatarBadge

Overlays

DialogPopoverTooltipDropdownMenuAlertDialog

Disclosure

TabsAccordion

Cards

AppCardArticleCardFeatureCard

Content

CarouselSectionHeaderEmptyStateAlertProgressSeparatorRowListRow

Chrome

PortalShellSidebarNavMobileNavTopBarAuthFrameColorSelectorFloatingNav

Select

A single-choice menu styled like the text field.

No category

Variations

Source

<script lang="ts">
  import 'portal-bits/styles.css';
  import 'portal-bits/tokens.css';
  import type { Snippet } from 'svelte';

  let { children }: { children: Snippet } = $props();
</script>

{@render children()}
<script lang="ts">
  import SelectExample from './SelectExample.svelte';
</script>

<SelectExample invalid={false} disabled={false} required={false} sizing={"stable"} />
<script lang="ts">
  import { Select, type SelectOption } from 'portal-bits';
  let {
    invalid = false,
    disabled = false,
    required = false,
    sizing = 'stable',
  }: {
    invalid?: boolean;
    disabled?: boolean;
    required?: boolean;
    sizing?: 'stable' | 'dynamic';
  } = $props();
  const options: SelectOption[] = [
    { value: 'notes', label: 'Notes' },
    { value: 'drafts', label: 'Drafts' },
    { value: 'research', label: 'Shared research notes' },
    { value: 'archived', label: 'Archived', disabled: true },
  ];
  let category = $state('');
</script>

<div class="field">
  <Select
    label="Record category"
    bind:value={category}
    placeholder="Choose a category"
    {options}
    {invalid}
    {required}
    {disabled}
    {sizing}
    aria-describedby={invalid ? 'category-error' : undefined}
  />
  {#if invalid}<p id="category-error" class="error">Choose a category.</p>{/if}
  <p aria-live="polite">{category || 'No category'}</p>
</div>

<style>
  .field {
    max-width: 340px;
  }
  .error {
    color: var(--portal-error);
  }
</style>

API reference

These tables list the props and snippets that Select declares. Attributes passed through to an element or primitive are listed under Forwarded attributes, not here.

Props

NameTypeDefaultRequiredBindingDescription
labelstringNoneYesOne-wayThe visible label, tied to the select with `for`.
valuestring""NoBindableThe selected option value. Empty means none selected.
optionsSelectOption[]NoneYesOne-wayChoices as `{ value, label, disabled? }`. Values must be unique.
placeholderstringNoneNoOne-wayAdds an enabled empty option with this text, so the choice can return to empty.
invalidbooleanfalseNoOne-waySets `aria-invalid` on the trigger. Render the error text yourself.
idstringGenerated idNoOne-wayThe trigger id. A stable id is generated when you omit it.
namestringNoneNoOne-waySubmits the selected value under this name.
requiredbooleanfalseNoOne-wayTakes part in native form validation, even without `name`.
disabledbooleanfalseNoOne-wayDisables the trigger and every option.
formstringNoneNoOne-wayThe id of the form that owns the control.
theme'light' | 'dark'lightNoOne-wayTheme scope for the portaled content. Portaled content does not inherit the page scope.
sizing'stable' | 'dynamic'stableNoOne-wayStable fills the field and never narrows below the longest option. Dynamic fits the selected label and eases between widths.
classstring""NoOne-wayExtra classes, added after the component classes.

Forwarded attributes

  • Button attributes except `value` and `type`: `aria-*` and handlers. They land on the trigger. The trigger is `type="button"`.

The primitive underneath is a Bits UI peer dependency. Its own options are documented in the Bits UI Select documentation. Only the props listed on this page are part of the Select contract. No data attributes are documented as stable.

Limits

  • Single choice only. Multiple selection and search are not supported.
  • Reserve the empty value for the placeholder. An option with an empty value collides with it.
  • The menu, trigger spacing, chevron, and dynamic width motion are adaptations. They reuse the input height, menu surface, type, and motion tokens.
  • Menu options stay on one line. The menu grows past the trigger to fit the longest option, up to the viewport width.

Related

  • Input
  • ToggleGroup
Previous Input Next Textarea

On this page

  • Preview
  • Source
  • API reference
  • Props
  • Forwarded attributes
  • Limits