Skip to content

PicklistField

<caos-picklist-field>

a field choosing one value from a fixed list

May be placed on a record page, a page inside an app and a step of a guided flow.

Name Attribute Description Type Default Required
options property only The selectable values, as an array of { value, label, qualifier?, image?, icon?, description?, amount? } or of plain strings. Setting it replaces the whole list and takes precedence over any declared <option> children. qualifier — a quieter second line under the label, what the option IS rather than merely its name — only ever shows once filterable is set; a native option has nowhere to draw a second line. image, icon, description and amount are what an option is drawn FROM in the cards presentation (see presentation), each optional and each simply not drawn when absent; outside that presentation they are carried and unshown, except that a description stands in for a missing qualifier. json none declared Optional
filterable filterable, also a property Draw a typeahead over a listbox instead of the native select. Required for a qualifier to actually show, and worth reaching for on its own once the set is long enough that typing beats scanning. Typing filters by substring against BOTH an option’s label and its qualifier, case-insensitively. The native select stays underneath, hidden, so name still forwards a value into a submitted form exactly as before. boolean false Optional
presentation presentation, markup only How the same declared list is drawn: dropdown (the default); cards, a row of selectable cards showing whichever of an option’s image, icon, description and amount it carries; or radio, plain radio buttons with every option visible. Each is a presentation of one list rather than a different field, so value, name, required, disabled, invalid and message all mean what they always did and the native select still submits. multiple wins over all of them — several values is the checkbox group — and cards and radio each displace filterable. Anything unrecognised reads as dropdown, so a presentation nothing can draw degrades to the list rather than to an empty field. Which one a real field uses is declared by the value set its options come from. The radio buttons are the platform’s own, so arrow-key movement, the single tab stop and the checked semantics are too. text — one of dropdown, cards, radio dropdown Optional
multiple multiple, also a property Choose several values instead of one: the same options drawn as a group of checkboxes named by the label, and what the field holds becomes values. required, disabled, invalid, message and name apply to the group; filterable, placeholder and value do nothing. Once somebody has used a required group and left nothing chosen, it shows as invalid on its own. boolean false Optional
values property only What is chosen when multiple is set, as a list of option values, written back on every change in the order the options list them. A value not among the options is kept, after the rest, and not drawn — so a value since retired from the list is not erased by the next save. json none declared Optional
dependsOn depends-on, also a property The field this dropdown waits on — declarative only, naming the relationship as metadata rather than coding it into a screen. Pair it with dependsOnValue, which the host keeps in sync with that field’s actual value; this input by itself does not fetch or filter anything. fieldRef none declared Optional
dependsOnValue depends-on-value, also a property The CURRENT value of the field named by dependsOn, written by the host every time it changes — the same “data in” shape value already is. While dependsOn is set and this is empty, the field renders with NO real options — its placeholder still showing, still enabled and focusable — because there is nothing yet to choose from; that is the whole of the waiting state, and it needs no other input to say so. Becoming non-empty calls optionsSource, if one is set, with the new value. text none declared Optional
optionsSource property only The injected result source for a dependent dropdown — `(dependsOnValue) => options Promise, in the same "data in, not queried" shape as CaosLookupField.search. Called once when dependsOnValuefirst becomes non-empty, and again every time it changes to a different value; whatever it returns becomes the option list. Optional — a host may instead set.options` itself in response to the same change, and the waiting state above still applies without this. json none declared
label label, markup only The field’s name, drawn above the control and pointed at the select, so clicking it focuses the control. It is also used as the accessible name. text none declared Optional
placeholder placeholder, markup only The “nothing chosen yet” line. Prepends a hidden, disabled, empty-valued first option and leaves it selected, drawn muted, until a real value is picked. Without it the first option is selected from the start. text none declared Optional
value value, also a property The chosen option’s value, not its label. It is written back on every change, so reading the attribute is reading the current selection. text none declared Optional
name name, markup only The form name forwarded to the inner select, for a field submitted as part of a form. text none declared Optional
required required, markup only A value has to be chosen. Draws the asterisk beside the label and marks the select required. boolean false Optional
disabled disabled, markup only You may not change the selection. Greys the control and refuses focus. boolean false Optional
invalid invalid, markup only The selection is wrong — usually nothing chosen on a required field. Turns the outline and the message red. boolean false Optional
message message, markup only The line under the control — muted helper text on its own, the red reason when invalid is also set. text none declared Optional
inline inline, markup only Draw the record-detail variant instead of the boxed form control: no box, an accent underline, inheriting the surrounding type, with the chevron pulled in tight. The label and the message are hidden. boolean false Optional
Name When it fires What it carries
change When a different option is chosen — or, with multiple, when any box is ticked or cleared — after value or values has been written back. nothing

