RadioField
Summary
Section titled “Summary”A radio-button field that composes an options source and a value binding independently — pick a static list or a dynamic source for the choices, and separately bind the value to post meta, a taxonomy, or a custom store.
When to use / When not to use
Section titled “When to use / When not to use”Use it when you need a small, always-visible set of choices and the combination of “where the options come from” and “where the value lives” is not one of the built-in easy-mode shapes — for example, options from one taxonomy while the value is written somewhere unrelated.
Do not reach for it for the common cases: use MetaRadioControl for a radio group bound to
post meta. There is no TaxonomyRadioControl easy-mode wrapper — compose optionsSource: { type: 'terms' } with valueBinding: { type: 'taxonomy' } directly, as shown below.
Use SelectField instead once the option list is long — a dropdown, not a wall of radio
buttons, is the better control for a large or dynamic set.
Import
Section titled “Import”import { RadioField } from '@isudev/gutenberg/fields';// or, skipping the barrel:import { RadioField } from '@isudev/gutenberg/fields/RadioField';| Name | Type | Default | Required | Description |
|---|---|---|---|---|
options |
FieldOption[] |
— | No | Static list of choices; takes precedence over optionsSource. |
optionsSource |
OptionsSource |
— | No | Dynamic source for the choices — terms, posts, users, postTypes, or manual. |
valueBinding |
ValueBinding |
— | No | Where the value is read from and written to — meta, taxonomy, or custom. |
value |
unknown |
— | No | Controlled value; when present the field is controlled and valueBinding is ignored. |
onChange |
( value: unknown ) => void |
— | No | Controlled change handler; its presence alone also makes the field controlled. |
onValueChange |
( value: unknown ) => void |
— | No | Called after the resolved onChange runs, regardless of binding mode. |
loadingComponent |
ReactNode |
— | No | Rendered instead of the control while options or the value are resolving. |
errorComponent |
ReactNode |
— | No | Rendered instead of the control when resolving fails. |
Every FieldOption is { label: string; value: string | number; disabled?: boolean }.
Examples
Section titled “Examples”Minimal: static options, controlled value
Section titled “Minimal: static options, controlled value”import { useState } from '@wordpress/element';import { RadioField } from '@isudev/gutenberg/fields';
const [ size, setSize ] = useState( 'medium' );
<RadioField label="Size" value={ size } onChange={ setSize } options={ [ { label: 'Small', value: 'small' }, { label: 'Medium', value: 'medium' }, { label: 'Large', value: 'large' }, ] }/>Options from content: terms, posts, and users
Section titled “Options from content: terms, posts, and users”// A small taxonomy, ordered alphabetically — value/onChange are still controlled here// to isolate what `optionsSource` alone does.<RadioField label="Format" value={ format } onChange={ setFormat } optionsSource={ { type: 'terms', taxonomy: 'post_format', valueField: 'slug', labelField: 'name', query: { orderby: 'name', order: 'asc' }, } }/>
// A handful of landing pages, used as a "redirect to" choice.<RadioField label="Redirect to" value={ redirectTo } onChange={ setRedirectTo } optionsSource={ { type: 'posts', postTypes: [ 'page' ], valueField: 'id', labelField: 'title', query: { per_page: 5, orderby: 'menu_order', order: 'asc' }, } }/>
// Editors and admins only, as a "reviewed by" choice.<RadioField label="Reviewed by" value={ reviewedBy } onChange={ setReviewedBy } optionsSource={ { type: 'users', roles: [ 'editor', 'administrator' ], valueField: 'id', labelField: 'name', } }/>Options from post types, or a manual list
Section titled “Options from post types, or a manual list”// Every public, viewable post type.<RadioField label="Post type" value={ postType } onChange={ setPostType } optionsSource={ { type: 'postTypes' } }/>
// A fixed list expressed as a source rather than the `options` prop — useful when a// shared helper builds `optionsSource` generically for every field it configures.<RadioField label="Priority" value={ priority } onChange={ setPriority } optionsSource={ { type: 'manual', options: [ { label: 'Low', value: 'low' }, { label: 'Medium', value: 'medium' }, { label: 'High', value: 'high' }, ], } }/>Value bound to post meta
Section titled “Value bound to post meta”<RadioField label="Layout" optionsSource={ { type: 'manual', options: [ { label: 'Full width', value: 'full' }, { label: 'Boxed', value: 'boxed' }, ], } } valueBinding={ { type: 'meta', key: '_layout_style' } }/>Value bound to taxonomy terms
Section titled “Value bound to taxonomy terms”<RadioField label="Genre" optionsSource={ { type: 'terms', taxonomy: 'genre', valueField: 'slug', labelField: 'name', } } valueBinding={ { type: 'taxonomy', taxonomy: 'genre' } }/>Value bound to a custom store
Section titled “Value bound to a custom store”import { useDispatch, useSelect } from '@wordpress/data';import { RadioField } from '@isudev/gutenberg/fields';
function ThemeModeField() { const themeMode = useSelect( ( select ) => select( 'my-plugin/settings' ).getThemeMode(), [] ); const { setThemeMode } = useDispatch( 'my-plugin/settings' );
return ( <RadioField label="Theme mode" optionsSource={ { type: 'manual', options: [ { label: 'Light', value: 'light' }, { label: 'Dark', value: 'dark' }, ], } } valueBinding={ { type: 'custom', value: themeMode, onChange: setThemeMode, } } /> );}Loading and error placeholders
Section titled “Loading and error placeholders”import { Notice, Spinner } from '@wordpress/components';import { RadioField } from '@isudev/gutenberg/fields';
<RadioField label="Author" optionsSource={ { type: 'users', roles: [ 'author', 'editor' ], } } valueBinding={ { type: 'meta', key: '_featured_author' } } loadingComponent={ <Spinner /> } errorComponent={ <Notice status="error" isDismissible={ false }> Could not load authors. </Notice> }/>Composition an easy-mode wrapper cannot express
Section titled “Composition an easy-mode wrapper cannot express”// Options list every "genre" term, but the pick is written to a separate// "featured-genre" taxonomy used only to drive a homepage query — the post's actual// genre assignment, read elsewhere in the editor, is untouched.<RadioField label="Feature under genre" help="Does not change the post's own genre — only where it's featured." optionsSource={ { type: 'terms', taxonomy: 'genre', valueField: 'slug', labelField: 'name', } } valueBinding={ { type: 'taxonomy', taxonomy: 'featured-genre', } }/>There is no easy-mode wrapper for this at all: MetaRadioControl only ever writes to meta,
and nothing wraps a taxonomy binding paired with a different source. RadioField lets the
options and the value diverge — options from one place, the value written somewhere else
entirely.
Behavior
Section titled “Behavior”- Built on
useFieldBinding, which composesuseOptionsSource(fromoptions/optionsSource) anduseValueBinding(fromvalueBinding, or controlledvalue/onChange). The two never interact by design — seeoptionsSource !== valueBinding. - WordPress’s
RadioControltakesselected, notvalue. The field does that renaming for you: consumers ofRadioFieldalways passvalue/readvalueback, exactly as withSelectField. optionsbeatsoptionsSourceoutright: ifoptionsis set,optionsSourceis never consulted, even when both are passed.- The field is controlled as soon as either
valueoronChangeis present — not only when both are — andvalueBindingis then ignored entirely. In development, passing both avalueBindingand a controlled prop logs aconsole.warnexplaining that the controlled props win. - Whatever the binding mode, the field’s own change handler always runs the resolved writer
first (the controlled
onChange, or the binding’s writer), then callsonValueChange(if provided) with the same value — useful for side effects like tracking without taking over the write. isLoadingis true while either the options source or the value binding is still resolving;errorprefers the options source’s error, falling back to the value binding’s.- Per source:
terms,posts, andusersare loading/erroring based oncore-data’sgetEntityRecordsresolution for that query;postTypesis the same viagetPostTypes.manualand staticoptionsnever load and never error. - Every fetching source (
terms,posts,users,postTypes) queries withper_page: -1by default — the whole collection, not a first page.queryis merged over that default, so narrow it (per_page,search,include) for anything that can grow: ausersor unboundedtermssource will otherwise fetch every record and render one radio button per row. - Per binding:
metaonly reports loading while the post type itself hasn’t resolved yet (not while the meta value is loading), and never reports an error.taxonomynever reports loading at all: its REST base falls back to the taxonomy slug synchronously whengetTaxonomy()hasn’t resolved, so the base is always truthy andisLoadingis alwaysfalse— whether or notrestBasewas passed. It never reports an error either.customnever reports loading or an error — the field trusts whatever is passed. In practice,errorComponentonly ever fires from a failed options fetch, never from the value side. - While
isLoadingorerroris true, the field rendersloadingComponent/errorComponent(or nothing, if omitted) instead ofRadioControl— the control is not mounted underneath. - Any prop besides the eight above (
label,help,disabled, …) is forwarded unchanged to the underlyingRadioControl.RadioControl’s owndisabledis a single flag for the whole group, and it — like every other pass-through prop — is spread onto every individual radio<input>, not just the wrapping<fieldset>. terms/posts/usersdefaultvalueField/labelFieldtoid/name(titlefor posts).postTypesignores both and always usesslug/singular name, filtering out non-viewable and internal post types (attachment,wp_block,wp_template,wp_template_part,wp_navigation,wp_font_family,wp_font_face).- All four options-source hooks and all three value-binding hooks are called on every
render regardless of which type is active (Rules of Hooks) — inactive ones receive
nulland skip fetching, so switchingoptionsSource.typeorvalueBinding.typeat runtime is safe.
Styling
Section titled “Styling”Ships no stylesheet. Renders @wordpress/components’ RadioControl directly and inherits
its editor chrome; there are no custom properties to override.
Gotchas
Section titled “Gotchas”RadioControlmatches the checked option with strict equality (option.value === selected) and itsonChangealways hands back the string value of the chosen<input>(event.target.value). IfoptionsSourceresolves numeric values (the defaultvalueField: 'id'forterms/posts/users) and that value round-trips back in asvalue, no option will show as checked —5 === '5'isfalse. UsevalueField: 'slug'(or another string field), or keep the numeric id consistently as a number on the way back in.- A
taxonomybinding reads and writes the entity property named after the taxonomy’s REST base, and untilgetTaxonomy()resolves it falls back to the taxonomy slug. Where the two differ — core’scategoryis exposed ascategories,post_tagastags— the first renders readundefinedfrom a property that does not exist, and a write inside that window goes to that non-existent property. There is no loading flag covering it, so nothing surfaces the problem. PassrestBaseexplicitly ({ type: 'taxonomy', taxonomy: 'category', restBase: 'categories' }) for any taxonomy whose REST base isn’t identical to its name. - Per-option
disabledon aFieldOptionhas no effect here:RadioControlonly supportslabel,value, anddescriptionper option and does not read a per-optiondisabledflag. Disable the whole group instead by forwarding a top-leveldisabledprop. - Passing only
onChangewithoutvalue(or the reverse) is enough to switch the field to controlled mode and silently dropvalueBinding— pass both, or neither. - If both
optionsandoptionsSourceare set,optionsSourceis ignored outright rather than merged with it — removeoptionsonce a field moves to a dynamic source. - An options list that resolves empty (no matching terms/posts/users, or a query that
matches nothing) renders nothing:
RadioControlreturnsnullfor an emptyoptionsarray, so there is no visible “no results” state. optionsSource: { type: 'postTypes' }has novalueField/labelField— check theOptionsSourcetype before assuming every source supports them.
Related
Section titled “Related”SelectField— same engine, a dropdown instead of radio buttons.MetaRadioControl— easy mode for a radio group bound to post meta.TaxonomySelectControl— the equivalent easy mode for taxonomy-bound options and value, as a dropdown.useCurrentPostType— what ameta/taxonomybinding falls back to whenpostTypeis omitted.