diff --git a/packages/lib/src/components/internal/Toggle/Toggle.scss b/packages/lib/src/components/internal/Toggle/Toggle.scss
new file mode 100644
index 0000000000..f015d14626
--- /dev/null
+++ b/packages/lib/src/components/internal/Toggle/Toggle.scss
@@ -0,0 +1,158 @@
+@use 'styles/mixins';
+@import 'styles/variable-generator';
+
+.adyen-checkout-toggle {
+ $component-root: &;
+ $label-padding: token(toggle-label-padding);
+
+ color: inherit;
+ cursor: pointer;
+ display: flex;
+ width: auto;
+
+ @include mixins.box-sizing-setter(true);
+
+ &--disabled {
+ cursor: not-allowed;
+ display: flex;
+ }
+
+ &--readonly {
+ pointer-events: none;
+ }
+
+ &--label-first {
+ align-items: flex-start;
+ flex-direction: row-reverse;
+ justify-content: flex-end;
+ }
+
+ &__input {
+ cursor: inherit;
+ opacity: 0;
+ position: absolute;
+ }
+
+ &__track {
+ align-items: center;
+ background-color: token(toggle-track-background-color);
+ border: token(toggle-track-border);
+ border-radius: token(toggle-track-border-radius);
+ display: flex;
+ height: token(toggle-track-height);
+ min-width: token(toggle-track-width);
+ padding: token(toggle-track-padding);
+ position: relative;
+
+ #{$component-root}__input:focus-visible + & {
+ @include mixins.b-focus-ring;
+ }
+
+ #{$component-root}__input:hover:enabled + & {
+ background-color: token(toggle-track-hover-background-color);
+ border-color: token(toggle-track-hover-border-color);
+ }
+
+ #{$component-root}__input:active:enabled + & {
+ background-color: token(toggle-track-active-background-color);
+ border-color: token(toggle-track-active-border-color);
+ }
+
+ #{$component-root}__input:disabled + & {
+ background-color: token(toggle-track-disabled-background-color);
+ border-color: token(toggle-track-disabled-border-color);
+ cursor: not-allowed;
+
+ path {
+ fill: #8d95a3
+ }
+ }
+
+ #{$component-root}--readonly #{$component-root}__input + & {
+ background-color: token(toggle-track-readonly-background-color);
+ border-color: token(toggle-track-readonly-border-color);
+ }
+
+ #{$component-root}__input:checked + & {
+ background-color: token(toggle-track-toggled-background-color);
+ border: token(toggle-track-toggled-border);
+ padding: token(toggle-track-toggled-padding);
+ }
+
+ #{$component-root}__input:checked:hover:enabled + & {
+ background-color: token(toggle-track-toggled-hover-background-color);
+ }
+
+ #{$component-root}__input:checked:active:enabled + & {
+ background-color: token(toggle-track-toggled-active-background-color);
+ }
+
+ #{$component-root}__input:checked:disabled + & {
+ background-color: token(toggle-track-toggled-disabled-background-color);
+ }
+
+ #{$component-root}--readonly #{$component-root}__input:checked + & {
+ background-color: token(toggle-track-toggled-readonly-background-color);
+ }
+ }
+
+ &__handle {
+ align-content: center;
+ background-color: token(toggle-handle-background-color);
+ border-radius: token(toggle-handle-border-radius);
+ color: token(toggle-handle-toggled-color);
+ display: inline-flex;
+ height: token(toggle-handle-height);
+ justify-content: center;
+ transition: token(toggle-handle-transition);
+ width: token(toggle-handle-width);
+
+ #{$component-root}__input:disabled + * & {
+ background-color: token(toggle-handle-disabled-background-color);
+ cursor: not-allowed;
+ }
+
+ #{$component-root}__input:checked + * & {
+ background-color: token(toggle-handle-toggled-background-color);
+ height: token(toggle-handle-toggled-height);
+ transform: translateX(100%);
+ width: token(toggle-handle-toggled-width);
+ }
+
+ #{$component-root}__input:checked:disabled + * & {
+ background-color: token(toggle-handle-toggled-disabled-background-color);
+ color: token(toggle-handle-toggled-disabled-color);
+ cursor: not-allowed;
+ }
+
+ #{$component-root}--readonly #{$component-root}__input:checked + * & {
+ background-color: token(toggle-handle-toggled-readonly-background-color);
+ }
+ }
+
+ &__label-container {
+ display: flex;
+ flex-direction: column;
+ padding-left: $label-padding;
+
+ @include mixins.adyen-checkout-text-body;
+
+ #{$component-root}--label-first > & {
+ padding-left: 0;
+ padding-right: $label-padding;
+ }
+ }
+
+ &__label {
+ vertical-align: baseline;
+
+ @include mixins.adyen-checkout-text-body;
+ }
+
+ &__description {
+ color: token(toggle-description-color);
+ padding-top: token(toggle-description-padding);
+
+ @include mixins.adyen-checkout-text-body;
+ }
+}
diff --git a/packages/lib/src/components/internal/Toggle/Toggle.test.tsx b/packages/lib/src/components/internal/Toggle/Toggle.test.tsx
new file mode 100644
index 0000000000..5529e5e720
--- /dev/null
+++ b/packages/lib/src/components/internal/Toggle/Toggle.test.tsx
@@ -0,0 +1,32 @@
+import { h } from 'preact';
+import { render, screen } from '@testing-library/preact';
+import userEvent from '@testing-library/user-event';
+import Toggle from './Toggle';
+
+test('should emit correct values', async () => {
+ const user = userEvent.setup();
+ const onChangeMock = jest.fn();
+
+ render();
+
+ await user.click(screen.getByRole('switch'));
+ expect(onChangeMock.mock.calls[0][0]).toBe(true);
+
+ await user.click(screen.getByRole('switch'));
+ expect(onChangeMock.mock.calls[1][0]).toBe(false);
+});
+
+test('should render as readonly', () => {
+ render();
+ expect(screen.getByRole('switch').getAttribute('aria-readonly')).toBe('true');
+});
+
+test('should render as disabled', () => {
+ render();
+ expect(screen.getByRole('switch')).toBeDisabled();
+});
+
+test('should render description', () => {
+ render();
+ expect(screen.getByText('Save all details')).toBeTruthy();
+});
diff --git a/packages/lib/src/components/internal/Toggle/Toggle.tsx b/packages/lib/src/components/internal/Toggle/Toggle.tsx
new file mode 100644
index 0000000000..4e2594345d
--- /dev/null
+++ b/packages/lib/src/components/internal/Toggle/Toggle.tsx
@@ -0,0 +1,78 @@
+import { h } from 'preact';
+import { useCallback, useMemo } from 'preact/hooks';
+import cx from 'classnames';
+import uuid from '../../../utils/uuid';
+import './Toggle.scss';
+
+interface ToggleProps {
+ label?: string;
+ labelPosition?: 'before' | 'after';
+ ariaLabel?: string;
+ description?: string;
+ checked: boolean;
+ disabled?: boolean;
+ readonly?: boolean;
+ onChange?(checked: boolean): void;
+}
+
+const Toggle = ({ label, labelPosition = 'after', ariaLabel, description, checked, disabled = false, readonly = false, onChange }: ToggleProps) => {
+ const descriptionId = useMemo(() => (description ? `toggle-description-${uuid()}` : null), [description]);
+ const computedAriaLabel = useMemo(() => ariaLabel ?? label, [ariaLabel, label]);
+
+ const conditionalClasses = cx({
+ 'adyen-checkout-toggle--label-first': labelPosition === 'before',
+ 'adyen-checkout-toggle--disabled': disabled,
+ 'adyen-checkout-toggle--readonly': readonly
+ });
+
+ const onInputChange = useCallback(
+ (event: Event) => {
+ onChange((event.target as HTMLInputElement).checked);
+ },
+ [onChange]
+ );
+
+ return (
+
+ );
+};
+
+export default Toggle;
diff --git a/packages/lib/src/components/internal/Toggle/index.ts b/packages/lib/src/components/internal/Toggle/index.ts
new file mode 100644
index 0000000000..c2ec545530
--- /dev/null
+++ b/packages/lib/src/components/internal/Toggle/index.ts
@@ -0,0 +1 @@
+export { default } from './Toggle';
diff --git a/packages/lib/src/styles/mixins.scss b/packages/lib/src/styles/mixins.scss
index ad418d0b20..926f55dd38 100644
--- a/packages/lib/src/styles/mixins.scss
+++ b/packages/lib/src/styles/mixins.scss
@@ -105,6 +105,12 @@ $adyen-checkout-media-query-l-min: 1024px;
line-height: token(text-title-line-height);
}
+@mixin adyen-checkout-text-body {
+ font-size: token(text-body-font-size);
+ font-weight: token(text-body-font-weight);
+ line-height: token(text-body-line-height);
+}
+
@mixin adyen-checkout-text-caption {
font-size: token(text-caption-font-size);
font-weight: token(text-caption-font-weight);
@@ -133,4 +139,4 @@ $adyen-checkout-media-query-l-min: 1024px;
margin: -1px;
padding: 0;
position: absolute;
-}
\ No newline at end of file
+}
diff --git a/packages/lib/src/styles/variable-generator.scss b/packages/lib/src/styles/variable-generator.scss
index 08e3ad21da..ba6322943e 100644
--- a/packages/lib/src/styles/variable-generator.scss
+++ b/packages/lib/src/styles/variable-generator.scss
@@ -1,5 +1,6 @@
@import '~@adyen/bento-design-tokens/dist/scss-map/bento/aliases';
@import '~@adyen/bento-design-tokens/dist/scss-map/bento/definitions';
+@import '~@adyen/bento-design-tokens/dist/scss-map/bento/components';
@function adyen-sdk-generate-css-variables($maps...) {
$adyen-output-map: ();
@@ -18,9 +19,9 @@
$adyen-tokens-map: ();
@if $generate-css-var {
- $adyen-tokens-map: adyen-sdk-generate-css-variables($color, $text, $focus-ring, $border, $spacer, $shadow);
+ $adyen-tokens-map: adyen-sdk-generate-css-variables($color, $text, $focus-ring, $border, $spacer, $shadow, $toggle);
} @else {
- $adyen-tokens-map: map-merge($color, $text, $focus-ring, $border, $spacer, $shadow)
+ $adyen-tokens-map: map-merge($color, $text, $focus-ring, $border, $spacer, $shadow, $toggle)
}
@return map-get($adyen-tokens-map, '#{$token}');
diff --git a/packages/lib/storybook/stories/internals/Toggle.stories.tsx b/packages/lib/storybook/stories/internals/Toggle.stories.tsx
new file mode 100644
index 0000000000..a8394429ae
--- /dev/null
+++ b/packages/lib/storybook/stories/internals/Toggle.stories.tsx
@@ -0,0 +1,65 @@
+import { Meta, StoryObj } from '@storybook/preact';
+import Toggle from '../../../src/components/internal/Toggle';
+import { useState } from 'preact/hooks';
+
+const meta: Meta = {
+ title: 'Internals/Toggle',
+ component: Toggle
+};
+
+export const Default: StoryObj = {
+ render: () => {
+ const [checked, setChecked] = useState(false);
+ return ;
+ },
+ parameters: {
+ controls: { exclude: ['useSessions', 'countryCode', 'shopperLocale', 'amount', 'showPayButton'] }
+ }
+};
+
+export const Disabled: StoryObj = {
+ render: () => {
+ return ;
+ },
+ parameters: {
+ controls: { exclude: ['useSessions', 'countryCode', 'shopperLocale', 'amount', 'showPayButton'] }
+ }
+};
+
+export const Readonly: StoryObj = {
+ render: () => {
+ return ;
+ },
+ parameters: {
+ controls: { exclude: ['useSessions', 'countryCode', 'shopperLocale', 'amount', 'showPayButton'] }
+ }
+};
+
+export const ToggleOnly: StoryObj = {
+ render: () => {
+ const [checked, setChecked] = useState(true);
+ return ;
+ },
+ parameters: {
+ controls: { exclude: ['useSessions', 'countryCode', 'shopperLocale', 'amount', 'showPayButton'] }
+ }
+};
+
+export const WithDescription: StoryObj = {
+ render: () => {
+ const [checked, setChecked] = useState(true);
+ return (
+
+ );
+ },
+ parameters: {
+ controls: { exclude: ['useSessions', 'countryCode', 'shopperLocale', 'amount', 'showPayButton'] }
+ }
+};
+
+export default meta;