Skip to main content

Command Palette

Search for a command to run...

DialogContent requires a DialogTitle for the component to be accessible for screen reader users. 해결하기

Updated
3 min read

바텀시트를 shadcn의 drawer를 이용하여 구현하던 중에 다음과 같이 콘솔 에러를 보게 되었다.

DialogContent requires a DialogTitle for the component to be accessible for screen reader users. 

If you want to hide the `DialogTitle`, you can wrap it with our VisuallyHidden component.

의미 파악해보기

DialogContent는 스크린 리더로 읽는 유저가 컴포넌트에 접근할 수 있도록 DialogTitle이 필요하며, DialogTitle을 숨기고 싶다면 VisuallyHidden을 이용해서 컴포넌트를 감싸라는 의미다.

Dialog와 Drawer

근데 분명 Drawer를 쓰고 있는데 왜 Dialog에 관한 오류가 나올까? 그렇다. 직접적으로 Dialog를 쓰고 있지는 않지만

// drawer.json
"dependencies": [
    "vaul",
    "@radix-ui/react-dialog"
  ],

shadcn/ui 레포지토리에서 확인해봤을 때 직접 구현한 부분은 찾기 어려웠지만 drawer가 radix-ui의 react-dialog를 의존성으로 가지고 있었다.

DialogTitle이 왜 필요한가?

w3c에 따르면 dialog는 다음과 같은 속성을 가져야 한다.

A value set for the aria-labelledby property that refers to a visible dialog title, ORA label specified by aria-label.

  • A value set for the aria-labelledby property that refers to a visible dialog title, OR

  • A label specified by aria-label.

즉, dialog를 구현할 때 DialogTitle을 생략하면, 스크린리더를 사용하는 사람들에게는 해당 다이얼로그가 무엇에 관한 것인지 알게 못하게 된다. 그래서 DialogTitle과 함께 DialogContent를 사용해야 한다.

// package/react/dialog/src/dialog.tsx

const TitleWarning: React.FC<TitleWarningProps> = ({ titleId }) => {
  const titleWarningContext = useWarningContext(TITLE_WARNING_NAME);

  **const MESSAGE = `\\`${titleWarningContext.contentName}\\` requires a \\`${titleWarningContext.titleName}\\` for the component to be accessible for screen reader users.

If you want to hide the \\`${titleWarningContext.titleName}\\`, you can wrap it with our VisuallyHidden component.

For more information, see <https://radix-ui.com/primitives/docs/components/${titleWarningContext.docsSlug}`;**>

  React.useEffect(() => {
    if (titleId) {
      const hasTitle = document.getElementById(titleId);
      if (!hasTitle) console.error(MESSAGE);
    }
  }, [MESSAGE, titleId]);

  return null;
};

해결 방법

VisuallyHidden 사용

사이드 프로젝트를 진행하면서 dialog를 띄울 때, title, description은 표시하지 않기로 했기 때문에 VisuallyHidden을 사용해서 시각적으로 숨기는 방법이 있다.

구현체

import * as React from 'react';
import { Primitive } from '@radix-ui/react-primitive';

/* -------------------------------------------------------------------------------------------------
 * VisuallyHidden
 * -----------------------------------------------------------------------------------------------*/

const VISUALLY_HIDDEN_STYLES = Object.freeze({
  // See: <https://github.com/twbs/bootstrap/blob/main/scss/mixins/_visually-hidden.scss>
  position: 'absolute',
  border: 0,
  width: 1,
  height: 1,
  padding: 0,
  margin: -1,
  overflow: 'hidden',
  clip: 'rect(0, 0, 0, 0)',
  whiteSpace: 'nowrap',
  wordWrap: 'normal',
}) satisfies React.CSSProperties;

const NAME = 'VisuallyHidden';

type VisuallyHiddenElement = React.ComponentRef<typeof Primitive.span>;
type PrimitiveSpanProps = React.ComponentPropsWithoutRef<typeof Primitive.span>;
interface VisuallyHiddenProps extends PrimitiveSpanProps {}

