← All components
forms / stable

PropertySelect

Compact searchable record value picker. Field layout aligns wrapping rail labels and content-sized value chips; people use grouped options with role chips.

LIVE PREVIEW
Loading preview…

When to use

  • Colour properties show swatches (`kind: "color"`), never a generic icon.
  • Status, priority, assignee, labels, clients and dates in record property bars or rails.
  • icon identifies the property: shown on empty chips and field labels. options[].leading identifies the value: shown in the selected chip and list, taking precedence over icon.
  • Plain selected options have no visual. allowNone uses a neutral minus in the chip and list; noneLabel customizes None or Unassigned.
  • Use statusOption, priorityOptions and personOption presets, or provide leading directly. Color and status hues use CSS token variables; avatar src is optional with initials fallback and shape company for clients.
  • Use selectionMode multiple with string[] value/onChange for labels. The popover stays open; the chip shows the first value and +N while announcing every selected label.
  • Default value chips are 28px. Compact work rows use presentation icon and size xs for 24px controls with 14px glyphs or 20px avatars; values stay in the accessible name and tooltip. Empty icon controls use the property icon, while the None list choice stays neutral.
  • searchable forces search for small lists; enable it for every person picker. OptionSection supplies group headings, while allowNone remains above them.

Consider an alternative

  • Bordered chips inside rails — compose with PropertyField for ghost values.
  • Conventional forms — use Select.
  • Free-form text entry — use ComboBox.
  • Calendar date entry — use DatePicker; this chip only chooses predefined text values.

Import

import { PropertySelect } from '@revido-llc/kit';

Keyboard interaction

  • Enter / Space — open and autofocus search
  • Arrow keys — move through options
  • Type in search — filter names, email keywords and role chips
  • Enter — pick; Space — toggle multiple selections
  • Escape — close and return focus

Props

Props documented by the Kit. Refer to the TypeScript types for the complete interface, including inherited props.

label (required)string—Accessible name and tooltip.
onChange (required)((id: string | null) => void) | ((ids: string[]) => void)—Receives an id or null in single mode; selected ids as string[] in multiple mode.
options (required)(Option | OptionSection)[]—Each option's leading is its value visual, shared by the closed chip and list.
value (required)string | string[] | null—Selected id or null in single mode; string[] in multiple mode. Selected ids in display order; the chip shows the first value and +N overflow.
allowNonebooleanfalseOffer a neutral None choice. Clearing returns null (single) or [] (multiple).
classNamestring—
disabledReasonstring—Tooltip and accessible explanation while disabled.
iconReactNode—Property icon: empty chip fallback and field label. A selected option uses only its own leading.
isDisabledbooleanfalseBlock changes while keeping the explanation focusable.
layout"chip" | "field"—A bordered bar chip or ghost rail row; defaults to field inside PropertyField, chip otherwise.
noneLabelstring"None"
placeholderstring"Select…"Empty text when allowNone is off.
presentation"value" | "icon""value"Icon-only quick control for compact work rows; the value stays in its accessible name and tooltip.
searchableboolean—Always show search, including small people lists. Filters labels, email keywords and role metadata.
searchThresholdnumber7Show search when the option count exceeds this value.
selectionMode"single" | "multiple""single"single picks one value; multiple toggles values and keeps the popover open.
size"xs" | "sm""sm"xs is 24px; sm is the existing 28px chip.

Examples

Status icons

Every workflow category, including blocked. The selected chip and list share their status glyph. API spelling for Canceled is cancelled.

"use client";

import { useState } from "react";
import { CircleDashed } from "lucide-react";
import { Icon, PropertySelect, type PropertyOption } from "@revido-llc/kit";

const options: PropertyOption[] = [
  { id: "backlog", label: "Backlog", leading: {"kind":"status","category":"backlog"} },
  { id: "todo", label: "To do", leading: {"kind":"status","category":"todo"} },
  { id: "active", label: "In progress", leading: {"kind":"status","category":"in-progress"} },
  { id: "review", label: "In review", leading: {"kind":"status","category":"in-review","color":"var(--info)"} },
  { id: "done", label: "Done", leading: {"kind":"status","category":"done"} },
  { id: "canceled", label: "Canceled", leading: {"kind":"status","category":"cancelled"} },
  { id: "blocked", label: "Blocked", leading: {"kind":"status","category":"blocked"} },
];

