diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md
index 5d05abc7026b22..f43697b07e0c73 100644
--- a/packages/components/CHANGELOG.md
+++ b/packages/components/CHANGELOG.md
@@ -6,6 +6,7 @@
- Migrate `Divider` from `reakit` to `ariakit` ([#55622](https://github.com/WordPress/gutenberg/pull/55622))
- Migrate `DisclosureContent` from `reakit` to `ariakit` and TypeScript ([#55639](https://github.com/WordPress/gutenberg/pull/55639))
+- Migrate `RadioGroup` from `reakit` to `ariakit` and TypeScript ([#55580](https://github.com/WordPress/gutenberg/pull/55580))
### Experimental
diff --git a/packages/components/src/radio-group/context.tsx b/packages/components/src/radio-group/context.tsx
new file mode 100644
index 00000000000000..c2fa1b009b4e1f
--- /dev/null
+++ b/packages/components/src/radio-group/context.tsx
@@ -0,0 +1,18 @@
+/**
+ * External dependencies
+ */
+// eslint-disable-next-line no-restricted-imports
+import type * as Ariakit from '@ariakit/react';
+
+/**
+ * WordPress dependencies
+ */
+import { createContext } from '@wordpress/element';
+
+export const RadioGroupContext = createContext< {
+ store?: Ariakit.RadioStore;
+ disabled?: boolean;
+} >( {
+ store: undefined,
+ disabled: undefined,
+} );
diff --git a/packages/components/src/radio-group/index.js b/packages/components/src/radio-group/index.js
deleted file mode 100644
index a94493c5f2fd8c..00000000000000
--- a/packages/components/src/radio-group/index.js
+++ /dev/null
@@ -1,51 +0,0 @@
-// @ts-nocheck
-
-/**
- * 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(
- { label, 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 (
-
-
-
- );
-}
-
-/**
- * @deprecated Use `RadioControl` or `ToggleGroupControl` instead.
- */
-export default forwardRef( RadioGroup );
diff --git a/packages/components/src/radio-group/index.tsx b/packages/components/src/radio-group/index.tsx
new file mode 100644
index 00000000000000..8bc35f8382ee34
--- /dev/null
+++ b/packages/components/src/radio-group/index.tsx
@@ -0,0 +1,65 @@
+/**
+ * External dependencies
+ */
+// eslint-disable-next-line no-restricted-imports
+import * as Ariakit from '@ariakit/react';
+
+/**
+ * WordPress dependencies
+ */
+import { useMemo, forwardRef } from '@wordpress/element';
+
+/**
+ * Internal dependencies
+ */
+import ButtonGroup from '../button-group';
+import type { WordPressComponentProps } from '../context';
+import { RadioGroupContext } from './context';
+import type { RadioGroupProps } from './types';
+
+function UnforwardedRadioGroup(
+ {
+ label,
+ checked,
+ defaultChecked,
+ disabled,
+ onChange,
+ children,
+ ...props
+ }: WordPressComponentProps< RadioGroupProps, 'div', false >,
+ ref: React.ForwardedRef< any >
+) {
+ const radioStore = Ariakit.useRadioStore( {
+ value: checked,
+ defaultValue: defaultChecked,
+ setValue: ( newValue ) => {
+ onChange?.( newValue ?? undefined );
+ },
+ } );
+
+ const contextValue = useMemo(
+ () => ( {
+ store: radioStore,
+ disabled,
+ } ),
+ [ radioStore, disabled ]
+ );
+
+ return (
+
+ { children } }
+ aria-label={ label }
+ ref={ ref }
+ { ...props }
+ />
+
+ );
+}
+
+/**
+ * @deprecated Use `RadioControl` or `ToggleGroupControl` instead.
+ */
+export const RadioGroup = forwardRef( UnforwardedRadioGroup );
+export default RadioGroup;
diff --git a/packages/components/src/radio-group/radio-context/index.js b/packages/components/src/radio-group/radio-context/index.js
deleted file mode 100644
index 58a7783dcb84e0..00000000000000
--- a/packages/components/src/radio-group/radio-context/index.js
+++ /dev/null
@@ -1,11 +0,0 @@
-/**
- * WordPress dependencies
- */
-import { createContext } from '@wordpress/element';
-
-const RadioContext = createContext( {
- state: null,
- setState: () => {},
-} );
-
-export default RadioContext;
diff --git a/packages/components/src/radio-group/radio.tsx b/packages/components/src/radio-group/radio.tsx
new file mode 100644
index 00000000000000..8187b4a4ba573e
--- /dev/null
+++ b/packages/components/src/radio-group/radio.tsx
@@ -0,0 +1,55 @@
+/**
+ * WordPress dependencies
+ */
+import { forwardRef, useContext } from '@wordpress/element';
+
+/**
+ * External dependencies
+ */
+// eslint-disable-next-line no-restricted-imports
+import * as Ariakit from '@ariakit/react';
+
+/**
+ * Internal dependencies
+ */
+import Button from '../button';
+import { RadioGroupContext } from './context';
+import type { WordPressComponentProps } from '../context';
+import type { RadioProps } from './types';
+
+function UnforwardedRadio(
+ {
+ value,
+ children,
+ ...props
+ }: WordPressComponentProps< RadioProps, 'button', false >,
+ ref: React.ForwardedRef< any >
+) {
+ const { store, disabled } = useContext( RadioGroupContext );
+
+ const selectedValue = store?.useState( 'value' );
+ const isChecked = selectedValue !== undefined && selectedValue === value;
+
+ return (
+
+ }
+ >
+ { children || value }
+
+ );
+}
+
+/**
+ * @deprecated Use `RadioControl` or `ToggleGroupControl` instead.
+ */
+export const Radio = forwardRef( UnforwardedRadio );
+export default Radio;
diff --git a/packages/components/src/radio-group/radio/index.js b/packages/components/src/radio-group/radio/index.js
deleted file mode 100644
index 446850c421b4c1..00000000000000
--- a/packages/components/src/radio-group/radio/index.js
+++ /dev/null
@@ -1,40 +0,0 @@
-// @ts-nocheck
-
-/**
- * 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 }
-
- );
-}
-
-/**
- * @deprecated Use `RadioControl` or `ToggleGroupControl` instead.
- */
-export default forwardRef( Radio );
diff --git a/packages/components/src/radio-group/stories/index.story.js b/packages/components/src/radio-group/stories/index.story.js
deleted file mode 100644
index 58125bf808be2b..00000000000000
--- a/packages/components/src/radio-group/stories/index.story.js
+++ /dev/null
@@ -1,83 +0,0 @@
-/**
- * WordPress dependencies
- */
-import { useState } from '@wordpress/element';
-
-/**
- * Internal dependencies
- */
-import Radio from '../radio';
-import RadioGroup from '../';
-
-export default {
- title: 'Components (Deprecated)/RadioGroup',
- component: RadioGroup,
- subcomponents: { Radio },
- parameters: {
- docs: {
- description: {
- component:
- 'This component is deprecated. Use `RadioControl` or `ToggleGroupControl` instead.',
- },
- },
- },
-};
-
-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( 1 );
-
- /* 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-group/stories/index.story.tsx b/packages/components/src/radio-group/stories/index.story.tsx
new file mode 100644
index 00000000000000..cfa0a253fb7a0b
--- /dev/null
+++ b/packages/components/src/radio-group/stories/index.story.tsx
@@ -0,0 +1,90 @@
+/**
+ * External dependencies
+ */
+import type { Meta, StoryFn } from '@storybook/react';
+
+/**
+ * Internal dependencies
+ */
+import { RadioGroup } from '..';
+import { Radio } from '../radio';
+
+/**
+ * WordPress dependencies
+ */
+import { useState } from '@wordpress/element';
+
+const meta: Meta< typeof RadioGroup > = {
+ title: 'Components (Deprecated)/RadioGroup',
+ component: RadioGroup,
+ // @ts-expect-error - See https://github.com/storybookjs/storybook/issues/23170
+ subcomponents: { Radio },
+ argTypes: {
+ onChange: { control: { type: null } },
+ children: { control: { type: null } },
+ checked: { control: { type: 'text' } },
+ },
+ parameters: {
+ actions: { argTypesRegex: '^on.*' },
+ controls: { expanded: true },
+ docs: { canvas: { sourceState: 'shown' } },
+ },
+};
+export default meta;
+
+const Template: StoryFn< typeof RadioGroup > = ( props ) => {
+ return ;
+};
+
+export const Default: StoryFn< typeof RadioGroup > = Template.bind( {} );
+Default.args = {
+ id: 'default-radiogroup',
+ label: 'options',
+ defaultChecked: 'option2',
+ children: (
+ <>
+ Option 1
+ Option 2
+ Option 3
+ >
+ ),
+};
+
+export const Disabled: StoryFn< typeof RadioGroup > = Template.bind( {} );
+Disabled.args = {
+ ...Default.args,
+ id: 'disabled-radiogroup',
+ disabled: true,
+};
+
+const ControlledTemplate: StoryFn< typeof RadioGroup > = ( {
+ checked: checkedProp,
+ onChange: onChangeProp,
+ ...props
+} ) => {
+ const [ checked, setChecked ] =
+ useState< React.ComponentProps< typeof RadioGroup >[ 'checked' ] >(
+ checkedProp
+ );
+
+ const onChange: typeof onChangeProp = ( value ) => {
+ setChecked( value );
+ onChangeProp?.( value );
+ };
+
+ return (
+
+ );
+};
+
+export const Controlled: StoryFn< typeof RadioGroup > = ControlledTemplate.bind(
+ {}
+);
+Controlled.args = {
+ ...Default.args,
+ checked: 'option2',
+ id: 'controlled-radiogroup',
+};
+Controlled.argTypes = {
+ checked: { control: { type: null } },
+};
diff --git a/packages/components/src/radio-group/types.ts b/packages/components/src/radio-group/types.ts
new file mode 100644
index 00000000000000..6422cf4b275f00
--- /dev/null
+++ b/packages/components/src/radio-group/types.ts
@@ -0,0 +1,39 @@
+export type RadioGroupProps = {
+ /**
+ * Accessible label for the radio group
+ */
+ label: string;
+ /**
+ * The `value` of the `Radio` element which should be selected.
+ * Indicates controlled usage of the component.
+ */
+ checked?: string | number;
+ /**
+ * The value of the radio element which is initially selected.
+ */
+ defaultChecked?: string | number;
+ /**
+ * Whether the `RadioGroup` should be disabled.
+ */
+ disabled?: boolean;
+ /**
+ * Called when a `Radio` element has been selected.
+ * Receives the `value` of the selected element as an argument.
+ */
+ onChange?: ( value: string | number | undefined ) => void;
+ /**
+ * The children elements, which should be a series of `Radio` components.
+ */
+ children: React.ReactNode;
+};
+
+export type RadioProps = {
+ /**
+ * The actual value of the radio element.
+ */
+ value: string | number;
+ /**
+ * Content displayed on the Radio element. If there aren't any children, `value` is displayed.
+ */
+ children?: React.ReactNode;
+};