Combobox

A searchable dropdown for selecting a value from a list of options.

View source
import * as Combobox from "@danielfrg/solid-ui/combobox"
import styles from "./index.module.css"

const FRUITS = ["Apple", "Banana", "Blueberry", "Grapes", "Pineapple"]

export function DemoComboboxHero() {
  return (
    <Combobox.Root
      options={FRUITS}
      placeholder="Search a fruit..."
      itemComponent={(props) => (
        <Combobox.Item item={props.item} class={styles.item}>
          <Combobox.ItemLabel>{props.item.rawValue}</Combobox.ItemLabel>
          <Combobox.ItemIndicator class={styles["item-indicator"]}>
            <CheckIcon />
          </Combobox.ItemIndicator>
        </Combobox.Item>
      )}
    >
      <Combobox.Control<string> class={styles.control} aria-label="Fruit">
        {(state) => (
          <>
            <Combobox.Input class={styles.input} />
            <Combobox.Trigger class={styles.trigger}>
              <Combobox.Icon class={styles.icon}>
                <ChevronDownIcon />
              </Combobox.Icon>
            </Combobox.Trigger>
          </>
        )}
      </Combobox.Control>
      <Combobox.Portal>
        <Combobox.Content class={styles.content}>
          <Combobox.Listbox class={styles.listbox} />
        </Combobox.Content>
      </Combobox.Portal>
    </Combobox.Root>
  )
}

function CheckIcon() {
  return (
    <svg width="15" height="15" viewBox="0 0 15 15" fill="none">
      <path
        d="M11.4669 3.72684C11.7558 3.91574 11.8369 4.30308 11.648 4.59198L7.39799 11.092C7.29783 11.2452 7.13556 11.3467 6.95402 11.3699C6.77247 11.3931 6.58989 11.3354 6.45446 11.2124L3.70446 8.71241C3.44905 8.48022 3.43023 8.08494 3.66242 7.82953C3.89461 7.57412 4.28989 7.5553 4.5453 7.78749L6.75292 9.79441L10.6018 3.90792C10.7907 3.61902 11.178 3.53795 11.4669 3.72684Z"
        fill="currentColor"
        fill-rule="evenodd"
        clip-rule="evenodd"
      />
    </svg>
  )
}

function ChevronDownIcon() {
  return (
    <svg width="15" height="15" viewBox="0 0 15 15" fill="none">
      <path
        d="M3.13523 6.15803C3.3241 5.95657 3.64052 5.94637 3.84197 6.13523L7.5 9.56464L11.158 6.13523C11.3595 5.94637 11.6759 5.95657 11.8648 6.15803C12.0536 6.35949 12.0434 6.67591 11.842 6.86477L7.84197 10.6148C7.64964 10.7951 7.35036 10.7951 7.15803 10.6148L3.15803 6.86477C2.95657 6.67591 2.94637 6.35949 3.13523 6.15803Z"
        fill="currentColor"
        fill-rule="evenodd"
        clip-rule="evenodd"
      />
    </svg>
  )
}
.control {
  box-sizing: border-box;
  display: inline-flex;
  align-items: center;
  width: 12rem;
  border: 1px solid var(--color-gray-300);
  border-radius: 0.375rem;
  background-color: transparent;
  overflow: hidden;
}

.control:focus-within {
  outline: 2px solid var(--color-blue);
  outline-offset: -1px;
}

.control[data-disabled] {
  opacity: 0.5;
  cursor: not-allowed;
}

.input {
  box-sizing: border-box;
  flex: 1;
  min-width: 0;
  height: 2.5rem;
  padding: 0 0.75rem;
  outline: 0;
  border: none;
  background-color: transparent;
  font-family: inherit;
  font-size: 1rem;
  line-height: 1.5rem;
  color: var(--color-gray-900);
}

.input::placeholder {
  color: var(--color-gray-500);
}

.trigger {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: 2rem;
  height: 2.5rem;
  outline: 0;
  border: none;
  background-color: transparent;
  cursor: pointer;
}

.trigger[data-disabled] {
  cursor: not-allowed;
}

.icon {
  display: flex;
  align-items: center;
  color: var(--color-gray-500);
}

.content {
  box-sizing: border-box;
  border-radius: 0.5rem;
  background-color: var(--color-gray-50);
  outline: 1px solid var(--color-gray-200);
  box-shadow:
    0 10px 15px -3px var(--color-gray-200),
    0 4px 6px -4px var(--color-gray-200);
  z-index: 50;
  animation: contentHide 150ms ease forwards;
}

.content[data-expanded] {
  animation: contentShow 150ms ease;
}

@media (prefers-color-scheme: dark) {
  .content {
    outline: 1px solid var(--color-gray-300);
    box-shadow: none;
  }
}

.listbox {
  margin: 0;
  overflow-y: auto;
  max-height: 20rem;
  padding: 0.25rem;
}

.item {
  box-sizing: border-box;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0.375rem 0.5rem;
  border-radius: 0.25rem;
  outline: 0;
  font-size: 1rem;
  line-height: 1.5rem;
  color: var(--color-gray-900);
  cursor: default;
  user-select: none;
}

.item[data-highlighted] {
  background-color: var(--color-gray-100);
}

.item[data-disabled] {
  opacity: 0.5;
}

.item-indicator {
  display: flex;
  align-items: center;
  flex-shrink: 0;
  margin-left: 0.5rem;
  color: var(--color-blue);
}

@keyframes contentShow {
  from {
    opacity: 0;
    transform: scale(0.96);
  }
  to {
    opacity: 1;
    transform: scale(1);
  }
}

@keyframes contentHide {
  from {
    opacity: 1;
    transform: scale(1);
  }
  to {
    opacity: 0;
    transform: scale(0.96);
  }
}

.description {
  font-size: 0.875rem;
  line-height: 1.25rem;
  color: var(--color-gray-600);
  margin-top: 0.25rem;
}

.error {
  font-size: 0.875rem;
  line-height: 1.25rem;
  color: #dc2626;
  margin-top: 0.25rem;
}

.status {
  margin: 1rem 0 0;
  font-size: 0.875rem;
  line-height: 1.25rem;
  color: var(--color-gray-600);
}

