diff --git a/apps/www/src/content/docs/components/switch/index.mdx b/apps/www/src/content/docs/components/switch/index.mdx
index 8110505ff..d3f75b98c 100644
--- a/apps/www/src/content/docs/components/switch/index.mdx
+++ b/apps/www/src/content/docs/components/switch/index.mdx
@@ -37,13 +37,3 @@ The Switch component comes in two sizes: large (default) and small.
Use the Switch component in a controlled manner to manage its state externally.
-
-## Accessibility
-
-The Switch component follows WAI-ARIA guidelines for toggle buttons:
-
-- Uses proper ARIA attributes (`aria-checked`, `aria-required`, `aria-label`)
-- Supports keyboard navigation (Space and Enter to toggle)
-- Includes proper labeling and description support
-- Changes cursor to 'not-allowed' when disabled
-- Associates labels with the switch using htmlFor
diff --git a/apps/www/src/content/docs/components/switch/props.ts b/apps/www/src/content/docs/components/switch/props.ts
index c4cc9d2be..baa6000c5 100644
--- a/apps/www/src/content/docs/components/switch/props.ts
+++ b/apps/www/src/content/docs/components/switch/props.ts
@@ -6,7 +6,7 @@ export interface SwitchProps {
defaultChecked?: boolean;
/** Event handler called when the checked state changes. */
- onCheckedChange?: (checked: boolean) => void;
+ onCheckedChange?: (checked: boolean, event: Event) => void;
/** When true, prevents the user from interacting with the switch. */
disabled?: boolean;
@@ -22,4 +22,28 @@ export interface SwitchProps {
/** A unique identifier for the switch. */
id?: string;
+
+ /** Identifies the field when a form is submitted. */
+ name?: string;
+
+ /** Additional CSS class names. */
+ className?: string;
+
+ /**
+ * Allows you to replace the component's HTML element with a different tag, or compose it with another component.
+ * Accepts a `ReactElement` or a function that returns the element to render.
+ *
+ * @remarks `ReactElement | function`
+ */
+ render?:
+ | React.ReactElement
+ | ((
+ props: React.HTMLAttributes,
+ state: {
+ checked: boolean;
+ disabled: boolean;
+ readOnly: boolean;
+ required: boolean;
+ }
+ ) => React.ReactElement);
}
diff --git a/packages/raystack/components/switch/__tests__/switch.test.tsx b/packages/raystack/components/switch/__tests__/switch.test.tsx
index 7b008402c..51980567a 100644
--- a/packages/raystack/components/switch/__tests__/switch.test.tsx
+++ b/packages/raystack/components/switch/__tests__/switch.test.tsx
@@ -42,7 +42,7 @@ describe('Switch', () => {
describe('Sizes', () => {
const sizes = ['small', 'large'] as const;
sizes.forEach(size => {
- it(`renders ${size} size by default`, () => {
+ it(`renders ${size} size`, () => {
render();
const switchElement = screen.getByRole('switch');
expect(switchElement).toHaveClass(styles[size]);
@@ -60,14 +60,14 @@ describe('Switch', () => {
render();
const switchElement = screen.getByRole('switch');
expect(switchElement).toHaveAttribute('aria-checked', 'false');
- expect(switchElement).toHaveAttribute('data-state', 'unchecked');
+ expect(switchElement).toHaveAttribute('data-unchecked');
});
it('renders as checked when checked prop is true', () => {
render();
const switchElement = screen.getByRole('switch');
expect(switchElement).toHaveAttribute('aria-checked', 'true');
- expect(switchElement).toHaveAttribute('data-state', 'checked');
+ expect(switchElement).toHaveAttribute('data-checked');
});
it('renders with defaultChecked', () => {
@@ -92,8 +92,7 @@ describe('Switch', () => {
it('renders as disabled when disabled prop is true', () => {
render();
const switchElement = screen.getByRole('switch');
- expect(switchElement).toBeDisabled();
- expect(switchElement).toHaveAttribute('data-disabled', 'true');
+ expect(switchElement).toHaveAttribute('data-disabled');
});
it('does not toggle when disabled', () => {
@@ -110,20 +109,19 @@ describe('Switch', () => {
it('can be disabled while checked', () => {
render();
const switchElement = screen.getByRole('switch');
- expect(switchElement).toBeDisabled();
+ expect(switchElement).toHaveAttribute('data-disabled');
expect(switchElement).toHaveAttribute('aria-checked', 'true');
- expect(switchElement).toHaveAttribute('data-disabled', 'true');
});
it('maintains disabled state with different sizes', () => {
const { rerender } = render();
let switchElement = screen.getByRole('switch');
- expect(switchElement).toBeDisabled();
+ expect(switchElement).toHaveAttribute('data-disabled');
expect(switchElement).toHaveClass(styles.small);
rerender();
switchElement = screen.getByRole('switch');
- expect(switchElement).toBeDisabled();
+ expect(switchElement).toHaveAttribute('data-disabled');
expect(switchElement).toHaveClass(styles.large);
});
});
@@ -137,7 +135,7 @@ describe('Switch', () => {
fireEvent.click(switchElement);
expect(handleChange).toHaveBeenCalledTimes(1);
- expect(handleChange).toHaveBeenCalledWith(true);
+ expect(handleChange).toHaveBeenCalledWith(true, expect.anything());
});
it('toggles from checked to unchecked', () => {
@@ -147,7 +145,7 @@ describe('Switch', () => {
const switchElement = screen.getByRole('switch');
fireEvent.click(switchElement);
- expect(handleChange).toHaveBeenCalledWith(false);
+ expect(handleChange).toHaveBeenCalledWith(false, expect.anything());
});
it('supports focus events', () => {
@@ -169,14 +167,10 @@ describe('Switch', () => {
render();
const switchElement = screen.getByRole('switch');
- await switchElement.focus();
+ switchElement.focus();
await user.keyboard('[Space]');
- expect(handleChange).toHaveBeenCalledWith(true);
- await user.keyboard('[Enter]');
-
- expect(handleChange).toHaveBeenCalledWith(false);
- expect(handleChange).toHaveBeenCalledTimes(2);
+ expect(handleChange).toHaveBeenCalledWith(true, expect.anything());
});
});
@@ -221,14 +215,14 @@ describe('Switch', () => {
it('supports required attribute', () => {
render();
const switchElement = screen.getByRole('switch');
- expect(switchElement).toHaveAttribute('aria-required', 'true');
+ expect(switchElement).toHaveAttribute('data-required');
});
it('works with required and disabled', () => {
render();
const switchElement = screen.getByRole('switch');
- expect(switchElement).toHaveAttribute('aria-required', 'true');
- expect(switchElement).toBeDisabled();
+ expect(switchElement).toHaveAttribute('data-required');
+ expect(switchElement).toHaveAttribute('data-disabled');
});
});
@@ -273,43 +267,31 @@ describe('Switch', () => {
const switchElement = screen.getByRole('switch');
expect(switchElement).toHaveAttribute('aria-describedby', 'description');
});
-
- it('supports aria-invalid', () => {
- render();
- const switchElement = screen.getByRole('switch');
- expect(switchElement).toHaveAttribute('aria-invalid', 'true');
- });
});
- describe('HTML Attributes', () => {
- it('supports name attribute', () => {
- render();
- const hiddenInput = document.querySelector('input[name="notifications"]');
- expect(hiddenInput).toHaveAttribute('name', 'notifications');
- });
-
+ describe('Data Attributes', () => {
it('supports data attributes', () => {
render();
const switchElement = screen.getByTestId('custom-switch');
expect(switchElement).toHaveAttribute('data-theme', 'dark');
});
- it('has data-state attribute for unchecked state', () => {
+ it('has data-unchecked attribute for unchecked state', () => {
render();
const switchElement = screen.getByRole('switch');
- expect(switchElement).toHaveAttribute('data-state', 'unchecked');
+ expect(switchElement).toHaveAttribute('data-unchecked');
});
- it('has data-state attribute for checked state', () => {
+ it('has data-checked attribute for checked state', () => {
render();
const switchElement = screen.getByRole('switch');
- expect(switchElement).toHaveAttribute('data-state', 'checked');
+ expect(switchElement).toHaveAttribute('data-checked');
});
it('has data-disabled attribute when disabled', () => {
render();
const switchElement = screen.getByRole('switch');
- expect(switchElement).toHaveAttribute('data-disabled', 'true');
+ expect(switchElement).toHaveAttribute('data-disabled');
});
it('does not have data-disabled when enabled', () => {
diff --git a/packages/raystack/components/switch/switch.module.css b/packages/raystack/components/switch/switch.module.css
index d6cce5ba9..9b1827901 100644
--- a/packages/raystack/components/switch/switch.module.css
+++ b/packages/raystack/components/switch/switch.module.css
@@ -21,24 +21,24 @@
height: 16px;
}
-.switch:not([data-disabled="true"]):hover {
+.switch:not([data-disabled]):hover {
background: var(--rs-color-background-neutral-secondary-hover);
}
-.switch[data-state="checked"] {
+.switch[data-checked] {
background: var(--rs-color-background-accent-emphasis);
}
-.switch[data-state="checked"]:not([data-disabled="true"]):hover {
+.switch[data-checked]:not([data-disabled]):hover {
background: var(--rs-color-background-accent-emphasis-hover);
}
-.switch[data-disabled="true"] {
+.switch[data-disabled] {
cursor: not-allowed;
background: var(--rs-color-background-neutral-primary);
}
-.switch[data-disabled="true"][data-state="checked"] {
+.switch[data-disabled][data-checked] {
background: var(--rs-color-background-accent-primary);
}
@@ -60,11 +60,11 @@
height: 12px;
}
-.switch[data-state="checked"] .thumb {
+.switch[data-checked] .thumb {
transform: translateX(16px);
}
/* Small switch thumb positioning */
-.switch.small[data-state="checked"] .thumb {
+.switch.small[data-checked] .thumb {
transform: translateX(12px);
}
diff --git a/packages/raystack/components/switch/switch.tsx b/packages/raystack/components/switch/switch.tsx
index bc0a40606..dcdc9ad52 100644
--- a/packages/raystack/components/switch/switch.tsx
+++ b/packages/raystack/components/switch/switch.tsx
@@ -1,6 +1,6 @@
-import { VariantProps, cva, cx } from 'class-variance-authority';
-import { Switch as SwitchPrimitive } from 'radix-ui';
-import { ComponentPropsWithoutRef, ElementRef, forwardRef } from 'react';
+import { Switch as SwitchPrimitive } from '@base-ui/react/switch';
+import { cva, type VariantProps } from 'class-variance-authority';
+import { forwardRef } from 'react';
import styles from './switch.module.css';
@@ -17,52 +17,19 @@ const switchVariants = cva(styles.switch, {
});
export interface SwitchProps
- extends ComponentPropsWithoutRef,
+ extends SwitchPrimitive.Root.Props,
VariantProps {}
-export const Switch = forwardRef<
- ElementRef,
- SwitchProps
->(
- (
- { className, disabled, required, size, name, value, ...props },
- forwardedRef
- ) => (
- <>
-
-
-
- {name && (
-
- )}
- >
+export const Switch = forwardRef(
+ ({ className, size, ...props }, forwardedRef) => (
+
+
+
)
);
-interface ThumbProps
- extends ComponentPropsWithoutRef {}
-
-const SwitchThumb = forwardRef<
- ElementRef,
- ThumbProps
->(({ className, ...props }, ref) => (
-
-));
-
Switch.displayName = 'Switch';