export default function Example() {
  const [value, setValue] = useState<string | null>("active");
  return (
    <PropertySelect
      label="Status" icon={<Icon as={CircleDashed} tone="tertiary" />}
      options={options} value={value} onChange={setValue}
      layout="chip" selectionMode="single"
      searchable={false}
      placeholder="Set status"
      allowNone={false} noneLabel="None"
      isDisabled={false} disabledReason="Only the project owner can change this property"
    />
  );
}

Priority icons

No priority, low, medium, high and urgent use the shared priority glyph family.

"use client";

import { useState } from "react";
import { Flag } from "lucide-react";
import { Icon, PropertySelect, type PropertyOption } from "@revido-llc/kit";

const options: PropertyOption[] = [
  { id: "none", label: "No priority", leading: {"kind":"priority","level":"none"} },
  { id: "low", label: "Low", leading: {"kind":"priority","level":"low"} },
  { id: "medium", label: "Medium", leading: {"kind":"priority","level":"medium"} },
  { id: "high", label: "High", leading: {"kind":"priority","level":"high"} },
  { id: "urgent", label: "Urgent", leading: {"kind":"priority","level":"urgent"} },
];

export default function Example() {
  const [value, setValue] = useState<string | null>("high");
  return (
    <PropertySelect
      label="Priority" icon={<Icon as={Flag} tone="tertiary" />}
      options={options} value={value} onChange={setValue}
      layout="chip" selectionMode="single"
      searchable={false}
      placeholder="Set priority"
      allowNone={false} noneLabel="None"
      isDisabled={false} disabledReason="Only the project owner can change this property"
    />
  );
}

Assignee avatars and Unassigned

People use circular avatars, optional src images and initials fallback. Unassigned clears to null with a neutral minus.

"use client";

import { useState } from "react";
import { UserPlus } from "lucide-react";
import { Icon, PropertySelect, type PropertyOption } from "@revido-llc/kit";

const options: PropertyOption[] = [
  { id: "ana", label: "Ana Costa", leading: {"kind":"avatar","name":"Ana Costa"}, meta: "QA", keywords: ["ana@revido.co","QA"] },
  { id: "sam", label: "Sam Lee", leading: {"kind":"avatar","name":"Sam Lee"}, meta: "Developer", keywords: ["sam@revido.co","Developer"] },
  { id: "maya", label: "Maya Patel", leading: {"kind":"avatar","name":"Maya Patel"}, meta: "PO", keywords: ["PO"] },
];

export default function Example() {
  const [value, setValue] = useState<string | null>("ana");
  return (
    <PropertySelect
      label="Assignee" icon={<Icon as={UserPlus} tone="tertiary" />}
      options={options} value={value} onChange={setValue}
      layout="chip" selectionMode="single"
      searchable={true}
      placeholder="Set assignee"
      allowNone={true} noneLabel="Unassigned"
      isDisabled={false} disabledReason="Only the project owner can change this property"
    />
  );
}

Labels with colour swatches

Multiple token-coloured labels; the first swatch and label stay visible with +2 overflow. Keep picking until Escape or outside click.

"use client";

import { useState } from "react";
import { Tags } from "lucide-react";
import { Icon, PropertySelect, type PropertyOption } from "@revido-llc/kit";

const options: PropertyOption[] = [
  { id: "design", label: "Design", leading: {"kind":"color","color":"var(--primary)"} },
  { id: "frontend", label: "Frontend", leading: {"kind":"color","color":"var(--success)"} },
  { id: "billing", label: "Billing", leading: {"kind":"color","color":"var(--warning)"} },
  { id: "security", label: "Security", leading: {"kind":"color","color":"var(--destructive)"} },
];