.form {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

.actions {
  display: flex;
  gap: 0.5rem;
}

.button {
  box-sizing: border-box;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  height: 2rem;
  padding: 0 0.75rem;
  outline: 0;
  border: 1px solid var(--color-gray-200);
  border-radius: 0.375rem;
  background-color: var(--color-gray-50);
  font-family: inherit;
  font-size: 0.875rem;
  font-weight: 500;
  color: var(--color-gray-900);
  cursor: pointer;
}

@media (hover: hover) {
  .button:hover {
    background-color: var(--color-gray-100);
  }
}

/* Multiple combobox control */
.controlMultiple {
  box-sizing: border-box;
  display: inline-flex;
  align-items: center;
  width: 16rem;
  min-height: 2.5rem;
  border: 1px solid var(--color-gray-300);
  border-radius: 0.375rem;
  background-color: transparent;
  padding: 0.25rem 0.25rem 0.25rem 0.5rem;
  gap: 0.25rem;
  flex-wrap: wrap;
}

.controlMultiple:focus-within {
  outline: 2px solid var(--color-blue);
  outline-offset: -1px;
}

.tags {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem;
  flex: 1;
  min-width: 0;
  align-items: center;
}

.tag {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  padding: 0 0.375rem;
  height: 1.5rem;
  border-radius: 0.25rem;
  background-color: var(--color-gray-100);
  font-size: 0.875rem;
  color: var(--color-gray-900);
}

.tagRemove {
  all: unset;
  display: inline-flex;
  align-items: center;
  cursor: pointer;
  color: var(--color-gray-500);
}

.tagRemove:hover {
  color: var(--color-gray-900);
}

.inputInline {
  box-sizing: border-box;
  flex: 1;
  min-width: 4rem;
  height: 1.5rem;
  padding: 0;
  outline: 0;
  border: none;
  background-color: transparent;
  font-family: inherit;
  font-size: 0.875rem;
  color: var(--color-gray-900);
}
:root {
  --color-blue: oklch(45% 50% 264deg);
  --color-red: oklch(50% 55% 31deg);
  --color-gray-50: oklch(98% 0.25% 264deg);
  --color-gray-100: oklch(12% 9.5% 264deg / 5%);
  --color-gray-200: oklch(12% 9% 264deg / 7%);
  --color-gray-300: oklch(12% 8.5% 264deg / 17%);
  --color-gray-400: oklch(12% 8% 264deg / 38%);
  --color-gray-500: oklch(12% 7.5% 264deg / 50%);
  --color-gray-600: oklch(12% 7% 264deg / 67%);
  --color-gray-700: oklch(12% 6% 264deg / 77%);
  --color-gray-800: oklch(12% 5% 264deg / 85%);
  --color-gray-900: oklch(12% 5% 264deg / 90%);
  --color-gray-950: oklch(12% 5% 264deg / 95%);
}

@media (prefers-color-scheme: dark) {
  :root {
    --color-blue: oklch(69% 50% 264deg);
    --color-red: oklch(80% 55% 31deg);
    --color-gray-50: oklch(17% 0.25% 264deg);
    --color-gray-100: oklch(28% 0.75% 264deg / 65%);
    --color-gray-200: oklch(29% 0.75% 264deg / 80%);
    --color-gray-300: oklch(35% 0.75% 264deg / 80%);
    --color-gray-400: oklch(47% 0.875% 264deg / 80%);
    --color-gray-500: oklch(64% 1% 264deg / 80%);
    --color-gray-600: oklch(82% 1% 264deg / 80%);
    --color-gray-700: oklch(92% 1.125% 264deg / 80%);
    --color-gray-800: oklch(93% 0.875% 264deg / 85%);
    --color-gray-900: oklch(95% 0.5% 264deg / 90%);
    --color-gray-950: oklch(94% 0.375% 264deg / 95%);
  }
}

Examples

Default value

Use defaultValue to pre-select an option without controlling the state.

View source
import * as Combobox from "@danielfrg/solid-ui/combobox"
import styles from "./index.module.css"

const FRUITS = ["Apple", "Banana", "Blueberry", "Grapes", "Pineapple"]

export function DemoComboboxDefaultValue() {
  return (
    <Combobox.Root
      options={FRUITS}
      defaultValue="Blueberry"
      itemComponent={(props) => (
        <Combobox.Item item={props.item} class={styles.item}>
          <Combobox.ItemLabel>{props.item.rawValue}</Combobox.ItemLabel>
          <Combobox.ItemIndicator class={styles["item-indicator"]}>
            <CheckIcon />
          </Combobox.ItemIndicator>
        </Combobox.Item>
      )}
    >
      <Combobox.Control<string> class={styles.control} aria-label="Fruit">
        {() => (
          <>
            <Combobox.Input class={styles.input} />
            <Combobox.Trigger class={styles.trigger}>
              <Combobox.Icon class={styles.icon}>
                <ChevronDownIcon />
              </Combobox.Icon>
            </Combobox.Trigger>
          </>
        )}
      </Combobox.Control>
      <Combobox.Portal>
        <Combobox.Content class={styles.content}>
          <Combobox.Listbox class={styles.listbox} />
        </Combobox.Content>
      </Combobox.Portal>
    </Combobox.Root>
  )
}

function CheckIcon() {
  return (
    <svg width="15" height="15" viewBox="0 0 15 15" fill="none">
      <path
        d="M11.4669 3.72684C11.7558 3.91574 11.8369 4.30308 11.648 4.59198L7.39799 11.092C7.29783 11.2452 7.13556 11.3467 6.95402 11.3699C6.77247 11.3931 6.58989 11.3354 6.45446 11.2124L3.70446 8.71241C3.44905 8.48022 3.43023 8.08494 3.66242 7.82953C3.89461 7.57412 4.28989 7.5553 4.5453 7.78749L6.75292 9.79441L10.6018 3.90792C10.7907 3.61902 11.178 3.53795 11.4669 3.72684Z"
        fill="currentColor"
        fill-rule="evenodd"
        clip-rule="evenodd"
      />
    </svg>
  )
}

function ChevronDownIcon() {
  return (
    <svg width="15" height="15" viewBox="0 0 15 15" fill="none">
      <path
        d="M3.13523 6.15803C3.3241 5.95657 3.64052 5.94637 3.84197 6.13523L7.5 9.56464L11.158 6.13523C11.3595 5.94637 11.6759 5.95657 11.8648 6.15803C12.0536 6.35949 12.0434 6.67591 11.842 6.86477L7.84197 10.6148C7.64964 10.7951 7.35036 10.7951 7.15803 10.6148L3.15803 6.86477C2.95657 6.67591 2.94637 6.35949 3.13523 6.15803Z"
        fill="currentColor"
        fill-rule="evenodd"
        clip-rule="evenodd"
      />
    </svg>
  )
}
.control {
  box-sizing: border-box;
  display: inline-flex;
  align-items: center;
  width: 12rem;
  border: 1px solid var(--color-gray-300);
  border-radius: 0.375rem;
  background-color: transparent;
  overflow: hidden;
}

.control:focus-within {
  outline: 2px solid var(--color-blue);
  outline-offset: -1px;
}

.control[data-disabled] {
  opacity: 0.5;
  cursor: not-allowed;
}

.input {
  box-sizing: border-box;
  flex: 1;
  min-width: 0;
  height: 2.5rem;
  padding: 0 0.75rem;
  outline: 0;
  border: none;
  background-color: transparent;
  font-family: inherit;
  font-size: 1rem;
  line-height: 1.5rem;
  color: var(--color-gray-900);
}

.input::placeholder {
  color: var(--color-gray-500);
}

.trigger {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: 2rem;
  height: 2.5rem;
  outline: 0;
  border: none;
  background-color: transparent;
  cursor: pointer;
}

.trigger[data-disabled] {
  cursor: not-allowed;
}

.icon {
  display: flex;
  align-items: center;
  color: var(--color-gray-500);
}

.content {
  box-sizing: border-box;
  border-radius: 0.5rem;
  background-color: var(--color-gray-50);
  outline: 1px solid var(--color-gray-200);
  box-shadow:
    0 10px 15px -3px var(--color-gray-200),
    0 4px 6px -4px var(--color-gray-200);
  z-index: 50;
  animation: contentHide 150ms ease forwards;
}

.content[data-expanded] {
  animation: contentShow 150ms ease;
}

@media (prefers-color-scheme: dark) {
  .content {
    outline: 1px solid var(--color-gray-300);
    box-shadow: none;
  }
}

.listbox {
  margin: 0;
  overflow-y: auto;
  max-height: 20rem;
  padding: 0.25rem;
}

.item {
  box-sizing: border-box;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0.375rem 0.5rem;
  border-radius: 0.25rem;
  outline: 0;
  font-size: 1rem;
  line-height: 1.5rem;
  color: var(--color-gray-900);
  cursor: default;
  user-select: none;
}

.item[data-highlighted] {
  background-color: var(--color-gray-100);
}

.item[data-disabled] {
  opacity: 0.5;
}

.item-indicator {
  display: flex;
  align-items: center;
  flex-shrink: 0;
  margin-left: 0.5rem;
  color: var(--color-blue);
}

@keyframes contentShow {
  from {
    opacity: 0;
    transform: scale(0.96);
  }
  to {
    opacity: 1;
    transform: scale(1);
  }
}

@keyframes contentHide {
  from {
    opacity: 1;
    transform: scale(1);
  }
  to {
    opacity: 0;
    transform: scale(0.96);
  }
}

.description {
  font-size: 0.875rem;
  line-height: 1.25rem;
  color: var(--color-gray-600);
  margin-top: 0.25rem;
}

.error {
  font-size: 0.875rem;
  line-height: 1.25rem;
  color: #dc2626;
  margin-top: 0.25rem;
}

.status {
  margin: 1rem 0 0;
  font-size: 0.875rem;
  line-height: 1.25rem;
  color: var(--color-gray-600);
}

.form {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

.actions {
  display: flex;
  gap: 0.5rem;
}

.button {
  box-sizing: border-box;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  height: 2rem;
  padding: 0 0.75rem;
  outline: 0;
  border: 1px solid var(--color-gray-200);
  border-radius: 0.375rem;
  background-color: var(--color-gray-50);
  font-family: inherit;
  font-size: 0.875rem;
  font-weight: 500;
  color: var(--color-gray-900);
  cursor: pointer;
}

@media (hover: hover) {
  .button:hover {
    background-color: var(--color-gray-100);
  }
}

/* Multiple combobox control */
.controlMultiple {
  box-sizing: border-box;
  display: inline-flex;
  align-items: center;
  width: 16rem;
  min-height: 2.5rem;
  border: 1px solid var(--color-gray-300);
  border-radius: 0.375rem;
  background-color: transparent;
  padding: 0.25rem 0.25rem 0.25rem 0.5rem;
  gap: 0.25rem;
  flex-wrap: wrap;
}

.controlMultiple:focus-within {
  outline: 2px solid var(--color-blue);
  outline-offset: -1px;
}

.tags {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem;
  flex: 1;
  min-width: 0;
  align-items: center;
}

.tag {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  padding: 0 0.375rem;
  height: 1.5rem;
  border-radius: 0.25rem;
  background-color: var(--color-gray-100);
  font-size: 0.875rem;
  color: var(--color-gray-900);
}

.tagRemove {
  all: unset;
  display: inline-flex;
  align-items: center;
  cursor: pointer;
  color: var(--color-gray-500);
}

.tagRemove:hover {
  color: var(--color-gray-900);
}

.inputInline {
  box-sizing: border-box;
  flex: 1;
  min-width: 4rem;
  height: 1.5rem;
  padding: 0;
  outline: 0;
  border: none;
  background-color: transparent;
  font-family: inherit;
  font-size: 0.875rem;
  color: var(--color-gray-900);
}
:root {
  --color-blue: oklch(45% 50% 264deg);
  --color-red: oklch(50% 55% 31deg);
  --color-gray-50: oklch(98% 0.25% 264deg);
  --color-gray-100: oklch(12% 9.5% 264deg / 5%);
  --color-gray-200: oklch(12% 9% 264deg / 7%);
  --color-gray-300: oklch(12% 8.5% 264deg / 17%);
  --color-gray-400: oklch(12% 8% 264deg / 38%);
  --color-gray-500: oklch(12% 7.5% 264deg / 50%);
  --color-gray-600: oklch(12% 7% 264deg / 67%);
  --color-gray-700: oklch(12% 6% 264deg / 77%);
  --color-gray-800: oklch(12% 5% 264deg / 85%);
  --color-gray-900: oklch(12% 5% 264deg / 90%);
  --color-gray-950: oklch(12% 5% 264deg / 95%);
}

@media (prefers-color-scheme: dark) {
  :root {
    --color-blue: oklch(69% 50% 264deg);
    --color-red: oklch(80% 55% 31deg);
    --color-gray-50: oklch(17% 0.25% 264deg);
    --color-gray-100: oklch(28% 0.75% 264deg / 65%);
    --color-gray-200: oklch(29% 0.75% 264deg / 80%);
    --color-gray-300: oklch(35% 0.75% 264deg / 80%);
    --color-gray-400: oklch(47% 0.875% 264deg / 80%);
    --color-gray-500: oklch(64% 1% 264deg / 80%);
    --color-gray-600: oklch(82% 1% 264deg / 80%);
    --color-gray-700: oklch(92% 1.125% 264deg / 80%);
    --color-gray-800: oklch(93% 0.875% 264deg / 85%);
    --color-gray-900: oklch(95% 0.5% 264deg / 90%);
    --color-gray-950: oklch(94% 0.375% 264deg / 95%);
  }
}

Controlled

Use value, onChange, and onInputChange to control the selection externally.

Selected: Blueberry

View source
import { createSignal } from "solid-js"
import * as Combobox from "@danielfrg/solid-ui/combobox"
import styles from "./index.module.css"

const FRUITS = ["Apple", "Banana", "Blueberry", "Grapes", "Pineapple"]

export function DemoComboboxControlled() {
  const [value, setValue] = createSignal<string | undefined>("Blueberry")

  return (
    <div>
      <Combobox.Root
        options={FRUITS}
        value={value()}
        onChange={setValue}
        onInputChange={(v) => {
          if (!v) setValue(undefined)
        }}
        itemComponent={(props) => (
          <Combobox.Item item={props.item} class={styles.item}>
            <Combobox.ItemLabel>{props.item.rawValue}</Combobox.ItemLabel>
            <Combobox.ItemIndicator class={styles["item-indicator"]}>
              <CheckIcon />
            </Combobox.ItemIndicator>
          </Combobox.Item>
        )}
      >
        <Combobox.Control<string> class={styles.control} aria-label="Fruit">
          {() => (
            <>
              <Combobox.Input class={styles.input} />
              <Combobox.Trigger class={styles.trigger}>
                <Combobox.Icon class={styles.icon}>
                  <ChevronDownIcon />
                </Combobox.Icon>
              </Combobox.Trigger>
            </>
          )}
        </Combobox.Control>
        <Combobox.Portal>
          <Combobox.Content class={styles.content}>
            <Combobox.Listbox class={styles.listbox} />
          </Combobox.Content>
        </Combobox.Portal>
      </Combobox.Root>
      <p class={styles.status}>Selected: {value() ?? "none"}</p>
    </div>
  )
}

function CheckIcon() {
  return (
    <svg width="15" height="15" viewBox="0 0 15 15" fill="none">
      <path
        d="M11.4669 3.72684C11.7558 3.91574 11.8369 4.30308 11.648 4.59198L7.39799 11.092C7.29783 11.2452 7.13556 11.3467 6.95402 11.3699C6.77247 11.3931 6.58989 11.3354 6.45446 11.2124L3.70446 8.71241C3.44905 8.48022 3.43023 8.08494 3.66242 7.82953C3.89461 7.57412 4.28989 7.5553 4.5453 7.78749L6.75292 9.79441L10.6018 3.90792C10.7907 3.61902 11.178 3.53795 11.4669 3.72684Z"
        fill="currentColor"
        fill-rule="evenodd"
        clip-rule="evenodd"
      />
    </svg>
  )
}

function ChevronDownIcon() {
  return (
    <svg width="15" height="15" viewBox="0 0 15 15" fill="none">
      <path
        d="M3.13523 6.15803C3.3241 5.95657 3.64052 5.94637 3.84197 6.13523L7.5 9.56464L11.158 6.13523C11.3595 5.94637 11.6759 5.95657 11.8648 6.15803C12.0536 6.35949 12.0434 6.67591 11.842 6.86477L7.84197 10.6148C7.64964 10.7951 7.35036 10.7951 7.15803 10.6148L3.15803 6.86477C2.95657 6.67591 2.94637 6.35949 3.13523 6.15803Z"
        fill="currentColor"
        fill-rule="evenodd"
        clip-rule="evenodd"
      />
    </svg>
  )
}
.control {
  box-sizing: border-box;
  display: inline-flex;
  align-items: center;
  width: 12rem;
  border: 1px solid var(--color-gray-300);
  border-radius: 0.375rem;
  background-color: transparent;
  overflow: hidden;
}

.control:focus-within {
  outline: 2px solid var(--color-blue);
  outline-offset: -1px;
}

.control[data-disabled] {
  opacity: 0.5;
  cursor: not-allowed;
}

.input {
  box-sizing: border-box;
  flex: 1;
  min-width: 0;
  height: 2.5rem;
  padding: 0 0.75rem;
  outline: 0;
  border: none;
  background-color: transparent;
  font-family: inherit;
  font-size: 1rem;
  line-height: 1.5rem;
  color: var(--color-gray-900);
}

.input::placeholder {
  color: var(--color-gray-500);
}

.trigger {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: 2rem;
  height: 2.5rem;
  outline: 0;
  border: none;
  background-color: transparent;
  cursor: pointer;
}

.trigger[data-disabled] {
  cursor: not-allowed;
}

.icon {
  display: flex;
  align-items: center;
  color: var(--color-gray-500);
}

.content {
  box-sizing: border-box;
  border-radius: 0.5rem;
  background-color: var(--color-gray-50);
  outline: 1px solid var(--color-gray-200);
  box-shadow:
    0 10px 15px -3px var(--color-gray-200),
    0 4px 6px -4px var(--color-gray-200);
  z-index: 50;
  animation: contentHide 150ms ease forwards;
}

.content[data-expanded] {
  animation: contentShow 150ms ease;
}

@media (prefers-color-scheme: dark) {
  .content {
    outline: 1px solid var(--color-gray-300);
    box-shadow: none;
  }
}

.listbox {
  margin: 0;
  overflow-y: auto;
  max-height: 20rem;
  padding: 0.25rem;
}

.item {
  box-sizing: border-box;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0.375rem 0.5rem;
  border-radius: 0.25rem;
  outline: 0;
  font-size: 1rem;
  line-height: 1.5rem;
  color: var(--color-gray-900);
  cursor: default;
  user-select: none;
}

.item[data-highlighted] {
  background-color: var(--color-gray-100);
}

.item[data-disabled] {
  opacity: 0.5;
}

.item-indicator {
  display: flex;
  align-items: center;
  flex-shrink: 0;
  margin-left: 0.5rem;
  color: var(--color-blue);
}

@keyframes contentShow {
  from {
    opacity: 0;
    transform: scale(0.96);
  }
  to {
    opacity: 1;
    transform: scale(1);
  }
}

@keyframes contentHide {
  from {
    opacity: 1;
    transform: scale(1);
  }
  to {
    opacity: 0;
    transform: scale(0.96);
  }
}

.description {
  font-size: 0.875rem;
  line-height: 1.25rem;
  color: var(--color-gray-600);
  margin-top: 0.25rem;
}

.error {
  font-size: 0.875rem;
  line-height: 1.25rem;
  color: #dc2626;
  margin-top: 0.25rem;
}

.status {
  margin: 1rem 0 0;
  font-size: 0.875rem;
  line-height: 1.25rem;
  color: var(--color-gray-600);
}

.form {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

.actions {
  display: flex;
  gap: 0.5rem;
}

.button {
  box-sizing: border-box;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  height: 2rem;
  padding: 0 0.75rem;
  outline: 0;
  border: 1px solid var(--color-gray-200);
  border-radius: 0.375rem;
  background-color: var(--color-gray-50);
  font-family: inherit;
  font-size: 0.875rem;
  font-weight: 500;
  color: var(--color-gray-900);
  cursor: pointer;
}

@media (hover: hover) {
  .button:hover {
    background-color: var(--color-gray-100);
  }
}

/* Multiple combobox control */
.controlMultiple {
  box-sizing: border-box;
  display: inline-flex;
  align-items: center;
  width: 16rem;
  min-height: 2.5rem;
  border: 1px solid var(--color-gray-300);
  border-radius: 0.375rem;
  background-color: transparent;
  padding: 0.25rem 0.25rem 0.25rem 0.5rem;
  gap: 0.25rem;
  flex-wrap: wrap;
}

.controlMultiple:focus-within {
  outline: 2px solid var(--color-blue);
  outline-offset: -1px;
}

.tags {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem;
  flex: 1;
  min-width: 0;
  align-items: center;
}

.tag {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  padding: 0 0.375rem;
  height: 1.5rem;
  border-radius: 0.25rem;
  background-color: var(--color-gray-100);
  font-size: 0.875rem;
  color: var(--color-gray-900);
}

.tagRemove {
  all: unset;
  display: inline-flex;
  align-items: center;
  cursor: pointer;
  color: var(--color-gray-500);
}

.tagRemove:hover {
  color: var(--color-gray-900);
}

.inputInline {
  box-sizing: border-box;
  flex: 1;
  min-width: 4rem;
  height: 1.5rem;
  padding: 0;
  outline: 0;
  border: none;
  background-color: transparent;
  font-family: inherit;
  font-size: 0.875rem;
  color: var(--color-gray-900);
}
:root {
  --color-blue: oklch(45% 50% 264deg);
  --color-red: oklch(50% 55% 31deg);
  --color-gray-50: oklch(98% 0.25% 264deg);
  --color-gray-100: oklch(12% 9.5% 264deg / 5%);
  --color-gray-200: oklch(12% 9% 264deg / 7%);
  --color-gray-300: oklch(12% 8.5% 264deg / 17%);
  --color-gray-400: oklch(12% 8% 264deg / 38%);
  --color-gray-500: oklch(12% 7.5% 264deg / 50%);
  --color-gray-600: oklch(12% 7% 264deg / 67%);
  --color-gray-700: oklch(12% 6% 264deg / 77%);
  --color-gray-800: oklch(12% 5% 264deg / 85%);
  --color-gray-900: oklch(12% 5% 264deg / 90%);
  --color-gray-950: oklch(12% 5% 264deg / 95%);
}

@media (prefers-color-scheme: dark) {
  :root {
    --color-blue: oklch(69% 50% 264deg);
    --color-red: oklch(80% 55% 31deg);
    --color-gray-50: oklch(17% 0.25% 264deg);
    --color-gray-100: oklch(28% 0.75% 264deg / 65%);
    --color-gray-200: oklch(29% 0.75% 264deg / 80%);
    --color-gray-300: oklch(35% 0.75% 264deg / 80%);
    --color-gray-400: oklch(47% 0.875% 264deg / 80%);
    --color-gray-500: oklch(64% 1% 264deg / 80%);
    --color-gray-600: oklch(82% 1% 264deg / 80%);
    --color-gray-700: oklch(92% 1.125% 264deg / 80%);
    --color-gray-800: oklch(93% 0.875% 264deg / 85%);
    --color-gray-900: oklch(95% 0.5% 264deg / 90%);
    --color-gray-950: oklch(94% 0.375% 264deg / 95%);
  }
}

Description

Use Combobox.Description to add a hint below the control.

Type to search or click to browse available fruits.
View source
import * as Combobox from "@danielfrg/solid-ui/combobox"
import styles from "./index.module.css"

const FRUITS = ["Apple", "Banana", "Blueberry", "Grapes", "Pineapple"]

export function DemoComboboxDescription() {
  return (
    <Combobox.Root
      options={FRUITS}
      placeholder="Search a fruit..."
      itemComponent={(props) => (
        <Combobox.Item item={props.item} class={styles.item}>
          <Combobox.ItemLabel>{props.item.rawValue}</Combobox.ItemLabel>
          <Combobox.ItemIndicator class={styles["item-indicator"]}>
            <CheckIcon />
          </Combobox.ItemIndicator>
        </Combobox.Item>
      )}
    >
      <Combobox.Control<string> class={styles.control} aria-label="Fruit">
        {() => (
          <>
            <Combobox.Input class={styles.input} />
            <Combobox.Trigger class={styles.trigger}>
              <Combobox.Icon class={styles.icon}>
                <ChevronDownIcon />
              </Combobox.Icon>
            </Combobox.Trigger>
          </>
        )}
      </Combobox.Control>
      <Combobox.Description class={styles.description}>
        Type to search or click to browse available fruits.
      </Combobox.Description>
      <Combobox.Portal>
        <Combobox.Content class={styles.content}>
          <Combobox.Listbox class={styles.listbox} />
        </Combobox.Content>
      </Combobox.Portal>
    </Combobox.Root>
  )
}

function CheckIcon() {
  return (
    <svg width="15" height="15" viewBox="0 0 15 15" fill="none">
      <path
        d="M11.4669 3.72684C11.7558 3.91574 11.8369 4.30308 11.648 4.59198L7.39799 11.092C7.29783 11.2452 7.13556 11.3467 6.95402 11.3699C6.77247 11.3931 6.58989 11.3354 6.45446 11.2124L3.70446 8.71241C3.44905 8.48022 3.43023 8.08494 3.66242 7.82953C3.89461 7.57412 4.28989 7.5553 4.5453 7.78749L6.75292 9.79441L10.6018 3.90792C10.7907 3.61902 11.178 3.53795 11.4669 3.72684Z"
        fill="currentColor"
        fill-rule="evenodd"
        clip-rule="evenodd"
      />
    </svg>
  )
}

function ChevronDownIcon() {
  return (
    <svg width="15" height="15" viewBox="0 0 15 15" fill="none">
      <path
        d="M3.13523 6.15803C3.3241 5.95657 3.64052 5.94637 3.84197 6.13523L7.5 9.56464L11.158 6.13523C11.3595 5.94637 11.6759 5.95657 11.8648 6.15803C12.0536 6.35949 12.0434 6.67591 11.842 6.86477L7.84197 10.6148C7.64964 10.7951 7.35036 10.7951 7.15803 10.6148L3.15803 6.86477C2.95657 6.67591 2.94637 6.35949 3.13523 6.15803Z"
        fill="currentColor"
        fill-rule="evenodd"
        clip-rule="evenodd"
      />
    </svg>
  )
}
.control {
  box-sizing: border-box;
  display: inline-flex;
  align-items: center;
  width: 12rem;
  border: 1px solid var(--color-gray-300);
  border-radius: 0.375rem;
  background-color: transparent;
  overflow: hidden;
}

.control:focus-within {
  outline: 2px solid var(--color-blue);
  outline-offset: -1px;
}

.control[data-disabled] {
  opacity: 0.5;
  cursor: not-allowed;
}

.input {
  box-sizing: border-box;
  flex: 1;
  min-width: 0;
  height: 2.5rem;
  padding: 0 0.75rem;
  outline: 0;
  border: none;
  background-color: transparent;
  font-family: inherit;
  font-size: 1rem;
  line-height: 1.5rem;
  color: var(--color-gray-900);
}

.input::placeholder {
  color: var(--color-gray-500);
}

.trigger {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: 2rem;
  height: 2.5rem;
  outline: 0;
  border: none;
  background-color: transparent;
  cursor: pointer;
}

.trigger[data-disabled] {
  cursor: not-allowed;
}

.icon {
  display: flex;
  align-items: center;
  color: var(--color-gray-500);
}

.content {
  box-sizing: border-box;
  border-radius: 0.5rem;
  background-color: var(--color-gray-50);
  outline: 1px solid var(--color-gray-200);
  box-shadow:
    0 10px 15px -3px var(--color-gray-200),
    0 4px 6px -4px var(--color-gray-200);
  z-index: 50;
  animation: contentHide 150ms ease forwards;
}

.content[data-expanded] {
  animation: contentShow 150ms ease;
}

@media (prefers-color-scheme: dark) {
  .content {
    outline: 1px solid var(--color-gray-300);
    box-shadow: none;
  }
}

.listbox {
  margin: 0;
  overflow-y: auto;
  max-height: 20rem;
  padding: 0.25rem;
}

.item {
  box-sizing: border-box;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0.375rem 0.5rem;
  border-radius: 0.25rem;
  outline: 0;
  font-size: 1rem;
  line-height: 1.5rem;
  color: var(--color-gray-900);
  cursor: default;
  user-select: none;
}

.item[data-highlighted] {
  background-color: var(--color-gray-100);
}

.item[data-disabled] {
  opacity: 0.5;
}

.item-indicator {
  display: flex;
  align-items: center;
  flex-shrink: 0;
  margin-left: 0.5rem;
  color: var(--color-blue);
}

@keyframes contentShow {
  from {
    opacity: 0;
    transform: scale(0.96);
  }
  to {
    opacity: 1;
    transform: scale(1);
  }
}

@keyframes contentHide {
  from {
    opacity: 1;
    transform: scale(1);
  }
  to {
    opacity: 0;
    transform: scale(0.96);
  }
}

.description {
  font-size: 0.875rem;
  line-height: 1.25rem;
  color: var(--color-gray-600);
  margin-top: 0.25rem;
}

.error {
  font-size: 0.875rem;
  line-height: 1.25rem;
  color: #dc2626;
  margin-top: 0.25rem;
}

.status {
  margin: 1rem 0 0;
  font-size: 0.875rem;
  line-height: 1.25rem;
  color: var(--color-gray-600);
}

.form {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

.actions {
  display: flex;
  gap: 0.5rem;
}

.button {
  box-sizing: border-box;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  height: 2rem;
  padding: 0 0.75rem;
  outline: 0;
  border: 1px solid var(--color-gray-200);
  border-radius: 0.375rem;
  background-color: var(--color-gray-50);
  font-family: inherit;
  font-size: 0.875rem;
  font-weight: 500;
  color: var(--color-gray-900);
  cursor: pointer;
}

@media (hover: hover) {
  .button:hover {
    background-color: var(--color-gray-100);
  }
}

/* Multiple combobox control */
.controlMultiple {
  box-sizing: border-box;
  display: inline-flex;
  align-items: center;
  width: 16rem;
  min-height: 2.5rem;
  border: 1px solid var(--color-gray-300);
  border-radius: 0.375rem;
  background-color: transparent;
  padding: 0.25rem 0.25rem 0.25rem 0.5rem;
  gap: 0.25rem;
  flex-wrap: wrap;
}

.controlMultiple:focus-within {
  outline: 2px solid var(--color-blue);
  outline-offset: -1px;
}

.tags {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem;
  flex: 1;
  min-width: 0;
  align-items: center;
}

.tag {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  padding: 0 0.375rem;
  height: 1.5rem;
  border-radius: 0.25rem;
  background-color: var(--color-gray-100);
  font-size: 0.875rem;
  color: var(--color-gray-900);
}

.tagRemove {
  all: unset;
  display: inline-flex;
  align-items: center;
  cursor: pointer;
  color: var(--color-gray-500);
}

.tagRemove:hover {
  color: var(--color-gray-900);
}

.inputInline {
  box-sizing: border-box;
  flex: 1;
  min-width: 4rem;
  height: 1.5rem;
  padding: 0;
  outline: 0;
  border: none;
  background-color: transparent;
  font-family: inherit;
  font-size: 0.875rem;
  color: var(--color-gray-900);
}
:root {
  --color-blue: oklch(45% 50% 264deg);
  --color-red: oklch(50% 55% 31deg);
  --color-gray-50: oklch(98% 0.25% 264deg);
  --color-gray-100: oklch(12% 9.5% 264deg / 5%);
  --color-gray-200: oklch(12% 9% 264deg / 7%);
  --color-gray-300: oklch(12% 8.5% 264deg / 17%);
  --color-gray-400: oklch(12% 8% 264deg / 38%);
  --color-gray-500: oklch(12% 7.5% 264deg / 50%);
  --color-gray-600: oklch(12% 7% 264deg / 67%);
  --color-gray-700: oklch(12% 6% 264deg / 77%);
  --color-gray-800: oklch(12% 5% 264deg / 85%);
  --color-gray-900: oklch(12% 5% 264deg / 90%);
  --color-gray-950: oklch(12% 5% 264deg / 95%);
}

@media (prefers-color-scheme: dark) {
  :root {
    --color-blue: oklch(69% 50% 264deg);
    --color-red: oklch(80% 55% 31deg);
    --color-gray-50: oklch(17% 0.25% 264deg);
    --color-gray-100: oklch(28% 0.75% 264deg / 65%);
    --color-gray-200: oklch(29% 0.75% 264deg / 80%);
    --color-gray-300: oklch(35% 0.75% 264deg / 80%);
    --color-gray-400: oklch(47% 0.875% 264deg / 80%);
    --color-gray-500: oklch(64% 1% 264deg / 80%);
    --color-gray-600: oklch(82% 1% 264deg / 80%);
    --color-gray-700: oklch(92% 1.125% 264deg / 80%);
    --color-gray-800: oklch(93% 0.875% 264deg / 85%);
    --color-gray-900: oklch(95% 0.5% 264deg / 90%);
    --color-gray-950: oklch(94% 0.375% 264deg / 95%);
  }
}

Error message

Use validationState and Combobox.ErrorMessage to show validation feedback.

Please select a fruit.
View source
import { createSignal } from "solid-js"
import * as Combobox from "@danielfrg/solid-ui/combobox"
import styles from "./index.module.css"

const FRUITS = ["Apple", "Banana", "Blueberry", "Grapes", "Pineapple"]

export function DemoComboboxErrorMessage() {
  const [value, setValue] = createSignal<string | undefined>(undefined)

  return (
    <Combobox.Root
      options={FRUITS}
      value={value()}
      onChange={setValue}
      validationState={!value() ? "invalid" : "valid"}
      placeholder="Search a fruit..."
      itemComponent={(props) => (
        <Combobox.Item item={props.item} class={styles.item}>
          <Combobox.ItemLabel>{props.item.rawValue}</Combobox.ItemLabel>
          <Combobox.ItemIndicator class={styles["item-indicator"]}>
            <CheckIcon />
          </Combobox.ItemIndicator>
        </Combobox.Item>
      )}
    >
      <Combobox.Control<string> class={styles.control} aria-label="Fruit">
        {() => (
          <>
            <Combobox.Input class={styles.input} />
            <Combobox.Trigger class={styles.trigger}>
              <Combobox.Icon class={styles.icon}>
                <ChevronDownIcon />
              </Combobox.Icon>
            </Combobox.Trigger>
          </>
        )}
      </Combobox.Control>
      <Combobox.ErrorMessage class={styles.error}>Please select a fruit.</Combobox.ErrorMessage>
      <Combobox.Portal>
        <Combobox.Content class={styles.content}>
          <Combobox.Listbox class={styles.listbox} />
        </Combobox.Content>
      </Combobox.Portal>
    </Combobox.Root>
  )
}

function CheckIcon() {
  return (
    <svg width="15" height="15" viewBox="0 0 15 15" fill="none">
      <path
        d="M11.4669 3.72684C11.7558 3.91574 11.8369 4.30308 11.648 4.59198L7.39799 11.092C7.29783 11.2452 7.13556 11.3467 6.95402 11.3699C6.77247 11.3931 6.58989 11.3354 6.45446 11.2124L3.70446 8.71241C3.44905 8.48022 3.43023 8.08494 3.66242 7.82953C3.89461 7.57412 4.28989 7.5553 4.5453 7.78749L6.75292 9.79441L10.6018 3.90792C10.7907 3.61902 11.178 3.53795 11.4669 3.72684Z"
        fill="currentColor"
        fill-rule="evenodd"
        clip-rule="evenodd"
      />
    </svg>
  )
}

function ChevronDownIcon() {
  return (
    <svg width="15" height="15" viewBox="0 0 15 15" fill="none">
      <path
        d="M3.13523 6.15803C3.3241 5.95657 3.64052 5.94637 3.84197 6.13523L7.5 9.56464L11.158 6.13523C11.3595 5.94637 11.6759 5.95657 11.8648 6.15803C12.0536 6.35949 12.0434 6.67591 11.842 6.86477L7.84197 10.6148C7.64964 10.7951 7.35036 10.7951 7.15803 10.6148L3.15803 6.86477C2.95657 6.67591 2.94637 6.35949 3.13523 6.15803Z"
        fill="currentColor"
        fill-rule="evenodd"
        clip-rule="evenodd"
      />
    </svg>
  )
}
.control {
  box-sizing: border-box;
  display: inline-flex;
  align-items: center;
  width: 12rem;
  border: 1px solid var(--color-gray-300);
  border-radius: 0.375rem;
  background-color: transparent;
  overflow: hidden;
}

.control:focus-within {
  outline: 2px solid var(--color-blue);
  outline-offset: -1px;
}

.control[data-disabled] {
  opacity: 0.5;
  cursor: not-allowed;
}

.input {
  box-sizing: border-box;
  flex: 1;
  min-width: 0;
  height: 2.5rem;
  padding: 0 0.75rem;
  outline: 0;
  border: none;
  background-color: transparent;
  font-family: inherit;
  font-size: 1rem;
  line-height: 1.5rem;
  color: var(--color-gray-900);
}

.input::placeholder {
  color: var(--color-gray-500);
}

.trigger {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: 2rem;
  height: 2.5rem;
  outline: 0;
  border: none;
  background-color: transparent;
  cursor: pointer;
}

.trigger[data-disabled] {
  cursor: not-allowed;
}

.icon {
  display: flex;
  align-items: center;
  color: var(--color-gray-500);
}

.content {
  box-sizing: border-box;
  border-radius: 0.5rem;
  background-color: var(--color-gray-50);
  outline: 1px solid var(--color-gray-200);
  box-shadow:
    0 10px 15px -3px var(--color-gray-200),
    0 4px 6px -4px var(--color-gray-200);
  z-index: 50;
  animation: contentHide 150ms ease forwards;
}

.content[data-expanded] {
  animation: contentShow 150ms ease;
}

@media (prefers-color-scheme: dark) {
  .content {
    outline: 1px solid var(--color-gray-300);
    box-shadow: none;
  }
}

.listbox {
  margin: 0;
  overflow-y: auto;
  max-height: 20rem;
  padding: 0.25rem;
}

.item {
  box-sizing: border-box;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0.375rem 0.5rem;
  border-radius: 0.25rem;
  outline: 0;
  font-size: 1rem;
  line-height: 1.5rem;
  color: var(--color-gray-900);
  cursor: default;
  user-select: none;
}

.item[data-highlighted] {
  background-color: var(--color-gray-100);
}

.item[data-disabled] {
  opacity: 0.5;
}

.item-indicator {
  display: flex;
  align-items: center;
  flex-shrink: 0;
  margin-left: 0.5rem;
  color: var(--color-blue);
}

@keyframes contentShow {
  from {
    opacity: 0;
    transform: scale(0.96);
  }
  to {
    opacity: 1;
    transform: scale(1);
  }
}

@keyframes contentHide {
  from {
    opacity: 1;
    transform: scale(1);
  }
  to {
    opacity: 0;
    transform: scale(0.96);
  }
}

.description {
  font-size: 0.875rem;
  line-height: 1.25rem;
  color: var(--color-gray-600);
  margin-top: 0.25rem;
}

.error {
  font-size: 0.875rem;
  line-height: 1.25rem;
  color: #dc2626;
  margin-top: 0.25rem;
}

.status {
  margin: 1rem 0 0;
  font-size: 0.875rem;
  line-height: 1.25rem;
  color: var(--color-gray-600);
}

.form {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

.actions {
  display: flex;
  gap: 0.5rem;
}

.button {
  box-sizing: border-box;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  height: 2rem;
  padding: 0 0.75rem;
  outline: 0;
  border: 1px solid var(--color-gray-200);
  border-radius: 0.375rem;
  background-color: var(--color-gray-50);
  font-family: inherit;
  font-size: 0.875rem;
  font-weight: 500;
  color: var(--color-gray-900);
  cursor: pointer;
}

@media (hover: hover) {
  .button:hover {
    background-color: var(--color-gray-100);
  }
}

/* Multiple combobox control */
.controlMultiple {
  box-sizing: border-box;
  display: inline-flex;
  align-items: center;
  width: 16rem;
  min-height: 2.5rem;
  border: 1px solid var(--color-gray-300);
  border-radius: 0.375rem;
  background-color: transparent;
  padding: 0.25rem 0.25rem 0.25rem 0.5rem;
  gap: 0.25rem;
  flex-wrap: wrap;
}

.controlMultiple:focus-within {
  outline: 2px solid var(--color-blue);
  outline-offset: -1px;
}

.tags {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem;
  flex: 1;
  min-width: 0;
  align-items: center;
}

.tag {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  padding: 0 0.375rem;
  height: 1.5rem;
  border-radius: 0.25rem;
  background-color: var(--color-gray-100);
  font-size: 0.875rem;
  color: var(--color-gray-900);
}

.tagRemove {
  all: unset;
  display: inline-flex;
  align-items: center;
  cursor: pointer;
  color: var(--color-gray-500);
}

.tagRemove:hover {
  color: var(--color-gray-900);
}

.inputInline {
  box-sizing: border-box;
  flex: 1;
  min-width: 4rem;
  height: 1.5rem;
  padding: 0;
  outline: 0;
  border: none;
  background-color: transparent;
  font-family: inherit;
  font-size: 0.875rem;
  color: var(--color-gray-900);
}
:root {
  --color-blue: oklch(45% 50% 264deg);
  --color-red: oklch(50% 55% 31deg);
  --color-gray-50: oklch(98% 0.25% 264deg);
  --color-gray-100: oklch(12% 9.5% 264deg / 5%);
  --color-gray-200: oklch(12% 9% 264deg / 7%);
  --color-gray-300: oklch(12% 8.5% 264deg / 17%);
  --color-gray-400: oklch(12% 8% 264deg / 38%);
  --color-gray-500: oklch(12% 7.5% 264deg / 50%);
  --color-gray-600: oklch(12% 7% 264deg / 67%);
  --color-gray-700: oklch(12% 6% 264deg / 77%);
  --color-gray-800: oklch(12% 5% 264deg / 85%);
  --color-gray-900: oklch(12% 5% 264deg / 90%);
  --color-gray-950: oklch(12% 5% 264deg / 95%);
}

@media (prefers-color-scheme: dark) {
  :root {
    --color-blue: oklch(69% 50% 264deg);
    --color-red: oklch(80% 55% 31deg);
    --color-gray-50: oklch(17% 0.25% 264deg);
    --color-gray-100: oklch(28% 0.75% 264deg / 65%);
    --color-gray-200: oklch(29% 0.75% 264deg / 80%);
    --color-gray-300: oklch(35% 0.75% 264deg / 80%);
    --color-gray-400: oklch(47% 0.875% 264deg / 80%);
    --color-gray-500: oklch(64% 1% 264deg / 80%);
    --color-gray-600: oklch(82% 1% 264deg / 80%);
    --color-gray-700: oklch(92% 1.125% 264deg / 80%);
    --color-gray-800: oklch(93% 0.875% 264deg / 85%);
    --color-gray-900: oklch(95% 0.5% 264deg / 90%);
    --color-gray-950: oklch(94% 0.375% 264deg / 95%);
  }
}

HTML form

Use the name prop with Combobox.HiddenSelect to include the value in a native form submission.

View source
import * as Combobox from "@danielfrg/solid-ui/combobox"
import styles from "./index.module.css"

const FRUITS = ["Apple", "Banana", "Blueberry", "Grapes", "Pineapple"]

export function DemoComboboxHtmlForm() {
  return (
    <form
      onSubmit={(e) => {
        e.preventDefault()
        const data = new FormData(e.currentTarget)
        window.alert(JSON.stringify(Object.fromEntries(data), null, 2))
      }}
      class={styles.form}
    >
      <Combobox.Root
        options={FRUITS}
        name="fruit"
        placeholder="Search a fruit..."
        itemComponent={(props) => (
          <Combobox.Item item={props.item} class={styles.item}>
            <Combobox.ItemLabel>{props.item.rawValue}</Combobox.ItemLabel>
            <Combobox.ItemIndicator class={styles["item-indicator"]}>
              <CheckIcon />
            </Combobox.ItemIndicator>
          </Combobox.Item>
        )}
      >
        <Combobox.HiddenSelect />
        <Combobox.Control<string> class={styles.control} aria-label="Fruit">
          {() => (
            <>
              <Combobox.Input class={styles.input} />
              <Combobox.Trigger class={styles.trigger}>
                <Combobox.Icon class={styles.icon}>
                  <ChevronDownIcon />
                </Combobox.Icon>
              </Combobox.Trigger>
            </>
          )}
        </Combobox.Control>
        <Combobox.Portal>
          <Combobox.Content class={styles.content}>
            <Combobox.Listbox class={styles.listbox} />
          </Combobox.Content>
        </Combobox.Portal>
      </Combobox.Root>
      <div class={styles.actions}>
        <button type="reset" class={styles.button}>
          Reset
        </button>
        <button type="submit" class={styles.button}>
          Submit
        </button>
      </div>
    </form>
  )
}

function CheckIcon() {
  return (
    <svg width="15" height="15" viewBox="0 0 15 15" fill="none">
      <path
        d="M11.4669 3.72684C11.7558 3.91574 11.8369 4.30308 11.648 4.59198L7.39799 11.092C7.29783 11.2452 7.13556 11.3467 6.95402 11.3699C6.77247 11.3931 6.58989 11.3354 6.45446 11.2124L3.70446 8.71241C3.44905 8.48022 3.43023 8.08494 3.66242 7.82953C3.89461 7.57412 4.28989 7.5553 4.5453 7.78749L6.75292 9.79441L10.6018 3.90792C10.7907 3.61902 11.178 3.53795 11.4669 3.72684Z"
        fill="currentColor"
        fill-rule="evenodd"
        clip-rule="evenodd"
      />
    </svg>
  )
}

function ChevronDownIcon() {
  return (
    <svg width="15" height="15" viewBox="0 0 15 15" fill="none">
      <path
        d="M3.13523 6.15803C3.3241 5.95657 3.64052 5.94637 3.84197 6.13523L7.5 9.56464L11.158 6.13523C11.3595 5.94637 11.6759 5.95657 11.8648 6.15803C12.0536 6.35949 12.0434 6.67591 11.842 6.86477L7.84197 10.6148C7.64964 10.7951 7.35036 10.7951 7.15803 10.6148L3.15803 6.86477C2.95657 6.67591 2.94637 6.35949 3.13523 6.15803Z"
        fill="currentColor"
        fill-rule="evenodd"
        clip-rule="evenodd"
      />
    </svg>
  )
}
.control {
  box-sizing: border-box;
  display: inline-flex;
  align-items: center;
  width: 12rem;
  border: 1px solid var(--color-gray-300);
  border-radius: 0.375rem;
  background-color: transparent;
  overflow: hidden;
}

.control:focus-within {
  outline: 2px solid var(--color-blue);
  outline-offset: -1px;
}

.control[data-disabled] {
  opacity: 0.5;
  cursor: not-allowed;
}

.input {
  box-sizing: border-box;
  flex: 1;
  min-width: 0;
  height: 2.5rem;
  padding: 0 0.75rem;
  outline: 0;
  border: none;
  background-color: transparent;
  font-family: inherit;
  font-size: 1rem;
  line-height: 1.5rem;
  color: var(--color-gray-900);
}

.input::placeholder {
  color: var(--color-gray-500);
}

.trigger {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: 2rem;
  height: 2.5rem;
  outline: 0;
  border: none;
  background-color: transparent;
  cursor: pointer;
}

.trigger[data-disabled] {
  cursor: not-allowed;
}

.icon {
  display: flex;
  align-items: center;
  color: var(--color-gray-500);
}

.content {
  box-sizing: border-box;
  border-radius: 0.5rem;
  background-color: var(--color-gray-50);
  outline: 1px solid var(--color-gray-200);
  box-shadow:
    0 10px 15px -3px var(--color-gray-200),
    0 4px 6px -4px var(--color-gray-200);
  z-index: 50;
  animation: contentHide 150ms ease forwards;
}

.content[data-expanded] {
  animation: contentShow 150ms ease;
}

@media (prefers-color-scheme: dark) {
  .content {
    outline: 1px solid var(--color-gray-300);
    box-shadow: none;
  }
}

.listbox {
  margin: 0;
  overflow-y: auto;
  max-height: 20rem;
  padding: 0.25rem;
}

.item {
  box-sizing: border-box;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0.375rem 0.5rem;
  border-radius: 0.25rem;
  outline: 0;
  font-size: 1rem;
  line-height: 1.5rem;
  color: var(--color-gray-900);
  cursor: default;
  user-select: none;
}

.item[data-highlighted] {
  background-color: var(--color-gray-100);
}

.item[data-disabled] {
  opacity: 0.5;
}

.item-indicator {
  display: flex;
  align-items: center;
  flex-shrink: 0;
  margin-left: 0.5rem;
  color: var(--color-blue);
}

@keyframes contentShow {
  from {
    opacity: 0;
    transform: scale(0.96);
  }
  to {
    opacity: 1;
    transform: scale(1);
  }
}

@keyframes contentHide {
  from {
    opacity: 1;
    transform: scale(1);
  }
  to {
    opacity: 0;
    transform: scale(0.96);
  }
}

.description {
  font-size: 0.875rem;
  line-height: 1.25rem;
  color: var(--color-gray-600);
  margin-top: 0.25rem;
}

.error {
  font-size: 0.875rem;
  line-height: 1.25rem;
  color: #dc2626;
  margin-top: 0.25rem;
}

.status {
  margin: 1rem 0 0;
  font-size: 0.875rem;
  line-height: 1.25rem;
  color: var(--color-gray-600);
}

.form {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

.actions {
  display: flex;
  gap: 0.5rem;
}

.button {
  box-sizing: border-box;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  height: 2rem;
  padding: 0 0.75rem;
  outline: 0;
  border: 1px solid var(--color-gray-200);
  border-radius: 0.375rem;
  background-color: var(--color-gray-50);
  font-family: inherit;
  font-size: 0.875rem;
  font-weight: 500;
  color: var(--color-gray-900);
  cursor: pointer;
}

@media (hover: hover) {
  .button:hover {
    background-color: var(--color-gray-100);
  }
}

/* Multiple combobox control */
.controlMultiple {
  box-sizing: border-box;
  display: inline-flex;
  align-items: center;
  width: 16rem;
  min-height: 2.5rem;
  border: 1px solid var(--color-gray-300);
  border-radius: 0.375rem;
  background-color: transparent;
  padding: 0.25rem 0.25rem 0.25rem 0.5rem;
  gap: 0.25rem;
  flex-wrap: wrap;
}

.controlMultiple:focus-within {
  outline: 2px solid var(--color-blue);
  outline-offset: -1px;
}

.tags {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem;
  flex: 1;
  min-width: 0;
  align-items: center;
}

.tag {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  padding: 0 0.375rem;
  height: 1.5rem;
  border-radius: 0.25rem;
  background-color: var(--color-gray-100);
  font-size: 0.875rem;
  color: var(--color-gray-900);
}

.tagRemove {
  all: unset;
  display: inline-flex;
  align-items: center;
  cursor: pointer;
  color: var(--color-gray-500);
}

.tagRemove:hover {
  color: var(--color-gray-900);
}

.inputInline {
  box-sizing: border-box;
  flex: 1;
  min-width: 4rem;
  height: 1.5rem;
  padding: 0;
  outline: 0;
  border: none;
  background-color: transparent;
  font-family: inherit;
  font-size: 0.875rem;
  color: var(--color-gray-900);
}
:root {
  --color-blue: oklch(45% 50% 264deg);
  --color-red: oklch(50% 55% 31deg);
  --color-gray-50: oklch(98% 0.25% 264deg);
  --color-gray-100: oklch(12% 9.5% 264deg / 5%);
  --color-gray-200: oklch(12% 9% 264deg / 7%);
  --color-gray-300: oklch(12% 8.5% 264deg / 17%);
  --color-gray-400: oklch(12% 8% 264deg / 38%);
  --color-gray-500: oklch(12% 7.5% 264deg / 50%);
  --color-gray-600: oklch(12% 7% 264deg / 67%);
  --color-gray-700: oklch(12% 6% 264deg / 77%);
  --color-gray-800: oklch(12% 5% 264deg / 85%);
  --color-gray-900: oklch(12% 5% 264deg / 90%);
  --color-gray-950: oklch(12% 5% 264deg / 95%);
}

@media (prefers-color-scheme: dark) {
  :root {
    --color-blue: oklch(69% 50% 264deg);
    --color-red: oklch(80% 55% 31deg);
    --color-gray-50: oklch(17% 0.25% 264deg);
    --color-gray-100: oklch(28% 0.75% 264deg / 65%);
    --color-gray-200: oklch(29% 0.75% 264deg / 80%);
    --color-gray-300: oklch(35% 0.75% 264deg / 80%);
    --color-gray-400: oklch(47% 0.875% 264deg / 80%);
    --color-gray-500: oklch(64% 1% 264deg / 80%);
    --color-gray-600: oklch(82% 1% 264deg / 80%);
    --color-gray-700: oklch(92% 1.125% 264deg / 80%);
    --color-gray-800: oklch(93% 0.875% 264deg / 85%);
    --color-gray-900: oklch(95% 0.5% 264deg / 90%);
    --color-gray-950: oklch(94% 0.375% 264deg / 95%);
  }
}

Object options

Use optionValue, optionTextValue, optionLabel, and optionDisabled to work with object arrays.

View source
import * as Combobox from "@danielfrg/solid-ui/combobox"
import styles from "./index.module.css"

interface Food {
  value: string
  label: string
  disabled: boolean
}

const FOODS: Food[] = [
  { value: "apple", label: "Apple", disabled: false },
  { value: "banana", label: "Banana", disabled: false },
  { value: "blueberry", label: "Blueberry", disabled: false },
  { value: "grapes", label: "Grapes", disabled: true },
  { value: "pineapple", label: "Pineapple", disabled: false },
]

export function DemoComboboxObject() {
  return (
    <Combobox.Root<Food>
      options={FOODS}
      optionValue="value"
      optionTextValue="label"
      optionLabel="label"
      optionDisabled="disabled"
      placeholder="Search a fruit..."
      itemComponent={(props) => (
        <Combobox.Item item={props.item} class={styles.item}>
          <Combobox.ItemLabel>{props.item.rawValue.label}</Combobox.ItemLabel>
          <Combobox.ItemIndicator class={styles["item-indicator"]}>
            <CheckIcon />
          </Combobox.ItemIndicator>
        </Combobox.Item>
      )}
    >
      <Combobox.Control<Food> class={styles.control} aria-label="Fruit">
        {() => (
          <>
            <Combobox.Input class={styles.input} />
            <Combobox.Trigger class={styles.trigger}>
              <Combobox.Icon class={styles.icon}>
                <ChevronDownIcon />
              </Combobox.Icon>
            </Combobox.Trigger>
          </>
        )}
      </Combobox.Control>
      <Combobox.Portal>
        <Combobox.Content class={styles.content}>
          <Combobox.Listbox class={styles.listbox} />
        </Combobox.Content>
      </Combobox.Portal>
    </Combobox.Root>
  )
}

function CheckIcon() {
  return (
    <svg width="15" height="15" viewBox="0 0 15 15" fill="none">
      <path
        d="M11.4669 3.72684C11.7558 3.91574 11.8369 4.30308 11.648 4.59198L7.39799 11.092C7.29783 11.2452 7.13556 11.3467 6.95402 11.3699C6.77247 11.3931 6.58989 11.3354 6.45446 11.2124L3.70446 8.71241C3.44905 8.48022 3.43023 8.08494 3.66242 7.82953C3.89461 7.57412 4.28989 7.5553 4.5453 7.78749L6.75292 9.79441L10.6018 3.90792C10.7907 3.61902 11.178 3.53795 11.4669 3.72684Z"
        fill="currentColor"
        fill-rule="evenodd"
        clip-rule="evenodd"
      />
    </svg>
  )
}

function ChevronDownIcon() {
  return (
    <svg width="15" height="15" viewBox="0 0 15 15" fill="none">
      <path
        d="M3.13523 6.15803C3.3241 5.95657 3.64052 5.94637 3.84197 6.13523L7.5 9.56464L11.158 6.13523C11.3595 5.94637 11.6759 5.95657 11.8648 6.15803C12.0536 6.35949 12.0434 6.67591 11.842 6.86477L7.84197 10.6148C7.64964 10.7951 7.35036 10.7951 7.15803 10.6148L3.15803 6.86477C2.95657 6.67591 2.94637 6.35949 3.13523 6.15803Z"
        fill="currentColor"
        fill-rule="evenodd"
        clip-rule="evenodd"
      />
    </svg>
  )
}
.control {
  box-sizing: border-box;
  display: inline-flex;
  align-items: center;
  width: 12rem;
  border: 1px solid var(--color-gray-300);
  border-radius: 0.375rem;
  background-color: transparent;
  overflow: hidden;
}

.control:focus-within {
  outline: 2px solid var(--color-blue);
  outline-offset: -1px;
}

.control[data-disabled] {
  opacity: 0.5;
  cursor: not-allowed;
}

.input {
  box-sizing: border-box;
  flex: 1;
  min-width: 0;
  height: 2.5rem;
  padding: 0 0.75rem;
  outline: 0;
  border: none;
  background-color: transparent;
  font-family: inherit;
  font-size: 1rem;
  line-height: 1.5rem;
  color: var(--color-gray-900);
}

.input::placeholder {
  color: var(--color-gray-500);
}

.trigger {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: 2rem;
  height: 2.5rem;
  outline: 0;
  border: none;
  background-color: transparent;
  cursor: pointer;
}

.trigger[data-disabled] {
  cursor: not-allowed;
}

.icon {
  display: flex;
  align-items: center;
  color: var(--color-gray-500);
}

.content {
  box-sizing: border-box;
  border-radius: 0.5rem;
  background-color: var(--color-gray-50);
  outline: 1px solid var(--color-gray-200);
  box-shadow:
    0 10px 15px -3px var(--color-gray-200),
    0 4px 6px -4px var(--color-gray-200);
  z-index: 50;
  animation: contentHide 150ms ease forwards;
}

.content[data-expanded] {
  animation: contentShow 150ms ease;
}

@media (prefers-color-scheme: dark) {
  .content {
    outline: 1px solid var(--color-gray-300);
    box-shadow: none;
  }
}

.listbox {
  margin: 0;
  overflow-y: auto;
  max-height: 20rem;
  padding: 0.25rem;
}

.item {
  box-sizing: border-box;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0.375rem 0.5rem;
  border-radius: 0.25rem;
  outline: 0;
  font-size: 1rem;
  line-height: 1.5rem;
  color: var(--color-gray-900);
  cursor: default;
  user-select: none;
}

.item[data-highlighted] {
  background-color: var(--color-gray-100);
}

.item[data-disabled] {
  opacity: 0.5;
}

.item-indicator {
  display: flex;
  align-items: center;
  flex-shrink: 0;
  margin-left: 0.5rem;
  color: var(--color-blue);
}

@keyframes contentShow {
  from {
    opacity: 0;
    transform: scale(0.96);
  }
  to {
    opacity: 1;
    transform: scale(1);
  }
}

@keyframes contentHide {
  from {
    opacity: 1;
    transform: scale(1);
  }
  to {
    opacity: 0;
    transform: scale(0.96);
  }
}

.description {
  font-size: 0.875rem;
  line-height: 1.25rem;
  color: var(--color-gray-600);
  margin-top: 0.25rem;
}

.error {
  font-size: 0.875rem;
  line-height: 1.25rem;
  color: #dc2626;
  margin-top: 0.25rem;
}

.status {
  margin: 1rem 0 0;
  font-size: 0.875rem;
  line-height: 1.25rem;
  color: var(--color-gray-600);
}

.form {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

.actions {
  display: flex;
  gap: 0.5rem;
}

.button {
  box-sizing: border-box;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  height: 2rem;
  padding: 0 0.75rem;
  outline: 0;
  border: 1px solid var(--color-gray-200);
  border-radius: 0.375rem;
  background-color: var(--color-gray-50);
  font-family: inherit;
  font-size: 0.875rem;
  font-weight: 500;
  color: var(--color-gray-900);
  cursor: pointer;
}

@media (hover: hover) {
  .button:hover {
    background-color: var(--color-gray-100);
  }
}

/* Multiple combobox control */
.controlMultiple {
  box-sizing: border-box;
  display: inline-flex;
  align-items: center;
  width: 16rem;
  min-height: 2.5rem;
  border: 1px solid var(--color-gray-300);
  border-radius: 0.375rem;
  background-color: transparent;
  padding: 0.25rem 0.25rem 0.25rem 0.5rem;
  gap: 0.25rem;
  flex-wrap: wrap;
}

.controlMultiple:focus-within {
  outline: 2px solid var(--color-blue);
  outline-offset: -1px;
}

.tags {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem;
  flex: 1;
  min-width: 0;
  align-items: center;
}

.tag {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  padding: 0 0.375rem;
  height: 1.5rem;
  border-radius: 0.25rem;
  background-color: var(--color-gray-100);
  font-size: 0.875rem;
  color: var(--color-gray-900);
}

.tagRemove {
  all: unset;
  display: inline-flex;
  align-items: center;
  cursor: pointer;
  color: var(--color-gray-500);
}

.tagRemove:hover {
  color: var(--color-gray-900);
}

.inputInline {
  box-sizing: border-box;
  flex: 1;
  min-width: 4rem;
  height: 1.5rem;
  padding: 0;
  outline: 0;
  border: none;
  background-color: transparent;
  font-family: inherit;
  font-size: 0.875rem;
  color: var(--color-gray-900);
}
:root {
  --color-blue: oklch(45% 50% 264deg);
  --color-red: oklch(50% 55% 31deg);
  --color-gray-50: oklch(98% 0.25% 264deg);
  --color-gray-100: oklch(12% 9.5% 264deg / 5%);
  --color-gray-200: oklch(12% 9% 264deg / 7%);
  --color-gray-300: oklch(12% 8.5% 264deg / 17%);
  --color-gray-400: oklch(12% 8% 264deg / 38%);
  --color-gray-500: oklch(12% 7.5% 264deg / 50%);
  --color-gray-600: oklch(12% 7% 264deg / 67%);
  --color-gray-700: oklch(12% 6% 264deg / 77%);
  --color-gray-800: oklch(12% 5% 264deg / 85%);
  --color-gray-900: oklch(12% 5% 264deg / 90%);
  --color-gray-950: oklch(12% 5% 264deg / 95%);
}

@media (prefers-color-scheme: dark) {
  :root {
    --color-blue: oklch(69% 50% 264deg);
    --color-red: oklch(80% 55% 31deg);
    --color-gray-50: oklch(17% 0.25% 264deg);
    --color-gray-100: oklch(28% 0.75% 264deg / 65%);
    --color-gray-200: oklch(29% 0.75% 264deg / 80%);
    --color-gray-300: oklch(35% 0.75% 264deg / 80%);
    --color-gray-400: oklch(47% 0.875% 264deg / 80%);
    --color-gray-500: oklch(64% 1% 264deg / 80%);
    --color-gray-600: oklch(82% 1% 264deg / 80%);
    --color-gray-700: oklch(92% 1.125% 264deg / 80%);
    --color-gray-800: oklch(93% 0.875% 264deg / 85%);
    --color-gray-900: oklch(95% 0.5% 264deg / 90%);
    --color-gray-950: oklch(94% 0.375% 264deg / 95%);
  }
}

Multiple selection

Use the multiple prop to allow selecting more than one option.

BlueberryGrapes
View source
import { createSignal, For } from "solid-js"
import * as Combobox from "@danielfrg/solid-ui/combobox"
import styles from "./index.module.css"

const FRUITS = ["Apple", "Banana", "Blueberry", "Grapes", "Pineapple"]

export function DemoComboboxMultiple() {
  const [values, setValues] = createSignal<string[]>(["Blueberry", "Grapes"])

  return (
    <Combobox.Root<string>
      multiple
      options={FRUITS}
      value={values()}
      onChange={setValues}
      placeholder="Search fruits..."
      itemComponent={(props) => (
        <Combobox.Item item={props.item} class={styles.item}>
          <Combobox.ItemLabel>{props.item.rawValue}</Combobox.ItemLabel>
          <Combobox.ItemIndicator class={styles["item-indicator"]}>
            <CheckIcon />
          </Combobox.ItemIndicator>
        </Combobox.Item>
      )}
    >
      <Combobox.Control<string> class={styles.controlMultiple} aria-label="Fruits">
        {(state) => (
          <>
            <div class={styles.tags}>
              <For each={state.selectedOptions()}>
                {(option) => (
                  <span class={styles.tag}>
                    {option}
                    <button
                      class={styles.tagRemove}
                      onPointerDown={(e) => e.stopPropagation()}
                      onClick={() => state.remove(option)}
                      aria-label={`Remove ${option}`}
                    >
                      <CloseIcon />
                    </button>
                  </span>
                )}
              </For>
              <Combobox.Input class={styles.inputInline} />
            </div>
            <Combobox.Trigger class={styles.trigger}>
              <Combobox.Icon class={styles.icon}>
                <ChevronDownIcon />
              </Combobox.Icon>
            </Combobox.Trigger>
          </>
        )}
      </Combobox.Control>
      <Combobox.Portal>
        <Combobox.Content class={styles.content}>
          <Combobox.Listbox class={styles.listbox} />
        </Combobox.Content>
      </Combobox.Portal>
    </Combobox.Root>
  )
}

function CheckIcon() {
  return (
    <svg width="15" height="15" viewBox="0 0 15 15" fill="none">
      <path
        d="M11.4669 3.72684C11.7558 3.91574 11.8369 4.30308 11.648 4.59198L7.39799 11.092C7.29783 11.2452 7.13556 11.3467 6.95402 11.3699C6.77247 11.3931 6.58989 11.3354 6.45446 11.2124L3.70446 8.71241C3.44905 8.48022 3.43023 8.08494 3.66242 7.82953C3.89461 7.57412 4.28989 7.5553 4.5453 7.78749L6.75292 9.79441L10.6018 3.90792C10.7907 3.61902 11.178 3.53795 11.4669 3.72684Z"
        fill="currentColor"
        fill-rule="evenodd"
        clip-rule="evenodd"
      />
    </svg>
  )
}

function ChevronDownIcon() {
  return (
    <svg width="15" height="15" viewBox="0 0 15 15" fill="none">
      <path
        d="M3.13523 6.15803C3.3241 5.95657 3.64052 5.94637 3.84197 6.13523L7.5 9.56464L11.158 6.13523C11.3595 5.94637 11.6759 5.95657 11.8648 6.15803C12.0536 6.35949 12.0434 6.67591 11.842 6.86477L7.84197 10.6148C7.64964 10.7951 7.35036 10.7951 7.15803 10.6148L3.15803 6.86477C2.95657 6.67591 2.94637 6.35949 3.13523 6.15803Z"
        fill="currentColor"
        fill-rule="evenodd"
        clip-rule="evenodd"
      />
    </svg>
  )
}

function CloseIcon() {
  return (
    <svg width="10" height="10" viewBox="0 0 10 10" fill="none">
      <path d="M7.5 2.5L2.5 7.5M2.5 2.5L7.5 7.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
    </svg>
  )
}
.control {
  box-sizing: border-box;
  display: inline-flex;
  align-items: center;
  width: 12rem;
  border: 1px solid var(--color-gray-300);
  border-radius: 0.375rem;
  background-color: transparent;
  overflow: hidden;
}

.control:focus-within {
  outline: 2px solid var(--color-blue);
  outline-offset: -1px;
}

.control[data-disabled] {
  opacity: 0.5;
  cursor: not-allowed;
}

.input {
  box-sizing: border-box;
  flex: 1;
  min-width: 0;
  height: 2.5rem;
  padding: 0 0.75rem;
  outline: 0;
  border: none;
  background-color: transparent;
  font-family: inherit;
  font-size: 1rem;
  line-height: 1.5rem;
  color: var(--color-gray-900);
}

.input::placeholder {
  color: var(--color-gray-500);
}

.trigger {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: 2rem;
  height: 2.5rem;
  outline: 0;
  border: none;
  background-color: transparent;
  cursor: pointer;
}

.trigger[data-disabled] {
  cursor: not-allowed;
}

.icon {
  display: flex;
  align-items: center;
  color: var(--color-gray-500);
}

.content {
  box-sizing: border-box;
  border-radius: 0.5rem;
  background-color: var(--color-gray-50);
  outline: 1px solid var(--color-gray-200);
  box-shadow:
    0 10px 15px -3px var(--color-gray-200),
    0 4px 6px -4px var(--color-gray-200);
  z-index: 50;
  animation: contentHide 150ms ease forwards;
}

.content[data-expanded] {
  animation: contentShow 150ms ease;
}

@media (prefers-color-scheme: dark) {
  .content {
    outline: 1px solid var(--color-gray-300);
    box-shadow: none;
  }
}

.listbox {
  margin: 0;
  overflow-y: auto;
  max-height: 20rem;
  padding: 0.25rem;
}

.item {
  box-sizing: border-box;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0.375rem 0.5rem;
  border-radius: 0.25rem;
  outline: 0;
  font-size: 1rem;
  line-height: 1.5rem;
  color: var(--color-gray-900);
  cursor: default;
  user-select: none;
}

.item[data-highlighted] {
  background-color: var(--color-gray-100);
}

.item[data-disabled] {
  opacity: 0.5;
}

.item-indicator {
  display: flex;
  align-items: center;
  flex-shrink: 0;
  margin-left: 0.5rem;
  color: var(--color-blue);
}

@keyframes contentShow {
  from {
    opacity: 0;
    transform: scale(0.96);
  }
  to {
    opacity: 1;
    transform: scale(1);
  }
}

@keyframes contentHide {
  from {
    opacity: 1;
    transform: scale(1);
  }
  to {
    opacity: 0;
    transform: scale(0.96);
  }
}

.description {
  font-size: 0.875rem;
  line-height: 1.25rem;
  color: var(--color-gray-600);
  margin-top: 0.25rem;
}

.error {
  font-size: 0.875rem;
  line-height: 1.25rem;
  color: #dc2626;
  margin-top: 0.25rem;
}

.status {
  margin: 1rem 0 0;
  font-size: 0.875rem;
  line-height: 1.25rem;
  color: var(--color-gray-600);
}

.form {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

.actions {
  display: flex;
  gap: 0.5rem;
}

.button {
  box-sizing: border-box;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  height: 2rem;
  padding: 0 0.75rem;
  outline: 0;
  border: 1px solid var(--color-gray-200);
  border-radius: 0.375rem;
  background-color: var(--color-gray-50);
  font-family: inherit;
  font-size: 0.875rem;
  font-weight: 500;
  color: var(--color-gray-900);
  cursor: pointer;
}

@media (hover: hover) {
  .button:hover {
    background-color: var(--color-gray-100);
  }
}

/* Multiple combobox control */
.controlMultiple {
  box-sizing: border-box;
  display: inline-flex;
  align-items: center;
  width: 16rem;
  min-height: 2.5rem;
  border: 1px solid var(--color-gray-300);
  border-radius: 0.375rem;
  background-color: transparent;
  padding: 0.25rem 0.25rem 0.25rem 0.5rem;
  gap: 0.25rem;
  flex-wrap: wrap;
}

.controlMultiple:focus-within {
  outline: 2px solid var(--color-blue);
  outline-offset: -1px;
}

.tags {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem;
  flex: 1;
  min-width: 0;
  align-items: center;
}

.tag {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  padding: 0 0.375rem;
  height: 1.5rem;
  border-radius: 0.25rem;
  background-color: var(--color-gray-100);
  font-size: 0.875rem;
  color: var(--color-gray-900);
}

.tagRemove {
  all: unset;
  display: inline-flex;
  align-items: center;
  cursor: pointer;
  color: var(--color-gray-500);
}

.tagRemove:hover {
  color: var(--color-gray-900);
}

.inputInline {
  box-sizing: border-box;
  flex: 1;
  min-width: 4rem;
  height: 1.5rem;
  padding: 0;
  outline: 0;
  border: none;
  background-color: transparent;
  font-family: inherit;
  font-size: 0.875rem;
  color: var(--color-gray-900);
}
:root {
  --color-blue: oklch(45% 50% 264deg);
  --color-red: oklch(50% 55% 31deg);
  --color-gray-50: oklch(98% 0.25% 264deg);
  --color-gray-100: oklch(12% 9.5% 264deg / 5%);
  --color-gray-200: oklch(12% 9% 264deg / 7%);
  --color-gray-300: oklch(12% 8.5% 264deg / 17%);
  --color-gray-400: oklch(12% 8% 264deg / 38%);
  --color-gray-500: oklch(12% 7.5% 264deg / 50%);
  --color-gray-600: oklch(12% 7% 264deg / 67%);
  --color-gray-700: oklch(12% 6% 264deg / 77%);
  --color-gray-800: oklch(12% 5% 264deg / 85%);
  --color-gray-900: oklch(12% 5% 264deg / 90%);
  --color-gray-950: oklch(12% 5% 264deg / 95%);
}

@media (prefers-color-scheme: dark) {
  :root {
    --color-blue: oklch(69% 50% 264deg);
    --color-red: oklch(80% 55% 31deg);
    --color-gray-50: oklch(17% 0.25% 264deg);
    --color-gray-100: oklch(28% 0.75% 264deg / 65%);
    --color-gray-200: oklch(29% 0.75% 264deg / 80%);
    --color-gray-300: oklch(35% 0.75% 264deg / 80%);
    --color-gray-400: oklch(47% 0.875% 264deg / 80%);
    --color-gray-500: oklch(64% 1% 264deg / 80%);
    --color-gray-600: oklch(82% 1% 264deg / 80%);
    --color-gray-700: oklch(92% 1.125% 264deg / 80%);
    --color-gray-800: oklch(93% 0.875% 264deg / 85%);
    --color-gray-900: oklch(95% 0.5% 264deg / 90%);
    --color-gray-950: oklch(94% 0.375% 264deg / 95%);
  }
}