diff --git a/src/components/radio/__snapshots__/radio.test.tsx.snap b/src/components/radio/__snapshots__/radio.test.tsx.snap
new file mode 100644
index 0000000..cfd4419
--- /dev/null
+++ b/src/components/radio/__snapshots__/radio.test.tsx.snap
@@ -0,0 +1,429 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`IonRadio should render a given option selected 1`] = `
+.c0 {
+ border: none;
+ padding: 0;
+ margin: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+}
+
+.c1 {
+ display: flex;
+ align-items: center;
+ justify-content: flex-start;
+ gap: 8px;
+}
+
+.c1 label {
+ font-size: 1.4rem;
+ line-height: 2rem;
+ font-weight: 400;
+ color: #505566;
+ cursor: pointer;
+}
+
+.c1 input[type='radio'] {
+ appearance: none;
+ width: 16px;
+ height: 16px;
+ margin: 4px;
+ border-radius: 50%;
+ border: 1px solid #aeb2bd;
+ background-color: #fcfcfd;
+ cursor: pointer;
+}
+
+.c1 input[type='radio']:focus-visible {
+ outline: 2px solid #146ff5;
+ outline-offset: 2px;
+}
+
+.c1 input[type='radio']::before {
+ content: '';
+ display: none;
+ width: 4px;
+ height: 4px;
+ border-radius: 50%;
+ background-color: #b5d2fc;
+ position: relative;
+ top: 50%;
+ left: 50%;
+ transform: scale(1) translate(-50%, -50%);
+}
+
+.c1 input[type='radio']:hover,
+.c1 input[type='radio']:focus-visible {
+ border: 1px solid #84b4fa;
+ background-color: #ebf3fe;
+ box-shadow: 0 0 0 4px #ebf3fe;
+}
+
+.c1 input[type='radio']:hover::before {
+ display: block;
+}
+
+.c1 input[type='radio']:active {
+ border: 1px solid #146ff5;
+ background-color: #b5d2fc;
+ box-shadow: none;
+}
+
+.c1 input[type='radio']:active::before {
+ display: block;
+ background-color: #ebf3fe;
+}
+
+.c1 input[type='radio']:disabled {
+ border: 1px solid #ced2db;
+ background-color: #e4e6eb;
+ cursor: not-allowed;
+ box-shadow: none;
+}
+
+.c1 input[type='radio']:disabled::before {
+ display: none;
+}
+
+.c1 input[type='radio']:checked {
+ border: 4px solid #0858ce;
+ background-color: #ebf3fe;
+}
+
+.c1 input[type='radio']:checked:hover,
+.c1 input[type='radio']:checked:focus-visible {
+ border: 4px solid #146ff5;
+ box-shadow: 0 0 0 4px #ebf3fe;
+}
+
+.c1 input[type='radio']:checked:hover::before,
+.c1 input[type='radio']:checked:focus-visible::before {
+ display: none;
+}
+
+.c1 input[type='radio']:checked:active {
+ border: 5px solid #06439d;
+ box-shadow: none;
+}
+
+.c1 input[type='radio']:checked:disabled {
+ border: 4px solid #f2f3f5;
+ background-color: #ced2db;
+ box-shadow: none;
+}
+
+
+`;
+
+exports[`IonRadio should render correctly when disabled 1`] = `
+.c0 {
+ border: none;
+ padding: 0;
+ margin: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+}
+
+.c1 {
+ display: flex;
+ align-items: center;
+ justify-content: flex-start;
+ gap: 8px;
+}
+
+.c1 label {
+ font-size: 1.4rem;
+ line-height: 2rem;
+ font-weight: 400;
+ color: #505566;
+ cursor: pointer;
+}
+
+.c1 input[type='radio'] {
+ appearance: none;
+ width: 16px;
+ height: 16px;
+ margin: 4px;
+ border-radius: 50%;
+ border: 1px solid #aeb2bd;
+ background-color: #fcfcfd;
+ cursor: pointer;
+}
+
+.c1 input[type='radio']:focus-visible {
+ outline: 2px solid #146ff5;
+ outline-offset: 2px;
+}
+
+.c1 input[type='radio']::before {
+ content: '';
+ display: none;
+ width: 4px;
+ height: 4px;
+ border-radius: 50%;
+ background-color: #b5d2fc;
+ position: relative;
+ top: 50%;
+ left: 50%;
+ transform: scale(1) translate(-50%, -50%);
+}
+
+.c1 input[type='radio']:hover,
+.c1 input[type='radio']:focus-visible {
+ border: 1px solid #84b4fa;
+ background-color: #ebf3fe;
+ box-shadow: 0 0 0 4px #ebf3fe;
+}
+
+.c1 input[type='radio']:hover::before {
+ display: block;
+}
+
+.c1 input[type='radio']:active {
+ border: 1px solid #146ff5;
+ background-color: #b5d2fc;
+ box-shadow: none;
+}
+
+.c1 input[type='radio']:active::before {
+ display: block;
+ background-color: #ebf3fe;
+}
+
+.c1 input[type='radio']:disabled {
+ border: 1px solid #ced2db;
+ background-color: #e4e6eb;
+ cursor: not-allowed;
+ box-shadow: none;
+}
+
+.c1 input[type='radio']:disabled::before {
+ display: none;
+}
+
+.c1 input[type='radio']:checked {
+ border: 4px solid #0858ce;
+ background-color: #ebf3fe;
+}
+
+.c1 input[type='radio']:checked:hover,
+.c1 input[type='radio']:checked:focus-visible {
+ border: 4px solid #146ff5;
+ box-shadow: 0 0 0 4px #ebf3fe;
+}
+
+.c1 input[type='radio']:checked:hover::before,
+.c1 input[type='radio']:checked:focus-visible::before {
+ display: none;
+}
+
+.c1 input[type='radio']:checked:active {
+ border: 5px solid #06439d;
+ box-shadow: none;
+}
+
+.c1 input[type='radio']:checked:disabled {
+ border: 4px solid #f2f3f5;
+ background-color: #ced2db;
+ box-shadow: none;
+}
+
+.c2 {
+ display: flex;
+ align-items: center;
+ justify-content: flex-start;
+ gap: 8px;
+}
+
+.c2 label {
+ font-size: 1.4rem;
+ line-height: 2rem;
+ font-weight: 400;
+ color: #505566;
+ cursor: pointer;
+ color: #aeb2bd;
+ cursor: not-allowed;
+}
+
+.c2 input[type='radio'] {
+ appearance: none;
+ width: 16px;
+ height: 16px;
+ margin: 4px;
+ border-radius: 50%;
+ border: 1px solid #aeb2bd;
+ background-color: #fcfcfd;
+ cursor: pointer;
+}
+
+.c2 input[type='radio']:focus-visible {
+ outline: 2px solid #146ff5;
+ outline-offset: 2px;
+}
+
+.c2 input[type='radio']::before {
+ content: '';
+ display: none;
+ width: 4px;
+ height: 4px;
+ border-radius: 50%;
+ background-color: #b5d2fc;
+ position: relative;
+ top: 50%;
+ left: 50%;
+ transform: scale(1) translate(-50%, -50%);
+}
+
+.c2 input[type='radio']:hover,
+.c2 input[type='radio']:focus-visible {
+ border: 1px solid #84b4fa;
+ background-color: #ebf3fe;
+ box-shadow: 0 0 0 4px #ebf3fe;
+}
+
+.c2 input[type='radio']:hover::before {
+ display: block;
+}
+
+.c2 input[type='radio']:active {
+ border: 1px solid #146ff5;
+ background-color: #b5d2fc;
+ box-shadow: none;
+}
+
+.c2 input[type='radio']:active::before {
+ display: block;
+ background-color: #ebf3fe;
+}
+
+.c2 input[type='radio']:disabled {
+ border: 1px solid #ced2db;
+ background-color: #e4e6eb;
+ cursor: not-allowed;
+ box-shadow: none;
+}
+
+.c2 input[type='radio']:disabled::before {
+ display: none;
+}
+
+.c2 input[type='radio']:checked {
+ border: 4px solid #0858ce;
+ background-color: #ebf3fe;
+}
+
+.c2 input[type='radio']:checked:hover,
+.c2 input[type='radio']:checked:focus-visible {
+ border: 4px solid #146ff5;
+ box-shadow: 0 0 0 4px #ebf3fe;
+}
+
+.c2 input[type='radio']:checked:hover::before,
+.c2 input[type='radio']:checked:focus-visible::before {
+ display: none;
+}
+
+.c2 input[type='radio']:checked:active {
+ border: 5px solid #06439d;
+ box-shadow: none;
+}
+
+.c2 input[type='radio']:checked:disabled {
+ border: 4px solid #f2f3f5;
+ background-color: #ced2db;
+ box-shadow: none;
+}
+
+
+`;
diff --git a/src/components/radio/index.ts b/src/components/radio/index.ts
new file mode 100644
index 0000000..1140e08
--- /dev/null
+++ b/src/components/radio/index.ts
@@ -0,0 +1 @@
+export * from './radio';
diff --git a/src/components/radio/radio.test.tsx b/src/components/radio/radio.test.tsx
new file mode 100644
index 0000000..9242d26
--- /dev/null
+++ b/src/components/radio/radio.test.tsx
@@ -0,0 +1,56 @@
+import userEvent from '@testing-library/user-event';
+import { renderWithTheme } from '../utils/test-utils';
+import { IonRadio, IonRadioProps } from './radio';
+
+const options = [
+ { label: 'Option 1', value: 'option1' },
+ { label: 'Option 2', value: 'option2' },
+];
+
+const sut = (props: IonRadioProps) => {
+ return renderWithTheme();
+};
+
+describe('IonRadio', () => {
+ it('should render a given option selected', () => {
+ const { container, getByRole } = sut({
+ name: 'radio',
+ options,
+ value: 'option1',
+ onChange: jest.fn(),
+ });
+
+ expect(getByRole('radio', { name: 'Option 1' })).toBeChecked();
+ expect(container).toMatchSnapshot();
+ });
+
+ it('should render correctly when disabled', () => {
+ const { container, getByRole } = sut({
+ name: 'radio',
+ options: [
+ ...options,
+ { label: 'Option 3', value: 'option3', disabled: true },
+ ],
+ value: 'option1',
+ onChange: jest.fn(),
+ });
+
+ expect(getByRole('radio', { name: 'Option 3' })).toBeDisabled();
+ expect(container).toMatchSnapshot();
+ });
+
+ it('should call onChange when an option is clicked', async () => {
+ const onChange = jest.fn();
+
+ const { getByLabelText } = sut({
+ name: 'radio',
+ options,
+ value: 'option1',
+ onChange,
+ });
+
+ await userEvent.click(getByLabelText('Option 2'));
+
+ expect(onChange).toHaveBeenCalledWith('option2');
+ });
+});
diff --git a/src/components/radio/radio.tsx b/src/components/radio/radio.tsx
new file mode 100644
index 0000000..ab5e21d
--- /dev/null
+++ b/src/components/radio/radio.tsx
@@ -0,0 +1,39 @@
+import { Container, RadioGroup } from './styles';
+
+interface Option {
+ label: string;
+ value: string;
+ disabled?: boolean;
+}
+
+export interface IonRadioProps {
+ options: Option[];
+ name: string;
+ onChange: (value: string) => void;
+ value?: Option['value'];
+}
+
+export const IonRadio = ({ name, options, value, onChange }: IonRadioProps) => {
+ const handleChange = (value: string) => {
+ onChange(value);
+ };
+
+ return (
+
+ {options.map(({ label, value: optionValue, disabled }) => (
+
+ handleChange(optionValue)}
+ />
+
+
+ ))}
+
+ );
+};
diff --git a/src/components/radio/styles.ts b/src/components/radio/styles.ts
new file mode 100644
index 0000000..827ebd9
--- /dev/null
+++ b/src/components/radio/styles.ts
@@ -0,0 +1,113 @@
+import { css, styled } from 'styled-components';
+
+export const RadioGroup = styled.fieldset`
+ border: none;
+ padding: 0;
+ margin: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+`;
+
+export const Container = styled.div<{ $disabled?: boolean }>`
+ ${({ theme, $disabled }) => css`
+ ${theme.utils.flex.start(8)}
+
+ label {
+ ${theme.font.size[14]}
+ font-weight: 400;
+ color: ${theme.colors.neutral[7]};
+ cursor: pointer;
+
+ ${$disabled &&
+ css`
+ color: ${theme.colors.neutral[5]};
+ cursor: not-allowed;
+ `}
+ }
+
+ input[type='radio'] {
+ appearance: none;
+ width: 16px;
+ height: 16px;
+ margin: 4px;
+ border-radius: 50%;
+ border: 1px solid ${theme.colors.neutral[5]};
+ background-color: ${theme.colors.neutral[1]};
+ ${theme.utils.focus}
+ cursor: pointer;
+
+ &::before {
+ content: '';
+ display: none;
+ width: 4px;
+ height: 4px;
+ border-radius: 50%;
+ background-color: ${theme.colors.primary[2]};
+ position: relative;
+ top: 50%;
+ left: 50%;
+ transform: scale(1) translate(-50%, -50%);
+ }
+
+ &:hover,
+ &:focus-visible {
+ border: 1px solid ${theme.colors.primary[3]};
+ background-color: ${theme.colors.primary[1]};
+ box-shadow: 0 0 0 4px ${theme.colors.primary[1]};
+ }
+
+ &:hover::before {
+ display: block;
+ }
+
+ &:active {
+ border: 1px solid ${theme.colors.primary[5]};
+ background-color: ${theme.colors.primary[2]};
+ box-shadow: none;
+
+ &::before {
+ display: block;
+ background-color: ${theme.colors.primary[1]};
+ }
+ }
+
+ &:disabled {
+ border: 1px solid ${theme.colors.neutral[4]};
+ background-color: ${theme.colors.neutral[3]};
+ cursor: not-allowed;
+ box-shadow: none;
+
+ &::before {
+ display: none;
+ }
+ }
+
+ &:checked {
+ border: 4px solid ${theme.colors.main.primary};
+ background-color: ${theme.colors.primary[1]};
+
+ &:hover,
+ &:focus-visible {
+ border: 4px solid ${theme.colors.primary[5]};
+ box-shadow: 0 0 0 4px ${theme.colors.primary[1]};
+
+ &::before {
+ display: none;
+ }
+ }
+
+ &:active {
+ border: 5px solid ${theme.colors.primary[7]};
+ box-shadow: none;
+ }
+
+ &:disabled {
+ border: 4px solid ${theme.colors.neutral[2]};
+ background-color: ${theme.colors.neutral[4]};
+ box-shadow: none;
+ }
+ }
+ }
+ `}
+`;
diff --git a/src/stories/radio/radio.stories.tsx b/src/stories/radio/radio.stories.tsx
new file mode 100644
index 0000000..15d153f
--- /dev/null
+++ b/src/stories/radio/radio.stories.tsx
@@ -0,0 +1,29 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import { useState } from 'react';
+import { IonRadio, IonRadioProps } from '../../components/radio';
+
+export default {
+ title: 'Ion/Data Entry/Radio',
+ component: IonRadio,
+} as ComponentMeta;
+
+const Template: ComponentStory = (args: IonRadioProps) => {
+ const [value, setValue] = useState(args.value);
+
+ const handleChange = (value: string) => {
+ setValue(value);
+ args.onChange && args.onChange(value);
+ };
+
+ return ;
+};
+
+export const Default = Template.bind({});
+Default.args = {
+ name: 'radio',
+ options: [
+ { label: 'Option 1', value: '1' },
+ { label: 'Option 2', value: '2' },
+ { label: 'Option 3', value: '3', disabled: true },
+ ],
+};