Nothing goes inside this piece.

Design token What it controls
--caos-font The typeface of the label, the control and the message.
--caos-text-body The size of the label and of the chosen value.
--caos-space-1 The gap between the label, the control and the message.
--caos-space-3 The vertical padding inside the control, and the chevron inset.
--caos-space-5 With –caos-space-2, the room kept on the right so the value never runs under the chevron.
--caos-space-2 Part of that same right-hand inset.
--caos-radius The corner radius of the control.
--caos-color-border The outline at rest.
--caos-color-border-strong The outline while the control has focus.
--caos-color-surface-2 The fill of the control.
--caos-color-text The colour of the chosen value.
--caos-color-text-muted The label, the unchosen placeholder, the chevron, the message and disabled text.
--caos-color-accent The hover outline, and the underline of the inline variant.
--caos-accent-wash The tint that fills the control while it has focus, in place of a focus ring.
--caos-color-danger The required asterisk, and the outline and message when invalid.
--caos-state-selected-bg The fill of the chosen card, and of the chosen row in the filterable list.
Name Description Arguments
focus() Move keyboard focus to the select, so typing a letter jumps to an option. The record-detail editor calls it on click-to-edit. options: FocusOptions

Nothing on this piece can be read back.

Part Which piece of it When it is there
::part(label) the label above the control Always
::part(select) the select element Always
::part(message) the line under the control Always
::part(combo) the typeahead box, once filterable is set Always
::part(input) the text input inside the typeahead Always
::part(listbox) the filtered results list, once filterable is set Always
::part(option) one result in that list Always
::part(group) the group of checkboxes, once multiple is set Always
::part(radios) the group of radio buttons, in the radio presentation only Always
::part(radio) one option in the radio presentation — the label wrapping its button and its text Always
::part(cards) the row of cards, once presentation is cards Always
::part(card) one card in that row Always

One value out of a set that is already known — handed over whole, whether that is a short fixed list read without typing or a longer one (optionally depending on another field) searched with filterable. The line that never moves is “known set, handed over” versus “queried per keystroke against live records” — the moment it is the latter, reach for one of the pieces below instead.

  • LookupField — The value points at another RECORD, so the options have to be searched live rather than handed over as a known set — even a filterable picklist is still given its whole answer at once.
  • FieldRenderer — The type is not known until the record is read — hand it the field metadata and it draws the right control, and edits a multi-select as a group of checkboxes.
  • OrderedPicker — Several values are chosen and their ORDER is part of the answer — columns left to right, entries top to bottom.
  • CheckboxField — There are exactly two answers and they are yes and no.
  • SegmentedFilter — The choice changes what is shown rather than recording a value on a record.
  • OverflowMenu — The list is a set of actions to perform rather than a value to store.

By default the control is a real <select> with a generated id and a label pointing at it, so it is announced with its name, clicking the label focuses it, and the whole native picker comes with it: keyboard opening, arrow keys, type-ahead by first letter, and the platform list on a touch device. Setting filterable swaps it for a real role="combobox" (aria-expanded, aria-controls, aria-autocomplete="list") over a role="listbox" of options, with an aria-activedescendant the arrow keys move rather than DOM focus — the same pattern LookupField uses for the same reason. required and invalid are forwarded as required/aria-invalid in both renders, and the label is also set as aria-label. A dependency that is not yet satisfied never disables the control — it stays focusable and its placeholder stays readable, which is what tells “waiting” apart from “refused”. What is left to the author is the option LABELS and qualifiers — they are all a person has to tell the options apart — and the placeholder wording, which is what says nothing has been chosen. With multiple, the boxes sit in a role="group" named by the label and described by the message, each a real checkbox; a required group that has been used and left empty is marked aria-invalid and its message says so.

The list handed over as value/label pairs, with a placeholder holding the “nothing chosen yet” state.