export default function Example() {
  const [value, setValue] = useState<string[]>(["design","frontend","billing"]);
  return (
    <PropertySelect
      label="Labels" icon={<Icon as={Tags} tone="tertiary" />}
      options={options} value={value} onChange={setValue}
      layout="chip" selectionMode="multiple"
      searchable={false}
      placeholder="Set labels"
      allowNone={true} noneLabel="None"
      isDisabled={false} disabledReason="Only the project owner can change this property"
    />
  );
}

Custom icons and tones

Use leading kind icon with Lucide glyphs and semantic tones. Danger is the red intent.

"use client";

import { useState } from "react";
import { Sparkles, Bug, CheckSquare2, Wrench } from "lucide-react";
import { Icon, PropertySelect, type PropertyOption } from "@revido-llc/kit";

const options: PropertyOption[] = [
  { id: "feature", label: "Feature", leading: { kind: "icon", icon: <Icon as={Sparkles} tone="primary" />, tone: "primary" } },
  { id: "bug", label: "Bug", leading: { kind: "icon", icon: <Icon as={Bug} tone="danger" />, tone: "danger" } },
  { id: "task", label: "Task", leading: { kind: "icon", icon: <Icon as={CheckSquare2} tone="info" />, tone: "info" } },
  { id: "chore", label: "Chore", leading: { kind: "icon", icon: <Icon as={Wrench} tone="secondary" />, tone: "secondary" } },
];

export default function Example() {
  const [value, setValue] = useState<string | null>("bug");
  return (
    <PropertySelect
      label="Type" icon={<Icon as={Sparkles} tone="tertiary" />}
      options={options} value={value} onChange={setValue}
      layout="chip" selectionMode="single"
      searchable={false}
      placeholder="Set type"
      allowNone={false} noneLabel="None"
      isDisabled={false} disabledReason="Only the project owner can change this property"
    />
  );
}

Project and company avatars

Company avatars use rounded squares via shape company, with optional src and a name fallback.

"use client";

import { useState } from "react";
import { FolderKanban } from "lucide-react";
import { Icon, PropertySelect, type PropertyOption } from "@revido-llc/kit";

const options: PropertyOption[] = [
  { id: "revido", label: "Revido", leading: {"kind":"avatar","name":"Revido","shape":"company"} },
  { id: "acme", label: "Acme Studio", leading: {"kind":"avatar","name":"Acme Studio","shape":"company"} },
];

export default function Example() {
  const [value, setValue] = useState<string | null>("revido");
  return (
    <PropertySelect
      label="Client" icon={<Icon as={FolderKanban} tone="tertiary" />}
      options={options} value={value} onChange={setValue}
      layout="chip" selectionMode="single"
      searchable={false}
      placeholder="Set client"
      allowNone={false} noneLabel="None"
      isDisabled={false} disabledReason="Only the project owner can change this property"
    />
  );
}

Due date with plain text values

Calendar is the empty property's icon. Selected predefined date text has no option leading. Use DatePicker for calendar input.

"use client";

import { useState } from "react";
import { Calendar } from "lucide-react";
import { Icon, PropertySelect, type PropertyOption } from "@revido-llc/kit";

const options: PropertyOption[] = [
  { id: "today", label: "Today" },
  { id: "tomorrow", label: "Tomorrow" },
  { id: "next-week", label: "Next week" },
];

export default function Example() {
  const [value, setValue] = useState<string | null>("tomorrow");
  return (
    <PropertySelect
      label="Due date" icon={<Icon as={Calendar} tone="tertiary" />}
      options={options} value={value} onChange={setValue}
      layout="chip" selectionMode="single"
      searchable={false}
      placeholder="Set due date"
      allowNone={false} noneLabel="None"
      isDisabled={false} disabledReason="Only the project owner can change this property"
    />
  );
}

Empty property and placeholder

With no value and allowNone off, show the property icon plus placeholder. Pick a person to replace it with their avatar.

"use client";

import { useState } from "react";
import { UserPlus } from "lucide-react";
import { Icon, PropertySelect, type PropertyOption } from "@revido-llc/kit";