const VisuallyHidden = React.forwardRef<VisuallyHiddenElement, VisuallyHiddenProps>(
  (props, forwardedRef) => {
    return (
      <Primitive.span
        {...props}
        ref={forwardedRef}
        style={{ ...VISUALLY_HIDDEN_STYLES, ...props.style }}
      />
    );
  }
);

VisuallyHidden.displayName = NAME;

/* -----------------------------------------------------------------------------------------------*/

const Root = VisuallyHidden;

export {
  VisuallyHidden,
  //
  Root,
  //
  VISUALLY_HIDDEN_STYLES,
};
export type { VisuallyHiddenProps };

구현체를 살펴보면, VISUALLY_HIDDEN_STYLES 를 이용하여 시각적으로 숨길려고 한 것으로 보인다. display : none 을 사용할 수도 있었겠지만 이렇게 하면 스크린리더가 읽지 못하기 때문에 다른 방법으로 사용한 것으로 파악된다.

실제 사용 예시

<VisuallyHidden asChild>
     <DrawerTitle>어제 드신 메뉴 어떠셨나요?</DrawerTitle>
    </VisuallyHidden>
    <VisuallyHidden asChild>
    <DrawerDescription>
     집밥을 챙겨 먹은 후 건강에 어떤 변화가 있었는지 알려주세요.
    </DrawerDescription>
</VisuallyHidden>

VisuallyHidden과 asChild prop을 이용해서 title, description을 모두 숨길 수 있다.

마무리

접근성에 관련된 경고를 구글링 해가면서 ‘이렇게 해결하면 되는구나’에서 끝낼 게 아니라, 해당 경고가 왜 뜨는지 이해하고 이를 해결하기 위해 등장한 VisuallyHidden의 구현체를 알아보면서 결코 모든게 마법처럼 이루어지는 게 아니란 걸 깨닫게 되었다.

이번에는 shadcn을 이용해서 나름 편하게 접근성을 챙길 수 있었지만, 추후에 직접 구현해야 할 때는 접근성을 잘 생각해서 UI를 개발해야 할 것이다.

More from this blog

토스 Frontend Fundamentals 2회차 모의고사 후기

진행하면서 느낀 점 1회차에 이어 2회차 토스 모의고사 후기를 쓴다. 지난 회차와 달리 기능 구현이 전부 완료되어 있는 상태에서 시작을 했고,어떤 관점으로 추상화와 유지보수성을 바라보는 시각을 비교하는 것이었다. 기능을 구현하면서 진행했을때보다 아예 모든 기능이 이미 구현된 상태에서 시작을 하다보니 어디서 어떻게 리팩토링을 진행하다보니 더 막막했던 것 같다

Mar 28, 20262 min read59
토스 Frontend Fundamentals 2회차 모의고사 후기

tailwind-merge에서 클래스네임은 어떻게 제어되는가?

tailwind-merge에서 클래스네임은 어떻게 제어되는가? 시작하기 전에 최근 shadcn을 이용해서 UI 작업을 하고 있다. 많은 사람들이 꼽는 장점 중에 하나로 커스텀이 쉽다는 것인데, 최근에 개인 프로젝트에서 컴포넌트를 만들면서 tailwind-merge를 사용하다가 고생한 경험이 있는데, 겪었던 경험에 대해 작성하고 tailwind-merge는

Mar 6, 20266 min read22
tailwind-merge에서 클래스네임은 어떻게 제어되는가?

Unicode LB13 규칙과 CSS 줄바꿈에 대하여

시작하기 전에 댓글 input에 한글,영어를 제외한 특수문자에서 줄바꿈이 되지 않고 overflow가 발생하는 문제를 겪었다. 특수문자에서는 왜 줄바꿈이 일어나지 않는지 알아보고, 한글/영어와 특수문자에서의 줄바꿈 차이를 알기 위해 글을 작성해보려고 한다. 한글 및 영어와 특수문자에서의 줄바꿈 댓글 컴포넌트의 CSS는 다음과 같이 폰트 관련 속성만 적용되어

Jan 29, 20263 min read31
Unicode LB13 규칙과 CSS 줄바꿈에 대하여

haseung-log

30 posts