Skip to content
kerfjsGitHub

CompactSelect

A small Select with no floating label, for table rows, pagers and toolbars.

SourcePackage @kerf-js/veneerTry it

Preview

tsx
import { CompactSelect } from '@kerf-js/veneer'export function Basic() {  return (  <CompactSelect    label="Region"    options={REGION_OPTIONS}    defaultValue="eu-west"  />  )}const REGIONS = [  { region: 'eu-west', requests: 34.2, share: 40 },  { region: 'us-east', requests: 24.8, share: 29 },  { region: 'ap-south', requests: 12.1, share: 14 },  { region: 'us-west', requests: 9.4, share: 11 },  { region: 'eu-north', requests: 5.6, share: 6 },]const REGION_OPTIONS = REGIONS.map((r) => ({  value: r.region,  label: r.region,}))

Installation

npm install @kerf-js/veneer

Try it

Change the controls: the preview and its code update together. The address keeps what you set, so you can link to it.

Live preview
Controls
Codeupdates as you change the controls
import { CompactSelect } from '@kerf-js/veneer'<CompactSelect options={REGIONS} label="Region" />

Usage

tsx
import { CompactSelect } from '@kerf-js/veneer'<CompactSelect  label="Region"  options={REGION_OPTIONS}  defaultValue="eu-west"/>

States

A compact select can show a placeholder when empty, hold a chosen value, or be disabled.

tsx
import { CompactSelect } from '@kerf-js/veneer'export function States() {  return (  <>    <CompactSelect      label="Region"      placeholder="Region"      options={REGION_OPTIONS}    />    <CompactSelect      label="Region"      options={REGION_OPTIONS}      defaultValue="us-east"    />    <CompactSelect      label="Region"      options={REGION_OPTIONS}      defaultValue="us-east"      disabled    />  </>  )}const REGIONS = [  { region: 'eu-west', requests: 34.2, share: 40 },  { region: 'us-east', requests: 24.8, share: 29 },  { region: 'ap-south', requests: 12.1, share: 14 },  { region: 'us-west', requests: 9.4, share: 11 },  { region: 'eu-north', requests: 5.6, share: 6 },]const REGION_OPTIONS = REGIONS.map((r) => ({  value: r.region,  label: r.region,}))

Rows per page

A compact select suits a small choice beside other content, here the number of rows per page.

tsx
import { derived, state } from '@kerf-js/core'import {  CompactSelect,  Row,  Stack,  Text} from '@kerf-js/veneer'export function Rows() {  const size = state<string | null>('5')  const rows = derived(() =>    PROJECTS.slice(0, Number(size.value)),  )  return (    <Stack gap="group">      <Stack gap="tight">        {rows.value.map((p) => (          <Row            key={p.id}            justify="between"          >            <Text>{p.name}</Text>            <Text subtle>{p.owner}</Text>          </Row>        ))}      </Stack>      <Row justify="between">        <Text subtle>          {rows.value.length} of {PROJECTS.length}        </Text>        <CompactSelect          label="Rows per page"          options={PAGE_SIZES}          value={size}        />      </Row>    </Stack>  )}type Status =  | 'Live'  | 'Building'  | 'Failed'  | 'Paused'const PROJECTS: {  id: string  name: string  owner: string  region: string  status: Status  deploys: number  uptime: number  latency: number  cost: number  updated: string}[] = [  {    id: 'storefront',    name: 'storefront',    owner: 'Priya Raman',    region: 'eu-west',    status: 'Live',    deploys: 412,    uptime: 99.98,    latency: 84,    cost: 1240,    updated: '2026-10-31',  },  {    id: 'checkout',    name: 'checkout',    owner: 'Tomás Oliveira',    region: 'eu-west',    status: 'Live',    deploys: 288,    uptime: 99.95,    latency: 112,    cost: 980,    updated: '2026-10-31',  },  {    id: 'search-api',    name: 'search-api',    owner: 'Lena Fischer',    region: 'us-east',    status: 'Building',    deploys: 196,    uptime: 99.91,    latency: 146,    cost: 1610,    updated: '2026-10-30',  },  {    id: 'billing',    name: 'billing',    owner: 'Mara Lindqvist',    region: 'us-east',    status: 'Live',    deploys: 154,    uptime: 99.99,    latency: 73,    cost: 720,    updated: '2026-10-29',  },  {    id: 'mobile-bff',    name: 'mobile-bff',    owner: 'Sam Okafor',    region: 'ap-south',    status: 'Failed',    deploys: 233,    uptime: 99.42,    latency: 208,    cost: 860,    updated: '2026-10-29',  },  {    id: 'notifier',    name: 'notifier',    owner: 'Jun Tanaka',    region: 'eu-north',    status: 'Live',    deploys: 97,    uptime: 99.97,    latency: 58,    cost: 310,    updated: '2026-10-27',  },  {    id: 'reports',    name: 'reports',    owner: 'Noor Haddad',    region: 'us-west',    status: 'Paused',    deploys: 41,    uptime: 99.8,    latency: 321,    cost: 190,    updated: '2026-10-22',  },  {    id: 'edge-cache',    name: 'edge-cache',    owner: 'Jun Tanaka',    region: 'global',    status: 'Live',    deploys: 318,    uptime: 100,    latency: 21,    cost: 1430,    updated: '2026-10-31',  },  {    id: 'auth',    name: 'auth',    owner: 'Mara Lindqvist',    region: 'eu-west',    status: 'Live',    deploys: 176,    uptime: 99.99,    latency: 66,    cost: 540,    updated: '2026-10-28',  },  {    id: 'ingest',    name: 'ingest',    owner: 'Lena Fischer',    region: 'us-east',    status: 'Live',    deploys: 264,    uptime: 99.93,    latency: 131,    cost: 2120,    updated: '2026-10-30',  },]const PAGE_SIZES = [  { value: '3', label: '3 rows' },  { value: '5', label: '5 rows' },  { value: '10', label: '10 rows' },]