const options: PropertyOption[] = [
  { id: "ana", label: "Ana Costa", leading: {"kind":"avatar","name":"Ana Costa"}, meta: "QA", keywords: ["ana@revido.co","QA"] },
  { id: "sam", label: "Sam Lee", leading: {"kind":"avatar","name":"Sam Lee"}, meta: "Developer", keywords: ["sam@revido.co","Developer"] },
  { id: "maya", label: "Maya Patel", leading: {"kind":"avatar","name":"Maya Patel"}, meta: "PO", keywords: ["PO"] },
];

export default function Example() {
  const [value, setValue] = useState<string | null>(null);
  return (
    <PropertySelect
      label="Assignee" icon={<Icon as={UserPlus} tone="tertiary" />}
      options={options} value={value} onChange={setValue}
      layout="chip" selectionMode="single"
      searchable={true}
      placeholder="Set assignee"
      allowNone={false} noneLabel="None"
      isDisabled={false} disabledReason="Only the project owner can change this property"
    />
  );
}

Disabled with reason

A disabled property remains focusable; hover or focus reveals the tooltip and accessible explanation.

"use client";

import { useState } from "react";
import { CircleDashed } from "lucide-react";
import { Icon, PropertySelect, type PropertyOption } from "@revido-llc/kit";

const options: PropertyOption[] = [
  { id: "backlog", label: "Backlog", leading: {"kind":"status","category":"backlog"} },
  { id: "todo", label: "To do", leading: {"kind":"status","category":"todo"} },
  { id: "active", label: "In progress", leading: {"kind":"status","category":"in-progress"} },
  { id: "review", label: "In review", leading: {"kind":"status","category":"in-review","color":"var(--info)"} },
  { id: "done", label: "Done", leading: {"kind":"status","category":"done"} },
  { id: "canceled", label: "Canceled", leading: {"kind":"status","category":"cancelled"} },
  { id: "blocked", label: "Blocked", leading: {"kind":"status","category":"blocked"} },
];

export default function Example() {
  const [value, setValue] = useState<string | null>("active");
  return (
    <PropertySelect
      label="Status" icon={<Icon as={CircleDashed} tone="tertiary" />}
      options={options} value={value} onChange={setValue}
      layout="chip" selectionMode="single"
      searchable={false}
      placeholder="Set status"
      allowNone={false} noneLabel="None"
      isDisabled={true} disabledReason="Only the project owner can change this property"
    />
  );
}

Searchable people

Search is always enabled for people, including small lists. Names and aliases filter while avatars remain visible.

"use client";

import { useState } from "react";
import { UserPlus } from "lucide-react";
import { Icon, PropertySelect, type PropertyOption } from "@revido-llc/kit";

const options: PropertyOption[] = [
  { id: "choice-1", label: "Teammate 1", leading: {"kind":"avatar","name":"Teammate 1"} },
  { id: "choice-2", label: "Teammate 2", leading: {"kind":"avatar","name":"Teammate 2"} },
  { id: "choice-3", label: "Teammate 3", leading: {"kind":"avatar","name":"Teammate 3"} },
  { id: "choice-4", label: "Teammate 4", leading: {"kind":"avatar","name":"Teammate 4"} },
  { id: "choice-5", label: "Teammate 5", leading: {"kind":"avatar","name":"Teammate 5"} },
  { id: "choice-6", label: "Teammate 6", leading: {"kind":"avatar","name":"Teammate 6"} },
  { id: "choice-7", label: "Teammate 7", leading: {"kind":"avatar","name":"Teammate 7"} },
  { id: "choice-8", label: "Teammate 8", leading: {"kind":"avatar","name":"Teammate 8"} },
  { id: "choice-9", label: "Teammate 9", leading: {"kind":"avatar","name":"Teammate 9"} },
  { id: "choice-10", label: "Teammate 10", leading: {"kind":"avatar","name":"Teammate 10"} },
  { id: "choice-11", label: "Teammate 11", leading: {"kind":"avatar","name":"Teammate 11"} },
  { id: "choice-12", label: "Teammate 12", leading: {"kind":"avatar","name":"Teammate 12"} },
  { id: "choice-13", label: "Teammate 13", leading: {"kind":"avatar","name":"Teammate 13"} },
  { id: "choice-14", label: "Teammate 14", leading: {"kind":"avatar","name":"Teammate 14"} },
  { id: "choice-15", label: "Teammate 15", leading: {"kind":"avatar","name":"Teammate 15"} },
  { id: "choice-16", label: "Teammate 16", leading: {"kind":"avatar","name":"Teammate 16"} },
  { id: "choice-17", label: "Teammate 17", leading: {"kind":"avatar","name":"Teammate 17"} },
  { id: "choice-18", label: "Teammate 18", leading: {"kind":"avatar","name":"Teammate 18"} },
  { id: "choice-19", label: "Teammate 19", leading: {"kind":"avatar","name":"Teammate 19"} },
  { id: "choice-20", label: "Teammate 20", leading: {"kind":"avatar","name":"Teammate 20"} },
];

