Radio Group

Radio Group (라디오 버튼 그룹)

Radio Group은 사용자가 여러 옵션 중 하나를 선택할 수 있게 해주는 컴포넌트야. 사용자가 모든 옵션을 한 번에 봐야 한다면 라디오 버튼을 쓰고, 옵션을 접어서 숨길 수 있다면 공간을 덜 차지하는 Select 컴포넌트를 고려해 봐. 기본적으로 가장 자주 쓰는 옵션을 선택된 상태로 두는 게 좋아.

출처: 문서

본문

Radio group

RadioGroup은 여러 Radio 컴포넌트를 묶어주는 유용한 래퍼로, 더 쉬운 API와 그룹에 대한 적절한 키보드 접근성을 제공해요.

import * as React from 'react';
import Radio from '@mui/material/Radio';
import RadioGroup from '@mui/material/RadioGroup';
import FormControlLabel from '@mui/material/FormControlLabel';
import FormControl from '@mui/material/FormControl';
import FormLabel from '@mui/material/FormLabel';

export default function RadioButtonsGroup() {
  const id = React.useId();
  return (
    <FormControl>
      <FormLabel id={`${id}-label`}>Gender</FormLabel>
      <RadioGroup
        aria-labelledby={`${id}-label`}
        defaultValue="female"
        name="radio-buttons-group"
      >
        <FormControlLabel value="female" control={<Radio />} label="Female" />
        <FormControlLabel value="male" control={<Radio />} label="Male" />
        <FormControlLabel value="other" control={<Radio />} label="Other" />
      </RadioGroup>
    </FormControl>
  );
}

방향 (Direction)

버튼을 가로로 배치하려면 row prop을 설정하면 돼요:

import * as React from 'react';
import Radio from '@mui/material/Radio';
import RadioGroup from '@mui/material/RadioGroup';
import FormControlLabel from '@mui/material/FormControlLabel';
import FormControl from '@mui/material/FormControl';
import FormLabel from '@mui/material/FormLabel';

export default function RowRadioButtonsGroup() {
  const id = React.useId();
  return (
    <FormControl>
      <FormLabel id={`${id}-label`}>Gender</FormLabel>
      <RadioGroup row aria-labelledby={`${id}-label`} name="row-radio-buttons-group">
        <FormControlLabel value="female" control={<Radio />} label="Female" />
        <FormControlLabel value="male" control={<Radio />} label="Male" />
        <FormControlLabel value="other" control={<Radio />} label="Other" />
        <FormControlLabel
          value="disabled"
          disabled
          control={<Radio />}
          label="other"
        />
      </RadioGroup>
    </FormControl>
  );
}

제어 (Controlled)

value와 onChange props를 이용해 라디오를 제어할 수 있어요:

import * as React from 'react';
import Radio from '@mui/material/Radio';
import RadioGroup from '@mui/material/RadioGroup';
import FormControlLabel from '@mui/material/FormControlLabel';
import FormControl from '@mui/material/FormControl';
import FormLabel from '@mui/material/FormLabel';

export default function ControlledRadioButtonsGroup() {
  const id = React.useId();
  const [value, setValue] = React.useState('female');

  const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    setValue((event.target as HTMLInputElement).value);
  };

  return (
    <FormControl>
      <FormLabel id={`${id}-label`}>Gender</FormLabel>
      <RadioGroup
        aria-labelledby={`${id}-label`}
        name="controlled-radio-buttons-group"
        value={value}
        onChange={handleChange}
      >
        <FormControlLabel value="female" control={<Radio />} label="Female" />
        <FormControlLabel value="male" control={<Radio />} label="Male" />
      </RadioGroup>
    </FormControl>
  );
}

단독 라디오 버튼 (Standalone radio buttons)

Radio는 RadioGroup 래퍼 없이도 단독으로 사용할 수 있어요.

import * as React from 'react';
import Radio from '@mui/material/Radio';

export default function RadioButtons() {
  const [selectedValue, setSelectedValue] = React.useState('a');

  const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    setSelectedValue(event.target.value);
  };

  return (
    <div>
      <Radio
        checked={selectedValue === 'a'}
        onChange={handleChange}
        value="a"
        name="radio-buttons"
        slotProps={{ input: { 'aria-label': 'A' } }}
      />
      <Radio
        checked={selectedValue === 'b'}
        onChange={handleChange}
        value="b"
        name="radio-buttons"
        slotProps={{ input: { 'aria-label': 'B' } }}
      />
    </div>
  );
}

