What is missing
The library has no control that lets a user type into a field to filter a list of options and pick
one or several of them. @ippon-ui/styles@0.1.1 ships a floating panel of actions
(ippon-dropdown) but no floating panel of options: nothing in the distributed CSS or docs
carries role="listbox", role="option", aria-activedescendant or aria-autocomplete. Every
consumer that needs a picker therefore rebuilds one on top of input-text and dropdown, and each
rebuild invents its own keyboard handling and its own ARIA wiring — the two parts that are hardest
to get right and the two the design system should own.
What exists today and why it does not cover it
| Component |
Why it falls short |
atom/input-text |
The atom is the native input, so there is no room inside its box for a leading search icon or a trailing counter. No -icon-leading alternative is documented. |
atom/icon |
Can sit next to an input, but has no documented way of sitting inside the input box. |
molecule/field |
Documented as stateless (Label + control + Helper text, the caller wires for/id and aria-describedby). It anticipates more controls — "textarea or select tomorrow" — but no such control exists to slot in. |
organism/dropdown |
A panel of actions: the trigger opens it through command="toggle-popover" / commandfor, and ippon-dropdown---buttons ionises buttons full width. Options are not buttons and must not carry button semantics. It has no active-option highlight, no selected state, and no busy / empty / failed content states. |
molecule/table |
Static and presentational: no selectable row, no active row, no empty state. |
The blocking point is the anchoring contract. dropdown is opened by an invoker button through
command / commandfor, and IpponDropdown hard-codes popover="auto". A combobox is anchored to
a text input, which is not an invoker, and has to open and close programmatically. That is the
one place where the existing organism cannot be reused as-is.
Design
https://www.figma.com/design/Tp97MEqQR8YTnjElPowa6k/Librairie_IPPON-2025?node-id=1410-155&t=hTsmxYga8DE27kYJ-0
The Figma node is a component set named Select Field with a single State property and four
values: Closed, Open & Non Active, Open & Active, With Value. Both open variants nest an
instance named Dropdown List — a bordered, shadowed panel of five option rows, each carrying a
checkbox, two of them checked in Open & Active. The field itself carries a magnifier and a visible
text caret, so it is typed into rather than being a button-styled select. It is therefore a
searchable multi-select combobox, not a search field and not a plain select.
Only a partial read of the file was possible (the Figma seat hit its tool-call limit before the
layer structure could be inspected), so the layout below is measured off the rendered frames rather
than read from auto-layout. The token bindings are read from the node.
Anatomy
- Label above the field,
Body/Body Medium, Neutral/Text&Icon/Tertiary, Spacing/Margin/S
(12) down to the field.
- Field box, 52 tall, surface
Neutral/Surface/Primary, border Spacing/Border/Width/M (1) in
Neutral/Border/Border, radius Spacing/Border/Radius/M (4), horizontal padding
Spacing/Margin/M (16).
- Leading magnifier, 16x16, 16 from the left edge.
Neutral/Text&Icon/Tertiary Inversed at
rest, Neutral/Text&Icon/Primary once open or filled.
- Text zone: placeholder, or an empty typing area with a caret in
Information 2/Text&Icon/Primary, or the selected labels joined by commas.
- Counter pill, 55x24, fully rounded,
Brand/Surface/Primary, containing the selection count in
Label/Label Small and a clear-all cross, both Brand/Text&Icon/On primary. Drawn only when at
least one option is selected. It is one pill for the whole selection — the design has no
removable per-value chips.
- Trailing chevron, a 10x6 solid triangle, 16 from the right edge,
Neutral/Text&Icon/Tertiary.
It is drawn identically in all four variants, so nothing says it rotates when open.
- Helper text below the field,
Body/Body Small, Neutral/Text&Icon/Tertiary. Drawn on the
closed variant only.
- Option panel, starting 10 below the field, same width, same surface, border and radius as the
field, plus a soft drop shadow (no named effect token was readable), with Spacing/Margin/XXS (4)
of inner padding top and bottom.
- Option row, 48 tall, full-bleed highlight, 16 of left padding to a 16x16 checkbox, then the
label in Body/Body Medium, Neutral/Text&Icon/Primary.
Semantic tokens bound by the node, in the vocabulary the library already uses:
| Role |
Token |
| Field and panel surface |
Neutral/Surface/Primary |
| Highlighted option row |
Neutral/Surface/Primary Hover |
| Field border, panel border, unchecked box |
Neutral/Border/Border |
| Typed text, selected values, option labels |
Neutral/Text&Icon/Primary |
| Label, helper text, chevron |
Neutral/Text&Icon/Tertiary |
| Placeholder, resting magnifier |
Neutral/Text&Icon/Tertiary Inversed |
| Focus ring |
Information 2/Surface/Primary |
| Text caret |
Information 2/Text&Icon/Primary |
| Counter pill fill, checked box fill |
Brand/Surface/Primary |
| Counter glyphs, check glyph |
Brand/Text&Icon/On primary |
States to support
Drawn in the design:
- closed / rest — placeholder and magnifier in the inversed tertiary colour, helper text below;
- open, focused, no selection — a 2px focus ring drawn outside the border with a ~2px gap,
caret visible, panel open, every checkbox unchecked;
- open, focused, with selection — the same plus the counter pill and the checked rows;
- closed with a value — no ring, selected labels joined by commas, counter pill kept, no helper
text;
- option row highlighted — a single full-bleed grey band. The design does not distinguish
pointer hover from keyboard active option, so one visual state has to serve both;
- checkbox checked / unchecked.
Not drawn, and needed before the component can ship — these need a design pass:
- disabled and read-only, for the field and for an individual option;
- error and success, to stay consistent with the
-error / -success alternatives of input-text;
- busy, while the options the consumer hands over are still being loaded;
- empty, when the typed query matches nothing;
- failed, when options could not be loaded;
- what happens past five rows (max panel height, scrolling) and what happens when the joined labels
overflow the field width (truncate or wrap);
- single-select. The node draws checkboxes on every row, so it only documents the multi-select case.
Atomic level and composition
Three pieces, in this order.
Atom — a search-capable control box. input-text is a naked native input and cannot host the
leading magnifier or the trailing counter. Either a new input-search atom, or an
-icon-leading / -icon-trailing alternative on input-text plus the wrapper element that makes it
possible. Whichever is chosen, the contract must stay identical to input-text — named by a label
through for/id, described by a helper-text through aria-describedby — so that field accepts
it as a control without any change. This part could reasonably be split into its own issue; it is
listed here because the combobox cannot be built without it.
Molecule — the option row and the option list. An option row (checkbox or check mark, label,
optional icon, badge or secondary text) with selected and active alternatives, and the list that
stacks them together with its busy, empty and failed content. Same nature as field: a composition
of atoms with presentational alternatives and no orchestration of its own.
Organism — combobox. It wires the control to the floating list, owns the expanded state, the
active option, the selection and the ARIA relations between the two. That orchestration is what makes
it an organism rather than a molecule, and why it must not be pushed into field, which the library
documents as stateless.
Reuse rather than reinvent: icon for the magnifier and per-option glyphs, label and helper-text
for naming and describing, badge for the counter pill and for optional per-option metadata,
progress for the busy list, text for the empty message, error-area for the failed list (it is
already specified to fill a zone whose content could not be loaded and offers a retry action),
separator for grouping, field as the surrounding layout. The panel should share the floating
surface of dropdown — an options alternative alongside ippon-dropdown---buttons rather than a
second floating surface — with anchoring to a non-invoker trigger as the one thing to extend.
Following CAP, roughly:
<div class="ippon-combobox">
<input class="ippon-input-text -icon-leading" role="combobox"
aria-expanded="true" aria-controls="picker-listbox" aria-autocomplete="list"
aria-activedescendant="picker-option-2" id="picker" />
<span class="ippon-icon ippon-ion-search ippon-combobox--icon" role="presentation"></span>
<span class="ippon-badge ippon-combobox--counter">2</span>
<ul class="ippon-dropdown ippon-dropdown---options" role="listbox" id="picker-listbox">
<li class="ippon-combobox--option -active" role="option" id="picker-option-2"
aria-selected="true">Option 2</li>
</ul>
</div>
Accessibility
The library already makes the caller wire for/id and aria-describedby in field; the combobox
should keep that contract and own everything the consumer cannot reasonably get right:
role="combobox" on the input, with aria-expanded, aria-controls and aria-autocomplete="list";
role="listbox" on the panel, role="option" with aria-selected and aria-disabled on each row,
and aria-multiselectable when several options can be picked;
- DOM focus never leaves the input: the active option is tracked with
aria-activedescendant;
- keyboard:
ArrowDown / ArrowUp move the active option and open the panel when it is closed,
Home / End jump to the first and last enabled option, Enter selects the active option,
Escape closes and leaves focus in the input, Tab closes without selecting. Disabled options are
skipped and the active option is scrolled into view;
- pointer: hovering a row makes it active, clicking it selects it without stealing focus from the
input;
- the list announces its own state:
aria-busy while loading, and a readable message for the empty
and failed cases rather than an empty panel;
- the selection counter must not be the only carrier of the information — the number of selected
options needs a text alternative.
React wrapper
An IpponCombobox exported from @ippon-ui/react (plus IpponInputSearch if the control atom is
built separately), following the conventions already in the package: dataSelector through
DataSelectable, classes built with clsx() and the CAP helpers, alternatives as union types,
booleans as optional props. Like IpponDropdown it should require an id, and derive
${id}-listbox and ${id}-option-${key} internally so the consumer never wires ARIA by hand. Like
IpponImportFile it should hand back domain values rather than DOM events, and discriminate the
single and multiple cases through a union.
type IpponComboboxProps<T> = DataSelectable & {
id: string;
value: string; // the query, fully controlled
onChange: (value: string) => void;
items: readonly T[];
itemKey: (item: T) => string;
itemLabel: (item: T) => string;
itemDescription?: (item: T) => string;
itemDisabled?: (item: T) => boolean;
renderItem?: (item: T, state: { active: boolean; selected: boolean }) => ReactNode;
placeholder?: string;
disabled?: boolean;
required?: boolean;
name?: string;
variant?: 'error' | 'success';
loading?: boolean; // renders progress, sets aria-busy
emptyState?: ReactNode;
error?: ReactNode;
onRetry?: () => void; // feeds the error-area retry action
open?: boolean;
defaultOpen?: boolean;
onOpenChange?: (open: boolean) => void;
onKeyDown?: KeyboardEventHandler;
} & (
| { multiple?: false; selection?: T; onSelect: (item: T) => void }
| {
multiple: true;
selection: readonly T[];
onSelect: (item: T) => void;
onDeselect: (item: T) => void;
onClear: () => void; // the cross on the counter pill
}
);
Generic over T so consumers keep their own types. aria-describedby and aria-labelledby must be
passed through so the component can live inside an IpponField whose label and helper text the caller
wires.
Out of scope
No data layer. The component never fetches, never debounces, never caches and never filters: it
renders the options it is handed, and the consumer owns the query lifecycle. No per-value removable
chips either — the design expresses the whole selection with a single counter pill.
Context
Found on @ippon-ui/styles@0.1.1 while building a form where a user picks entries out of a list too
long to render in full, and has to type to narrow it down. The current workaround is an
IpponInputText placed inside an ippon-dropdown whose rows are IpponButtons: it looks close
enough, but the panel is a menu of actions rather than a listbox, keyboard users get button
semantics and tab-through instead of arrow-key traversal, and screen readers are never told that the
field controls a list of options. That is exactly the part a design system should not leave to each
consumer.
Happy to take this one if the shape above is agreed — the missing states listed under "States to
support" would need a design pass first.
What is missing
The library has no control that lets a user type into a field to filter a list of options and pick
one or several of them.
@ippon-ui/styles@0.1.1ships a floating panel of actions(
ippon-dropdown) but no floating panel of options: nothing in the distributed CSS or docscarries
role="listbox",role="option",aria-activedescendantoraria-autocomplete. Everyconsumer that needs a picker therefore rebuilds one on top of
input-textanddropdown, and eachrebuild invents its own keyboard handling and its own ARIA wiring — the two parts that are hardest
to get right and the two the design system should own.
What exists today and why it does not cover it
atom/input-text-icon-leadingalternative is documented.atom/iconmolecule/fieldfor/idandaria-describedby). It anticipates more controls — "textarea or select tomorrow" — but no such control exists to slot in.organism/dropdowncommand="toggle-popover"/commandfor, andippon-dropdown---buttonsionises buttons full width. Options are not buttons and must not carry button semantics. It has no active-option highlight, no selected state, and no busy / empty / failed content states.molecule/tableThe blocking point is the anchoring contract.
dropdownis opened by an invoker button throughcommand/commandfor, andIpponDropdownhard-codespopover="auto". A combobox is anchored toa text input, which is not an invoker, and has to open and close programmatically. That is the
one place where the existing organism cannot be reused as-is.
Design
https://www.figma.com/design/Tp97MEqQR8YTnjElPowa6k/Librairie_IPPON-2025?node-id=1410-155&t=hTsmxYga8DE27kYJ-0
The Figma node is a component set named Select Field with a single
Stateproperty and fourvalues:
Closed,Open & Non Active,Open & Active,With Value. Both open variants nest aninstance named
Dropdown List— a bordered, shadowed panel of five option rows, each carrying acheckbox, two of them checked in
Open & Active. The field itself carries a magnifier and a visibletext caret, so it is typed into rather than being a button-styled select. It is therefore a
searchable multi-select combobox, not a search field and not a plain select.
Only a partial read of the file was possible (the Figma seat hit its tool-call limit before the
layer structure could be inspected), so the layout below is measured off the rendered frames rather
than read from auto-layout. The token bindings are read from the node.
Anatomy
Body/Body Medium,Neutral/Text&Icon/Tertiary,Spacing/Margin/S(12) down to the field.
Neutral/Surface/Primary, borderSpacing/Border/Width/M(1) inNeutral/Border/Border, radiusSpacing/Border/Radius/M(4), horizontal paddingSpacing/Margin/M(16).Neutral/Text&Icon/Tertiary Inversedatrest,
Neutral/Text&Icon/Primaryonce open or filled.Information 2/Text&Icon/Primary, or the selected labels joined by commas.Brand/Surface/Primary, containing the selection count inLabel/Label Smalland a clear-all cross, bothBrand/Text&Icon/On primary. Drawn only when atleast one option is selected. It is one pill for the whole selection — the design has no
removable per-value chips.
Neutral/Text&Icon/Tertiary.It is drawn identically in all four variants, so nothing says it rotates when open.
Body/Body Small,Neutral/Text&Icon/Tertiary. Drawn on theclosed variant only.
field, plus a soft drop shadow (no named effect token was readable), with
Spacing/Margin/XXS(4)of inner padding top and bottom.
label in
Body/Body Medium,Neutral/Text&Icon/Primary.Semantic tokens bound by the node, in the vocabulary the library already uses:
Neutral/Surface/PrimaryNeutral/Surface/Primary HoverNeutral/Border/BorderNeutral/Text&Icon/PrimaryNeutral/Text&Icon/TertiaryNeutral/Text&Icon/Tertiary InversedInformation 2/Surface/PrimaryInformation 2/Text&Icon/PrimaryBrand/Surface/PrimaryBrand/Text&Icon/On primaryStates to support
Drawn in the design:
caret visible, panel open, every checkbox unchecked;
text;
pointer hover from keyboard active option, so one visual state has to serve both;
Not drawn, and needed before the component can ship — these need a design pass:
-error/-successalternatives ofinput-text;overflow the field width (truncate or wrap);
Atomic level and composition
Three pieces, in this order.
Atom — a search-capable control box.
input-textis a naked native input and cannot host theleading magnifier or the trailing counter. Either a new
input-searchatom, or an-icon-leading/-icon-trailingalternative oninput-textplus the wrapper element that makes itpossible. Whichever is chosen, the contract must stay identical to
input-text— named by alabelthrough
for/id, described by ahelper-textthrougharia-describedby— so thatfieldacceptsit as a control without any change. This part could reasonably be split into its own issue; it is
listed here because the combobox cannot be built without it.
Molecule — the option row and the option list. An option row (checkbox or check mark, label,
optional icon, badge or secondary text) with
selectedandactivealternatives, and the list thatstacks them together with its busy, empty and failed content. Same nature as
field: a compositionof atoms with presentational alternatives and no orchestration of its own.
Organism —
combobox. It wires the control to the floating list, owns the expanded state, theactive option, the selection and the ARIA relations between the two. That orchestration is what makes
it an organism rather than a molecule, and why it must not be pushed into
field, which the librarydocuments as stateless.
Reuse rather than reinvent:
iconfor the magnifier and per-option glyphs,labelandhelper-textfor naming and describing,
badgefor the counter pill and for optional per-option metadata,progressfor the busy list,textfor the empty message,error-areafor the failed list (it isalready specified to fill a zone whose content could not be loaded and offers a retry action),
separatorfor grouping,fieldas the surrounding layout. The panel should share the floatingsurface of
dropdown— an options alternative alongsideippon-dropdown---buttonsrather than asecond floating surface — with anchoring to a non-invoker trigger as the one thing to extend.
Following CAP, roughly:
Accessibility
The library already makes the caller wire
for/idandaria-describedbyinfield; the comboboxshould keep that contract and own everything the consumer cannot reasonably get right:
role="combobox"on the input, witharia-expanded,aria-controlsandaria-autocomplete="list";role="listbox"on the panel,role="option"witharia-selectedandaria-disabledon each row,and
aria-multiselectablewhen several options can be picked;aria-activedescendant;ArrowDown/ArrowUpmove the active option and open the panel when it is closed,Home/Endjump to the first and last enabled option,Enterselects the active option,Escapecloses and leaves focus in the input,Tabcloses without selecting. Disabled options areskipped and the active option is scrolled into view;
input;
aria-busywhile loading, and a readable message for the emptyand failed cases rather than an empty panel;
options needs a text alternative.
React wrapper
An
IpponComboboxexported from@ippon-ui/react(plusIpponInputSearchif the control atom isbuilt separately), following the conventions already in the package:
dataSelectorthroughDataSelectable, classes built withclsx()and theCAPhelpers, alternatives as union types,booleans as optional props. Like
IpponDropdownit should require anid, and derive${id}-listboxand${id}-option-${key}internally so the consumer never wires ARIA by hand. LikeIpponImportFileit should hand back domain values rather than DOM events, and discriminate thesingle and multiple cases through a union.
Generic over
Tso consumers keep their own types.aria-describedbyandaria-labelledbymust bepassed through so the component can live inside an
IpponFieldwhose label and helper text the callerwires.
Out of scope
No data layer. The component never fetches, never debounces, never caches and never filters: it
renders the options it is handed, and the consumer owns the query lifecycle. No per-value removable
chips either — the design expresses the whole selection with a single counter pill.
Context
Found on
@ippon-ui/styles@0.1.1while building a form where a user picks entries out of a list toolong to render in full, and has to type to narrow it down. The current workaround is an
IpponInputTextplaced inside anippon-dropdownwhose rows areIpponButtons: it looks closeenough, but the panel is a menu of actions rather than a listbox, keyboard users get button
semantics and tab-through instead of arrow-key traversal, and screen readers are never told that the
field controls a list of options. That is exactly the part a design system should not leave to each
consumer.
Happy to take this one if the shape above is agreed — the missing states listed under "States to
support" would need a design pass first.