Skip to content

Instantly share code, notes, and snippets.

@Armster15
Last active May 8, 2022 22:53
Show Gist options
  • Save Armster15/bb4a1a81b372b8a214307de324622696 to your computer and use it in GitHub Desktop.
Save Armster15/bb4a1a81b372b8a214307de324622696 to your computer and use it in GitHub Desktop.
An unstyled React button with accessibility!
import React, { useState } from 'react';
import { useFocusVisibleListener } from '@react-aria/interactions';
import type { ButtonProps as HTMLButtonProps } from 'react-html-props';
export interface AccessibleButtonProps extends HTMLButtonProps {
/**
* Whether the button should lose it's focus when the mouse
* is released
* @default false
*/
unfocusOnMouseUp?: boolean;
/**
* Whether the button should lose it's focus when the mouse
* leaves the area of the actual button
* @default false
*/
unfocusOnMouseLeave?: boolean;
/**
* The class to apply on focus visible
* @default "focus-ring"
*/
focusRingClass?: string;
}
/**
* An unstyled button element that has accessibility built in.
* On focus visible, it applies the `focus-ring` class, as defined in
* index.css, and also removes any transitions
*/
export const AccessibleButton: React.FC<AccessibleButtonProps> = ({
onMouseUp,
onMouseLeave,
className,
unfocusOnMouseUp = false,
unfocusOnMouseLeave = false,
focusRingClass = 'focus-ring',
...props
}) => {
// react-aria does have a useFocusVisible hook but the problem
// with that is that it does not work without JS (bad if you
// are using something like Next.js), and the initial value is
// true, when I personally want it to be set by default to false
const [isFocusVisible, setIsFocusVisible] = useState(false);
// Triggered when focus visible changes to either true or false
useFocusVisibleListener((isFocusVisibleResult) => {
setIsFocusVisible(isFocusVisibleResult);
}, []);
return (
<button
className={className + " " + focusRingClass}
style={isFocusVisible ? { transition: 'none' } : {}}
onMouseUp={
unfocusOnMouseUp
? (e) => {
if (!isFocusVisible) {
e.currentTarget.blur();
}
onMouseUp?.(e);
}
: onMouseUp
}
onMouseLeave={
unfocusOnMouseLeave
? (e) => {
if (!isFocusVisible) {
e.currentTarget.blur();
}
onMouseLeave?.(e);
}
: onMouseLeave
}
{...props}
/>
);
};

This component does have a few required dependencies:

  1. @react-aria/interactions

    • For the wonderful useFocusVisible hook it provides
    • On Bundlephobia, the entire package at the time of writing weighs 24.8 kB minified, however the useFocusVisible hook itself weights ~7.2kB
  2. react-html-props

    • An amazing library that simplifies HTML props
    • This just provides a bunch of types for TypeScript, so nothing from this library should be included in the final build
/*
This is an example of a focus-ring class.
I used TailwindCSS but you can use anything really to define your focus-ring class
*/
@tailwind base;
@tailwind components;
@tailwind utilities;
.focus-ring {
@apply focus-visible:ring-4 focus-visible:ring-cyan-400 focus-visible:ring-offset-4 outline-none;
}
.focus-ring:focus-visible {
transition-duration: 0ms !important;
}
{
"name": "accessible-button",
"version": "1.0.0",
"main": "AccessibleButton.tsx",
"dependencies": {
"@react-aria/interactions": "^3.8.3"
},
"devDependencies": {
"react-html-props": "^1.0.32"
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment