Tutorial

How To Build a Custom Toggle Switch with React

JavaScriptReact

Introduction

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.

Table displaying Checkbox vs. Toggle Switch in disabled and enable states

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:

Animated Gif of Notifications Toggle Switch turning on and revealing Email Address field and Filter Notifications and turning off News Feeds, Likes and Comments, and Account Sign-In

Prerequisites

Before getting started, you need the following:

Step 1 — Getting Started

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:

src/index.js
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:

Initial View

Next, you will create your toggle component.

Step 2 — Creating the ToggleSwitch Component

Before 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:

src/components/ToggleSwitch/index.js
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.

Initializing the ToggleSwitch State

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.

src/components/ToggleSwitch/index.js
// ...

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.

Toggling the ToggleSwitch

Let’s go ahead and add the method that toggles the switch when it is clicked. Add the following code to the file:

src/components/ToggleSwitch/index.js
// ...

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.

Rendering the ToggleSwitch

Finally, let’s implement the render() method of the ToggleSwitch component. Add the following code to the file:

src/components/ToggleSwitch/index.js
// ...

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:

  1. First, the enabled state is destructured from the component state.
  2. Next, you destructure the component props and extract the restProps that will be passed down to the switch. This enables you to intercept and isolate the special props of the component.
  3. Next, you use classnames to construct the classes for the switch and the inner toggler, based on the theme and the enabled state of the component.
  4. Finally, you render the DOM elements with the appropriate props and classes. Notice that you passed in this.toggleSwitch as the click event listener on the switch.

Save and close the file.

You’ve now created the ToggleSwitch.

Step 3 — Styling the 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:

src/components/ToggleSwitch/index.scss
// 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.

Implementing the Switch Mixin

Here is the implementation of the switch mixin:

src/components/ToggleSwitch/index.scss
// ...

@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.

Creating Themed Switches

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:

src/components/ToggleSwitch/index.scss
// ...

@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:

Animated Gif of Default and Graphite-Small Toggle Switches turning on and off

Step 4 — Building the Sample App

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:

src/App.js
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:

Animated Gif of Notifications Toggle Switch turning on and off

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:

src/App.js
// ...

{ 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:

src/App.js
// ...

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:

Animated Gif of Notifications Toggle Switch turning on and revealing Email Address field and Filter Notifications and turning off News Feeds, Likes and Comments, and Account Sign-In

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.

Step 5 — Addressing Accessibility Concerns

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.

Conclusion

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.

0 Comments

Creative Commons License