ReactでRadioボタンにデザインを当てようとすると、 display: none
を使う方法を紹介している記事が多いが、その方法ではアクセシビリティが消えてしまう。
そこで、以下のページのCheckboxを参考にしてRadioボタンをアクセシビリティを維持したままReactで実装したのでメモ的にコードを残しておこうと思う。
import { css } from '@emotion/core';
import styled from '@emotion/styled';
import React from 'react';
import * as colors from '@/app/components/styles/colors';
export enum RadioSize {
Small,
Large,
}
type RadioProps = {
label?: string;
size?: RadioSize;
} & InternalRadioProps;
type InternalRadioProps = JSX.IntrinsicElements['input'];
// Hide checkbox visually but remain accessible to screen readers.
// Source: https://polished.js.org/docs/#hidevisually
const HiddenRadio = styled.input`
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
white-space: nowrap;
border: 0;
clip: rect(0 0 0 0);
clip-path: inset(50%);
`;
HiddenRadio.defaultProps = { type: 'radio' };
const Dot = styled.circle`
cx: 12;
cy: 12;
r: 6;
`;
const Circle = styled.circle`
cx: 12;
cy: 12;
r: 10;
stroke-width: 3;
`;
const CheckedIcon = styled.svg`
fill: none;
Circle {
stroke: ${colors.BLUE_600};
}
Dot {
fill: ${colors.BLUE_600};
}
`;
CheckedIcon.defaultProps = { viewBox: '0 0 24 24' };
const UncheckedIcon = styled.svg`
fill: none;
Circle {
stroke: ${colors.BLUE_600};
}
`;
UncheckedIcon.defaultProps = { viewBox: '0 0 24 24' };
const radioSize = (size?: RadioSize) => {
switch (size) {
case undefined:
case RadioSize.Small:
return css`
width: 16px;
height: 16px;
border-radius: 8px;
`;
case RadioSize.Large:
return css`
width: 20px;
height: 20px;
border-radius: 10px;
`;
default: {
const typeCheck: never = size;
return typeCheck;
}
}
};
type StyledRadioProps = {
checked?: boolean;
disabled?: boolean;
size?: RadioSize;
};
const checkedStyles = css`
CheckedIcon {
display: inline;
}
UncheckedIcon {
display: none;
}
`;
const uncheckedStyles = css`
CheckedIcon {
display: none;
}
UncheckedIcon {
display: inline;
}
`;
const disabledStyles = css`
Circle {
stroke: ${colors.DARK_BLUE_200};
}
Dot {
fill: ${colors.DARK_BLUE_200};
}
`;
const StyledRadio = styled.div<StyledRadioProps>`
display: inline-block;
line-height: 0;
transition: all 150ms;
${/* sc-selector */ HiddenRadio}:focus + & {
box-shadow: 0 0 0 2px ${colors.ORANGE};
}
${/* sc-declaration */ ({ checked }) => (checked ? checkedStyles : uncheckedStyles)}
${/* sc-declaration */ ({ disabled }) => disabled && disabledStyles}
${/* sc-declaration */ ({ size }) => radioSize(size)}
`;
const RadioContainer = styled.div`
display: inline-flex;
align-items: center;
justify-content: center;
`;
function InternalRadio({ checked, disabled, size, ...props }: InternalRadioProps): JSX.Element {
return (
<RadioContainer>
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
<HiddenRadio checked={checked} disabled={disabled} {...props} />
<StyledRadio checked={checked} disabled={disabled} size={size}>
<CheckedIcon>
<Circle />
<Dot />
</CheckedIcon>
<UncheckedIcon>
<Circle />
</UncheckedIcon>
</StyledRadio>
</RadioContainer>
);
}
const Label = styled.label`
display: inline-flex;
align-items: center;
justify-content: center;
`;
const LabelText = styled.span`
margin-left: 4px;
`;
export function Radio({ label, ...props }: RadioProps): JSX.Element {
return (
<Label>
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
<InternalRadio {...props} />
{label && <LabelText>{label}</LabelText>}
</Label>
);
}