diff --git a/docs/manifest.json b/docs/manifest.json
index 75b5514dca1e96..537f6ea671988d 100644
--- a/docs/manifest.json
+++ b/docs/manifest.json
@@ -929,6 +929,12 @@
"markdown_source": "../packages/components/src/radio-control/README.md",
"parent": "components"
},
+ {
+ "title": "RadioGroup",
+ "slug": "radio-group",
+ "markdown_source": "../packages/components/src/radio-group/README.md",
+ "parent": "components"
+ },
{
"title": "RangeControl",
"slug": "range-control",
diff --git a/packages/components/src/button-group/index.js b/packages/components/src/button-group/index.js
index 6f4fb672c8c6ef..2ef2b5e94c6018 100644
--- a/packages/components/src/button-group/index.js
+++ b/packages/components/src/button-group/index.js
@@ -3,10 +3,15 @@
*/
import classnames from 'classnames';
-function ButtonGroup( { className, ...props } ) {
+/**
+ * WordPress dependencies
+ */
+import { forwardRef } from '@wordpress/element';
+
+function ButtonGroup( { className, ...props }, ref ) {
const classes = classnames( 'components-button-group', className );
- return
;
+ return ;
}
-export default ButtonGroup;
+export default forwardRef( ButtonGroup );
diff --git a/packages/components/src/date-time/test/__snapshots__/time.js.snap b/packages/components/src/date-time/test/__snapshots__/time.js.snap
index e27e600f089f22..b65d4fddd151c0 100644
--- a/packages/components/src/date-time/test/__snapshots__/time.js.snap
+++ b/packages/components/src/date-time/test/__snapshots__/time.js.snap
@@ -317,7 +317,7 @@ exports[`TimePicker matches the snapshot when the is12hour prop is specified 1`]
value="00"
/>
-
PM
-
+
@@ -498,7 +498,7 @@ exports[`TimePicker matches the snapshot when the is12hour prop is true 1`] = `
value="00"
/>
-
PM
-
+
diff --git a/packages/components/src/index.js b/packages/components/src/index.js
index 75ddd43d7daf71..f5142afb2f86e0 100644
--- a/packages/components/src/index.js
+++ b/packages/components/src/index.js
@@ -71,6 +71,8 @@ export { default as PanelRow } from './panel/row';
export { default as Placeholder } from './placeholder';
export { default as Popover } from './popover';
export { default as QueryControls } from './query-controls';
+export { default as __experimentalRadio } from './radio';
+export { default as __experimentalRadioGroup } from './radio-group';
export { default as RadioControl } from './radio-control';
export { default as RangeControl } from './range-control';
export { default as ResizableBox } from './resizable-box';
diff --git a/packages/components/src/radio-context/index.js b/packages/components/src/radio-context/index.js
new file mode 100644
index 00000000000000..58a7783dcb84e0
--- /dev/null
+++ b/packages/components/src/radio-context/index.js
@@ -0,0 +1,11 @@
+/**
+ * WordPress dependencies
+ */
+import { createContext } from '@wordpress/element';
+
+const RadioContext = createContext( {
+ state: null,
+ setState: () => {},
+} );
+
+export default RadioContext;
diff --git a/packages/components/src/radio-control/README.md b/packages/components/src/radio-control/README.md
index 2a55a81718fb15..f08f7da87b4953 100644
--- a/packages/components/src/radio-control/README.md
+++ b/packages/components/src/radio-control/README.md
@@ -121,3 +121,4 @@ A function that receives the value of the new option that is being selected as i
* To select one or more items from a set, use the `CheckboxControl` component.
* To toggle a single setting on or off, use the `ToggleControl` component.
+* To format as a button group, use the `RadioGroup` component.
diff --git a/packages/components/src/radio-group/README.md b/packages/components/src/radio-group/README.md
new file mode 100644
index 00000000000000..7c6eab29b0bf1e
--- /dev/null
+++ b/packages/components/src/radio-group/README.md
@@ -0,0 +1,87 @@
+# RadioGroup
+
+Use a RadioGroup component when you want users to select one option from a small set of options.
+
+![RadioGroup component](https://wordpress.org/gutenberg/files/2018/12/s_96EC471FE9C9D91A996770229947AAB54A03351BDE98F444FD3C1BF0CED365EA_1541792995815_ButtonGroup.png)
+
+## Table of contents
+
+1. [Design guidelines](#design-guidelines)
+2. [Development guidelines](#development-guidelines)
+3. [Related components](#related-components)
+
+## Design guidelines
+
+### Usage
+
+#### Selected action
+
+Only one option in a radio group can be selected and active at a time. Selecting one option deselects any other.
+
+### Best practices
+
+Radio groups should:
+
+- **Be clearly and accurately labeled.**
+- **Clearly communicate that clicking or tapping will trigger an action.**
+- **Use established colors appropriately.** For example, only use red buttons for actions that are difficult or impossible to undo.
+- **Have consistent locations in the interface.**
+- **Have a default option already selected.**
+
+### States
+
+#### Active and available radio groups
+
+A radio group’s state makes it clear which option is active. Hover and focus states express the available selection options for buttons in a button group.
+
+#### Disabled radio groups
+
+Radio groups that cannot be selected can either be given a disabled state, or be hidden.
+
+## Development guidelines
+
+### Usage
+
+#### Controlled
+
+```jsx
+import { Radio, RadioGroup } from '@wordpress/components';
+import { useState } from '@wordpress/element';
+
+const MyControlledRadioRadioGroup = () => {
+ const [ checked, setChecked ] = useState( '25' );
+ return (
+
+ 25%
+ 50%
+ 75%
+ 100%
+
+ );
+};
+```
+
+#### Uncontrolled
+
+When using the RadioGroup component as an uncontrolled component, the default value can be set with the `defaultChecked` prop.
+
+```jsx
+import { Radio, RadioGroup } from '@wordpress/components';
+import { useState } from '@wordpress/element';
+
+const MyUncontrolledRadioRadioGroup = () => {
+ return (
+
+ 25%
+ 50%
+ 75%
+ 100%
+
+ );
+};
+```
+
+## Related components
+
+- For simple buttons that are related, use a `ButtonGroup` component.
+- For traditional radio options, use a `RadioControl` component.
diff --git a/packages/components/src/radio-group/index.js b/packages/components/src/radio-group/index.js
new file mode 100644
index 00000000000000..11f24605297923
--- /dev/null
+++ b/packages/components/src/radio-group/index.js
@@ -0,0 +1,53 @@
+/**
+ * External dependencies
+ */
+import { useRadioState, RadioGroup as ReakitRadioGroup } from 'reakit/Radio';
+
+/**
+ * WordPress dependencies
+ */
+import { forwardRef } from '@wordpress/element';
+
+/**
+ * Internal dependencies
+ */
+import ButtonGroup from '../button-group';
+import RadioContext from '../radio-context';
+
+function RadioGroup(
+ {
+ accessibilityLabel,
+ checked,
+ defaultChecked,
+ disabled,
+ onChange,
+ ...props
+ },
+ ref
+) {
+ const radioState = useRadioState( {
+ state: defaultChecked,
+ baseId: props.id,
+ } );
+ const radioContext = {
+ ...radioState,
+ disabled,
+ // controlled or uncontrolled
+ state: checked || radioState.state,
+ setState: onChange || radioState.setState,
+ };
+
+ return (
+
+
+
+ );
+}
+
+export default forwardRef( RadioGroup );
diff --git a/packages/components/src/radio-group/stories/index.js b/packages/components/src/radio-group/stories/index.js
new file mode 100644
index 00000000000000..5844cd82016b57
--- /dev/null
+++ b/packages/components/src/radio-group/stories/index.js
@@ -0,0 +1,71 @@
+/**
+ * WordPress dependencies
+ */
+import { useState } from '@wordpress/element';
+
+/**
+ * Internal dependencies
+ */
+import Radio from '../../radio';
+import RadioGroup from '../';
+
+export default { title: 'Components/RadioGroup', component: RadioGroup };
+
+export const _default = () => {
+ /* eslint-disable no-restricted-syntax */
+ return (
+
+ Option 1
+ Option 2
+ Option 3
+
+ );
+ /* eslint-enable no-restricted-syntax */
+};
+
+export const disabled = () => {
+ /* eslint-disable no-restricted-syntax */
+ return (
+
+ Option 1
+ Option 2
+ Option 3
+
+ );
+ /* eslint-enable no-restricted-syntax */
+};
+
+const ControlledRadioGroupWithState = () => {
+ const [ checked, setChecked ] = useState( 'option2' );
+
+ /* eslint-disable no-restricted-syntax */
+ return (
+
+ Option 1
+ Option 2
+ Option 3
+
+ );
+ /* eslint-enable no-restricted-syntax */
+};
+
+export const controlled = () => {
+ return ;
+};
diff --git a/packages/components/src/radio/index.js b/packages/components/src/radio/index.js
new file mode 100644
index 00000000000000..6d7305002e3fce
--- /dev/null
+++ b/packages/components/src/radio/index.js
@@ -0,0 +1,36 @@
+/**
+ * External dependencies
+ */
+import { Radio as ReakitRadio } from 'reakit/Radio';
+
+/**
+ * WordPress dependencies
+ */
+import { useContext, forwardRef } from '@wordpress/element';
+
+/**
+ * Internal dependencies
+ */
+import Button from '../button';
+import RadioContext from '../radio-context';
+
+function Radio( { children, value, ...props }, ref ) {
+ const radioContext = useContext( RadioContext );
+ const checked = radioContext.state === value;
+
+ return (
+
+ { children || value }
+
+ );
+}
+
+export default forwardRef( Radio );
diff --git a/packages/components/src/radio/stories/index.js b/packages/components/src/radio/stories/index.js
new file mode 100644
index 00000000000000..b64c35ed7ba17f
--- /dev/null
+++ b/packages/components/src/radio/stories/index.js
@@ -0,0 +1,20 @@
+/**
+ * Internal dependencies
+ */
+import RadioGroup from '../../radio-group';
+import Radio from '../';
+
+export default { title: 'Components/Radio', component: Radio };
+
+export const _default = () => {
+ // Radio components must be a descendent of a RadioGroup component.
+ /* eslint-disable no-restricted-syntax */
+ return (
+ // id is required for server side rendering
+
+ Option 1
+ Option 2
+
+ );
+ /* eslint-enable no-restricted-syntax */
+};
diff --git a/storybook/test/__snapshots__/index.js.snap b/storybook/test/__snapshots__/index.js.snap
index 29079fd8792325..321b96d00f17af 100644
--- a/storybook/test/__snapshots__/index.js.snap
+++ b/storybook/test/__snapshots__/index.js.snap
@@ -4369,6 +4369,55 @@ exports[`Storyshots Components/Popover Positioning 1`] = `
`;
+exports[`Storyshots Components/Radio Default 1`] = `
+
+
+
+
+`;
+
exports[`Storyshots Components/RadioControl Default 1`] = `
`;
+exports[`Storyshots Components/RadioGroup Controlled 1`] = `
+
+
+
+
+
+`;
+
+exports[`Storyshots Components/RadioGroup Default 1`] = `
+
+
+
+
+
+`;
+
+exports[`Storyshots Components/RadioGroup Disabled 1`] = `
+
+
+
+
+
+`;
+
exports[`Storyshots Components/RangeControl Custom Marks 1`] = `
.emotion-36 {
-webkit-tap-highlight-color: transparent;