{
"id": "example",
"section": "Options as data",
"columns": 1,
"items": [
{
"id": "picklist_field_1",
"type": "component",
"key": "picklist_field",
"inputs": {
"label": "Order type",
"placeholder": "Choose a type",
"options": [
{
"value": "standard",
"label": "Standard"
},
{
"value": "renewal",
"label": "Renewal"
},
{
"value": "replacement",
"label": "Replacement"
}
]
}
}
]
}
<caos-picklist-field id="picklistField1" label="Order type" placeholder="Choose a type"></caos-picklist-field>
// What an attribute cannot hold, set as a property — the same values the preview is drawn with.
const picklistField1 = document.getElementById('picklistField1');
picklistField1.options = [
{
"value": "standard",
"label": "Standard"
},
{
"value": "renewal",
"label": "Renewal"
},
{
"value": "replacement",
"label": "Replacement"
}
];

The same field with its options written as <option> children instead — read once at connect, and the form that survives being written as plain markup.

Cannot be shown as a page placement:

<caos-picklist-field label="Order type" placeholder="Choose a type">
<option value="standard">Standard</option>
<option value="renewal">Renewal</option>
<option value="replacement">Replacement</option>
</caos-picklist-field>

A field with a value already selected beside one the page has refused — the same control, and the only thing separating them is what the page decided.

{
"id": "example",
"section": "Chosen, and refused",
"columns": 1,
"items": [
{
"id": "picklist_field_1",
"type": "component",
"key": "picklist_field",
"inputs": {
"label": "Order type",
"value": "renewal",
"options": [
{
"value": "standard",
"label": "Standard"
},
{
"value": "renewal",
"label": "Renewal"
}
]
}
},
{
"id": "picklist_field_2",
"type": "component",
"key": "picklist_field",
"inputs": {
"label": "Region",
"placeholder": "Choose a region",
"required": true,
"invalid": true,
"message": "A region is needed before this order can be submitted.",
"options": [
{
"value": "north",
"label": "North"
},
{
"value": "south",
"label": "South"
}
]
}
}
]
}
<caos-picklist-field id="picklistField1" label="Order type" value="renewal"></caos-picklist-field>
<caos-picklist-field id="picklistField2" label="Region" placeholder="Choose a region" required invalid message="A region is needed before this order can be submitted."></caos-picklist-field>
// What an attribute cannot hold, set as a property — the same values the preview is drawn with.
const picklistField1 = document.getElementById('picklistField1');
const picklistField2 = document.getElementById('picklistField2');
picklistField1.options = [
{
"value": "standard",
"label": "Standard"
},
{
"value": "renewal",
"label": "Renewal"
}
];
picklistField2.options = [
{
"value": "north",
"label": "North"
},
{
"value": "south",
"label": "South"
}
];

The Event field renders with no choices and its placeholder still showing until an Event kind is picked, then populates through an injected optionsSource — the cascading behaviour is wired by the page, never coded into the piece.

The page around it: wires the Event field to the Event kind field beside it: an optionsSource answering from the kind, and dependsOnValue kept in sync with the kind field’s own value on every change.

{
"id": "example",
"section": "One dropdown waiting on another",
"columns": 1,
"items": [
{
"id": "picklist_field_1",
"type": "component",
"key": "picklist_field",
"inputs": {
"label": "Event kind",
"placeholder": "Choose a kind",
"options": [
{
"value": "created",
"label": "Created"
},
{
"value": "updated",
"label": "Updated"
}
]
}
},
{
"id": "picklist_field_2",
"type": "component",
"key": "picklist_field",
"inputs": {
"label": "Event",
"placeholder": "Choose an event kind first",
"depends_on": "event_kind"
}
}
]
}
<caos-picklist-field id="picklistField1" label="Event kind" placeholder="Choose a kind"></caos-picklist-field>
<caos-picklist-field label="Event" placeholder="Choose an event kind first" depends-on="event_kind"></caos-picklist-field>
// What an attribute cannot hold, set as a property — the same values the preview is drawn with.
const picklistField1 = document.getElementById('picklistField1');
picklistField1.options = [
{
"value": "created",
"label": "Created"
},
{
"value": "updated",
"label": "Updated"
}
];

A long list, told apart by what each one is

Section titled “A long list, told apart by what each one is”

Filterable draws a qualifier under each option and searches both lines when typing — the difference between a usable picker and an alphabetical wall once a set runs to hundreds of entries.