export default function Example() {
  const [value, setValue] = useState<string | null>("choice-1");
  return (
    <PropertySelect
      label="Assignee" icon={<Icon as={UserPlus} tone="tertiary" />}
      options={options} value={value} onChange={setValue}
      layout="chip" selectionMode="single"
      searchable={true}
      placeholder="Set assignee"
      allowNone={false} noneLabel="None"
      isDisabled={false} disabledReason="Only the project owner can change this property"
    />
  );
}

Rail field layout

Each row has a fixed wrapping label column and a value chip aligned to the left. Use label icons throughout a group or omit them throughout.

"use client";

import { useState } from "react";
import { CircleDashed, Flag, UserPlus, Tags, Sparkles, Calendar, Bug, CheckSquare2, Wrench } from "lucide-react";
import { Icon, PropertySelect, PropertyGrid, type PropertyOption } from "@revido-llc/kit";

const statusOptions: PropertyOption[] = [
  { id: "backlog", label: "Backlog", leading: {"kind":"status","category":"backlog"} },
  { id: "todo", label: "To do", leading: {"kind":"status","category":"todo"} },
  { id: "active", label: "In progress", leading: {"kind":"status","category":"in-progress"} },
  { id: "review", label: "In review", leading: {"kind":"status","category":"in-review","color":"var(--info)"} },
  { id: "done", label: "Done", leading: {"kind":"status","category":"done"} },
  { id: "canceled", label: "Canceled", leading: {"kind":"status","category":"cancelled"} },
  { id: "blocked", label: "Blocked", leading: {"kind":"status","category":"blocked"} },
];
const priorityOptions: PropertyOption[] = [
  { id: "none", label: "No priority", leading: {"kind":"priority","level":"none"} },
  { id: "low", label: "Low", leading: {"kind":"priority","level":"low"} },
  { id: "medium", label: "Medium", leading: {"kind":"priority","level":"medium"} },
  { id: "high", label: "High", leading: {"kind":"priority","level":"high"} },
  { id: "urgent", label: "Urgent", leading: {"kind":"priority","level":"urgent"} },
];
const assigneeOptions: PropertyOption[] = [
  { id: "ana", label: "Ana Costa", leading: {"kind":"avatar","name":"Ana Costa"}, meta: "QA", keywords: ["ana@revido.co","QA"] },
  { id: "sam", label: "Sam Lee", leading: {"kind":"avatar","name":"Sam Lee"}, meta: "Developer", keywords: ["sam@revido.co","Developer"] },
  { id: "maya", label: "Maya Patel", leading: {"kind":"avatar","name":"Maya Patel"}, meta: "PO", keywords: ["PO"] },
];
const labelsOptions: PropertyOption[] = [
  { id: "design", label: "Design", leading: {"kind":"color","color":"var(--primary)"} },
  { id: "frontend", label: "Frontend", leading: {"kind":"color","color":"var(--success)"} },
  { id: "billing", label: "Billing", leading: {"kind":"color","color":"var(--warning)"} },
  { id: "security", label: "Security", leading: {"kind":"color","color":"var(--destructive)"} },
];
const typeOptions: PropertyOption[] = [
  { id: "feature", label: "Feature", leading: { kind: "icon", icon: <Icon as={Sparkles} tone="primary" />, tone: "primary" } },
  { id: "bug", label: "Bug", leading: { kind: "icon", icon: <Icon as={Bug} tone="danger" />, tone: "danger" } },
  { id: "task", label: "Task", leading: { kind: "icon", icon: <Icon as={CheckSquare2} tone="info" />, tone: "info" } },
  { id: "chore", label: "Chore", leading: { kind: "icon", icon: <Icon as={Wrench} tone="secondary" />, tone: "secondary" } },
];
const dueOptions: PropertyOption[] = [
  { id: "today", label: "Today" },
  { id: "tomorrow", label: "Tomorrow" },
  { id: "next-week", label: "Next week" },
];

