# Custom select

Custom select is a neobrutalist React component from Cruise UI. A select with our own list (check on the chosen option), for when native won't do.

Install (with the @cruiseui registry in components.json, see Installation):

```bash
npx shadcn@latest add @cruiseui/custom-select
```

Import: `import { CruiseCustomSelect } from "@/components/cruise/custom-select";`

## When to use

Outside forms, or when options need more than text.

## When not to

Forms (use the native Select). Long lists (use a Combobox).

## Example (React)

```tsx
import { CruiseCustomSelect } from "@/components/cruise/custom-select";

const areas = ["Ikeja", "Yaba", "Surulere", "Lekki", "Victoria Island", "Ajah"].map((label) => ({
  value: label.toLowerCase().replace(/\s+/g, "-"),
  label,
}));

export default function CustomSelectExample() {
  return (
    <div className="max-w-sm">
      <CruiseCustomSelect label="Delivery area" options={areas} defaultValue="yaba" />
    </div>
  );
}
```

## HTML

Needs theme.css (https://cruiseui.dev/theme.css).

```html
<div class="menu-wrap" data-menu>
  <button type="button" class="brutal btn btn-secondary" id="order-btn" aria-haspopup="menu" aria-expanded="false" aria-controls="order-menu">
    Order options
    <svg class="menu-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg>
  </button>
  <div class="menu" role="menu" id="order-menu" aria-labelledby="order-btn" hidden>
    <div class="menu-item" role="menuitem" tabindex="-1">Edit order<span class="menu-hint">E</span></div>
    <div class="menu-item" role="menuitem" tabindex="-1">Share receipt</div>
    <div class="menu-sep" role="separator"></div>
    <div class="menu-item" role="menuitem" tabindex="-1" data-danger>Cancel order</div>
  </div>
</div>

<!-- Custom select: same script, role="listbox" -->
<div class="field">
  <span class="field-label" id="area-label">Delivery area</span>
  <div class="menu-wrap" data-menu style="display: block">
    <button type="button" class="select select-trigger" aria-haspopup="listbox" aria-expanded="false" aria-controls="area-list" aria-labelledby="area-label area-value">
      <span class="select-value" id="area-value">Yaba</span>
      <svg class="menu-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg>
    </button>
    <div class="menu" role="listbox" id="area-list" aria-labelledby="area-label" hidden>
      <div class="menu-item" role="option" tabindex="-1" aria-selected="false"><svg class="menu-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg>Ikeja</div>
      <div class="menu-item" role="option" tabindex="-1" aria-selected="true"><svg class="menu-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg>Yaba</div>
    </div>
  </div>
</div>
```

## Props: CruiseCustomSelect

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `label` | `string` | — | — |
| `options` | `CruiseSelectOption<V>[]` | — | — |
| `value?` | `V \| null` | — | — |
| `defaultValue?` | `V \| null` | — | — |
| `onValueChange?` | `((value: V \| null) => void)` | — | — |
| `placeholder?` | `string` | `"Choose…"` | — |
| `hint?` | `string` | — | — |
| `error?` | `string` | — | — |
| `disabled?` | `boolean` | — | — |
| `popupClassName?` | `string` | — | Extra classes on the list, e.g. "nepa-mode" (it renders in <body>). |
| `className?` | `string` | — | — |

## Accessibility

- listbox / option roles with aria-selected; arrows, Home/End and typeahead.
- Always a visible label.
