;
+export const Primary: Story = {
+ args: {
+ isDisabled: false,
+ isSelected: false,
+ size: 'large',
+ },
+ render: SwitchStory,
+};
+
+export const Sizes: Story = {
+ render: () => (
+
+
+ Switch/Large
+
+
+
+ Switch/Medium
+
+
+
+ Switch/Small
+
+
+
+ ),
+};
+
+export const Selected: Story = {
+ args: {
+ isDisabled: false,
+ isSelected: true,
+ size: 'large',
+ },
+ render: SwitchStory,
+};
+
+export const Disabled: Story = {
+ args: {
+ isDisabled: true,
+ isSelected: false,
+ size: 'large',
+ },
+ render: SwitchStory,
+};
+
+export const DisabledSelected: Story = {
+ args: {
+ isDisabled: true,
+ isSelected: true,
+ size: 'large',
+ },
+ render: SwitchStory,
+};
+
+const SwitchClickStory = () => {
+ const [selected, setSelected] = useState(false);
+
+ const handleClick = (selected: boolean) => setSelected(selected);
+
+ return (
+
+ {selected ? 'on' : 'off'}
+
+
+ );
+};
+
+export const Click: Story = {
+ render: SwitchClickStory,
+};
diff --git a/src/components/Switch/Switch.style.ts b/src/components/Switch/Switch.style.ts
new file mode 100644
index 0000000..b4897ee
--- /dev/null
+++ b/src/components/Switch/Switch.style.ts
@@ -0,0 +1,77 @@
+import { styled } from 'styled-components';
+import { DefaultTheme } from 'styled-components/dist/types';
+import { match } from 'ts-pattern';
+
+import { SwitchSize } from './Switch.type';
+
+interface StyledSwitchProps {
+ $isDisabled: boolean;
+ $isSelected: boolean;
+ $size: SwitchSize;
+}
+
+const sizeStyle = {
+ large: {
+ track: {
+ width: 48,
+ height: 30,
+ padding: 2.5,
+ },
+ thumb: {
+ width: 25,
+ height: 25,
+ },
+ },
+ medium: {
+ track: {
+ width: 32,
+ height: 20,
+ padding: 2,
+ },
+ thumb: {
+ width: 16,
+ height: 16,
+ },
+ },
+ small: {
+ track: {
+ width: 24,
+ height: 16,
+ padding: 1.5,
+ },
+ thumb: {
+ width: 13,
+ height: 13,
+ },
+ },
+} as const;
+
+const getTrackColor = (arg: { $isDisabled: boolean; $isSelected: boolean; theme: DefaultTheme }) =>
+ match(arg)
+ .with({ $isDisabled: true }, ({ theme }) => theme.semantic.color.switchDisabled)
+ .with({ $isSelected: true }, ({ theme }) => theme.semantic.color.switchSelected)
+ .otherwise(({ theme }) => theme.semantic.color.switchUnselected);
+
+export const StyledTrack = styled.div`
+ ${({ $size }) => sizeStyle[$size].track}
+
+ border-radius: 999px;
+ background-color: ${({ $isDisabled, $isSelected, theme }) =>
+ getTrackColor({ $isDisabled, $isSelected, theme })};
+ cursor: ${({ $isDisabled }) => ($isDisabled ? 'not-allowed' : 'pointer')};
+`;
+
+const getThumbTransform = (size: SwitchSize) => {
+ const { track, thumb } = sizeStyle[size];
+ return track.width - 2 * track.padding - thumb.width;
+};
+
+export const StyledThumb = styled.div`
+ ${({ $size }) => sizeStyle[$size].thumb}
+
+ border-radius: 50%;
+ background-color: ${({ theme }) => theme.semantic.color.switchThumb};
+ transform: ${({ $size, $isSelected }) =>
+ $isSelected && `translateX(${getThumbTransform($size)}px)`};
+ transition: ${({ $isSelected }) => ($isSelected ? '150ms ease-out' : '150ms ease-in')};
+`;
diff --git a/src/components/Switch/Switch.tsx b/src/components/Switch/Switch.tsx
new file mode 100644
index 0000000..ffddf17
--- /dev/null
+++ b/src/components/Switch/Switch.tsx
@@ -0,0 +1,45 @@
+import { forwardRef, useState } from 'react';
+
+import { StyledTrack, StyledThumb } from './Switch.style';
+import { SwitchProps } from './Switch.type';
+
+export const Switch = forwardRef(
+ ({ isDisabled = false, isSelected = false, size, onSelectedChange, ...props }, ref) => {
+ const [innerSelected, setInnerSelected] = useState(isSelected);
+
+ const handleSwitchClick = () => {
+ if (isDisabled) return;
+
+ setInnerSelected((prev) => !prev);
+ onSelectedChange?.(!innerSelected);
+ };
+
+ const handleKeyDown = (event: React.KeyboardEvent) => {
+ if (event.code !== 'Space' || isDisabled) return;
+
+ event.preventDefault();
+ setInnerSelected((prev) => !prev);
+ onSelectedChange?.(!innerSelected);
+ };
+
+ return (
+
+
+
+ );
+ }
+);
+
+Switch.displayName = 'Switch';
diff --git a/src/components/Switch/Switch.type.ts b/src/components/Switch/Switch.type.ts
new file mode 100644
index 0000000..2d9195a
--- /dev/null
+++ b/src/components/Switch/Switch.type.ts
@@ -0,0 +1,8 @@
+export type SwitchSize = 'small' | 'medium' | 'large';
+
+export interface SwitchProps extends React.HTMLAttributes {
+ isDisabled?: boolean;
+ isSelected?: boolean;
+ size: SwitchSize;
+ onSelectedChange?: (selected: boolean) => void;
+}
diff --git a/src/components/Switch/index.ts b/src/components/Switch/index.ts
new file mode 100644
index 0000000..31bb43b
--- /dev/null
+++ b/src/components/Switch/index.ts
@@ -0,0 +1,2 @@
+export { Switch } from './Switch';
+export type { SwitchProps } from './Switch.type';
diff --git a/src/components/index.ts b/src/components/index.ts
index 85d88b6..a366aed 100644
--- a/src/components/index.ts
+++ b/src/components/index.ts
@@ -45,3 +45,6 @@ export type {
export { Textarea } from './Textarea';
export type { TextareaProps } from './Textarea';
+
+export { Switch } from './Switch';
+export type { SwitchProps } from './Switch';
diff --git a/src/style/foundation/color/semanticColor/semanticColor.type.ts b/src/style/foundation/color/semanticColor/semanticColor.type.ts
index fb81c97..32e273b 100644
--- a/src/style/foundation/color/semanticColor/semanticColor.type.ts
+++ b/src/style/foundation/color/semanticColor/semanticColor.type.ts
@@ -96,6 +96,8 @@ export type SemanticPaginationBrandColor = MergeVariants<'pagination', 'brand',
export type SemanticPaginationBasicColor = MergeVariants<'pagination', 'basic', SelectableVariant>;
+export type SemanticSwitchColor = MergeVariants<'switch', SelectableVariantWithDisabled | 'thumb'>;
+
export type SemanticColorType =
| SemanticBackgroundBasicColor
| SemanticBackgroundBrandColor
@@ -119,6 +121,7 @@ export type SemanticColorType =
| SemanticCheckboxColor
| SemanticChipColor
| SemanticPaginationBrandColor
- | SemanticPaginationBasicColor;
+ | SemanticPaginationBasicColor
+ | SemanticSwitchColor;
export type SemanticColorPalette = Readonly>;
diff --git a/src/style/foundation/color/semanticColor/semanticColorPalette.ts b/src/style/foundation/color/semanticColor/semanticColorPalette.ts
index 5d6057d..2f2a42b 100644
--- a/src/style/foundation/color/semanticColor/semanticColorPalette.ts
+++ b/src/style/foundation/color/semanticColor/semanticColorPalette.ts
@@ -87,4 +87,9 @@ export const semanticColorPalette: SemanticColorPalette = {
paginationBasicSelected: primitiveColorPalette.neutralBlack,
paginationBasicUnselected: primitiveColorPalette.gray200,
+
+ switchSelected: primitiveColorPalette.violet500,
+ switchUnselected: primitiveColorPalette.gray300,
+ switchDisabled: primitiveColorPalette.gray200,
+ switchThumb: primitiveColorPalette.neutralWhite,
} as const;