diff --git a/packages/react/src/components/OptionSwatch/OptionSwatch.api.mdx b/packages/react/src/components/OptionSwatch/OptionSwatch.api.mdx
new file mode 100644
index 000000000..bbedebd58
--- /dev/null
+++ b/packages/react/src/components/OptionSwatch/OptionSwatch.api.mdx
@@ -0,0 +1,37 @@
+import { Meta } from '@storybook/addon-docs';
+import LinkTo from '@storybook/addon-links/react';
+import { TableInterface } from '~storybook/components/TableInterface';
+
+
+
+# OptionSwatch API
+
+```js
+import { OptionSwatch } from '@esfront/react';
+```
+
+## Component name
+
+The name `ESOptionSwatch` can be used when providing default props or style overrides in the theme.
+
+## Props
+
+
+
+
+
+## CSS
+
+
+
+
+
+## Demos
+
+
diff --git a/packages/react/src/components/OptionSwatch/OptionSwatch.classes.ts b/packages/react/src/components/OptionSwatch/OptionSwatch.classes.ts
new file mode 100644
index 000000000..a627e7740
--- /dev/null
+++ b/packages/react/src/components/OptionSwatch/OptionSwatch.classes.ts
@@ -0,0 +1,21 @@
+import { generateUtilityClass, generateUtilityClasses } from '@mui/material';
+
+export type OptionSwatchClasses = {
+ /** Class name applied to the root element. */
+ root: string;
+ /** State class applied to the root element if `checked={true}`. */
+ checked: string;
+ /** State class applied to the root element if `disabled={true}`. */
+ disabled: string;
+};
+export type OptionSwatchClassKey = keyof OptionSwatchClasses;
+
+export function getOptionSwatchUtilityClass(slot: string): string {
+ return generateUtilityClass('ESOptionSwatch', slot);
+}
+
+export const optionSwatchClasses: OptionSwatchClasses = generateUtilityClasses('ESOptionSwatch', [
+ 'root',
+ 'checked',
+ 'disabled',
+]);
diff --git a/packages/react/src/components/OptionSwatch/OptionSwatch.stories.tsx b/packages/react/src/components/OptionSwatch/OptionSwatch.stories.tsx
new file mode 100644
index 000000000..c1ca12c7b
--- /dev/null
+++ b/packages/react/src/components/OptionSwatch/OptionSwatch.stories.tsx
@@ -0,0 +1,42 @@
+import { ComponentProps } from 'react';
+
+import { Meta, StoryObj } from '@storybook/react';
+
+import { OptionSwatch } from './OptionSwatch';
+
+type Args = ComponentProps;
+
+const meta: Meta = {
+ tags: ['autodocs'],
+ component: OptionSwatch,
+ parameters: {
+ references: ['OptionSwatch'],
+ },
+ argTypes: {
+ inputProps: {
+ table: {
+ disable: true,
+ },
+ },
+ inputRef: {
+ table: {
+ disable: true,
+ },
+ },
+ value: {
+ table: {
+ disable: true,
+ },
+ },
+ },
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Demo: Story = {
+ render: (args) => {
+ return ;
+ },
+};
diff --git a/packages/react/src/components/OptionSwatch/OptionSwatch.tsx b/packages/react/src/components/OptionSwatch/OptionSwatch.tsx
new file mode 100644
index 000000000..22c8ca455
--- /dev/null
+++ b/packages/react/src/components/OptionSwatch/OptionSwatch.tsx
@@ -0,0 +1,154 @@
+import * as React from 'react';
+
+import { OptionSwatchProps } from './OptionSwatch.types';
+
+import clsx from 'clsx';
+import { getOptionSwatchUtilityClass, optionSwatchClasses } from './OptionSwatch.classes';
+
+import { styled } from '@mui/material/styles';
+import { useDefaultProps } from '@mui/system/DefaultPropsProvider';
+import { createChainedFunction } from '@mui/material';
+import composeClasses from '@mui/utils/composeClasses';
+
+import { ButtonBase } from '../ButtonBase';
+import { useRadioGroup } from '../RadioGroup';
+
+type OptionSwatchOwnerState = {
+ classes?: OptionSwatchProps['classes'];
+ checked?: OptionSwatchProps['checked'];
+ disabled?: OptionSwatchProps['disabled'];
+};
+
+const useUtilityClasses = (ownerState: OptionSwatchOwnerState) => {
+ const { classes, checked, disabled } = ownerState;
+
+ const slots = {
+ root: ['root', checked && 'checked', disabled && 'disabled'],
+ };
+
+ const composedClasses = composeClasses(slots, getOptionSwatchUtilityClass, classes);
+
+ return {
+ ...classes,
+ ...composedClasses,
+ };
+};
+
+const OptionSwatchRoot = styled(ButtonBase, {
+ name: 'ESOptionSwatch',
+ slot: 'Root',
+ overridesResolver: (props, styles) => {
+ const { ownerState } = props;
+
+ return [styles.root, ownerState.checked && styles.checked, ownerState.disabled && styles.disabled];
+ },
+})<{ ownerState: OptionSwatchOwnerState }>(({ theme }) => ({
+ background: 'transparent',
+ color: theme.vars.palette.monoA.A600,
+ '--hovered': theme.vars.palette.monoA.A50,
+ '--pressed': theme.vars.palette.monoA.A150,
+
+ [`&.${optionSwatchClasses.disabled}`]: {
+ cursor: 'not-allowed',
+ pointerEvents: 'auto',
+ },
+
+ variants: [
+ {
+ props: {
+ checked: true,
+ },
+ style: {
+ //
+ },
+ },
+ {
+ props: {
+ disabled: true,
+ },
+ style: {
+ //
+ },
+ },
+ ],
+}));
+
+function areEqualValues(a: any, b: any) {
+ if (typeof b === 'object' && b !== null) {
+ return a === b;
+ }
+
+ // The value could be a number, the DOM will stringify it anyway.
+ return String(a) === String(b);
+}
+
+export const OptionSwatch = React.forwardRef(function OptionSwatch(inProps, ref) {
+ const props = useDefaultProps({ props: inProps, name: 'ESOptionSwatch' });
+ const {
+ className,
+ classes: classesProp,
+ sx,
+
+ checked: checkedProp,
+ defaultChecked,
+ disabled,
+
+ id,
+ name: nameProp,
+ value,
+
+ inputProps,
+ inputRef,
+
+ onChange: onChangeProp,
+ } = props;
+
+ const radioGroup = useRadioGroup();
+
+ let checked = checkedProp;
+ const onChange = createChainedFunction(onChangeProp as never, radioGroup?.onChange as never);
+ let name = nameProp;
+
+ if (radioGroup) {
+ if (typeof checked === 'undefined') {
+ checked = areEqualValues(radioGroup.value, props.value);
+ }
+
+ if (typeof name === 'undefined') {
+ name = radioGroup.name;
+ }
+ }
+
+ const ownerState = {
+ classes: classesProp,
+ checked,
+ disabled,
+ };
+
+ const classes = useUtilityClasses(ownerState);
+
+ return (
+
+
+
+ );
+});
diff --git a/packages/react/src/components/OptionSwatch/OptionSwatch.types.ts b/packages/react/src/components/OptionSwatch/OptionSwatch.types.ts
new file mode 100644
index 000000000..39ed90ad0
--- /dev/null
+++ b/packages/react/src/components/OptionSwatch/OptionSwatch.types.ts
@@ -0,0 +1,63 @@
+import { OptionSwatchClasses } from './OptionSwatch.classes';
+
+import { SxProps, Theme } from '@mui/material';
+
+export interface OptionSwatchProps {
+ /** Class applied to the root element. */
+ className?: string;
+ /** Override or extend the styles applied to the component. */
+ classes?: Partial;
+ /** The system prop that allows defining system overrides as well as additional CSS styles. */
+ sx?: SxProps;
+
+ /**
+ * If `true`, the component is checked.
+ */
+ checked?: boolean;
+
+ /**
+ * The default checked state. Use when the component is not controlled.
+ */
+ defaultChecked?: boolean;
+
+ /**
+ * If `true`, the component is disabled.
+ * @default false
+ */
+ disabled?: boolean;
+
+ /**
+ * The id of the `input` element.
+ */
+ id?: string;
+
+ /**
+ * The name used to reference the value of the control.
+ * If you don't provide this prop, it falls back to a randomly generated name.
+ */
+ name?: string;
+
+ /**
+ * [Attributes] applied to the `input` element.
+ */
+ inputProps?: React.InputHTMLAttributes;
+
+ /**
+ * Pass a ref to the `input` element.
+ */
+ inputRef?: React.Ref;
+
+ /**
+ * The value of the component. The DOM API casts this to a string.
+ * The browser uses "on" as the default value.
+ */
+ value?: string | number | readonly string[];
+
+ /**
+ * Callback fired when the state is changed.
+ *
+ * @param {React.ChangeEvent} event The event source of the callback.
+ * You can pull out the new checked state by accessing `event.target.checked` (boolean).
+ */
+ onChange?: (event: React.ChangeEvent) => void;
+}
diff --git a/packages/react/src/components/OptionSwatch/index.ts b/packages/react/src/components/OptionSwatch/index.ts
new file mode 100644
index 000000000..193b99aed
--- /dev/null
+++ b/packages/react/src/components/OptionSwatch/index.ts
@@ -0,0 +1,4 @@
+export { OptionSwatch } from './OptionSwatch';
+export type { OptionSwatchClasses, OptionSwatchClassKey } from './OptionSwatch.classes';
+export { optionSwatchClasses } from './OptionSwatch.classes';
+export type { OptionSwatchProps } from './OptionSwatch.types';
diff --git a/packages/react/src/components/index.ts b/packages/react/src/components/index.ts
index a66e136a6..2c244b07c 100644
--- a/packages/react/src/components/index.ts
+++ b/packages/react/src/components/index.ts
@@ -43,6 +43,7 @@ export * from './locale';
export * from './MadeBy';
export * from './MenuGroup';
export * from './MenuItem';
+export * from './OptionSwatch';
export * from './PageHGroup';
export * from './Pagination';
export * from './PasswordField';