Skip to Content
Accessible Toggle Buttons – Material UI

Accessible Toggle Buttons – Material UI

If you have never worked with Material UI or with building Accessible React components, and want to get going fast, this guide is for you. In a similar article, I wrote about how to make Accessible Toggle Buttons with React from scratch. In this article, I’m going to cover those same principles but I’m using Material UI to speed up my development process. So if you’re looking for a quick project to get into React, Material UI, or Accessibility, you’ve come to the right place!

Estimated reading time: 13 minutes

Check out the source code here, and to see the finished product, check out the app here.

Brief Explanation of Accessibility Concepts In Our App

This toggle button app is completely keyboard accessible, screen reader accessible, and user friendly. We accomplish this by using a semantic HTML Button component styled by Material UI and extended by us. We incorporate the aria-pressed attribute to leverage the screen reader functionality to explain our buttons to the user. And finally, we utilize React’s conditional rendering and hidden elements to communicate state to our users regardless of their assistive technology.

Package Installations for React & Material UI

Start by creating a react app. Here is how I do that in VSCode.

  1. Open a project folder and then open a new terminal in VSCode
  2. In the terminal paste this and hit enter: npx create-react-app my-toggle-buttons
  3. Next paste this into the terminal and hit enter, (this will bring you into your working directory): cd my-toggle-buttons
  4. Finally Paste in this to start up your React app: npm start

That’s it, that will start off your React app 🎉

Now let’s install MUI while we’re at it:

  1. In your same terminal control + c to turn off the React App
  2. Now paste this into your terminal to install your Material UI dependencies: npm install @mui/material @emotion/react @emotion/styled @mui/icons-material

Well you’re all set! Everything you need to get started is now in your package.json and project folders. Let’s clean up our app files and get this show on the road.

Scaffolding The Accessible Toggle Buttons

  1. Delete the App.test.js, logo.svg, reportWebVitals.js and setupTests.js files from the /src directory.
  2. In the index.js file, remove import reportWebVitals from ‘./reportWebVitals’; and reportWebVitals(); from line 17 then save.
  3. Create a new folder inside the /src folder called ‘components
  4. Inside the /components folder, Create a file titled ‘MyButtonTheme.js’
  5. In the src/App.css file delete the contents of this file and save.
  6. In the src/App.js file delete everything inside of the return statement and place a div inside.

Your App.js file should look like this:

// import statements here
import * as React from 'react';

// react functional component here
function App() {  
return <div>My Buttons</div>
}

// export statements here
export default App;

Creating & Using A Material UI Theme

One of the great features of Material UI is its ability to set custom styles across all of your components easily by setting a “Theme”. Since I know I want my buttons to look really close to my original project, I’m going to use this theme functionality from Material UI.

In my MyButtonTheme.js file, the only import I’ll need is the createTheme import from Material UI. To learn more about the arguments the createTheme function accepts, check out the Material UI docs here for more.

Add this to the top of this file:

import { createTheme } from '@mui/material'

Next I’ll create my theme component. Add the following to your file to get started:

const MyButtonTheme = createTheme({})

export { MyButtonTheme }

Inside the MyButtonTheme component we’re going to set our properties like this:

const MyButtonTheme = createTheme({
components: {
  MuiButton: {
   variants: [],
   defaultProps: {},
 },
},
})

Inside our variant array we’re going to finally set our theme values:

variants: [
{
  props: {variant: 'bold'}, // name of our theme variant
  style: {
   fontWeight: 'bold',
   border: '0.125rem solid transparent',
   color: 'white',
   backgroundColor: '#595959',
   boxShadow: '0.125em 0.125em 0 #fff, 0.25em 0.25em #000',
  },
},
],

Note that in the props: { variant: } property we’ve setting a name here. We’re going to use this name to identify the theme variant in our components later.

Next up we’ll set the behaviors of our Button props with the defaultProps properties:

      defaultProps: {
        disableFocusRipple: true,
        disableRipple: true,
      },

If you want to see what properties you can adjust with the Button api, check out more on the Buttons properties here.

Your final file should look like this:

import { createTheme } from '@mui/material'

const MyButtonTheme = createTheme({
  components: {
    MuiButton: {
      variants: [
        {
          props: { variant: 'bold' },
          style: {
            fontWeight: 'bold',
            border: '4px solid black',
            color: 'black',
            boxShadow: '0.125em 0.125em 0 #fff, 0.25em 0.25em #000',
          },
        },
      ],
      defaultProps: {
        disableFocusRipple: true,
        disableRipple: true
      },
    },
  },
})
export { MyButtonTheme }

