An accessible WAI-ARIA 1.1-compliant Radio Group React component.
yarn add @palmerhq/radio-group
Or try it out in your browser on CodeSandbox
Note: This package uses
Array.prototype.findIndex
, so be sure that you have properly polyfilled.
import * as React from 'react';
import { RadioGroup, Radio } from '@palmerhq/radio-group';
import '@palmerhq/radio-group/styles.css'; // use the default styles
function App() {
const [value, setValue] = React.useState<string | undefined>();
return (
<>
<h3 id="color">Color</h3>
<RadioGroup
labelledBy="color"
value={value}
onChange={value => setValue(value)}
>
<Radio value="blue">Blue</Radio>
<Radio value="red">Red</Radio>
<Radio value="green">Green</Radio>
</RadioGroup>
</>
);
}
import * as React from 'react';
import { Formik, Form, useField } from 'formik';
import { RadioGroup, Radio } from '@palmerhq/radio-group';
import '@palmerhq/radio-group/styles.css'; // use the default styles
function FRadioGroup(props) {
const [{ onChange, onBlur, ...field }] = useField(props.name);
return (
<RadioGroup
{...props}
{...field}
labelledBy={props.name}
onBlur={onBlur(props.name)}
onChange={onChange(props.name)}
/>
);
}
function App() {
return (
<Formik
initialValues={{ color: '' }}
validationSchema={Yup.object().shape({
color: Yup.string().required(),
})}
onSubmit={(values, { setSubmitting }) => {
setTimeout(() => {
alert(JSON.stringify(values, null, 2));
setSubmitting(false);
}, 500);
}}
>
<Form>
<h3 id="color">Color</h3>
<FRadioGroup name="color">
<Radio value="blue">Blue</Radio>
<Radio value="red">Red</Radio>
<Radio value="green">Green</Radio>
</FRadioGroup>
</Form>
</Formik>
);
}
This renders a div
and will pass through all props to the DOM element. It's children must be <Radio>
components.
This should match the id
you used to label the radio group.
<h3 id="color">Color</h3>
<RadioGroup labelledBy="color">
{/* ... */}
</RadioGroup>
A callback function that will be fired with the value
of the newly selected item.
import * as React from 'react';
import { RadioGroup, Radio } from '@palmerhq/radio-group';
import '@palmerhq/radio-group/styles.css'; // use the default styles
function App() {
const [value, setValue] = React.useState<string | undefined>();
return (
<>
<h3 id="color">Color</h3>
<RadioGroup
labelledBy="color"
value={value}
onChange={value => setValue(value)}
>
<Radio value="blue">Blue</Radio>
<Radio value="red">Red</Radio>
<Radio value="green">Green</Radio>
</RadioGroup>
</>
);
}
Required
The children of a <RadioGroup>
can ONLY be <Radio>
components. In order to support compliant keyboard behavior, each sibling must know the value of the whole group and so React.Children.map
is used internally.
<h3 id="color">Color</h3>
<RadioGroup labelledBy="color">
{/* ... */}
</RadioGroup>
Required
The current value of the radio group. This is shallowly compared to each value
prop of the child <Radio>
components to determine which item is active.
Component to use a the wrapper. Default is <div>
.
Whether to autoFocus the selected radio option.
This renders a div
with a data attribute data-palmerhq-radio
and all the relevant perfect aria attributes. The React component will pass through all props to the DOM element.
Required
The value of the radio button. This will be set / passed back to the <RadioGroup onChange>
when the item is selected.
Callback function for when the item is focused. When focused, a data attribute data-palmerhq-radio-focus
is set to "true"
. You can thus apply the selector to manage focus style like so:
[data-palmerhq-radio][data-palmerhq-radio-focus='true'] {
background: blue;
}
Callback function for when the item is blurred
Component to use as radio. Default is <div>
.
For reference, the underlying HTML DOM structure are all div
s and looks as follows.
<div role="radiogroup" aria-labelledby="color" data-palmerhq-radio-group="true">
<div
role="radio"
tabindex="0"
aria-checked="false"
data-palmerhq-radio="true"
data-palmerhq-radio-focus="false"
>
Red
</div>
<div
role="radio"
tabindex="-1"
aria-checked="false"
data-palmerhq-radio="true"
data-palmerhq-radio-focus="false"
>
Green
</div>
<div
role="radio"
tabindex="-1"
aria-checked="false"
data-palmerhq-radio="true"
data-palmerhq-radio-focus="false"
>
Blue
</div>
</div>
These are the default styles. Copy and paste the following into your app to customize them.
[data-palmerhq-radio-group] {
padding: 0;
margin: 0;
list-style: none;
}
[data-palmerhq-radio-group]:focus {
outline: none;
}
[data-palmerhq-radio] {
border: 2px solid transparent;
border-radius: 5px;
display: inline-block;
position: relative;
padding: 0.125em;
padding-left: 1.5em;
padding-right: 0.5em;
cursor: default;
outline: none;
}
[data-palmerhq-radio] + [data-palmerhq-radio] {
margin-left: 1em;
}
[data-palmerhq-radio]::before,
[data-palmerhq-radio]::after {
position: absolute;
top: 50%;
left: 7px;
transform: translate(-20%, -50%);
content: '';
}
[data-palmerhq-radio]::before {
width: 14px;
height: 14px;
border: 1px solid hsl(0, 0%, 66%);
border-radius: 100%;
background-image: linear-gradient(to bottom, hsl(300, 3%, 93%), #fff 60%);
}
[data-palmerhq-radio]:active::before {
background-image: linear-gradient(
to bottom,
hsl(300, 3%, 73%),
hsl(300, 3%, 93%)
);
}
[data-palmerhq-radio][aria-checked='true']::before {
border-color: hsl(216, 80%, 50%);
background: hsl(217, 95%, 68%);
background-image: linear-gradient(
to bottom,
hsl(217, 95%, 68%),
hsl(216, 80%, 57%)
);
}
[data-palmerhq-radio][aria-checked='true']::after {
display: block;
border: 0.1875em solid #fff;
border-radius: 100%;
transform: translate(25%, -50%);
}
[data-palmerhq-radio][aria-checked='mixed']:active::before,
[data-palmerhq-radio][aria-checked='true']:active::before {
background-image: linear-gradient(
to bottom,
hsl(216, 80%, 57%),
hsl(217, 95%, 68%) 60%
);
}
[data-palmerhq-radio]:hover::before {
border-color: hsl(216, 94%, 65%);
}
[data-palmerhq-radio][data-palmerhq-radio-focus='true'] {
border-color: hsl(216, 94%, 73%);
background-color: hsl(216, 80%, 97%);
}
[data-palmerhq-radio]:hover {
background-color: hsl(216, 80%, 92%);
}
- Uses CSS attribute selectors for synchronizing
aria-checked
state with the visual state indicator. - Uses CSS
:hover
and:focus
pseudo-selectors for styling visual keyboard focus and hover. - Focus indicator encompasses both radio button and label, making it easier to perceive which option is being chosen.
- Hover changes background of both radio button and label, making it easier to perceive that clicking either the label or button will activate the radio button.
Key | Function |
---|---|
Tab |
|
Space |
|
Right arrow |
|
Down arrow |
|
Left arrow |
|
Up arrow |
|
Role | Attributes | Element | Usage |
---|---|---|---|
radiogroup |
div |
|
|
aria-labelledby="[IDREF]" |
div |
Refers to the element that contains the label of the radio group. | |
radio |
div |
|
|
tabindex="-1" |
div |
|
|
tabindex="0" |
div |
|
|
aria-checked="false" |
div |
|
|
aria-checked="true" |
div |
|
- Jared Palmer (@jaredpalmer)
MIT License