diff --git a/packages/core/src/components/index.scss b/packages/core/src/components/index.scss
index fc1dd84aa..93892b2e6 100644
--- a/packages/core/src/components/index.scss
+++ b/packages/core/src/components/index.scss
@@ -44,6 +44,8 @@
@forward './validation/validation';
@use './accordion/accordion.scss';
@forward './accordion/accordion.scss';
+@use './switch/switch.scss';
+@forward './switch/switch.scss';
@mixin components() {
@include asterisk.Asterisk();
@@ -69,4 +71,5 @@
@include tooltip.Tooltip();
@include validation.Validation();
@include accordion.Accordion();
+ @include switch.Switch();
}
diff --git a/packages/core/src/components/index.ts b/packages/core/src/components/index.ts
index cf9e912c1..faa986538 100644
--- a/packages/core/src/components/index.ts
+++ b/packages/core/src/components/index.ts
@@ -12,6 +12,7 @@ export * from './progress/progress';
export * from './radio/radio';
export * from './select/select';
export * from './spinner/spinner';
+export * from './switch/switch';
export * from './textarea/textarea';
export * from './tooltip/tooltip';
export * from './validation/validation';
diff --git a/packages/core/src/components/switch/switch.scss b/packages/core/src/components/switch/switch.scss
new file mode 100644
index 000000000..3dea19757
--- /dev/null
+++ b/packages/core/src/components/switch/switch.scss
@@ -0,0 +1,132 @@
+/* stylelint-disable selector-max-compound-selectors */
+
+@use '../../animations';
+@use '../../helpers';
+@use '../../mixins';
+
+@mixin Switch() {
+ @if not mixins.includes('Switch') {
+ @include _Switch();
+ }
+}
+
+@mixin _Switch() {
+ .ods-switch-label {
+ align-items: center;
+ color: helpers.color('content-main');
+ cursor: pointer;
+ display: inline-flex;
+ gap: helpers.space(1);
+ position: relative;
+
+ input {
+ opacity: 0;
+ pointer-events: none;
+ position: absolute;
+ }
+
+ // Hover
+ input + .ods-switch-indicator:hover {
+ border-color: #636670;
+ }
+
+ input + .ods-switch-indicator:hover::before {
+ background-color: #636670;
+ }
+
+ // Checked
+ input:checked + .ods-switch-indicator {
+ background-color: helpers.color('background-input-selected');
+ border-color: helpers.color('border-input-selected');
+ }
+
+ input:checked + .ods-switch-indicator::before {
+ background-color: helpers.color('background-input');
+ transform: translateY(-50%) translateX(helpers.space(2));
+ }
+
+ input:checked + .ods-switch-indicator .ods-icon {
+ opacity: 1;
+ transform: translateY(-50%) translateX(helpers.space(2));
+ }
+
+ input:checked + .ods-switch-indicator:hover {
+ background-color: #5666f9;
+ border-color: #5666f9;
+ }
+
+ input:checked + .ods-switch-indicator:hover .ods-icon {
+ border-color: #5666f9;
+ }
+
+ // Focus
+ input:focus + .ods-switch-indicator {
+ box-shadow: //
+ 0 0 0 helpers.space(0.25) helpers.color('border-focus-inner'),
+ 0 0 0 helpers.space(0.5) helpers.color('border-action-focus');
+ }
+
+ // Disabled
+ input:disabled + .ods-switch-indicator {
+ cursor: not-allowed;
+ }
+
+ input:disabled + .ods-switch-indicator,
+ input:disabled + .ods-switch-indicator:hover,
+ input:disabled + .ods-switch-indicator .ods-icon,
+ input:disabled + .ods-switch-indicator:hover .ods-icon {
+ border-color: helpers.color('border-disabled');
+ }
+
+ input:disabled:checked + .ods-switch-indicator,
+ input:disabled + .ods-switch-indicator::before {
+ background-color: helpers.color('background-disabled');
+ }
+
+ input:disabled:checked + .ods-switch-indicator::before {
+ background-color: #fff;
+ }
+ }
+
+ .ods-switch-indicator {
+ $border-width: helpers.space(0.25);
+
+ background-color: helpers.color('background-input');
+ border: $border-width solid helpers.color('border-input');
+ border-radius: helpers.border-radius('full');
+ box-sizing: border-box;
+ height: helpers.space(3);
+ margin: helpers.space(1.5) helpers.space(1);
+ position: relative;
+ width: helpers.space(5);
+
+ // slider
+ &::before {
+ background-color: #828893;
+ border-radius: helpers.border-radius('full');
+ content: '';
+ display: inline-block;
+ height: 18px;
+ left: 1px;
+ position: absolute;
+ top: 50%;
+ transform: translateY(-50%);
+ transition: transform calc(var(--ods-transition-duration) * 2) ease-out;
+ width: 18px;
+ }
+
+ // check icon
+ .ods-icon {
+ content: '';
+ fill: helpers.color('border-input-selected');
+ height: helpers.space(2);
+ left: 2px;
+ opacity: 0;
+ position: absolute;
+ top: calc(50% - 1px);
+ transform: translateY(-50%) translateX(0);
+ transition: transform calc(var(--ods-transition-duration) * 2) ease-out;
+ width: helpers.space(2);
+ }
+ }
+}
diff --git a/packages/core/src/components/switch/switch.ts b/packages/core/src/components/switch/switch.ts
new file mode 100644
index 000000000..1403d1ed3
--- /dev/null
+++ b/packages/core/src/components/switch/switch.ts
@@ -0,0 +1,4 @@
+export interface SwitchProps {
+ disabled?: boolean;
+ checked?: boolean;
+}
diff --git a/packages/react/src/components/index.ts b/packages/react/src/components/index.ts
index b19eb3447..7c2efbe88 100644
--- a/packages/react/src/components/index.ts
+++ b/packages/react/src/components/index.ts
@@ -20,6 +20,7 @@ export * from './select/option';
export * from './select/option-group';
export * from './select/select';
export * from './spinner/spinner';
+export * from './switch/switch';
export * from './textarea/textarea';
export * from './tooltip/tooltip';
export * from './validation/validation';
diff --git a/packages/react/src/components/switch/switch.stories.tsx b/packages/react/src/components/switch/switch.stories.tsx
new file mode 100644
index 000000000..9a6d6dbed
--- /dev/null
+++ b/packages/react/src/components/switch/switch.stories.tsx
@@ -0,0 +1,62 @@
+import { color, type SwitchProps } from '@onfido/castor';
+import { Field, FieldLabel, Fieldset, Switch } from '@onfido/castor-react';
+import React, { useState, type ChangeEvent } from 'react';
+import { Meta, Story } from '../../../../../docs';
+
+export default {
+ title: 'React/Switch',
+ component: Switch,
+ argTypes: {
+ checked: {
+ type: 'boolean',
+ },
+ children: { description: 'Acts as a label for the ``.' },
+ disabled: {
+ type: 'boolean',
+ },
+ },
+ args: {
+ children: '',
+ },
+ parameters: {},
+} as Meta;
+
+export const Playground: Story = {};
+
+export const Examples: Story = {
+ render: () => {
+ const [switchState, setSwitchState] = useState(true);
+ const handleOnChange = (event: ChangeEvent) => {
+ setSwitchState(event.target.checked);
+ };
+
+ return (
+
+ );
+ },
+};
diff --git a/packages/react/src/components/switch/switch.tsx b/packages/react/src/components/switch/switch.tsx
new file mode 100644
index 000000000..e8c5f2825
--- /dev/null
+++ b/packages/react/src/components/switch/switch.tsx
@@ -0,0 +1,28 @@
+import { c, classy, m, type SwitchProps as BaseProps } from '@onfido/castor';
+import React, { type FC } from 'react';
+import { Icon } from '../icon/icon';
+
+let idCount = 0;
+
+export const Switch: FC = ({
+ id = `castor_switch_${++idCount}`,
+ disabled,
+ className,
+ children,
+ ...props
+}) => (
+
+);
+
+export type SwitchProps = BaseProps & Omit;
+
+type SwitchElementProps = JSX.IntrinsicElements['input'];