크기 (Size)

size prop을 쓰거나 svg 아이콘들의 font size를 커스터마이징해서 라디오 크기를 바꿀 수 있어요.

import * as React from 'react';
import Radio from '@mui/material/Radio';

export default function SizeRadioButtons() {
  const [selectedValue, setSelectedValue] = React.useState('a');
  const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    setSelectedValue(event.target.value);
  };

  const controlProps = (item: string) => ({
    checked: selectedValue === item,
    onChange: handleChange,
    value: item,
    name: 'size-radio-button-demo',
    inputProps: { 'aria-label': item },
  });

  return (
    <div>
      <Radio {...controlProps('a')} size="small" />
      <Radio {...controlProps('b')} />
      <Radio
        {...controlProps('c')}
        sx={{
          '& .MuiSvgIcon-root': {
            fontSize: 28,
          },
        }}
      />
    </div>
  );
}

색상 (Color)

import * as React from 'react';
import { pink } from '@mui/material/colors';
import Radio from '@mui/material/Radio';

export default function ColorRadioButtons() {
  const [selectedValue, setSelectedValue] = React.useState('a');

  const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    setSelectedValue(event.target.value);
  };

  const controlProps = (item: string) => ({
    checked: selectedValue === item,
    onChange: handleChange,
    value: item,
    name: 'color-radio-button-demo',
    inputProps: { 'aria-label': item },
  });

  return (
    <div>
      <Radio {...controlProps('a')} />
      <Radio {...controlProps('b')} color="secondary" />
      <Radio {...controlProps('c')} color="success" />
      <Radio {...controlProps('d')} color="default" />
      <Radio
        {...controlProps('e')}
        sx={{
          color: pink[800],
          '&.Mui-checked': {
            color: pink[600],
          },
        }}
      />
    </div>
  );
}

라벨 배치 (Label placement)

FormControlLabel 컴포넌트의 labelPlacement prop으로 라벨의 위치를 바꿀 수 있어요:

import * as React from 'react';
import Radio from '@mui/material/Radio';
import RadioGroup from '@mui/material/RadioGroup';
import FormControlLabel from '@mui/material/FormControlLabel';
import FormControl from '@mui/material/FormControl';
import FormLabel from '@mui/material/FormLabel';

export default function FormControlLabelPlacement() {
  const id = React.useId();
  return (
    <FormControl>
      <FormLabel id={`${id}-label`}>Label placement</FormLabel>
      <RadioGroup
        row
        aria-labelledby={`${id}-label`}
        name="position"
        defaultValue="top"
      >
        <FormControlLabel
          value="bottom"
          control={<Radio />}
          label="Bottom"
          labelPlacement="bottom"
        />
        <FormControlLabel value="end" control={<Radio />} label="End" />
      </RadioGroup>
    </FormControl>
  );
}

오류 표시 (Show error)

일반적으로 라디오 버튼은 기본적으로 값이 선택되어 있어야 해요. 그렇지 않은 경우, 폼 제출 시 값이 선택되지 않았으면 오류를 표시할 수 있어요:

import * as React from 'react';
import Radio from '@mui/material/Radio';
import RadioGroup from '@mui/material/RadioGroup';
import FormControlLabel from '@mui/material/FormControlLabel';
import FormControl from '@mui/material/FormControl';
import FormHelperText from '@mui/material/FormHelperText';
import FormLabel from '@mui/material/FormLabel';
import Button from '@mui/material/Button';

export default function ErrorRadios() {
  const id = React.useId();
  const [value, setValue] = React.useState('');
  const [error, setError] = React.useState(false);
  const [helperText, setHelperText] = React.useState('Choose wisely');

  const handleRadioChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    setValue(event.target.value);
    setError(false);
    setHelperText('Choose wisely');
  };

  const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
    event.preventDefault();

    if (value === 'best') {
      setHelperText('You got it!');
      setError(false);
    } else if (value === 'worst') {
      setHelperText('Sorry, wrong answer!');
      setError(true);
    } else {
      setHelperText('Please select an option.');
      setError(true);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <FormControl sx={{ m: 3 }} error={error} variant="standard">
        <FormLabel id={`${id}-label`}>Pop quiz: MUI is…</FormLabel>
        <RadioGroup
          aria-labelledby={`${id}-label`}
          name="quiz"
          value={value}
          onChange={handleRadioChange}
        >
          <FormControlLabel value="best" control={<Radio />} label="The best!" />
          <FormControlLabel value="worst" control={<Radio />} label="The worst." />
        </RadioGroup>
        <FormHelperText>{helperText}</FormHelperText>
        <Button sx={{ mt: 1, mr: 1 }} type="submit" variant="outlined">
          Check Answer
        </Button>
      </FormControl>
    </form>
  );
}