export default function RecordProperties() {
  const [status, setStatus] = useState<string | null>("active");
  const [priority, setPriority] = useState<string | null>("high");
  const [assignee, setAssignee] = useState<string | null>("ana");
  const [labels, setLabels] = useState<string[]>(["design","frontend","billing"]);
  const [type, setType] = useState<string | null>("bug");
  const [due, setDue] = useState<string | null>("tomorrow");
  return (
    <PropertyGrid>
      <PropertySelect
        label="Status" icon={<Icon as={CircleDashed} tone="tertiary" />}
        options={statusOptions} value={status} onChange={setStatus}
        layout="field" selectionMode="single"
        searchable={false}
        placeholder="Set status"
        allowNone={false} noneLabel="Unassigned"
        isDisabled={false} disabledReason="Only the project owner can change this property"
      />
      <PropertySelect
        label="Priority" icon={<Icon as={Flag} tone="tertiary" />}
        options={priorityOptions} value={priority} onChange={setPriority}
        layout="field" selectionMode="single"
        searchable={false}
        placeholder="Set priority"
        allowNone={false} noneLabel="Unassigned"
        isDisabled={false} disabledReason="Only the project owner can change this property"
      />
      <PropertySelect
        label="Assignee" icon={<Icon as={UserPlus} tone="tertiary" />}
        options={assigneeOptions} value={assignee} onChange={setAssignee}
        layout="field" selectionMode="single"
        searchable={true}
        placeholder="Set assignee"
        allowNone={true} noneLabel="Unassigned"
        isDisabled={false} disabledReason="Only the project owner can change this property"
      />
      <PropertySelect
        label="Labels" icon={<Icon as={Tags} tone="tertiary" />}
        options={labelsOptions} value={labels} onChange={setLabels}
        layout="field" selectionMode="multiple"
        searchable={false}
        placeholder="Set labels"
        allowNone={false} noneLabel="Unassigned"
        isDisabled={false} disabledReason="Only the project owner can change this property"
      />
      <PropertySelect
        label="Type" icon={<Icon as={Sparkles} tone="tertiary" />}
        options={typeOptions} value={type} onChange={setType}
        layout="field" selectionMode="single"
        searchable={false}
        placeholder="Set type"
        allowNone={false} noneLabel="Unassigned"
        isDisabled={false} disabledReason="Only the project owner can change this property"
      />
      <PropertySelect
        label="Due date" icon={<Icon as={Calendar} tone="tertiary" />}
        options={dueOptions} value={due} onChange={setDue}
        layout="field" selectionMode="single"
        searchable={false}
        placeholder="Set due date"
        allowNone={false} noneLabel="Unassigned"
        isDisabled={false} disabledReason="Only the project owner can change this property"
      />
    </PropertyGrid>
  );
}

Record property bar

Status, priority, assignee, multiple labels and due date compose the property bar used in RecordDrawer.

"use client";

import { useState } from "react";
import { CircleDashed, Flag, UserPlus, Tags, Calendar } from "lucide-react";
import { Icon, PropertySelect, type PropertyOption } from "@revido-llc/kit";