{
"id": "example",
"section": "A long list, told apart by what each one is",
"columns": 1,
"items": [
{
"id": "picklist_field_1",
"type": "component",
"key": "picklist_field",
"inputs": {
"label": "Object",
"placeholder": "Search objects…",
"filterable": true,
"options": [
{
"value": "account",
"label": "Account",
"qualifier": "Standard object"
},
{
"value": "contact",
"label": "Contact",
"qualifier": "Standard object"
},
{
"value": "acme__project",
"label": "Project",
"qualifier": "Custom object"
},
{
"value": "acme__shipment",
"label": "Shipment",
"qualifier": "Custom object"
}
]
}
}
]
}
<caos-picklist-field id="picklistField1" label="Object" placeholder="Search objects…" filterable></caos-picklist-field>
// What an attribute cannot hold, set as a property — the same values the preview is drawn with.
const picklistField1 = document.getElementById('picklistField1');
picklistField1.options = [
{
"value": "account",
"label": "Account",
"qualifier": "Standard object"
},
{
"value": "contact",
"label": "Contact",
"qualifier": "Standard object"
},
{
"value": "acme__project",
"label": "Project",
"qualifier": "Custom object"
},
{
"value": "acme__shipment",
"label": "Shipment",
"qualifier": "Custom object"
}
];

Multiple set: the ways a contact may be reached, as one box per option, with email already ticked.

{
"id": "example",
"section": "Several values",
"columns": 1,
"items": [
{
"id": "picklist_field_1",
"type": "component",
"key": "picklist_field",
"inputs": {
"label": "Contact by",
"multiple": true,
"options": [
{
"value": "email",
"label": "Email"
},
{
"value": "phone",
"label": "Phone"
},
{
"value": "sms",
"label": "Text message"
},
{
"value": "post",
"label": "Post"
}
],
"values": [
"email"
]
}
}
]
}
<caos-picklist-field id="picklistField1" label="Contact by" multiple></caos-picklist-field>
// What an attribute cannot hold, set as a property — the same values the preview is drawn with.
const picklistField1 = document.getElementById('picklistField1');
picklistField1.options = [
{
"value": "email",
"label": "Email"
},
{
"value": "phone",
"label": "Phone"
},
{
"value": "sms",
"label": "Text message"
},
{
"value": "post",
"label": "Post"
}
];
picklistField1.values = [
"email"
];

A required group: clear the one ticked box and it turns red and says a choice is needed.

{
"id": "example",
"section": "Several values, one required",
"columns": 1,
"items": [
{
"id": "picklist_field_1",
"type": "component",
"key": "picklist_field",
"inputs": {
"label": "Regions served",
"multiple": true,
"required": true,
"options": [
{
"value": "north",
"label": "North"
},
{
"value": "south",
"label": "South"
},
{
"value": "east",
"label": "East"
}
],
"values": [
"north"
]
}
}
]
}
<caos-picklist-field id="picklistField1" label="Regions served" multiple required></caos-picklist-field>
// What an attribute cannot hold, set as a property — the same values the preview is drawn with.
const picklistField1 = document.getElementById('picklistField1');
picklistField1.options = [
{
"value": "north",
"label": "North"
},
{
"value": "south",
"label": "South"
},
{
"value": "east",
"label": "East"
}
];
picklistField1.values = [
"north"
];

The same group disabled: what is chosen still reads clearly, and none of the boxes can be changed.

{
"id": "example",
"section": "Several values, not changeable",
"columns": 1,
"items": [
{
"id": "picklist_field_1",
"type": "component",
"key": "picklist_field",
"inputs": {
"label": "Contact by",
"multiple": true,
"disabled": true,
"options": [
{
"value": "email",
"label": "Email"
},
{
"value": "phone",
"label": "Phone"
},
{
"value": "sms",
"label": "Text message"
}
],
"values": [
"email",
"sms"
]
}
}
]
}
<caos-picklist-field id="picklistField1" label="Contact by" multiple disabled></caos-picklist-field>
// What an attribute cannot hold, set as a property — the same values the preview is drawn with.
const picklistField1 = document.getElementById('picklistField1');
picklistField1.options = [
{
"value": "email",
"label": "Email"
},
{
"value": "phone",
"label": "Phone"
},
{
"value": "sms",
"label": "Text message"
}
];
picklistField1.values = [
"email",
"sms"
];