DialogContent requires a DialogTitle for the component to be accessible for screen reader users. 해결하기
바텀시트를 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-labelledbyproperty that refers to a visible dialog title, ORA label specified byaria-label.
A value set for the
aria-labelledbyproperty that refers to a visible dialog title, ORA 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를 개발해야 할 것이다.