const statusOptions: PropertyOption[] = [
  { id: "backlog", label: "Backlog", leading: {"kind":"status","category":"backlog"} },
  { id: "todo", label: "To do", leading: {"kind":"status","category":"todo"} },
  { id: "active", label: "In progress", leading: {"kind":"status","category":"in-progress"} },
  { id: "review", label: "In review", leading: {"kind":"status","category":"in-review","color":"var(--info)"} },
  { id: "done", label: "Done", leading: {"kind":"status","category":"done"} },
  { id: "canceled", label: "Canceled", leading: {"kind":"status","category":"cancelled"} },
  { id: "blocked", label: "Blocked", leading: {"kind":"status","category":"blocked"} },
];
const priorityOptions: PropertyOption[] = [
  { id: "none", label: "No priority", leading: {"kind":"priority","level":"none"} },
  { id: "low", label: "Low", leading: {"kind":"priority","level":"low"} },
  { id: "medium", label: "Medium", leading: {"kind":"priority","level":"medium"} },
  { id: "high", label: "High", leading: {"kind":"priority","level":"high"} },
  { id: "urgent", label: "Urgent", leading: {"kind":"priority","level":"urgent"} },
];
const assigneeOptions: PropertyOption[] = [
  { id: "ana", label: "Ana Costa", leading: {"kind":"avatar","name":"Ana Costa"}, meta: "QA", keywords: ["ana@revido.co","QA"] },
  { id: "sam", label: "Sam Lee", leading: {"kind":"avatar","name":"Sam Lee"}, meta: "Developer", keywords: ["sam@revido.co","Developer"] },
  { id: "maya", label: "Maya Patel", leading: {"kind":"avatar","name":"Maya Patel"}, meta: "PO", keywords: ["PO"] },
];
const labelsOptions: PropertyOption[] = [
  { id: "design", label: "Design", leading: {"kind":"color","color":"var(--primary)"} },
  { id: "frontend", label: "Frontend", leading: {"kind":"color","color":"var(--success)"} },
  { id: "billing", label: "Billing", leading: {"kind":"color","color":"var(--warning)"} },
  { id: "security", label: "Security", leading: {"kind":"color","color":"var(--destructive)"} },
];
const dueOptions: PropertyOption[] = [
  { id: "today", label: "Today" },
  { id: "tomorrow", label: "Tomorrow" },
  { id: "next-week", label: "Next week" },
];

export default function RecordProperties() {
  const [status, setStatus] = useState<string | null>("active");
  const [priority, setPriority] = useState<string | null>("high");
  const [assignee, setAssignee] = useState<string | null>("ana");
  const [labels, setLabels] = useState<string[]>(["design","frontend","billing"]);
  const [due, setDue] = useState<string | null>("tomorrow");
  return (
    <div className="flex flex-wrap items-center gap-2">
      <PropertySelect
        label="Status" icon={<Icon as={CircleDashed} tone="tertiary" />}
        options={statusOptions} value={status} onChange={setStatus}
        layout="chip" selectionMode="single"
        searchable={false}
        placeholder="Set status"
        allowNone={false} noneLabel="Unassigned"
        isDisabled={false} disabledReason="Only the project owner can change this property"
      />
      <PropertySelect
        label="Priority" icon={<Icon as={Flag} tone="tertiary" />}
        options={priorityOptions} value={priority} onChange={setPriority}
        layout="chip" selectionMode="single"
        searchable={false}
        placeholder="Set priority"
        allowNone={false} noneLabel="Unassigned"
        isDisabled={false} disabledReason="Only the project owner can change this property"
      />
      <PropertySelect
        label="Assignee" icon={<Icon as={UserPlus} tone="tertiary" />}
        options={assigneeOptions} value={assignee} onChange={setAssignee}
        layout="chip" selectionMode="single"
        searchable={true}
        placeholder="Set assignee"
        allowNone={true} noneLabel="Unassigned"
        isDisabled={false} disabledReason="Only the project owner can change this property"
      />
      <PropertySelect
        label="Labels" icon={<Icon as={Tags} tone="tertiary" />}
        options={labelsOptions} value={labels} onChange={setLabels}
        layout="chip" selectionMode="multiple"
        searchable={false}
        placeholder="Set labels"
        allowNone={false} noneLabel="Unassigned"
        isDisabled={false} disabledReason="Only the project owner can change this property"
      />
      <PropertySelect
        label="Due date" icon={<Icon as={Calendar} tone="tertiary" />}
        options={dueOptions} value={due} onChange={setDue}
        layout="chip" selectionMode="single"
        searchable={false}
        placeholder="Set due date"
        allowNone={false} noneLabel="Unassigned"
        isDisabled={false} disabledReason="Only the project owner can change this property"
      />
    </div>
  );
}

