# Dropdown

> One value from a set, rendered as a select. It folds in the former dedicated enum type.

Source: CBX documentation, version 4.0 preview (unreleased). Canonical page: https://docs.configbox.at/docs/4.0-preview/technical/property-types/dropdown. Last updated 2026-07-29.

---
One value from a set, rendered as a select. It folds in the former dedicated `enum` type.

**Storage kind:** `column` · **Column:** `varchar(50)` — a choice key is a short token, not prose

Use it when the set of valid values is a closed list. Reach for [`radio`](https://docs.configbox.at/docs/4.0-preview/technical/property-types/radio) when the set is
short enough that showing every option at once helps; for [`join`](https://docs.configbox.at/docs/4.0-preview/technical/property-types/join) when the options are
rows in a table and the stored value is their id; for [`pseudojoin`](https://docs.configbox.at/docs/4.0-preview/technical/property-types/pseudojoin) when a model
method computes the options and the stored value is a string key.

## Settings

| Setting | Meaning |
|---|---|
| `choices` | `value => label` map. A **static** set |
| `items` | Legacy alias for `choices`; used only when `choices` is absent |
| `modelClass` / `modelMethod` | A **dynamic** set, fetched from a model |
| `defaultlabel` | Placeholder shown before a choice is made |
| `default` | Pre-selected value on a new record |

## Static versus dynamic sets

This is the distinction that matters, and it reaches well beyond rendering. A dropdown is *static*
when it declares `choices` (or the legacy `items`), and *dynamic* when its options come from
`modelClass` + `modelMethod`.

| | Static (`choices`) | Dynamic (`modelClass`/`modelMethod`) |
|---|---|---|
| `check()` validates membership | **yes** — an out-of-set value is refused | no — the set is not known here |
| List filtering | **exact match** / `IN (…)` | base behaviour: `LIKE '%value%'` |
| Filter dropdown options | the choice labels | the base's possible values |
| `enum` in generated JSON / MCP schemas | **published** | not published |

Two consequences worth internalising:

**Membership validation is a real gate, not a nicety.** The base `check()` only enforces `required`,
so without a static `choices` map a tampered POST can store anything the column will hold. The
column stays a plain `varchar` rather than a SQL `ENUM` precisely so that such a value produces a
friendly refusal from `check()` instead of a fatal INSERT error.

**Exact filtering is a correctness fix.** The base filter is a substring `LIKE`, so filtering a
static dropdown on `draft` would also match `draft_archived`. A static dropdown filters with `=` or
`IN (…)` instead.

Publishing an `enum` for a dynamic set would be worse than publishing none: it would be a snapshot
that goes stale the moment the underlying data changes. The model validates instead.

## Notes

- Keys are what gets stored, labels are display only. Keep keys short and stable — renaming one is a
  data migration, and `varchar(50)` is sized for tokens rather than sentences. A picker whose keys
  are genuinely longer carries an explicit `dataType`.
- `default` becomes the column default in generated DDL. A dropdown that is `NOT NULL` with no
  default has nothing to fall back on.
- `getValueSet()` reads `choices`, then the legacy `items`, and publishes whichever it finds as the
  `enum` of the generated schemas. It used to read `choices` only, which meant a definition supplying
  its set through `items` rendered and validated normally while documenting itself as an
  unconstrained string — `product.visualization_type` shipped with no `enum` and its `composite`
  option was invisible to every consumer of the API description. Prefer `choices` in new definitions
  all the same; `items` is supported, not recommended.

@see radio.md — the same thing rendered differently
@see join.md · pseudojoin.md — when the options come from data
@see ../com_configbox_type_generation.md — where a static choice set becomes an `enum`
