Custom RadioControl Component for WordPress

I recently found myself wanting to extend the WordPress block editor’s RadioControl component. Unfortunately this component is not designed to be extended with additional functionality, so I needed a different solution. The rest of this post covers how to create a custom RadioControl component for the WordPress block editor.

Get the Core Code

We’ll start with the core code for the RadioControl block, which lives in /wp-includes/js/dist/components.js and looks like this:

function RadioControl({
  label,
  className,
  selected,
  help,
  onChange,
  options = [],
  ...props
}) {
  const instanceId = Object(external_wp_compose_["useInstanceId"])(RadioControl);
  const id = `inspector-radio-control-${instanceId}`;

  const onChangeValue = event => onChange(event.target.value);

  return !Object(external_lodash_["isEmpty"])(options) && Object(external_wp_element_["createElement"])(base_control, {
    label: label,
    id: id,
    help: help,
    className: classnames_default()(className, 'components-radio-control')
  }, options.map((option, index) => Object(external_wp_element_["createElement"])("div", {
    key: `${id}-${index}`,
    className: "components-radio-control__option"
  }, Object(external_wp_element_["createElement"])("input", Object(esm_extends["a" /* default */])({
    id: `${id}-${index}`,
    className: "components-radio-control__input",
    type: "radio",
    name: id,
    value: option.value,
    onChange: onChangeValue,
    checked: option.value === selected,
    "aria-describedby": !!help ? `${id}__help` : undefined
  }, props)), Object(external_wp_element_["createElement"])("label", {
    htmlFor: `${id}-${index}`
  }, option.label))));
}

Initial Refactoring

In order to make this work for us as our own custom component we’ll need to make a few modifications, which will also make the code easier to read:

import PropTypes from 'prop-types';
import { useInstanceId } from '@wordpress/compose';
import { BaseControl } from '@wordpress/components';
import classnames from 'classnames';

export default function NwdCustomRadioControl({
  label,
  hideLabelFromVision,
  className,
  selected,
  help,
  onChange,
  options = [],
  ...props
}) {
  const instanceId = useInstanceId(NwdCustomRadioControl);
  const id = `inspector-radio-control-nwd-${instanceId}`;

  const onChangeValue = event => onChange(event.target.value);

  if (! options?.length) {
    return null;
  }
  
  return (
    <BaseControl
      label={ label }
      id={ id }
      help={ help }
      className={ classnames(className, 'components-radio-control') }
    >
      { options.map((option, index) => (
        <div
      key={ `${ id }-${ index }` }
      className="components-radio-control__option"
    >
      <input
        id={ `${ id }-${ index }` }
        className="components-radio-control__input"
        type="radio"
        name={ `${ id }-${ index }` }
        value={ option.value }
        onChange={ onChangeValue }
        checked={ option.value === selected }
        aria-describedby={
          ! help ? `${ id }__help` : undefined
        }
      />
      <label 
        className="components-radio-control__label"
        htmlFor={ `${ id }-${ index }` }
      >
        { option.label }
      </label>
    </div>
      ))}
  )
}

NwdCustomRadioControl.propTypes = {
  label: PropTypes.string,
  hideLabelFromVision: PropTypes.bool,
  help: PropTypes.string,
  selected: PropTypes.string.isRequired,
  onChange: PropTypes.func.isRequired,
  options: PropTypes.array,
  className: PropTypes.string,
}

NwdCustomRadioControl.defaultProps = {
  label: '',
  hideLabelFromVision: true,
  help: '',
  options: [],
  className: '',
};

Customization

The refactored code above should work just as the default RadioControl does. From there we can add our own custom functionality. In my case I needed the ability to toggle a “disabled” attribute on specific radio options, depending on the state of a different element on the page. Adding the “disabled” attribute can be done like this:

<input
  id={ `${ id }-${ index }` }
  className="components-radio-control__input"
  type="radio"
  name={ `${ id }-${ index }` }
  value={ option.value }
  onChange={ onChangeValue }
  checked={ option.value === selected }
  aria-describedby={
    ! help ? `${ id }__help` : undefined
  }
  disabled={ option.disabled }
/>

If we want the corresponding label to appear differently when the radio option is disabled, we can add a class and target it with CSS:

<label 
  className={
      option.disabled ? 
      'components-radio-control__label disabled' : 
      'components-radio-control__label'
  }
  htmlFor={ `${ id }-${ index }` }
>
  { option.label }
</label>

I’m not showing it here, but in my case I used opacity: 0.5; on the label for a disabled radio option.

With both of those changes in place, our complete component looks like:

import PropTypes from 'prop-types';
import { useInstanceId } from '@wordpress/compose';
import { BaseControl } from '@wordpress/components';
import classnames from 'classnames';

export default function NwdCustomRadioControl({
  label,
  hideLabelFromVision,
  className,
  selected,
  help,
  onChange,
  options = [],
  ...props
}) {
  const instanceId = useInstanceId(NwdCustomRadioControl);
  const id = `inspector-radio-control-nwd-${instanceId}`;

  const onChangeValue = event => onChange(event.target.value);

  if (! options?.length) {
    return null;
  }
  
  return (
    <BaseControl
      label={ label }
      id={ id }
      help={ help }
      className={ classnames(className, 'components-radio-control') }
    >
      { options.map((option, index) => (
        <div
      key={ `${ id }-${ index }` }
      className="components-radio-control__option"
    >
      <input
        id={ `${ id }-${ index }` }
        className="components-radio-control__input"
        type="radio"
        name={ `${ id }-${ index }` }
        value={ option.value }
        onChange={ onChangeValue }
        checked={ option.value === selected }
        aria-describedby={
          ! help ? `${ id }__help` : undefined
        }
        disabled={ option.disabled }
      />
      <label 
        className={
            option.disabled ? 
            'components-radio-control__label disabled' : 
            'components-radio-control__label'
        }
        htmlFor={ `${ id }-${ index }` }
      >
        { option.label }
      </label>
    </div>
      ))}
  )
}

NwdCustomRadioControl.propTypes = {
  label: PropTypes.string,
  hideLabelFromVision: PropTypes.bool,
  help: PropTypes.string,
  selected: PropTypes.string.isRequired,
  onChange: PropTypes.func.isRequired,
  options: PropTypes.array,
  className: PropTypes.string,
}

NwdCustomRadioControl.defaultProps = {
  label: '',
  hideLabelFromVision: true,
  help: '',
  options: [],
  className: '',
};

Using the above code will allow you to achieve something like the following image, where we see “Option Four” is disabled.

the author profile picture

Ryan Neilson is a software developer from Nova Scotia, Canada, where he lives with his wife and their menagerie of pets. He has worked extensively with PHP, WordPress, JavaScript, React, SCSS and CSS. Ryan can be found professionally at X-Team, where he works as a lead software engineer.