Adding Focus and hover styles

One benefit to styling your components by their accessibility attributes is that it forces you to think of usability on a broader scale and for more than one type of user. I’m using the aria-pressed attribute to add specificity to my CSS, which allows these styles to overwrite any Material UI styles, as well as styling for the various states of my button.

  • The [aria-pressed] attribute styles the button regardless of the state its in.
  • The [aria-pressed]:focus and :hover styles control how the button looks when it has keyboard or mouse focus.
  • The [aria-pressed=’true’]:focus which means the button is pressed will alter the look once more to communicate the state of our buttons.

In our App.css file add the following styles to further adjust the styling and behavior of our buttons:

[aria-pressed] {
    position: relative;
    top: -0.25rem;
    left: -0.25rem;
    width: 125px;
    height: 40px;
}
[aria-pressed]:focus, [aria-pressed]:hover, button:disabled {
    color: #000;
    background-color: lightgray;
    border: 2px solid #ffffff;
    outline: 2px solid transparent;
    box-shadow: 0 0 0 0.25rem #222;
}
[aria-pressed='true']:focus {
    box-shadow: 0 0 0 0.25rem #222, inset 0 0 0 0.15rem #595959,
      inset 0.25em 0.25em 0 #fff;
}

Incorporating the Theme in our App

Now it’s magic time. We need to use our theme in our app so all of this work we’ve done can be inherited by our components. To do that we first need to import our theme and our CSS into our App.js file.

Add the following to the top of your App.js file:

import { StyledEngineProvider, ThemeProvider } from '@mui/material';
import { MyButtonTheme } from './components/MyButtonTheme'
import './App.css'

Now inside your App function’s return statement let’s add these components in:

// react functional component here
function App() {  
return (
   <StyledEngineProvider injectFirst>
    <ThemeProvider theme={MyButtonTheme}>
     <div>My Buttons</div>
    </ThemeProvider>
   </StyledEngineProvider>
)
}

Let’s add in a quick Material UI component for layout.

In your Import statement area of the file add:

import Stack from '@mui/material/Stack';

Then in your App return statement add this Stack component like so:

// react functional component here
function App() {  
return (
  <Stack sx={{ padding: '2rem' }} direction='row' spacing={2}>
   <StyledEngineProvider injectFirst>
    <ThemeProvider theme={MyButtonTheme}>
     <div>My Buttons</div>
    </ThemeProvider>
   </StyledEngineProvider>
  </Stack>
)
}

Can we finally add some Buttons now?

You betchya! We are finally at the place where we can use our custom theme in conjunction with Material UI Button components to make accessible toggle buttons. Let’s get right into it.

In your App.js file, first import the Material UI Button in your import statement section:

import Button from '@mui/material/Button';
import DeleteIcon from '@mui/icons-material/Delete';

Next, inside of your return statement, remove the div and replace it with your first button:

<Button
  variant='bold'
  size='large'
  sx={{ ml: 2 }}
  startIcon={<DeleteIcon />}
>
Delete
</Button>

As you can see, we’ve set our variant to our custom theme variant name which is “bold” (as described earlier in this post).

Next we’re going to add two more buttons just like this but that use different icons and have different functionality.

I’ll import the Icons I’m going to be using:

import ThumbUpOffAltIcon from '@mui/icons-material/ThumbUpOffAlt'
import SendIcon from '@mui/icons-material/Send';

Then I’ll add these two buttons below the first:

          <Button
            variant='bold'
            size='large'
            sx={{ ml: 2 }}
            endIcon={<SendIcon />}
          >
           Send
          </Button>
          <Button
            variant='bold'
            size='large'
            sx={{ ml: 2 }}
            endIcon={<ThumbUpOffAltIcon />}
          >
           Like
          </Button>

Accessible Toggle Buttons – Audio Feedback & Screen readers

Now that we have our Buttons in place and looking great, I want to add some functionality to them. I want them to do something when they get clicked. I’ll start by handling what they should do for users who use a screenreader.

React.useState for aria states

I’m attaching state to the aria-pressed and disabled attributes. When the button is clicked, the state of the aria-pressed and disabled attributes will temporarily be true.

Below are the pieces of state for each button. Since the value is a boolean and needs to be the same for the disabled attribute and the aria-pressed attribute, I just created the state like this for each button:

// set state for the button props
  const [deletAble, setDeleteAble] = React.useState(false)
  const [sendable, setSendable] = React.useState(false)
  const [likable, setLikable] = React.useState(false)

