Skip to content

Toggle switch styles #2074

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
May 23, 2022
Merged
5 changes: 5 additions & 0 deletions .changeset/smooth-lies-stare.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@primer/css": minor
---

Add styles for the ToggleSwitch component
104 changes: 104 additions & 0 deletions docs/src/stories/components/ToggleSwitch/ToggleSwitch.stories.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import React from 'react'

export default {
title: 'Components/ToggleSwitch',
parameters: {
layout: 'padded'
},
excludeStories: ['ToggleSwitchTemplate'],
controls: { expanded: true },
argTypes: {
checked: {
control: {type: 'boolean'},
description: 'checkbox state'
},
disabled: {
description: 'disabled field',
control: {type: 'boolean'}
},
size: {
options: ['medium', 'small'],
control: {
type: 'inline-radio'
},
description: 'size'
},
labelPosition: {
options: ['start', 'end'],
control: {
type: 'inline-radio'
},
description: 'label position'
}
}
}

function classNamesForSwitch(disabled, checked, size, labelPosition) {
const classNames = ['ToggleSwitch'];

if (checked) {
classNames.push("ToggleSwitch--checked")
}
if (disabled) {
classNames.push("ToggleSwitch--disabled")
}
if (size === 'small') {
classNames.push("ToggleSwitch--small")
}
if (labelPosition === 'end') {
classNames.push('ToggleSwitch--statusAtEnd')
}

return classNames.join(' ')
}

export const ToggleSwitchTemplate = ({disabled, checked, size, labelPosition}) => (
<>
<toggle-switch class={classNamesForSwitch(disabled, checked, size, labelPosition)}>
<span aria-hidden="true" className="ToggleSwitch-status">
<div className="ToggleSwitch-statusOn" style={{visibility: checked ? 'visible' : 'hidden' }}>On</div>
<div className="ToggleSwitch-statusOff" style={{visibility: checked ? 'hidden' : 'visible' }}>Off</div>
</span>

<button
className="ToggleSwitch-track"
role="switch"
aria-checked={checked ? 'true' : 'false'}
aria-disabled={disabled ? "true" : "false"}>
<div className="ToggleSwitch-icons" aria-hidden="true">
<div className="ToggleSwitch-lineIcon">
<svg
width={size === 'small' ? 12 : 16}
height={size === 'small' ? 12 : 16}
viewBox="0 0 16 16"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg">
<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" />
</svg>
</div>

<div className="ToggleSwitch-circleIcon">
<svg
width={size === 'small' ? 12 : 16}
height={size === 'small' ? 12 : 16}
viewBox="0 0 16 16"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg">
<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" />
</svg>
</div>
</div>

<div className="ToggleSwitch-knob" />
</button>
</toggle-switch>
</>
)

export const Playground = ToggleSwitchTemplate.bind({})
Playground.args = {
disabled: false,
checked: false,
size: 'medium',
labelPosition: 'start'
}
1 change: 1 addition & 0 deletions src/product/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,4 @@
@import '../subhead/index.scss';
@import '../timeline/index.scss';
@import '../toasts/index.scss';
@import '../toggle-switch/index.scss';
2 changes: 2 additions & 0 deletions src/toggle-switch/index.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
@import '../support/index.scss';
@import './toggle-switch.scss';
225 changes: 225 additions & 0 deletions src/toggle-switch/toggle-switch.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
.ToggleSwitch {
align-items: center;
display: inline-flex;
gap: $spacer-2;

&:hover {
.ToggleSwitch-knob {
background-color: var(--color-btn-hover-bg);
}
}

&:active {
.ToggleSwitch-knob {
background-color: var(--color-btn-active-bg);
}
}
}

.ToggleSwitch-track {
position: relative;
display: block;
width: $spacer-8;
height: $spacer-5;
padding: 0;
overflow: hidden;
text-decoration: none;
cursor: pointer;
user-select: none;
background-color: var(--color-switch-track-bg);
border: $border-width $border-style var(--color-switch-track-border);
border-radius: $border-radius;
transition-timing-function: cubic-bezier(0.5, 1, 0.89, 1);
transition-duration: 80ms;
transition-property: background-color, border-color;
appearance: none;

&:focus,
&:focus-visible {
outline-offset: 0;
}

@media (pointer: coarse) {
&::before {
@include minTouchTarget(calc($spacer-6 + $spacer-1));
}
}

@media (prefers-reduced-motion) {
transition: none;

* {
transition: none;
}
}
}