커스터마이징 (Customization)

여기 컴포넌트를 커스터마이징하는 예시가 있어요. 자세한 내용은 overrides 문서 페이지에서 확인할 수 있어요.

import * as React from 'react';
import { styled } from '@mui/material/styles';
import Radio, { RadioProps } from '@mui/material/Radio';
import RadioGroup from '@mui/material/RadioGroup';
import FormControlLabel from '@mui/material/FormControlLabel';
import FormControl from '@mui/material/FormControl';
import FormLabel from '@mui/material/FormLabel';

const BpIcon = styled('span')(({ theme }) => ({
  borderRadius: '50%',
  width: 16,
  height: 16,
  boxShadow: 'inset 0 0 0 1px rgba(16,22,26,.2), inset 0 -1px 0 rgba(16,22,26,.1)',
  backgroundColor: '#f5f8fa',
  backgroundImage: 'linear-gradient(180deg,hsla(0,0%,100%,.8),hsla(0,0%,100%,0))',
  '.Mui-focusVisible &': {
    outline: '2px auto rgba(19,124,189,.6)',
    outlineOffset: 2,
  },
  'input:hover ~ &': {
    backgroundColor: '#ebf1f5',
    ...theme.applyStyles('dark', {
      backgroundColor: '#30404d',
    }),
  },
  'input:disabled ~ &': {
    boxShadow: 'none',
    background: 'rgba(206,217,224,.5)',
    ...theme.applyStyles('dark', {
      background: 'rgba(57,75,89,.5)',
    }),
    '@media (forced-colors: active)': {
      outline: '1px solid GrayText',
    },
  },
  '@media (forced-colors: active)': {
    outline: '1px solid ButtonText',
  },
  ...theme.applyStyles('dark', {
    boxShadow: '0 0 0 1px rgb(16 22 26 / 40%)',
    backgroundColor: '#394b59',
    backgroundImage: 'linear-gradient(180deg,hsla(0,0%,100%,.05),hsla(0,0%,100%,0))',
  }),
}));

const BpCheckedIcon = styled(BpIcon)({
  backgroundColor: '#137cbd',
  backgroundImage: 'linear-gradient(180deg,hsla(0,0%,100%,.1),hsla(0,0%,100%,0))',
  '&::before': {
    display: 'block',
    width: 16,
    height: 16,
    backgroundImage: 'radial-gradient(#fff,#fff 28%,transparent 32%)',
    content: '""',
    '@media (forced-colors: active)': {
      backgroundImage: 'none',
      backgroundColor: 'ButtonText',
      borderRadius: '50%',
      width: 8,
      height: 8,
      margin: 4,
    },
  },
  'input:hover ~ &': {
    backgroundColor: '#106ba3',
  },
  '@media (forced-colors: active)': {
    outline: '2px solid ButtonText',
  },
});

// Inspired by blueprintjs
function BpRadio(props: RadioProps) {
  return (
    <Radio
      disableRipple
      color="default"
      checkedIcon={<BpCheckedIcon />}
      icon={<BpIcon />}
      {...props}
    />
  );
}

export default function CustomizedRadios() {
  const id = React.useId();
  return (
    <FormControl>
      <FormLabel id={`${id}-label`}>Gender</FormLabel>
      <RadioGroup
        defaultValue="female"
        aria-labelledby={`${id}-label`}
        name="customized-radios"
      >
        <FormControlLabel value="female" control={<BpRadio />} label="Female" />
        <FormControlLabel value="male" control={<BpRadio />} label="Male" />
        <FormControlLabel value="other" control={<BpRadio />} label="Other" />
        <FormControlLabel
          value="disabled"
          disabled
          control={<BpRadio />}
          label="(Disabled option)"
        />
      </RadioGroup>
    </FormControl>
  );
}