A role in each row

Give each row its own compact select, with defaultValue set to the person's current role.

tsx
import {  CompactSelect,  Row,  Stack,  Text} from '@kerf-js/veneer'import type { Role } from '@kerf-js/veneer'export function Roles() {  return (  <Stack    gap="related"    divided  >    {TEAM.map((p) => (      <Row        key={p.id}        justify="between"        gap="group"      >        <Stack gap="hair">          <Text>{p.name}</Text>          <Text            role="caption"            subtle          >            {p.email}          </Text>        </Stack>        <CompactSelect          label={`Role for ${p.name}`}          options={TEAM_ROLES}          defaultValue={p.role}        />      </Row>    ))}  </Stack>  )}const PEOPLE = [  {    id: 'mara',    name: 'Mara Lindqvist',    email: 'mara@kerf.dev',    role: 'Owner',    team: 'Platform',    initials: 'ML',  },  {    id: 'jun',    name: 'Jun Tanaka',    email: 'jun@kerf.dev',    role: 'Admin',    team: 'Platform',    initials: 'JT',  },  {    id: 'priya',    name: 'Priya Raman',    email: 'priya@kerf.dev',    role: 'Member',    team: 'Web',    initials: 'PR',  },  {    id: 'tomas',    name: 'Tomás Oliveira',    email: 'tomas@kerf.dev',    role: 'Member',    team: 'Web',    initials: 'TO',  },  {    id: 'lena',    name: 'Lena Fischer',    email: 'lena@kerf.dev',    role: 'Member',    team: 'Data',    initials: 'LF',  },  {    id: 'noor',    name: 'Noor Haddad',    email: 'noor@kerf.dev',    role: 'Viewer',    team: 'Data',    initials: 'NH',  },  {    id: 'sam',    name: 'Sam Okafor',    email: 'sam@kerf.dev',    role: 'Member',    team: 'Mobile',    initials: 'SO',  },  {    id: 'ines',    name: 'Inés Vidal',    email: 'ines@kerf.dev',    role: 'Viewer',    team: 'Mobile',    initials: 'IV',  },]const ROLE_OPTIONS = [  {    value: 'Admin',    label: 'Admin',    hint: 'Manages people and billing',  },  {    value: 'Member',    label: 'Member',    hint: 'Deploys and edits projects',  },  {    value: 'Viewer',    label: 'Viewer',    hint: 'Reads logs and metrics',  },]const TEAM_ROLES = ROLE_OPTIONS.map((r) => ({  value: r.value,  label: r.value,}))const TEAM = PEOPLE.slice(1, 5)

When to use

Use it for

  • A choice inside a row or a toolbar, repeated many times

Reach for something else

  • Select, when it's a field in a form
  • Menu, when it runs an action rather than holding a value

Accessibility

EnterSpace
Opens, or picks the highlighted option
↑↓
Moves through the options
Esc
Closes and returns focus
  • With nothing drawn for label, make it specific: "Role for Jun Tanaka", not "Role".

API Reference

PropTypeDefaultDescription
labelstring—The accessible name. Not drawn.
options(Option | OptionGroup)[]—The list, the same as Select.
placeholderstring—Sample text shown while empty. Never a substitute for a label.
valuestring | State<string | null> | null—A string, or a State to bind.
defaultValuestring | null—The starting value, when the component owns the state.
openboolean | State<boolean>—A state box that holds whether it is open.
defaultOpenbooleanfalseStarts open, when the component keeps its own open state.
presentationPresentation—auto opens a popover, or a sheet when the viewport is under 600px wide; popover and sheet force one.
searchableboolean—Puts a search field at the top of the list. On by itself past 15 options.
namestring—Puts it in a form with a hidden input.
idstring—The element id. One is made for you if you leave it out.
disabledbooleanfalseTurns it off, and takes it out of the tab order.
describedbystring—The id of text that explains it.
EventTypeDefaultDescription
onValueChange((value: string, reason: ChangeReason) => void)—Called with the picked option's value, and whether a pointer, a key or code picked it.
onOpenChange((open: boolean) => void)—Called with true when the list opens and false when it closes.

Parts

Every element carries a stable name, so you can style it from your own CSS: [data-scope="field"] [data-part="control"]

controlsweep-basesweepinputvalue

RelatedSelectTable