POST Design System Using the system
Components

Select

A choice from a list that is too long to show all at once.

Single select shows the chosen item. Multi-select shows a count, because a list of chosen names quickly outgrows the control.

Watch out. With only two or three options, radios are faster — they show every choice without a click.
Sub-brand
Theme
Liverendered from the shipped recipe

States

closed
hover
focus
open
selected
multi
disabled
error

Code

The HTML for the live example above is on the page - use your browser's inspector, or copy the CSS here and the markup from the preview. Load post-ui.css once per site and this component works anywhere.

/* ============================================================================
   POST UI System — components
   HAND-WRITTEN. Not generated: this file encodes the RULES the components follow,
   which is not the same thing as whatever the Figma canvas currently looks like.
   The contract it implements (variant axes + reasoning) is synced in components.json.

   Import order:  tokens.css  ->  type.css  ->  components.css

   NAMING. Every class is `post-` prefixed. This ships into Webflow projects where
   `.button`, `.field`, `.label` or `.text-body` are exactly the classes a site
   already has, and an unprefixed collision would silently restyle real content.
   Block `post-x` · modifier `post-x--y` · state `is-*`.

   THE FOUR RULES EVERYTHING BELOW OBEYS
   1. Tokens are named by scale, never by consumer — space/m, stroke/m, icon-size/m
      and text/md all mean "the middle step".
   2. Strokes are altitude, not component — s detail · m component · l section.
   3. Optical padding — outer padding is uniform, the LABEL carries space/xs left and
      right, and the gap is 0. Text ends up inset by outer+xs while an icon sits at
      outer, which is correct: a glyph carries internal whitespace and text does not.
      One recipe covers icon-left, icon-right, both, or neither.
   4. Control height comes from the ICON TIER, never from text. Every control label
      carries min-height = its icon-size token. Without it the icon becomes the
      tallest child and toggling an optional icon grows the control — a chip jumped
      28 -> 32px exactly this way. With it, toggling an icon changes width only.

   currentColor is load-bearing. Outline/ghost controls bind their label fill AND
   their stroke to the same `action` token, so the border is written as
   `border-color: currentColor` and one colour change moves both.
   ============================================================================ */

/* ---- select ---- */
/* Form-scale select; Level Filter is its nav-scale sibling. Display rule: single
   select shows the chosen item, multi shows a COUNT ("3 spaces selected") — the
   count is content, so the component does not need a variant for it. */
/* Figma pins every Select variant to a FIXED width with the trigger set to FILL — the
   box is a stable slot and the VALUE flexes inside it. Without a declared width the
   component shrink-wraps wherever it is a flex/grid child, so the box resized on every
   pick: "Select a space" -> "The Hall" -> "3 selected" each produced a different width
   and shifted whatever sat beside it. The container owns the width; the value never does. */
.post-select { position: relative; width: 100%; }
/* The value is the flexible part, so it truncates rather than pushing the box open.
   min-width:0 is required — a flex item's default min-width:auto refuses to shrink
   below its content and would defeat the ellipsis. */
.post-select__value {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.post-select__control {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-m);
  width: 100%;
  box-sizing: border-box;
  padding: var(--space-m);
  border: var(--stroke-m) solid var(--border);
  background: var(--surface);
  color: var(--text);
  font-family: var(--font-post);
  font-size: var(--text-sm);
  font-weight: 500;
  line-height: var(--icon-size-s);
  cursor: pointer;
  text-align: left;
}
.post-select__control .post-icon { width: var(--icon-size-s); height: var(--icon-size-s); color: var(--action); }
/* One chevron glyph, rotated — not two icons. The icon describes what happens to the
   page, not what the widget is called. */
.post-select__control .post-icon { transition: transform 0.15s ease; }
.post-select.is-open .post-select__control .post-icon { transform: rotate(180deg); }
.post-select__control:hover,
.post-select.is-hover .post-select__control {
  border-color: var(--hover-ink);
  background: var(--hover-surface);
}
.post-select.is-open .post-select__control,
.post-select.is-focus .post-select__control { border-color: var(--action); }
.post-select.is-focus .post-select__control { outline: 2px solid var(--action); outline-offset: 2px; }
.post-select.is-error .post-select__control { border-color: var(--feedback-error-text); background: var(--feedback-error-surface); }
.post-select.is-disabled { opacity: 0.4; pointer-events: none; }
.post-select__panel {
  position: absolute;
  z-index: 20;
  left: 0; right: 0;
  margin-top: calc(-1 * var(--stroke-m));
  border: var(--stroke-m) solid var(--border);
  background: var(--surface);
  display: none;
  max-height: 16em;
  overflow-y: auto;
}
.post-select.is-open .post-select__panel { display: block; }
.post-select__option {
  display: block;
  width: 100%;
  box-sizing: border-box;
  padding: var(--space-m);
  border: none;
  background: none;
  text-align: left;
  color: var(--text);
  font-family: var(--font-post);
  font-size: var(--text-sm);
  font-weight: 500;
  line-height: var(--icon-size-s);
  cursor: pointer;
}
.post-select__option:hover,
.post-select__option.is-hover { background: var(--hover-surface); color: var(--hover-ink); }
.post-select__option[aria-selected="true"] { background: var(--action); color: var(--on-action); }