Why Button Set is its own option type instead of a display mode on radio
The question: A button set stores the same value a radio stores — one key from a fixed set of
choices. Everything else is presentation. So should it be a new option type at all, or just
'display' => 'buttons' on radio?
Context
Comparing the Unyson+ option types against Redux Framework's field list turned up a genuine gap: no
segmented control. For three or more choices you get radio (a vertical list) or select (choices
hidden behind a click). switch covers two choices with custom labels, and image-picker covers
visual choices, but there was nothing between them.
The gap matters most in the page-builder modal, where the panel is narrow and settings like alignment, heading tag, size preset and layout appear constantly. A three-item vertical radio list costs three rows; a segmented control costs one. Across a builder panel that compounds.
The obvious objection is that this is only presentation. The stored value is identical, so the usual advice applies — extend the existing type with a display flag rather than duplicating storage semantics behind a second name.
Options considered
A display mode on radio. 'display' => 'buttons'. No new type in the index, no new docs page,
no new screenshots, no preset-library consideration. Every existing radio option could opt in by
adding one key, and the value shape is untouched by construction. This is what most codebases should
do, and it was the initial recommendation.
A new button-set type. A 73rd type to document and maintain, but discoverable in the
option-type index, and able to cover both single and multi select in one place.
Both — a type that internally delegates to radio. Rejected quickly: it buys the discoverability
of a type and the indirection cost of a flag, while making the value logic harder to follow.
Decision
Its own type: button-set. Three things decided it.
The framework's own precedent runs the other way. Unyson+ already ships short-text,
medium-text, short-select, medium-select and predefined-colors-color-picker-compact — types
that differ from their base type only in width and chrome. Given that, a display flag on radio
would have been the inconsistent choice, not the principled one. A convention that a codebase
already contradicts five times over is not that codebase's convention.
Discoverability is the product. For a framework whose pitch is the breadth of its option types,
a flag documented inside another type's page is effectively invisible. People browse the index; they
do not read radio's optional attributes hoping for a layout switch.
A flag on radio could not have covered multi-select. This is the argument that settles it.
Redux's button_set handles single and multiple selection, and so does ours: 'multiple' => true
switches from radio to checkbox semantics. A display flag on radio would have needed a twin flag on
checkboxes, leaving developers to pick the right base type first and only then discover they could
style it. One type that asks "single or multiple?" is a better question to answer than "radio or
checkboxes, and can either look like buttons?"
Supporting decisions:
- Value shape follows the existing types, not Redux. Single mode returns the choice key as a
plain string, exactly like
radio. Multiple mode returnsarray( key => true ), exactly likecheckboxes. Internal consistency was worth more than matching Redux's shape, and it means an existingradioorcheckboxesoption can switch tobutton-setwith no data migration — a one-line change totype. - No third-party library. Unlike the colour picker, where Coloris earns its place, a segmented
control is native
<input type="radio|checkbox">with the input visually hidden and its<label>styled. Keyboard navigation, focus rings, screen-reader group semantics and form serialization come from the browser. A JS library rendering<div>buttons would have to reimplement all of it in ARIA. The only JavaScript in the type is ~40 lines implementingallow_deselect. - Colours are deliberately not configurable. The control inherits
--wp-admin-theme-colorthrough the framework's--fw-accenttoken, so it follows the admin colour scheme the user chose. A per-option colour would override that preference, break panel consistency, and move contrast policing onto option authors.
Why this doesn't generalise
Worth stating plainly, because the reasoning here inverts the usual advice: this is not a licence to create a type per visual variant. It held because the framework already had presentation-variant types, because the index is the discovery surface, and because the multi-select requirement crossed two base types. Absent those, a display flag remains the right default.
