Skip to content

Commit 5cfae2c

Browse files
camertronmperrotti
andauthored
Toggle switch styles (#2074)
* Add styles for the ToggleSwitch component * Add changeset * Disable stylelint temporarily; commit stylelint fixes * Refactor CSS; add stories * Clean up; rename ToggleSwitch-switch to ToggleSwitch-track * Address a bunch of PR feedback: * Rename ToggleSwitch-bg -> ToggleSwitch-icons * Rename ToggleSwitch-label -> ToggleSwitch-status * Collapse border-* styles into a single border-style property * Replace CSS with touch target @include * Remove unnecessary :after pseudo-element * Remove sr-only span, as it's redundant * Collapse border-* properties again (stylelint didn't like argument order last time) Co-authored-by: Mike Perrotti <[email protected]>
1 parent 8354de5 commit 5cfae2c

File tree

5 files changed

+337
-0
lines changed

5 files changed

+337
-0
lines changed

.changeset/smooth-lies-stare.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@primer/css": minor
3+
---
4+
5+
Add styles for the ToggleSwitch component
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import React from 'react'
2+
3+
export default {
4+
title: 'Components/ToggleSwitch',
5+
parameters: {
6+
layout: 'padded'
7+
},
8+
excludeStories: ['ToggleSwitchTemplate'],
9+
controls: { expanded: true },
10+
argTypes: {
11+
checked: {
12+
control: {type: 'boolean'},
13+
description: 'checkbox state'
14+
},
15+
disabled: {
16+
description: 'disabled field',
17+
control: {type: 'boolean'}
18+
},
19+
size: {
20+
options: ['medium', 'small'],
21+
control: {
22+
type: 'inline-radio'
23+
},
24+
description: 'size'
25+
},
26+
labelPosition: {
27+
options: ['start', 'end'],
28+
control: {
29+
type: 'inline-radio'
30+
},
31+
description: 'label position'
32+
}
33+
}
34+
}
35+
36+
function classNamesForSwitch(disabled, checked, size, labelPosition) {
37+
const classNames = ['ToggleSwitch'];
38+
39+
if (checked) {
40+
classNames.push("ToggleSwitch--checked")
41+
}
42+
if (disabled) {
43+
classNames.push("ToggleSwitch--disabled")
44+
}
45+
if (size === 'small') {
46+
classNames.push("ToggleSwitch--small")
47+
}
48+
if (labelPosition === 'end') {
49+
classNames.push('ToggleSwitch--statusAtEnd')
50+
}
51+
52+
return classNames.join(' ')
53+
}
54+
55+
export const ToggleSwitchTemplate = ({disabled, checked, size, labelPosition}) => (
56+
<>
57+
<toggle-switch class={classNamesForSwitch(disabled, checked, size, labelPosition)}>
58+
<span aria-hidden="true" className="ToggleSwitch-status">
59+
<div className="ToggleSwitch-statusOn" style={{visibility: checked ? 'visible' : 'hidden' }}>On</div>
60+
<div className="ToggleSwitch-statusOff" style={{visibility: checked ? 'hidden' : 'visible' }}>Off</div>
61+
</span>
62+
63+
<button
64+
className="ToggleSwitch-track"
65+
role="switch"
66+
aria-checked={checked ? 'true' : 'false'}
67+
aria-disabled={disabled ? "true" : "false"}>
68+
<div className="ToggleSwitch-icons" aria-hidden="true">
69+
<div className="ToggleSwitch-lineIcon">
70+
<svg
71+
width={size === 'small' ? 12 : 16}
72+
height={size === 'small' ? 12 : 16}
73+
viewBox="0 0 16 16"
74+
fill="currentColor"
75+
xmlns="http://www.w3.org/2000/svg">
76+
<path fill-rule="evenodd" d="M8 2a.75.75 0 0 1 .75.75v11.5a.75.75 0 0 1-1.5 0V2.75A.75.75 0 0 1 8 2Z" />
77+
</svg>
78+
</div>
79+
80+
<div className="ToggleSwitch-circleIcon">
81+
<svg
82+
width={size === 'small' ? 12 : 16}
83+
height={size === 'small' ? 12 : 16}
84+
viewBox="0 0 16 16"
85+
fill="currentColor"
86+
xmlns="http://www.w3.org/2000/svg">
87+
<path fill-rule="evenodd" d="M8 12.5a4.5 4.5 0 1 0 0-9 4.5 4.5 0 0 0 0 9ZM8 14A6 6 0 1 0 8 2a6 6 0 0 0 0 12Z" />
88+
</svg>
89+
</div>
90+
</div>
91+
92+
<div className="ToggleSwitch-knob" />
93+
</button>
94+
</toggle-switch>
95+
</>
96+
)
97+
98+
export const Playground = ToggleSwitchTemplate.bind({})
99+
Playground.args = {
100+
disabled: false,
101+
checked: false,
102+
size: 'medium',
103+
labelPosition: 'start'
104+
}

