Skip to content

Commit c6aa554

Browse files
feat(switch): add neutral variant and improve off-state accessibility (#251)
1 parent e6d2c5a commit c6aa554

File tree

9 files changed

+606
-203
lines changed

9 files changed

+606
-203
lines changed
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
"@cloudflare/kumo": minor
3+
---
4+
5+
Add neutral variant to Switch component and improve accessibility
6+
7+
- New `variant="neutral"` option: monochrome switch with squircle shape, matching the design from stratus
8+
- Improved off-state visibility for default variant with darker background/ring colors
9+
- Removed `error` variant (not useful for toggle switches)
10+
- Added defensive fallback so invalid variant values don't cause runtime crashes

packages/kumo-docs-astro/src/components/demos/SwitchDemo.tsx

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,87 @@ export function SwitchOnDemo() {
1919
export function SwitchDisabledDemo() {
2020
return <Switch label="Disabled" checked={false} disabled />;
2121
}
22+
23+
/** Neutral variant - monochrome switch for subtle, less prominent toggles */
24+
export function SwitchNeutralDemo() {
25+
const [checked, setChecked] = useState(false);
26+
return (
27+
<Switch
28+
label="Neutral switch"
29+
variant="neutral"
30+
checked={checked}
31+
onCheckedChange={setChecked}
32+
/>
33+
);
34+
}
35+
36+
/** Neutral variant in different states */
37+
export function SwitchNeutralStatesDemo() {
38+
return (
39+
<div className="flex flex-col gap-4">
40+
<Switch
41+
label="Neutral off"
42+
variant="neutral"
43+
checked={false}
44+
onCheckedChange={() => {}}
45+
/>
46+
<Switch
47+
label="Neutral on"
48+
variant="neutral"
49+
checked={true}
50+
onCheckedChange={() => {}}
51+
/>
52+
<Switch
53+
label="Neutral disabled"
54+
variant="neutral"
55+
checked={false}
56+
disabled
57+
/>
58+
</div>
59+
);
60+
}
61+
62+
/** All variants comparison */
63+
export function SwitchVariantsDemo() {
64+
return (
65+
<div className="flex flex-col gap-4">
66+
<Switch
67+
label="Default variant"
68+
checked={true}
69+
onCheckedChange={() => {}}
70+
/>
71+
<Switch
72+
label="Neutral variant"
73+
variant="neutral"
74+
checked={true}
75+
onCheckedChange={() => {}}
76+
/>
77+
</div>
78+
);
79+
}
80+
81+
/** All sizes comparison */
82+
export function SwitchSizesDemo() {
83+
return (
84+
<div className="flex flex-col gap-4">
85+
<Switch
86+
label="Small"
87+
size="sm"
88+
checked={true}
89+
onCheckedChange={() => {}}
90+
/>
91+
<Switch
92+
label="Base (default)"
93+
size="base"
94+
checked={true}
95+
onCheckedChange={() => {}}
96+
/>
97+
<Switch
98+
label="Large"
99+
size="lg"
100+
checked={true}
101+
onCheckedChange={() => {}}
102+
/>
103+
</div>
104+
);
105+
}

packages/kumo-docs-astro/src/pages/components/switch.mdx

Lines changed: 59 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ import {
1515
SwitchBasicDemo,
1616
SwitchOffDemo,
1717
SwitchOnDemo,
18+
SwitchDisabledDemo,
19+
SwitchNeutralDemo,
20+
SwitchNeutralStatesDemo,
21+
SwitchVariantsDemo,
22+
SwitchSizesDemo,
1823
} from "~/components/demos/SwitchDemo";
1924

2025
{/* Demo */}
@@ -74,12 +79,60 @@ export default function Example() {
7479
<SwitchOffDemo client:visible />
7580
</ComponentExample>
7681

77-
<Heading level={3}>On State</Heading>
78-
<ComponentExample
79-
code={`<Switch label="Switch" checked={true} onCheckedChange={() => {}} />`}
80-
>
81-
<SwitchOnDemo client:visible />
82-
</ComponentExample>
82+
<Heading level={3}>On State</Heading>
83+
<ComponentExample
84+
code={`<Switch label="Switch" checked={true} onCheckedChange={() => {}} />`}
85+
>
86+
<SwitchOnDemo client:visible />
87+
</ComponentExample>
88+
89+
<Heading level={3}>Disabled</Heading>
90+
<ComponentExample code={`<Switch label="Disabled" checked={false} disabled />`}>
91+
<SwitchDisabledDemo client:visible />
92+
</ComponentExample>
93+
94+
<Heading level={3}>Variants</Heading>
95+
96+
The Switch supports two variants: `default` (pill shape, blue when on) and `neutral` (squircle shape, monochrome).
97+
98+
<ComponentExample
99+
code={`<Switch label="Default variant" checked={true} onCheckedChange={() => {}} />
100+
<Switch label="Neutral variant" variant="neutral" checked={true} onCheckedChange={() => {}} />`}
101+
>
102+
<SwitchVariantsDemo client:visible />
103+
</ComponentExample>
104+
105+
<Heading level={3}>Neutral Variant</Heading>
106+
107+
The neutral variant uses monochrome colors and a squircle shape, ideal for subtle, less prominent toggles.
108+
109+
<ComponentExample
110+
code={`<Switch label="Neutral switch" variant="neutral" checked={checked} onCheckedChange={setChecked} />`}
111+
>
112+
<SwitchNeutralDemo client:visible />
113+
</ComponentExample>
114+
115+
<Heading level={3}>Neutral States</Heading>
116+
<ComponentExample
117+
code={`<Switch label="Neutral off" variant="neutral" checked={false} onCheckedChange={() => {}} />
118+
<Switch label="Neutral on" variant="neutral" checked={true} onCheckedChange={() => {}} />
119+
<Switch label="Neutral disabled" variant="neutral" checked={false} disabled />`}
120+
>
121+
<SwitchNeutralStatesDemo client:visible />
122+
</ComponentExample>
123+
124+
<Heading level={3}>Sizes</Heading>
125+
126+
Three sizes available: `sm`, `base` (default), and `lg`.
127+
128+
<ComponentExample
129+
code={`<Switch label="Small" size="sm" checked={true} onCheckedChange={() => {}} />
130+
<Switch label="Base (default)" size="base" checked={true} onCheckedChange={() => {}} />
131+
<Switch label="Large" size="lg" checked={true} onCheckedChange={() => {}} />`}
132+
>
133+
<SwitchSizesDemo client:visible />
134+
</ComponentExample>
135+
83136
</ComponentSection>
84137

85138
{/* API Reference */}

packages/kumo-figma/src/generators/switch.test.ts

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ const componentData = registry.components.Switch;
2727
const props = componentData.props;
2828
const variantProp = props.variant as {
2929
values: string[];
30-
classes: Record<string, string>;
3130
descriptions: Record<string, string>;
3231
default: string;
3332
};
@@ -45,17 +44,6 @@ describe("Switch Generator - Registry Validation", () => {
4544
expect(variantProp.values.length).toBeGreaterThan(0);
4645
});
4746

48-
it("should have classes defined for all variants", () => {
49-
for (const variant of variantProp.values) {
50-
// Note: Only "error" variant has classes defined in registry
51-
// "default" variant has no classes (uses default styles)
52-
if (variant === "error") {
53-
expect(variantProp.classes[variant]).toBeDefined();
54-
expect(typeof variantProp.classes[variant]).toBe("string");
55-
}
56-
}
57-
});
58-
5947
it("should have descriptions defined for all variants", () => {
6048
for (const variant of variantProp.values) {
6149
expect(variantProp.descriptions[variant]).toBeDefined();
@@ -103,8 +91,7 @@ describe("Switch Generator - Variant Configuration", () => {
10391
const config = getSwitchVariantConfig();
10492
expect(config.values).toBeDefined();
10593
expect(Array.isArray(config.values)).toBe(true);
106-
expect(config.classes).toBeDefined();
107-
expect(typeof config.classes).toBe("object");
94+
// Note: Switch variants don't use CSS classes - behavior is logic-based
10895
expect(config.descriptions).toBeDefined();
10996
expect(typeof config.descriptions).toBe("object");
11097
expect(config.default).toBeDefined();

packages/kumo-figma/src/generators/switch.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,6 @@ const switchProps = registry.components.Switch.props;
5757

5858
const variantProp = switchProps.variant as {
5959
values: string[];
60-
classes: Record<string, string>;
6160
descriptions: Record<string, string>;
6261
default: string;
6362
};
@@ -914,7 +913,6 @@ export async function generateSwitchGroupComponents(
914913
export function getSwitchVariantConfig() {
915914
return {
916915
values: variantProp.values,
917-
classes: variantProp.classes,
918916
descriptions: variantProp.descriptions,
919917
default: variantProp.default,
920918
};

0 commit comments

Comments
 (0)