Building web applications usually involves making provisions for user interactions. One of the significant ways of making provision for user interactions is through forms. Different form components exist for taking different kinds of input from the user. For example, a password component takes sensitive information from a user and masks it so that it is not visible.
Most times, the information you need to get from a user is boolean-like — for example, yes or no, true or false, enable or disable, on or off, etc. Traditionally, the checkbox form component is used for getting these kinds of input. However, in modern interface designs, toggle switches are commonly used as checkbox replacements, although there are some accessibility concerns.
In this tutorial, you will see how to build a custom toggle switch component with React. At the end of the tutorial, you will have a demo React app that uses your custom toggle switch component.
Here is a demo of the final application you will build in this tutorial:
Before getting started, you need the following:
Node.js and npm version 5.2 or higher installed on your machine. To install Node and check your npm version, please refer to the How to Install Node.js and Create a Local Development Environment guides for your environment. Using npm 5.2 or higher will allow you to leverage the npx
command. npx
will allow you to run create-react-app
without downloading the package globally.
This tutorial assumes that you are already familiar with React. If not, you can check the How To Code in React.js tutorial series or read React Documentation to learn more about React.
To get started, create a new React application with npx
and create-react-app
. You can name the application whatever you wish, but this tutorial will use react-toggle-switch
:
- npx create-react-app react-toggle-switch
Next, you will install the dependencies you need for your application. Use the terminal window to navigate to the project directory:
- cd react-toggle-switch
Run the following command to install the required dependencies:
- npm install bootstrap@4.5.0 lodash@4.17.15 prop-types@15.7.2 classnames@2.2.6 node-sass@4.14.1
Note: Ensure the version of node-sass
you are installing is compatible with your environment by referencing the quick guide for minimum support.
You installed the bootstrap
package as a dependency for your application since you will need some default styling. To include Bootstrap in the application, edit the src/index.js
file and add the following line before every other import
statement:
import "bootstrap/dist/css/bootstrap.min.css";
Start the application by running the following command with npm
:
- npm start
With the application started, development can begin. Notice that a browser tab was opened for you with live reloading functionality. Live reloading will keep in sync with changes to the application as you develop.
At this point, the application view should look like the following screenshot:
Next, you will create your toggle component.
ToggleSwitch
ComponentBefore building the component, create a new directory named components
inside the src
directory of your project.
- mkdir -p src/components
Next, create another new directory named ToggleSwitch
inside the components
directory.
- mkdir -p src/components/ToggleSwitch
You will create two new files inside src/components/ToggleSwitch
, namely: index.js
and index.scss
. Create and open the index.js
file with your favorite text editor:
- nano src/components/ToggleSwitch/index.js
Add the following content into the src/components/ToggleSwitch/index.js
file:
import PropTypes from 'prop-types';
import classnames from 'classnames';
import isString from 'lodash/isString';
import React, { Component } from 'react';
import isBoolean from 'lodash/isBoolean';
import isFunction from 'lodash/isFunction';
import './index.scss';
class ToggleSwitch extends Component {}
ToggleSwitch.propTypes = {
theme: PropTypes.string,
enabled: PropTypes.oneOfType([
PropTypes.bool,
PropTypes.func
]),
onStateChanged: PropTypes.func
}
export default ToggleSwitch;
In this code snippet, you created the ToggleSwitch
component and added type checks for some of its props.
theme
: is a string
indicating the style and color for the toggle switch.enabled
: can be either a boolean
or a function
that returns a boolean
, and it determines the state of the toggle switch when it is rendered.onStateChanged
: is a callback function that will be called when the state of the toggle switch changes. This is useful for triggering actions on the parent component when the switch is toggled.In the following code snippet, you initialize the state of the ToggleSwitch
component and define some component methods for getting the state of the toggle switch.
// ...
class ToggleSwitch extends Component {
state = { enabled: this.enabledFromProps() }
isEnabled = () => this.state.enabled
enabledFromProps() {
let { enabled } = this.props;
// If enabled is a function, invoke the function
enabled = isFunction(enabled) ? enabled() : enabled;
// Return enabled if it is a boolean, otherwise false
return isBoolean(enabled) && enabled;
}
}
// ...
Here, the enabledFromProps()
method resolves the enabled
prop that was passed and returns a boolean
indicating if the switch should be enabled when it is rendered. If the enabled
prop is a boolean
, it returns the boolean value. If it is a function
, it first invokes the function before determining if the returned value is a boolean
. Otherwise, it returns false
.
Notice that you used the return value from enabledFromProps()
to set the initial enabled
state. You also added the isEnabled()
method to get the current enabled
state.
Let’s go ahead and add the method that toggles the switch when it is clicked. Add the following code to the file:
// ...
class ToggleSwitch extends Component {
// ...other class members here
toggleSwitch = evt => {
evt.persist();
evt.preventDefault();
const { onClick, onStateChanged } = this.props;
this.setState({ enabled: !this.state.enabled }, () => {
const state = this.state;
// Augument the event object with SWITCH_STATE
const switchEvent = Object.assign(evt, { SWITCH_STATE: state });
// Execute the callback functions
isFunction(onClick) && onClick(switchEvent);
isFunction(onStateChanged) && onStateChanged(state);
});
}
}
// ...
Since this method will be triggered as a click
event listener, you have declared it with the evt
parameter. First, this method toggles the current enabled
state using the logical NOT
(!
) operator. When the state has been updated, it triggers the callback functions passed to the onClick
and onStateChanged
props.
Notice that since onClick
requires an event as its first argument, you augmented the event with an additional SWITCH_STATE
property containing the new state object. However, the onStateChanged
callback is called with the new state object.
Finally, let’s implement the render()
method of the ToggleSwitch
component. Add the following code to the file:
// ...
class ToggleSwitch extends Component {
// ...other class members here
render() {
const { enabled } = this.state;
// Isolate special props and store the remaining as restProps
const { enabled: _enabled, theme, onClick, className, onStateChanged, ...restProps } = this.props;
// Use default as a fallback theme if valid theme is not passed
const switchTheme = (theme && isString(theme)) ? theme : 'default';
const switchClasses = classnames(
`switch switch--${switchTheme}`,
className
)
const togglerClasses = classnames(
'switch-toggle',
`switch-toggle--${enabled ? 'on' : 'off'}`
)
return (
<div className={switchClasses} onClick={this.toggleSwitch} {...restProps}>
<div className={togglerClasses}></div>
</div>
)
}
}
// ...
A lot is going on in this render()
method, so let’s break it down a bit:
enabled
state is destructured from the component state.restProps
that will be passed down to the switch. This enables you to intercept and isolate the special props of the component.theme
and the enabled
state of the component.this.toggleSwitch
as the click
event listener on the switch.Save and close the file.
You’ve now created the ToggleSwitch
.
ToggleSwitch
Now that you have the ToggleSwitch
component and its required functionality, you can go ahead and write the styles for it.
Open the index.scss
file with your favorite text editor:
- nano src/components/ToggleSwitch/index.scss
Add the following code snippet to the file:
// DEFAULT COLOR VARIABLES
$ball-color: #ffffff;
$active-color: #62c28e;
$inactive-color: #cccccc;
// DEFAULT SIZING VARIABLES
$switch-size: 32px;
$ball-spacing: 2px;
$stretch-factor: 1.625;
// DEFAULT CLASS VARIABLE
$switch-class: 'switch-toggle';
/* SWITCH MIXIN */
@mixin switch($size: $switch-size, $spacing: $ball-spacing, $stretch: $stretch-factor, $color: $active-color, $class: $switch-class) {}
Here, you defined some default variables and created a switch
mixin. In the next section, you will implement the mixin, but first, let’s examine the parameters of the switch
mixin:
$size
: The height of the switch element. It must have a length unit. It defaults to 32px
.$spacing
: The space between the round ball and the switch container. It must have a length unit. It defaults to 2px
.$stretch
: A factor used to determine the extent to which the width of the switch element should be stretched. It must be a unitless number. It defaults to 1.625
.$color
: The color of the switch when in the active state. This must be a valid color value. Note that the circular ball is always white irrespective of this color.$class
: The base class for identifying the switch. This is used to create the state classes of the switch dynamically. It defaults to 'switch-toggle'
. Hence, the default state classes are .switch-toggle--on
and .switch-toggle--off
.Here is the implementation of the switch
mixin:
// ...
@mixin switch($size: $switch-size, $spacing: $ball-spacing, $stretch: $stretch-factor, $color: $active-color, $class: $switch-class) {
// SELECTOR VARIABLES
$self: '.' + $class;
$on: #{$self}--on;
$off: #{$self}--off;
// SWITCH VARIABLES
$active-color: $color;
$switch-size: $size;
$ball-spacing: $spacing;
$stretch-factor: $stretch;
$ball-size: $switch-size - ($ball-spacing * 2);
$ball-slide-size: ($switch-size * ($stretch-factor - 1) + $ball-spacing);
// SWITCH STYLES
height: $switch-size;
width: $switch-size * $stretch-factor;
cursor: pointer !important;
user-select: none !important;
position: relative !important;
display: inline-block;
&#{$on},
&#{$off} {
&::before,
&::after {
content: '';
left: 0;
position: absolute !important;
}
&::before {
height: inherit;
width: inherit;
border-radius: $switch-size / 2;
will-change: background;
transition: background .4s .3s ease-out;
}
&::after {
top: $ball-spacing;
height: $ball-size;
width: $ball-size;
border-radius: $ball-size / 2;
background: $ball-color !important;
will-change: transform;
transition: transform .4s ease-out;
}
}
&#{$on} {
&::before {
background: $active-color !important;
}
&::after {
transform: translateX($ball-slide-size);
}
}
&#{$off} {
&::before {
background: $inactive-color !important;
}
&::after {
transform: translateX($ball-spacing);
}
}
}
In this mixin, you start by setting some variables based on the parameters passed to the mixin. Next, you create the styles. Notice that you are using the ::after
and ::before
pseudo-elements to dynamically create the components of the switch. ::before
creates the switch container while ::after
creates the circular ball.
Also, notice how you constructed the state classes from the base class and assign them to variables. The $on
variable maps to the selector for the enabled state, while the $off
variable maps to the selector for the disabled state.
You also ensured that the base class (.switch-toggle
) must be used together with a state class (.switch-toggle--on
or .switch-toggle--off
) for the styles to be available. Hence, you used the &#{$on}
and &#{$off}
selectors.
Now that you have your switch
mixin, you will continue to create some themed styles for the toggle switch. You will create two themes: default
and graphite-small
.
Append the following code snippet to the src/components/ToggleSwitch/index.scss
file:
// ...
@function get-switch-class($selector) {
// First parse the selector using `selector-parse`
// Extract the first selector in the first list using `nth` twice
// Extract the first simple selector using `simple-selectors` and `nth`
// Extract the class name using `str-slice`
@return str-slice(nth(simple-selectors(nth(nth(selector-parse($selector), 1), 1)), 1), 2);
}
.switch {
$self: &;
$toggle: #{$self}-toggle;
$class: get-switch-class($toggle);
// default theme
&#{$self}--default > #{$toggle} {
// Always pass the $class to the mixin
@include switch($class: $class);
}
// graphite-small theme
&#{$self}--graphite-small > #{$toggle} {
// A smaller switch with a `gray` active color
// Always pass the $class to the mixin
@include switch($color: gray, $size: 20px, $class: $class);
}
}
Here you first create a Sass function named get-switch-class
that takes a $selector
as the parameter. It runs the $selector
through a chain of Sass functions and tries to extract the first class name. For example, if it receives:
.class-1 .class-2, .class-3 .class-4
, it returns class-1
..class-5.class-6 > .class-7.class-8
, it returns class-5
.Next, you define styles for the .switch
class. You dynamically set the toggle class to .switch-toggle
and assign it to the $toggle
variable. Notice that you assign the class name returned from the get-switch-class()
function call to the $class
variable. Finally, you include the switch
mixin with the necessary parameters to create the theme classes.
Notice that the structure of the selector for the themed switch looks like this: &#{$self}--default > #{$toggle}
(using the default theme as an example). Putting everything together, this means that the element’s hierarchy should look like the following in order for the styles to be applied:
<!-- Use the default theme: switch--default -->
<element class="switch switch--default">
<!-- The switch is in enabled state: switch-toggle--on -->
<element class="switch-toggle switch-toggle--on"></element>
</element>
Here is a demo showing what the toggle switch themes look like:
Now that you have the ToggleSwitch
React component with the necessary styling let’s go ahead and start creating the sample app you saw at the beginning of the tutorial.
Modify the src/App.js
file to look like the following code snippet:
import classnames from 'classnames';
import snakeCase from 'lodash/snakeCase';
import React, { Component } from 'react';
import Switch from './components/ToggleSwitch';
import './App.css';
// List of activities that can trigger notifications
const ACTIVITIES = [
'News Feeds', 'Likes and Comments', 'Live Stream', 'Upcoming Events',
'Friend Requests', 'Nearby Friends', 'Birthdays', 'Account Sign-In'
];
class App extends Component {
// Initialize app state, all activities are enabled by default
state = { enabled: false, only: ACTIVITIES.map(snakeCase) }
toggleNotifications = ({ enabled }) => {
const { only } = this.state;
this.setState({ enabled, only: enabled ? only : ACTIVITIES.map(snakeCase) });
}
render() {
const { enabled } = this.state;
const headingClasses = classnames(
'font-weight-light h2 mb-0 pl-4',
enabled ? 'text-dark' : 'text-secondary'
);
return (
<div className="App position-absolute text-left d-flex justify-content-center align-items-start pt-5 h-100 w-100">
<div className="d-flex flex-wrap mt-5" style={{width: 600}}>
<div className="d-flex p-4 border rounded align-items-center w-100">
<Switch theme="default"
className="d-flex"
enabled={enabled}
onStateChanged={this.toggleNotifications}
/>
<span className={headingClasses}>Notifications</span>
</div>
{/* ... Notification options here ... */}
</div>
</div>
);
}
}
export default App;
Here you initialize the ACTIVITIES
constant with an array of activities that can trigger notifications. Next, you initialized the app state with two properties:
enabled
: a boolean
that indicates whether notifications are enabled.only
: an array
that contains all the activities that are enabled to trigger notifications.You used the snakeCase
utility from Lodash to convert the activities to snakecase before updating the state. Hence, 'News Feeds'
becomes 'news_feeds'
.
Next, you defined the toggleNotifications()
method that updates the app state based on the state it receives from the notification switch. This is used as the callback function passed to the onStateChanged
prop of the toggle switch. Notice that when the app is enabled, all activities will be enabled by default, since the only
state property is populated with all the activities.
Finally, you rendered the DOM elements for the app and left a slot for the notification options, which will be added soon. At this point, the app should look like the following screenshot:
Next, go ahead and look for the line that has this comment:
{/* ... Notification options here ... */}
And replace it with the following content in order to render the notification options:
// ...
{ enabled && (
<div className="w-100 mt-5">
<div className="container-fluid px-0">
<div className="pt-5">
<div className="d-flex justify-content-between align-items-center">
<span className="d-block font-weight-bold text-secondary small">Email Address</span>
<span className="text-secondary small mb-1 d-block">
<small>Provide a valid email address with which to receive notifications.</small>
</span>
</div>
<div className="mt-2">
<input type="text" placeholder="mail@domain.com" className="form-control" style={{ fontSize: 14 }} />
</div>
</div>
<div className="pt-5 mt-4">
<div className="d-flex justify-content-between align-items-center border-bottom pb-2">
<span className="d-block font-weight-bold text-secondary small">Filter Notifications</span>
<span className="text-secondary small mb-1 d-block">
<small>Select the account activities for which to receive notifications.</small>
</span>
</div>
<div className="mt-5">
<div className="row flex-column align-content-start" style={{ maxHeight: 180 }}>
{ this.renderNotifiableActivities() }
</div>
</div>
</div>
</div>
</div>
) }
You may notice that you made a call to this.renderNotifiableActivities()
to render the activities. Let’s go ahead and implement this method and the other remaining methods.
Add the following methods to the App
component:
// ...
class App extends Component {
// ...
toggleActivityEnabled = activity => ({ enabled }) => {
let { only } = this.state;
if (enabled && !only.includes(activity)) {
only.push(activity);
return this.setState({ only });
}
if (!enabled && only.includes(activity)) {
only = only.filter(item => item !== activity);
return this.setState({ only });
}
}
renderNotifiableActivities() {
const { only } = this.state;
return ACTIVITIES.map((activity, index) => {
const key = snakeCase(activity);
const enabled = only.includes(key);
const activityClasses = classnames(
'small mb-0 pl-3',
enabled ? 'text-dark' : 'text-secondary'
);
return (
<div key={index} className="col-5 d-flex mb-3">
<Switch theme="graphite-small"
className="d-flex"
enabled={enabled}
onStateChanged={ this.toggleActivityEnabled(key) }
/>
<span className={activityClasses}>{ activity }</span>
</div>
);
})
}
// ...
}
Here, you have implemented the renderNotifiableActivities
method. You iterate through all the activities using ACTIVITIES.map()
and render each with a toggle switch for it. Notice that the toggle switch uses the graphite-small
theme. You also detect the enabled
state of each activity by checking whether it already exists in the only
state variable.
Finally, you defined the toggleActivityEnabled
method which was used to provide the callback function for the onStateChanged
prop of each activity’s toggle switch. You defined it as a higher-order function so that you can pass the activity as an argument and return the callback function. It checks if an activity is already enabled and updates the state accordingly.
Now, the app should look like the following screenshot:
If you prefer to have all the activities disabled by default, instead of enabled as shown in the initial screenshot, then you could make the following changes to the App
component:
[src/App.js]
// ...
class App extends Component {
// Initialize app state, all activities are disabled by default
state = { enabled: false, only: [] }
toggleNotifications = ({ enabled }) => {
const { only } = this.state;
this.setState({ enabled, only: enabled ? only : [] });
}
}
In this step, you have completed building your toggle switch. In the next step, you will learn how to improve the accessibility to the application.
Using toggle switches in your applications instead of traditional checkboxes can enable you to create neater interfaces, especially because it is challenging to style a traditional checkbox how you’d like.
However, using toggle switches instead of checkboxes has some accessibility issues, since the user-agent may not be able to interpret the component’s function correctly.
A few things can be done to improve the accessibility of the toggle switch and enable user-agents to understand the role correctly. For example, you can use the following ARIA attributes:
<switch-element tabindex="0" role="switch" aria-checked="true" aria-labelledby="#label-element"></switch-element>
You can also listen to more events on the toggle switch to create more ways the user can interact with the component.
In this tutorial, you created a custom toggle switch for your React applications with proper styling that supports different themes. You have explored how you can use it in your application instead of traditional checkboxes. Additionally, you explored the accessibility concerns involved and what you can do to make improvements.
For the complete source code of this tutorial, check out the react-toggle-switch-demo repository on GitHub. You can also get a live demo of this tutorial on Code Sandbox.
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!