Literal anatomy

A controlled property value; bind persistence in the consuming app.

import { useState } from "react";
import { PropertySelect } from "@revido-llc/kit";
export default function PropertyAnatomy() {
  const [value, setValue] = useState<string | null>("todo");
  return <PropertySelect label="Status" options={[{ id: "todo", label: "Todo" }, { id: "done", label: "Done" }]} value={value} onChange={setValue} />;
}

Grouped people with roles

Unassigned stays above section headers. Search names, email addresses or roles; role chips appear in the list only.

"use client";
import { useState } from "react";
import { PropertySelect, personOption, type OptionSection } from "@revido-llc/kit";
const options: OptionSection[] = [
  { id: "project", label: "In this project", options: [personOption({ id: "ana", name: "Ana Costa", email: "ana@revido.co", role: "QA" })] },
  { id: "other", label: "Other people", options: [personOption({ id: "sam", name: "Sam Lee", email: "sam@revido.co", role: "Developer" })] },
];
export default function Example() {
  const [value, setValue] = useState<string | null>("ana");
  return <PropertySelect label="Assignee" searchable options={options} value={value} onChange={setValue} allowNone noneLabel="Unassigned" />;
}

Compact icon controls

Always-visible status, priority and person controls in a work row. Closed triggers omit labels and chevrons; open pickers retain their full option content.

"use client";

import { useState } from "react";
import { CircleDashed, Flag, UserPlus } from "lucide-react";
import { Icon, PropertySelect, type PropertyOption } from "@revido-llc/kit";

const statusOptions: PropertyOption[] = [
  { id: "backlog", label: "Backlog", leading: {"kind":"status","category":"backlog"} },
  { id: "todo", label: "To do", leading: {"kind":"status","category":"todo"} },
  { id: "active", label: "In progress", leading: {"kind":"status","category":"in-progress"} },
  { id: "review", label: "In review", leading: {"kind":"status","category":"in-review","color":"var(--info)"} },
  { id: "done", label: "Done", leading: {"kind":"status","category":"done"} },
  { id: "canceled", label: "Canceled", leading: {"kind":"status","category":"cancelled"} },
  { id: "blocked", label: "Blocked", leading: {"kind":"status","category":"blocked"} },
];
const priorityOptions: PropertyOption[] = [
  { id: "none", label: "No priority", leading: {"kind":"priority","level":"none"} },
  { id: "low", label: "Low", leading: {"kind":"priority","level":"low"} },
  { id: "medium", label: "Medium", leading: {"kind":"priority","level":"medium"} },
  { id: "high", label: "High", leading: {"kind":"priority","level":"high"} },
  { id: "urgent", label: "Urgent", leading: {"kind":"priority","level":"urgent"} },
];
const assigneeOptions: PropertyOption[] = [
  { id: "ana", label: "Ana Costa", leading: {"kind":"avatar","name":"Ana Costa"}, meta: "QA", keywords: ["ana@revido.co","QA"] },
  { id: "sam", label: "Sam Lee", leading: {"kind":"avatar","name":"Sam Lee"}, meta: "Developer", keywords: ["sam@revido.co","Developer"] },
  { id: "maya", label: "Maya Patel", leading: {"kind":"avatar","name":"Maya Patel"}, meta: "PO", keywords: ["PO"] },
];

export default function CompactControls() {
  const [status, setStatus] = useState<string | null>("active");
  const [priority, setPriority] = useState<string | null>("high");
  const [assignee, setAssignee] = useState<string | null>("ana");
  return <div className="flex items-center gap-1">
    <PropertySelect label="Status" presentation="icon" size="xs"
      options={statusOptions} value={status} onChange={setStatus} />
    <PropertySelect label="Priority" presentation="icon" size="xs"
      options={priorityOptions} value={priority} onChange={setPriority} />
    <PropertySelect label="Assignee" presentation="icon" size="xs"
      options={assigneeOptions} value={assignee} onChange={setAssignee} searchable allowNone noneLabel="Unassigned" icon={<Icon as={UserPlus} size="sm" />} />
  </div>;
}

Related components