src/product/index.scss

+1
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,4 @@
2525
@import '../subhead/index.scss';
2626
@import '../timeline/index.scss';
2727
@import '../toasts/index.scss';
28+
@import '../toggle-switch/index.scss';

src/toggle-switch/index.scss

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
@import '../support/index.scss';
2+
@import './toggle-switch.scss';

src/toggle-switch/toggle-switch.scss

+225
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
.ToggleSwitch {
2+
align-items: center;
3+
display: inline-flex;
4+
gap: $spacer-2;
5+
6+
&:hover {
7+
.ToggleSwitch-knob {
8+
background-color: var(--color-btn-hover-bg);
9+
}
10+
}
11+
12+
&:active {
13+
.ToggleSwitch-knob {
14+
background-color: var(--color-btn-active-bg);
15+
}
16+
}
17+
}
18+
19+
.ToggleSwitch-track {
20+
position: relative;
21+
display: block;
22+
width: $spacer-8;
23+
height: $spacer-5;
24+
padding: 0;
25+
overflow: hidden;
26+
text-decoration: none;
27+
cursor: pointer;
28+
user-select: none;
29+
background-color: var(--color-switch-track-bg);
30+
border: $border-width $border-style var(--color-switch-track-border);
31+
border-radius: $border-radius;
32+
transition-timing-function: cubic-bezier(0.5, 1, 0.89, 1);
33+
transition-duration: 80ms;
34+
transition-property: background-color, border-color;
35+
appearance: none;
36+
37+
&:focus,
38+
&:focus-visible {
39+
outline-offset: 0;
40+
}
41+
42+
@media (pointer: coarse) {
43+
&::before {
44+
@include minTouchTarget(calc($spacer-6 + $spacer-1));
45+
}
46+
}
47+
48+
@media (prefers-reduced-motion) {
49+
transition: none;
50+
51+
* {
52+
transition: none;
53+
}
54+
}
55+
}
56+
57+
.ToggleSwitch-track[aria-checked='true'][aria-disabled='true'] {
58+
background-color: var(--color-canvas-subtle);
59+
border-color: var(--color-border-subtle);
60+
61+
&:hover,
62+
&:active {
63+
background-color: var(--color-canvas-subtle);
64+
65+
// This is the most straightforward way of setting the knob's styles when the
66+
// switch is both checked and disabled.
67+
68+
// stylelint-disable-next-line selector-max-specificity
69+
.ToggleSwitch-knob {
70+
background-color: var(--color-switch-knob-checked-disabled-bg);
71+
}
72+
}
73+
74+
.ToggleSwitch-knob {
75+
background-color: var(--color-switch-knob-checked-disabled-bg);
76+
}
77+
}
78+
79+
.ToggleSwitch-track[aria-checked='true'] {
80+
background-color: var(--color-switch-track-checked-bg);
81+
border-color: var(--color-switch-track-checked-border);
82+
83+
&:hover {
84+
background-color: var(--color-switch-track-checked-hover-bg);
85+
}
86+
87+
&:active {
88+
background-color: var(--color-switch-track-checked-active-bg);
89+
}
90+
91+
.ToggleSwitch-knob {
92+
background-color: var(--color-switch-knob-checked-bg);
93+
border: 0;
94+
transform: translateX(calc(100% + 1px));
95+
}
96+
97+
.ToggleSwitch-lineIcon {
98+
transform: translateX(0%);
99+
}
100+
101+
.ToggleSwitch-circleIcon {
102+
transform: translateX(100%);
103+
}
104+
}
105+
106+
.ToggleSwitch-track[aria-disabled='true'] {
107+
cursor: not-allowed;
108+
background-color: var(--color-canvas-subtle);
109+
border-color: var(--color-border-subtle);
110+
transition-property: none;
111+
112+
&:hover,
113+
&:active {
114+
.ToggleSwitch-knob {
115+
background-color: var(--color-btn-bg);
116+
}
117+
}
118+
119+
.ToggleSwitch-knob {
120+
border-color: var(--color-border-default);
121+
box-shadow: none;
122+
123+
&:hover,
124+
&:active {
125+
background-color: var(--color-btn-bg);
126+
}
127+
}
128+
129+
.ToggleSwitch-lineIcon {
130+
color: var(--color-fg-subtle);
131+
}
132+
133+
.ToggleSwitch-circleIcon {
134+
color: var(--color-fg-subtle);
135+
}
136+
}
137+
138+
.ToggleSwitch-icons {
139+
display: flex;
140+
align-items: center;
141+
width: 100%;
142+
height: 100%;
143+
overflow: hidden;
144+
}
145+
146+
.ToggleSwitch-lineIcon {
147+
line-height: 0;
148+
color: var(--color-accent-fg);
149+
transition-duration: 80ms;
150+
transition-property: transform;
151+
transform: translateX(-100%);
152+
flex: 1 0 50%;
153+
}
154+
155+
.ToggleSwitch-circleIcon {
156+
line-height: 0;
157+
color: var(--color-fg-default);
158+
transition-duration: 80ms;
159+
transition-property: transform;
160+
transform: translateX(0);
161+
flex: 1 0 50%;
162+
}
163+
164+
.ToggleSwitch-knob {
165+
position: absolute;
166+
top: -1px;
167+
bottom: -1px;
168+
z-index: 1;
169+
width: 50%;
170+
background-color: var(--color-btn-bg);
171+
border: $border-width $border-style var(--color-switch-track-border);
172+
border-radius: $border-radius;
173+
box-shadow: var(--color-shadow-medium), var(--color-btn-inset-shadow);
174+
transition-timing-function: cubic-bezier(0.5, 1, 0.89, 1);
175+
transition-duration: 80ms;
176+
transition-property: transform;
177+
transform: translateX(-1px);
178+
179+
@media (prefers-reduced-motion) {
180+
transition: none;
181+
}
182+
}
183+
184+
.ToggleSwitch-status {
185+
position: relative;
186+
font-size: $body-font-size;
187+
line-height: $body-line-height;
188+
color: var(--color-fg-default);
189+
text-align: right;
190+
}
191+
192+
.ToggleSwitch--small {
193+
.ToggleSwitch-status {
194+
font-size: $font-size-small;
195+
}
196+
197+
.ToggleSwitch-track {
198+
width: $spacer-7;
199+
height: $spacer-4;
200+
}
201+
}
202+
203+
.ToggleSwitch--disabled {
204+
.ToggleSwitch-status {
205+
color: var(--color-fg-muted);
206+
}
207+
}
208+
209+
.ToggleSwitch-statusOn {
210+
height: 0;
211+
visibility: hidden;
212+
}
213+
214+
.ToggleSwitch-statusOff {
215+
height: auto;
216+
visibility: visible;
217+
}
218+
219+
.ToggleSwitch--statusAtEnd {
220+
flex-direction: row-reverse;
221+
222+
.ToggleSwitch-status {
223+
text-align: left;
224+
}
225+
}

0 commit comments

Comments
 (0)