diff --git a/.ng-dev/commit-message.mts b/.ng-dev/commit-message.mts
index 56b1531b30e9..e67662f70f6a 100644
--- a/.ng-dev/commit-message.mts
+++ b/.ng-dev/commit-message.mts
@@ -13,6 +13,7 @@ export const commitMessage: CommitMessageConfig = {
'cdk-experimental/combobox',
'cdk-experimental/listbox',
'cdk-experimental/popover-edit',
+ 'cdk-experimental/radio',
'cdk-experimental/scrolling',
'cdk-experimental/selection',
'cdk-experimental/table-scroll-container',
diff --git a/src/cdk-experimental/config.bzl b/src/cdk-experimental/config.bzl
index 89dd51af4665..9e3cac5be0eb 100644
--- a/src/cdk-experimental/config.bzl
+++ b/src/cdk-experimental/config.bzl
@@ -5,6 +5,7 @@ CDK_EXPERIMENTAL_ENTRYPOINTS = [
"deferred-content",
"listbox",
"popover-edit",
+ "radio",
"scrolling",
"selection",
"tabs",
diff --git a/src/cdk-experimental/radio/BUILD.bazel b/src/cdk-experimental/radio/BUILD.bazel
new file mode 100644
index 000000000000..f9b2a2571ab0
--- /dev/null
+++ b/src/cdk-experimental/radio/BUILD.bazel
@@ -0,0 +1,17 @@
+load("//tools:defaults.bzl", "ng_project")
+
+package(default_visibility = ["//visibility:public"])
+
+ng_project(
+ name = "radio",
+ srcs = glob(
+ ["**/*.ts"],
+ exclude = ["**/*.spec.ts"],
+ ),
+ deps = [
+ "//:node_modules/@angular/core",
+ "//src/cdk-experimental/ui-patterns",
+ "//src/cdk/a11y",
+ "//src/cdk/bidi",
+ ],
+)
diff --git a/src/cdk-experimental/radio/index.ts b/src/cdk-experimental/radio/index.ts
new file mode 100644
index 000000000000..52b3c7a5156f
--- /dev/null
+++ b/src/cdk-experimental/radio/index.ts
@@ -0,0 +1,9 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.dev/license
+ */
+
+export * from './public-api';
diff --git a/src/cdk-experimental/radio/public-api.ts b/src/cdk-experimental/radio/public-api.ts
new file mode 100644
index 000000000000..0fa6cc894d73
--- /dev/null
+++ b/src/cdk-experimental/radio/public-api.ts
@@ -0,0 +1,9 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.dev/license
+ */
+
+export {CdkRadioGroup, CdkRadioButton} from './radio';
diff --git a/src/cdk-experimental/radio/radio.ts b/src/cdk-experimental/radio/radio.ts
new file mode 100644
index 000000000000..063cefd4f7f9
--- /dev/null
+++ b/src/cdk-experimental/radio/radio.ts
@@ -0,0 +1,173 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.dev/license
+ */
+
+import {
+ AfterViewInit,
+ booleanAttribute,
+ computed,
+ contentChildren,
+ Directive,
+ effect,
+ ElementRef,
+ inject,
+ input,
+ model,
+ signal,
+} from '@angular/core';
+import {RadioButtonPattern, RadioGroupPattern} from '../ui-patterns';
+import {Directionality} from '@angular/cdk/bidi';
+import {toSignal} from '@angular/core/rxjs-interop';
+import {_IdGenerator} from '@angular/cdk/a11y';
+
+/**
+ * A radio button group container.
+ *
+ * Radio groups are used to group multiple radio buttons or radio group labels so they function as
+ * a single form control. The CdkRadioGroup is meant to be used in conjunction with CdkRadioButton
+ * as follows:
+ *
+ * ```html
+ *
+ *
+ *
+ *
+ *
+ * ```
+ */
+@Directive({
+ selector: '[cdkRadioGroup]',
+ exportAs: 'cdkRadioGroup',
+ host: {
+ 'role': 'radiogroup',
+ 'class': 'cdk-radio-group',
+ '[attr.tabindex]': 'pattern.tabindex()',
+ '[attr.aria-readonly]': 'pattern.readonly()',
+ '[attr.aria-disabled]': 'pattern.disabled()',
+ '[attr.aria-orientation]': 'pattern.orientation()',
+ '[attr.aria-activedescendant]': 'pattern.activedescendant()',
+ '(keydown)': 'pattern.onKeydown($event)',
+ '(pointerdown)': 'pattern.onPointerdown($event)',
+ '(focusin)': 'onFocus()',
+ },
+})
+export class CdkRadioGroup implements AfterViewInit {
+ /** The directionality (LTR / RTL) context for the application (or a subtree of it). */
+ private readonly _directionality = inject(Directionality);
+
+ /** The CdkRadioButtons nested inside of the CdkRadioGroup. */
+ private readonly _cdkRadioButtons = contentChildren(CdkRadioButton, {descendants: true});
+
+ /** A signal wrapper for directionality. */
+ protected textDirection = toSignal(this._directionality.change, {
+ initialValue: this._directionality.value,
+ });
+
+ /** The RadioButton UIPatterns of the child CdkRadioButtons. */
+ protected items = computed(() => this._cdkRadioButtons().map(radio => radio.pattern));
+
+ /** Whether the radio group is vertically or horizontally oriented. */
+ orientation = input<'vertical' | 'horizontal'>('vertical');
+
+ /** Whether focus should wrap when navigating. */
+ wrap = input(false, {transform: booleanAttribute}); // Radio groups typically don't wrap
+
+ /** Whether disabled items in the group should be skipped when navigating. */
+ skipDisabled = input(true, {transform: booleanAttribute});
+
+ /** The focus strategy used by the radio group. */
+ focusMode = input<'roving' | 'activedescendant'>('roving');
+
+ /** Whether the radio group is disabled. */
+ disabled = input(false, {transform: booleanAttribute});
+
+ /** Whether the radio group is readonly. */
+ readonly = input(false, {transform: booleanAttribute});
+
+ /** The value of the currently selected radio button. */
+ value = model([]); // TODO: Change this to just be model(V|null).
+
+ /** The current index that has been navigated to. */
+ activeIndex = model(0);
+
+ /** The RadioGroup UIPattern. */
+ pattern: RadioGroupPattern = new RadioGroupPattern({
+ ...this,
+ items: this.items,
+ textDirection: this.textDirection,
+ });
+
+ /** Whether the radio group has received focus yet. */
+ private _hasFocused = signal(false);
+
+ /** Whether the radio buttons in the group have been initialized. */
+ private _isViewInitialized = signal(false);
+
+ constructor() {
+ effect(() => {
+ if (this._isViewInitialized() && !this._hasFocused()) {
+ this.pattern.setDefaultState();
+ }
+ });
+ }
+
+ ngAfterViewInit() {
+ this._isViewInitialized.set(true);
+ }
+
+ onFocus() {
+ this._hasFocused.set(true);
+ }
+}
+
+/** A selectable radio button in a CdkRadioGroup. */
+@Directive({
+ selector: '[cdkRadioButton]',
+ exportAs: 'cdkRadioButton',
+ host: {
+ 'role': 'radio',
+ 'class': 'cdk-radio-button',
+ '[class.cdk-active]': 'pattern.active()',
+ '[attr.tabindex]': 'pattern.tabindex()',
+ '[attr.aria-checked]': 'pattern.selected()',
+ '[attr.aria-disabled]': 'pattern.disabled()',
+ },
+})
+export class CdkRadioButton {
+ /** A reference to the radio button element. */
+ private readonly _elementRef = inject(ElementRef);
+
+ /** The parent CdkRadioGroup. */
+ private readonly _cdkRadioGroup = inject(CdkRadioGroup);
+
+ /** A unique identifier for the radio button. */
+ private readonly _generatedId = inject(_IdGenerator).getId('cdk-radio-button-');
+
+ /** A unique identifier for the radio button. */
+ protected id = computed(() => this._generatedId);
+
+ /** The value associated with the radio button. */
+ protected value = input.required();
+
+ /** The parent RadioGroup UIPattern. */
+ protected group = computed(() => this._cdkRadioGroup.pattern);
+
+ /** A reference to the radio button element to be focused on navigation. */
+ protected element = computed(() => this._elementRef.nativeElement);
+
+ /** Whether the radio button is disabled. */
+ disabled = input(false, {transform: booleanAttribute});
+
+ /** The RadioButton UIPattern. */
+ pattern = new RadioButtonPattern({
+ ...this,
+ id: this.id,
+ value: this.value,
+ group: this.group,
+ element: this.element,
+ });
+}
diff --git a/src/cdk-experimental/ui-patterns/BUILD.bazel b/src/cdk-experimental/ui-patterns/BUILD.bazel
index fee524d85d42..4b8299b571ee 100644
--- a/src/cdk-experimental/ui-patterns/BUILD.bazel
+++ b/src/cdk-experimental/ui-patterns/BUILD.bazel
@@ -12,6 +12,7 @@ ts_project(
"//:node_modules/@angular/core",
"//src/cdk-experimental/ui-patterns/behaviors/signal-like",
"//src/cdk-experimental/ui-patterns/listbox",
+ "//src/cdk-experimental/ui-patterns/radio",
"//src/cdk-experimental/ui-patterns/tabs",
],
)
diff --git a/src/cdk-experimental/ui-patterns/public-api.ts b/src/cdk-experimental/ui-patterns/public-api.ts
index 9b11949ed77c..06383ea9b5bf 100644
--- a/src/cdk-experimental/ui-patterns/public-api.ts
+++ b/src/cdk-experimental/ui-patterns/public-api.ts
@@ -8,5 +8,7 @@
export * from './listbox/listbox';
export * from './listbox/option';
+export * from './radio/radio-group';
+export * from './radio/radio';
export * from './behaviors/signal-like/signal-like';
export * from './tabs/tabs';
diff --git a/src/cdk-experimental/ui-patterns/radio/BUILD.bazel b/src/cdk-experimental/ui-patterns/radio/BUILD.bazel
new file mode 100644
index 000000000000..8efc2eaae9fe
--- /dev/null
+++ b/src/cdk-experimental/ui-patterns/radio/BUILD.bazel
@@ -0,0 +1,19 @@
+load("//tools:defaults.bzl", "ts_project")
+
+package(default_visibility = ["//visibility:public"])
+
+ts_project(
+ name = "radio",
+ srcs = glob(
+ ["**/*.ts"],
+ exclude = ["**/*.spec.ts"],
+ ),
+ deps = [
+ "//:node_modules/@angular/core",
+ "//src/cdk-experimental/ui-patterns/behaviors/event-manager",
+ "//src/cdk-experimental/ui-patterns/behaviors/list-focus",
+ "//src/cdk-experimental/ui-patterns/behaviors/list-navigation",
+ "//src/cdk-experimental/ui-patterns/behaviors/list-selection",
+ "//src/cdk-experimental/ui-patterns/behaviors/signal-like",
+ ],
+)
diff --git a/src/cdk-experimental/ui-patterns/radio/radio-group.ts b/src/cdk-experimental/ui-patterns/radio/radio-group.ts
new file mode 100644
index 000000000000..5d1332165fe9
--- /dev/null
+++ b/src/cdk-experimental/ui-patterns/radio/radio-group.ts
@@ -0,0 +1,212 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.dev/license
+ */
+
+import {computed, signal} from '@angular/core';
+import {KeyboardEventManager} from '../behaviors/event-manager/keyboard-event-manager';
+import {PointerEventManager} from '../behaviors/event-manager/pointer-event-manager';
+import {ListFocus, ListFocusInputs} from '../behaviors/list-focus/list-focus';
+import {ListNavigation, ListNavigationInputs} from '../behaviors/list-navigation/list-navigation';
+import {ListSelection, ListSelectionInputs} from '../behaviors/list-selection/list-selection';
+import {SignalLike} from '../behaviors/signal-like/signal-like';
+import {RadioButtonPattern} from './radio';
+
+/** The selection operations that the radio group can perform. */
+interface SelectOptions {
+ selectOne?: boolean;
+}
+
+/** Represents the required inputs for a radio group. */
+export type RadioGroupInputs = ListNavigationInputs> &
+ // Radio groups are always single-select.
+ Omit, V>, 'multi' | 'selectionMode'> &
+ ListFocusInputs> & {
+ /** Whether the radio group is disabled. */
+ disabled: SignalLike;
+ /** Whether the radio group is readonly. */
+ readonly: SignalLike;
+ };
+
+/** Controls the state of a radio group. */
+export class RadioGroupPattern {
+ /** Controls navigation for the radio group. */
+ navigation: ListNavigation>;
+
+ /** Controls selection for the radio group. */
+ selection: ListSelection, V>;
+
+ /** Controls focus for the radio group. */
+ focusManager: ListFocus>;
+
+ /** Whether the radio group is vertically or horizontally oriented. */
+ orientation: SignalLike<'vertical' | 'horizontal'>;
+
+ /** Whether the radio group is disabled. */
+ disabled = computed(() => this.inputs.disabled() || this.focusManager.isListDisabled());
+
+ /** Whether the radio group is readonly. */
+ readonly: SignalLike;
+
+ /** The tabindex of the radio group (if using activedescendant). */
+ tabindex = computed(() => this.focusManager.getListTabindex());
+
+ /** The id of the current active radio button (if using activedescendant). */
+ activedescendant = computed(() => this.focusManager.getActiveDescendant());
+
+ /** The key used to navigate to the previous radio button. */
+ prevKey = computed(() => {
+ if (this.inputs.orientation() === 'vertical') {
+ return 'ArrowUp';
+ }
+ return this.inputs.textDirection() === 'rtl' ? 'ArrowRight' : 'ArrowLeft';
+ });
+
+ /** The key used to navigate to the next radio button. */
+ nextKey = computed(() => {
+ if (this.inputs.orientation() === 'vertical') {
+ return 'ArrowDown';
+ }
+ return this.inputs.textDirection() === 'rtl' ? 'ArrowLeft' : 'ArrowRight';
+ });
+
+ /** The keydown event manager for the radio group. */
+ keydown = computed(() => {
+ const manager = new KeyboardEventManager();
+
+ // Readonly mode allows navigation but not selection changes.
+ if (this.readonly()) {
+ return manager
+ .on(this.prevKey, () => this.prev())
+ .on(this.nextKey, () => this.next())
+ .on('Home', () => this.first())
+ .on('End', () => this.last());
+ }
+
+ // Default behavior: navigate and select on arrow keys, home, end.
+ // Space/Enter also select the focused item.
+ return manager
+ .on(this.prevKey, () => this.prev({selectOne: true}))
+ .on(this.nextKey, () => this.next({selectOne: true}))
+ .on('Home', () => this.first({selectOne: true}))
+ .on('End', () => this.last({selectOne: true}))
+ .on(' ', () => this.selection.selectOne())
+ .on('Enter', () => this.selection.selectOne());
+ });
+
+ /** The pointerdown event manager for the radio group. */
+ pointerdown = computed(() => {
+ const manager = new PointerEventManager();
+
+ if (this.readonly()) {
+ // Navigate focus only in readonly mode.
+ return manager.on(e => this.goto(e));
+ }
+
+ // Default behavior: navigate and select on click.
+ return manager.on(e => this.goto(e, {selectOne: true}));
+ });
+
+ constructor(readonly inputs: RadioGroupInputs) {
+ this.readonly = inputs.readonly;
+ this.orientation = inputs.orientation;
+
+ this.focusManager = new ListFocus(inputs);
+ this.navigation = new ListNavigation({...inputs, focusManager: this.focusManager});
+ this.selection = new ListSelection({
+ ...inputs,
+ // Radio groups are always single-select and selection follows focus.
+ multi: signal(false),
+ selectionMode: signal('follow'),
+ focusManager: this.focusManager,
+ });
+ }
+
+ /** Handles keydown events for the radio group. */
+ onKeydown(event: KeyboardEvent) {
+ if (!this.disabled()) {
+ this.keydown().handle(event);
+ }
+ }
+
+ /** Handles pointerdown events for the radio group. */
+ onPointerdown(event: PointerEvent) {
+ if (!this.disabled()) {
+ this.pointerdown().handle(event);
+ }
+ }
+
+ /** Navigates to the first enabled radio button in the group. */
+ first(opts?: SelectOptions) {
+ this._navigate(opts, () => this.navigation.first());
+ }
+
+ /** Navigates to the last enabled radio button in the group. */
+ last(opts?: SelectOptions) {
+ this._navigate(opts, () => this.navigation.last());
+ }
+
+ /** Navigates to the next enabled radio button in the group. */
+ next(opts?: SelectOptions) {
+ this._navigate(opts, () => this.navigation.next());
+ }
+
+ /** Navigates to the previous enabled radio button in the group. */
+ prev(opts?: SelectOptions) {
+ this._navigate(opts, () => this.navigation.prev());
+ }
+
+ /** Navigates to the radio button associated with the given pointer event. */
+ goto(event: PointerEvent, opts?: SelectOptions) {
+ const item = this._getItem(event);
+ this._navigate(opts, () => this.navigation.goto(item));
+ }
+
+ /**
+ * Sets the radio group to its default initial state.
+ *
+ * Sets the active index to the selected radio button if one exists and is focusable.
+ * Otherwise, sets the active index to the first focusable radio button.
+ */
+ setDefaultState() {
+ let firstItem: RadioButtonPattern | null = null;
+
+ for (const item of this.inputs.items()) {
+ if (this.focusManager.isFocusable(item)) {
+ if (!firstItem) {
+ firstItem = item;
+ }
+ if (item.selected()) {
+ this.inputs.activeIndex.set(item.index());
+ return;
+ }
+ }
+ }
+
+ if (firstItem) {
+ this.inputs.activeIndex.set(firstItem.index());
+ }
+ }
+
+ /** Safely performs a navigation operation and updates selection if needed. */
+ private _navigate(opts: SelectOptions = {}, operation: () => boolean) {
+ const moved = operation();
+ if (moved && opts.selectOne) {
+ this.selection.selectOne();
+ }
+ }
+
+ /** Finds the RadioButtonPattern associated with a pointer event target. */
+ private _getItem(e: PointerEvent): RadioButtonPattern | undefined {
+ if (!(e.target instanceof HTMLElement)) {
+ return undefined;
+ }
+
+ // Assumes the target or its ancestor has role="radio"
+ const element = e.target.closest('[role="radio"]');
+ return this.inputs.items().find(i => i.element() === element);
+ }
+}
diff --git a/src/cdk-experimental/ui-patterns/radio/radio.ts b/src/cdk-experimental/ui-patterns/radio/radio.ts
new file mode 100644
index 000000000000..f5eafadcf515
--- /dev/null
+++ b/src/cdk-experimental/ui-patterns/radio/radio.ts
@@ -0,0 +1,75 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.dev/license
+ */
+
+import {computed} from '@angular/core';
+import {ListSelection, ListSelectionItem} from '../behaviors/list-selection/list-selection';
+import {ListNavigation, ListNavigationItem} from '../behaviors/list-navigation/list-navigation';
+import {ListFocus, ListFocusItem} from '../behaviors/list-focus/list-focus';
+import {SignalLike} from '../behaviors/signal-like/signal-like';
+
+/**
+ * Represents the properties exposed by a radio group that need to be accessed by a radio button.
+ * This exists to avoid circular dependency errors between the radio group and radio button.
+ */
+interface RadioGroupLike {
+ focusManager: ListFocus>;
+ selection: ListSelection, V>;
+ navigation: ListNavigation>;
+}
+
+/** Represents the required inputs for a radio button in a radio group. */
+export interface RadioButtonInputs
+ extends ListNavigationItem,
+ ListSelectionItem,
+ ListFocusItem {
+ /** A reference to the parent radio group. */
+ group: SignalLike | undefined>;
+}
+
+/** Represents a radio button within a radio group. */
+export class RadioButtonPattern {
+ /** A unique identifier for the radio button. */
+ id: SignalLike;
+
+ /** The value associated with the radio button. */
+ value: SignalLike;
+
+ /** The position of the radio button within the group. */
+ index = computed(
+ () =>
+ this.group()
+ ?.navigation.inputs.items()
+ .findIndex(i => i.id() === this.id()) ?? -1,
+ );
+
+ /** Whether the radio button is currently the active one (focused). */
+ active = computed(() => this.group()?.focusManager.activeItem() === this);
+
+ /** Whether the radio button is selected. */
+ selected = computed(() => this.group()?.selection.inputs.value().includes(this.value()));
+
+ /** Whether the radio button is disabled. */
+ disabled: SignalLike;
+
+ /** A reference to the parent radio group. */
+ group: SignalLike | undefined>;
+
+ /** The tabindex of the radio button. */
+ tabindex = computed(() => this.group()?.focusManager.getItemTabindex(this));
+
+ /** The HTML element associated with the radio button. */
+ element: SignalLike;
+
+ constructor(inputs: RadioButtonInputs) {
+ this.id = inputs.id;
+ this.value = inputs.value;
+ this.group = inputs.group;
+ this.element = inputs.element;
+ this.disabled = inputs.disabled;
+ }
+}
diff --git a/src/components-examples/cdk-experimental/radio/BUILD.bazel b/src/components-examples/cdk-experimental/radio/BUILD.bazel
new file mode 100644
index 000000000000..f33b43cf96d0
--- /dev/null
+++ b/src/components-examples/cdk-experimental/radio/BUILD.bazel
@@ -0,0 +1,29 @@
+load("//tools:defaults.bzl", "ng_project")
+
+package(default_visibility = ["//visibility:public"])
+
+ng_project(
+ name = "radio",
+ srcs = glob(["**/*.ts"]),
+ assets = glob([
+ "**/*.html",
+ "**/*.css",
+ ]),
+ deps = [
+ "//:node_modules/@angular/core",
+ "//:node_modules/@angular/forms",
+ "//src/cdk-experimental/radio",
+ "//src/material/checkbox",
+ "//src/material/form-field",
+ "//src/material/select",
+ ],
+)
+
+filegroup(
+ name = "source-files",
+ srcs = glob([
+ "**/*.html",
+ "**/*.css",
+ "**/*.ts",
+ ]),
+)
diff --git a/src/components-examples/cdk-experimental/radio/cdk-radio/cdk-radio-example.css b/src/components-examples/cdk-experimental/radio/cdk-radio/cdk-radio-example.css
new file mode 100644
index 000000000000..37f40b58c271
--- /dev/null
+++ b/src/components-examples/cdk-experimental/radio/cdk-radio/cdk-radio-example.css
@@ -0,0 +1,86 @@
+.example-radio-controls {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+ padding-bottom: 16px;
+}
+
+.example-radio-group {
+ gap: 4px;
+ margin: 0;
+ padding: 8px;
+ max-height: 50vh;
+ border: 1px solid var(--mat-sys-outline);
+ border-radius: var(--mat-sys-corner-extra-small);
+ display: flex;
+ list-style: none;
+ flex-direction: column;
+ overflow: scroll;
+ user-select: none;
+}
+
+.example-radio-group[aria-orientation='horizontal'] {
+ flex-direction: row;
+}
+
+.example-radio-group[aria-disabled='true'] {
+ background-color: var(--mat-sys-surface-dim);
+ pointer-events: none;
+}
+
+.example-radio-button {
+ gap: 16px;
+ padding: 16px;
+ display: flex;
+ cursor: pointer;
+ position: relative;
+ align-items: center;
+ border-radius: var(--mat-sys-corner-extra-small);
+}
+
+/* Basic visual indicator for the radio button */
+.example-radio-indicator {
+ width: 16px;
+ height: 16px;
+ border-radius: 50%;
+ border: 2px solid var(--mat-sys-outline);
+ display: inline-block;
+ position: relative;
+}
+
+.example-radio-button[aria-checked='true'] .example-radio-indicator::after {
+ content: '';
+ display: block;
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ background-color: var(--mat-sys-primary);
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+}
+
+.example-radio-button.cdk-active,
+.example-radio-button[aria-disabled='false']:hover {
+ outline: 1px solid var(--mat-sys-outline);
+ background: var(--mat-sys-surface-container);
+}
+
+.example-radio-button[aria-disabled='false']:focus-within {
+ outline: 2px solid var(--mat-sys-primary);
+ background: var(--mat-sys-surface-container);
+}
+
+.example-radio-button.cdk-active[aria-disabled='true'],
+.example-radio-button[aria-disabled='true']:focus-within {
+ outline: 2px solid var(--mat-sys-outline);
+}
+
+.example-radio-button[aria-disabled='true'] {
+ cursor: default;
+}
+
+.example-radio-button[aria-disabled='true'] span:not(.example-radio-indicator) {
+ opacity: 0.3;
+}
diff --git a/src/components-examples/cdk-experimental/radio/cdk-radio/cdk-radio-example.html b/src/components-examples/cdk-experimental/radio/cdk-radio/cdk-radio-example.html
new file mode 100644
index 000000000000..abdf7197c620
--- /dev/null
+++ b/src/components-examples/cdk-experimental/radio/cdk-radio/cdk-radio-example.html
@@ -0,0 +1,38 @@
+