useRadioGroup

고급 커스터마이징 케이스를 위해 useRadioGroup() 훅이 노출돼 있어요. 이 훅은 부모 라디오 그룹의 컨텍스트 값을 반환해요. Radio 컴포넌트도 내부적으로 이 훅을 사용해요.

API

import { useRadioGroup } from '@mui/material/RadioGroup';

반환 값 (Returns)

value (object):

  • value.name (string [optional]): 컨트롤의 값 참조에 사용하는 name.
  • value.onChange (func [optional]): 라디오 버튼이 선택될 때 실행되는 콜백.
  • value.value (any [optional]): 선택된 라디오 버튼의 값.

예시 (Example)

import { styled } from '@mui/material/styles';
import RadioGroup, { useRadioGroup } from '@mui/material/RadioGroup';
import FormControlLabel, {
  FormControlLabelProps,
} from '@mui/material/FormControlLabel';
import Radio from '@mui/material/Radio';

interface StyledFormControlLabelProps extends FormControlLabelProps {
  checked: boolean;
}

const StyledFormControlLabel = styled((props: StyledFormControlLabelProps) => (
  <FormControlLabel {...props} />
))(({ theme }) => ({
  variants: [
    {
      props: { checked: true },
      style: {
        '.MuiFormControlLabel-label': {
          color: theme.palette.primary.main,
        },
      },
    },
  ],
}));

function MyFormControlLabel(props: FormControlLabelProps) {
  const radioGroup = useRadioGroup();

  let checked = false;

  if (radioGroup) {
    checked = radioGroup.value === props.value;
  }

  return <StyledFormControlLabel checked={checked} {...props} />;
}

export default function UseRadioGroup() {
  return (
    <RadioGroup name="use-radio-group" defaultValue="first">
      <MyFormControlLabel value="first" label="First" control={<Radio />} />
      <MyFormControlLabel value="second" label="Second" control={<Radio />} />
    </RadioGroup>
  );
}

언제 쓸까 (When to use)

접근성 (Accessibility)

(WAI-ARIA: https://www.w3.org/WAI/ARIA/apg/patterns/radio/)

  • 모든 폼 컨트롤에는 라벨이 있어야 해요. 여기에는 라디오 버튼, 체크박스, 스위치가 포함돼요. 대부분의 경우 <label> 요소(FormControlLabel)를 사용해서 처리해요.
  • 라벨을 사용할 수 없을 때는 input 컴포넌트에 직접 속성을 추가해야 해요. 이 경우 slotProps.input 속성을 통해 추가 속성(예: aria-label, aria-labelledby, title)을 적용할 수 있어요.
<Radio
  value="radioA"
  slotProps={{
    input: { 'aria-label': 'Radio A' },
  }}
/>

FormControl / FormControlLabel / FormLabel / Radio / RadioGroup API

임포트 (Import):

import FormControl from '@mui/material/FormControl';
import FormControlLabel from '@mui/material/FormControlLabel';
import FormLabel from '@mui/material/FormLabel';
import Radio from '@mui/material/Radio';
import RadioGroup from '@mui/material/RadioGroup';
// or
import { FormControl, FormControlLabel, FormLabel, Radio, RadioGroup } from '@mui/material';

주요 props 요약:

  • FormControl: error, disabled, required, fullWidth, margin, size, variant, color 등을 제공해요. ref는 루트 HTMLDivElement로 전달돼요.
  • FormControlLabel: control(필수), label, labelPlacement('bottom' | 'end' | 'start' | 'top', 기본 'end'), disableTypography, required, value 등을 제공해요. ref는 HTMLLabelElement로 전달돼요.
  • FormLabel: error, disabled, filled, focused, required, color 등을 제공해요. ref는 HTMLLabelElement로 전달돼요.
  • Radio: checked, checkedIcon, color, disabled, disableRipple, icon, size, value, slotProps, slots 등을 제공해요. ref는 루트 HTMLSpanElement로 전달되고, ButtonBase의 props도 상속받아요.
  • RadioGroup: defaultValue, name, onChange, value 등을 제공해요. ref는 HTMLDivElement로 전달되고, FormGroup의 props도 상속받아요.

테마에서 MuiRadio, MuiRadioGroup 등을 사용하면 각 컴포넌트의 기본 props를 바꿀 수 있어요.

더 알아보기 (Learn more)