.ToggleSwitch-track[aria-checked='true'][aria-disabled='true'] {
background-color: var(--color-canvas-subtle);
border-color: var(--color-border-subtle);

&:hover,
&:active {
background-color: var(--color-canvas-subtle);

// This is the most straightforward way of setting the knob's styles when the
// switch is both checked and disabled.

// stylelint-disable-next-line selector-max-specificity
.ToggleSwitch-knob {
background-color: var(--color-switch-knob-checked-disabled-bg);
}
}

.ToggleSwitch-knob {
background-color: var(--color-switch-knob-checked-disabled-bg);
}
}

.ToggleSwitch-track[aria-checked='true'] {
background-color: var(--color-switch-track-checked-bg);
border-color: var(--color-switch-track-checked-border);

&:hover {
background-color: var(--color-switch-track-checked-hover-bg);
}

&:active {
background-color: var(--color-switch-track-checked-active-bg);
}

.ToggleSwitch-knob {
background-color: var(--color-switch-knob-checked-bg);
border: 0;
transform: translateX(calc(100% + 1px));
}

.ToggleSwitch-lineIcon {
transform: translateX(0%);
}

.ToggleSwitch-circleIcon {
transform: translateX(100%);
}
}

.ToggleSwitch-track[aria-disabled='true'] {
cursor: not-allowed;
background-color: var(--color-canvas-subtle);
border-color: var(--color-border-subtle);
transition-property: none;

&:hover,
&:active {
.ToggleSwitch-knob {
background-color: var(--color-btn-bg);
}
}

.ToggleSwitch-knob {
border-color: var(--color-border-default);
box-shadow: none;

&:hover,
&:active {
background-color: var(--color-btn-bg);
}
}

.ToggleSwitch-lineIcon {
color: var(--color-fg-subtle);
}

.ToggleSwitch-circleIcon {
color: var(--color-fg-subtle);
}
}

.ToggleSwitch-icons {
display: flex;
align-items: center;
width: 100%;
height: 100%;
overflow: hidden;
}

.ToggleSwitch-lineIcon {
line-height: 0;
color: var(--color-accent-fg);
transition-duration: 80ms;
transition-property: transform;
transform: translateX(-100%);
flex: 1 0 50%;
}

.ToggleSwitch-circleIcon {
line-height: 0;
color: var(--color-fg-default);
transition-duration: 80ms;
transition-property: transform;
transform: translateX(0);
flex: 1 0 50%;
}

.ToggleSwitch-knob {
position: absolute;
top: -1px;
bottom: -1px;
z-index: 1;
width: 50%;
background-color: var(--color-btn-bg);
border: $border-width $border-style var(--color-switch-track-border);
border-radius: $border-radius;
box-shadow: var(--color-shadow-medium), var(--color-btn-inset-shadow);
transition-timing-function: cubic-bezier(0.5, 1, 0.89, 1);
transition-duration: 80ms;
transition-property: transform;
transform: translateX(-1px);

@media (prefers-reduced-motion) {
transition: none;
}
}

.ToggleSwitch-status {
position: relative;
font-size: $body-font-size;
line-height: $body-line-height;
color: var(--color-fg-default);
text-align: right;
}

.ToggleSwitch--small {
.ToggleSwitch-status {
font-size: $font-size-small;
}

.ToggleSwitch-track {
width: $spacer-7;
height: $spacer-4;
}
}

.ToggleSwitch--disabled {
.ToggleSwitch-status {
color: var(--color-fg-muted);
}
}

.ToggleSwitch-statusOn {
height: 0;
visibility: hidden;
}

.ToggleSwitch-statusOff {
height: auto;
visibility: visible;
}

.ToggleSwitch--statusAtEnd {
flex-direction: row-reverse;

.ToggleSwitch-status {
text-align: left;
}
}