Then apply the state to your buttons:

<Button
  variant='bold'
  size='large'
  sx={{ ml: 2 }}
  disabled={deletAble}
  aria-pressed={deletAble}
  startIcon={<DeleteIcon />}
>
Delete
</Button>          
<Button
  variant='bold'
  size='large'
  sx={{ ml: 2 }}
  disabled={sendable}
  aria-pressed={sendable}
  endIcon={<SendIcon />}
>
 Send
</Button>
<Button
  variant='bold'
  size='large'
  sx={{ ml: 2 }}
  disabled={likable}
  aria-pressed={likable}
  endIcon={<ThumbUpOffAltIcon />}
>
  Like
</Button>

OnClick functions per button

I’ve added an onClick function per button. Let’s just pretend that each button truly does perform a different function when its clicked.

Below, add these functions before your return statement and after your state declarations:

// event handlers for each button
  const onDelete = () => {
    setDeleteAble(true)
    setTimeout(() => {
      setDeleteAble(false)
    }, 5000)
  }
  const onSend = () => {
    setSendable(true)
    setTimeout(() => {
      setSendable(false)
    }, 5000)
  }
  const onLike = () => {
    setLikable(true)
    setTimeout(() => {
      setLikable(false)
      }, 5000)
  }

Each function is using a setTimeout function to reset the state of the button. This allows us to pretend that the button is actually communicating to something like a server or an API.

Each of our Buttons will now need to call the appropriate function when clicked.

Apply the onClick functions like so:

<Button
  variant='bold'
  size='large'
  sx={{ ml: 2 }}
  onClick={onDelete}
  disabled={deletAble}
  aria-pressed={deletAble}
  startIcon={<DeleteIcon />}
>
Delete
</Button>          
<Button
  variant='bold'
  size='large'
  sx={{ ml: 2 }}
  onClick={onSend}
  disabled={sendable}
  aria-pressed={sendable}
  endIcon={<SendIcon />}
>
 Send
</Button>
<Button
  variant='bold'
  size='large'
  sx={{ ml: 2 }}
  onClick={onLike}
  disabled={likable}
  aria-pressed={likable}
  endIcon={<ThumbUpOffAltIcon />}
>
  Like
</Button>

What the screenreader says about state

The screenreader will alert the user that the button is a toggle button when the aria-pressed attribute is present. The screenreader will also read out that the button is ‘dimmed’ while the button is disabled. In this way the state of the button, and the state of the app are communicated to the user.

Aria-Live vs Conditional Rendering

While you might be tempted to include an Aria-Live region to explain what’s going on to a screenreader, I did not use an Aria-Live region in this app. The reason is because a conditionally rendered div of text will be read out to the screen reader but by adding the aria-live=”polite” attribute, the same text would be read out twice instead of once. This isn’t great for user experience so instead of using a polite region to announce some content has changed to the user, I’m conditionally rendering a hidden div with the active verb of the button.

In the code below, if the deletable state is true, (meaning the aria-pressed attribute and the disabled attribute are both “true”), then I will render a hidden div with the active verb. I’m using a React.Fragment to prevent unnecessary nesting within my app. (You may notice I’m rendering the CircularProgress component from Material UI, don’t worry we’ll get to that in the next section).

{deletAble === true ? (
              <>
                <CircularProgress style={{ width: '20px', height: '20px' }} />
                <div className='visually-hidden'>
                  Deleting
                </div>
              </>
            ) : (
              'Delete'
            )}

As you can see there is a class applied to this div, use the CSS below in your App.css file to make this class hide the text for you.

.visually-hidden {
    position: absolute !important;
    clip: rect(1px, 1px, 1px, 1px) !important;
    padding: 0 !important;
    border: 0 !important;
    height: 1px !important;
    width: 1px !important;
    overflow: hidden !important;
}

Visual Feedback for React Toggle Buttons – Cognitive Load and Color Dependence

Taking into account the concept above, I am also conditionally rendering a circular progress bar component from Material UI. This ‘processing’ circle, along with the temporary ‘disabled’ state communicate visually to users that the button is busy doing something and can’t be pressed again.

Adding the spinner to appear on the button and removing the text of the button visually communicate to the user that something is going on. The disabled button inherits styles from Material UI that prevent the button from being usable while also visually communicating that the button is dimmed.

Let’s take a look below at the Circular Progress component:

{sendable === true ? (
              <>
                <CircularProgress
                  color='success'
                  style={{ width: '20px', height: '20px' }}
                />
                <div className='visually-hidden'>Sending</div>
              </>
            ) : (
              'Send'
            )}

I’ve added the color prop of ‘success’ to this component. That will change the color of this spinnner component to green. Which is great that’s my favorite color and it suggests that the request I sent by clicking the button is a success 😀. However, color dependence isn’t great for usability 😔.

People with low vision, or color blindness won’t know that the message was successfully sent. The reason, in this instance, it’s okay that the color is green is because the component is going to communicate that something is happening by its very nature. I could choose any color for this CircularProgress component it doesn’t really matter, but it is important to think about.

Thought Exercise: What would you do instead to communicate to the user if the request was a success or failure?

Tab Sequence

Note that while the button has a disabled state its tabindex is set to -1. This comes from Material UI. The disabled state removes the button from the tab order preventing the keyboard user from selecting the button again while it’s disabled. This is great for usability! Not only are keyboard users unable to hit enter (or space) on the button again but you can’t accidentally click the button again either if let’s say your cat decides to walk all over your keyboard.🐱

The complete App.js file

Your App.js file should now look like this:

import * as React from 'react';
import { StyledEngineProvider, ThemeProvider } from '@mui/material';
import CircularProgress from '@mui/material/CircularProgress'
import Button from '@mui/material/Button';
import DeleteIcon from '@mui/icons-material/Delete';
import ThumbUpOffAltIcon from '@mui/icons-material/ThumbUpOffAlt'
import SendIcon from '@mui/icons-material/Send';
import Stack from '@mui/material/Stack';
import { MyButtonTheme } from './components/MyButtonTheme'
import './App.css'

function App() {
// set state for the button props
  const [deletAble, setDeleteAble] = React.useState(false)
  const [sendable, setSendable] = React.useState(false)
  const [likeable, setLikeable] = React.useState(false)

// event handlers for each button
  const onDelete = () => {
    setDeleteAble(true)
    setTimeout(() => {
      setDeleteAble(false)
    }, 5000)
  }
  const onSend = () => {
    setSendable(true)
    setTimeout(() => {
      setSendable(false)
    }, 5000)
  }
  const onLike = () => {
    setLikeable(true)
    setTimeout(() => {
      setLikeable(false)
      }, 5000)
  }
  return (
    <Stack sx={{ padding: '2rem' }} direction='row' spacing={2}>
      <StyledEngineProvider injectFirst>
        <ThemeProvider theme={MyButtonTheme}>
          <Button
            variant='bold'
            size='large'
            sx={{ ml: 2 }}
            onClick={onDelete}
            disabled={deletAble}
            aria-pressed={deletAble}
            startIcon={<DeleteIcon />}
          >
            {deletAble === true ? (
              <>
                <CircularProgress style={{ width: '20px', height: '20px' }} />
                <div className='visually-hidden'>
                  Deleting
                </div>
              </>
            ) : (
              'Delete'
            )}
          </Button>
          <Button
            variant='bold'
            size='large'
            sx={{ ml: 2 }}
            onClick={onSend}
            disabled={sendable}
            aria-pressed={sendable}
            endIcon={<SendIcon />}
          >
            {sendable === true ? (
              <>
                <CircularProgress style={{ width: '20px', height: '20px' }} />
                <div className='visually-hidden'>
                  Sending
                </div>
              </>
            ) : (
              'Send'
            )}
          </Button>
          <Button
            aria-pressed={likeable}
            variant='bold'
            size='large'
            sx={{ ml: 2 }}
            disabled={likeable}
            onClick={onLike}
            startIcon={<ThumbUpOffAltIcon />}
          >
            {likeable === true ? (
              <>
                <CircularProgress style={{ width: '20px', height: '20px' }} />
                <div className='visually-hidden'>Liking</div>
              </>
            ) : (
              'Like'
            )}
          </Button>
        </ThemeProvider>
      </StyledEngineProvider>
    </Stack>
  )
}

export default App;

In conclusion

Now we have created a React App, we have installed Material UI, we extended Material UI by making our own Theme, and we applied that theme to our Buttons. We have utilized the Accessible nature of the Material UI buttons and have included the aria-pressed and disabled attributes to control and communicate the state of our buttons. Finally we took advantage of React conditional rendering to render a spinner and a hidden text element to communicate the status of our button to a user regardless of their assistive technology.

These buttons are now user friendly for keyboard and screen reader users and so many more!

Photo by analuisa gamboa on Unsplash