접근성 최적의 모범 사례

빠른 요약

나쁜 ARIA보다는 ARIA가 없는 것이 더 낫습니다 aria-*, role, 및 tabindex를 사용하기 전에 다음 권장 사항을 확인하세요. 접근성이 내장된 의미있는 HTML을 사용하고, 이상적으로 관련된 스크린 리더 및 브라우저 조합으로 테스트하세요.

WebAIM의 상위 백만 홈페이지의 접근성 분석에서 “ARIA가 높은 감지 가능한 오류와 관련이 있다”고 발견했습니다. 아마도 ARIA의 오용이 증가된 오류의 큰 원인 중 하나일 것이므로, 의심스러울 때에는 aria-*, role, 및 tabindex를 사용하지 말고 의미 있는 HTML을 사용하세요.

macOS에서 키보드 탐색 기능 활성화하기

기본적으로, macOS는 tab 키를 텍스트 상자 및 디렉터리 전용으로 제한합니다. 전체 키보드 탐색을 활성화하려면:

  1. 시스템 환경설정을 엽니다.
  2. 키보드를 선택합니다.
  3. 단축키 탭을 엽니다.
  4. 설정 컨트롤 간 이동에 키보드 탐색 사용을 활성화합니다.

macos 브라우저별 키보드 탐색 활성화에 대해 자세히 알아보세요.

빠른 체크리스트

  • 텍스트, 선택, 체크박스, 라디오, 파일, 및 토글 입력에 접근 가능한 이름이 있습니다.
  • 버튼, 링크, 및 이미지에 설명적인 접근 가능한 이름이 있습니다.
  • 아이콘
  • 상호작용 요소는 Tab 키로 접근이 가능하며 시각적으로 포커스 상태가 보입니다.
  • 툴팁이 있는 요소는 Tab 키로 초점을 맞출 수 있습니다.
  • 어떤 role, tabindex, 또는 aria-* 속성이 불필요한가요?
  • 어떤 div 또는 span 요소가 p, button, 또는 time과 같은 더 의미 있는 HTML 요소로 대체될 수 있나요?

문서 개요 제공

스크린 리더 사용자가 내용 탐색에 사용하는 주요 메커니즘은 제목입니다. 따라서 페이지의 제목 구조는 좋은 차례로 목차와 같이 의미가 있어야 합니다. 다음을 확인해야 합니다:

  • 페이지에는 h1 요소가 하나뿐입니다.
  • 제목 수준이 건너뛰지 않습니다.
  • 제목 수준이 올바르게 중첩되어 있습니다.

스크린 리더 사용자를 위한 접근 가능한 이름 제공

접근성 있는 이름으로 마크업을 제공하려면 다음을 확인하세요:

  • 각 입력 필드에는 연관된 label이 있습니다.
  • 버튼과 링크는 가시 텍스트 또는 가시 텍스트가 없는 경우에는 aria-label이 있습니다(예를 들어 콘텐츠가 없는 아이콘 버튼의 경우).
  • 이미지에는 alt 속성이 있습니다.
  • fieldset는 첫 번째 자식으로 legend가 있습니다.
  • figure는 첫 번째 자식으로 figcaption가 있습니다.
  • table는 첫 번째 자식으로 caption이 있습니다.

체크박스와 라디오 입력의 그룹은 legend가 있는 fieldset에 함께 그룹화되어 있어야 합니다. legend는 체크박스와 라디오 입력의 그룹에 레이블을 제공합니다.

label, 자식 텍스트, 또는 자식 요소가 시각적으로 원하지 않을 때에는 화면 리더 외에는 요소를 숨기기 위해 .gl-sr-only를 사용하세요.

접근 가능한 이름 제공 예시

다음 소목은 HTML 요소에 접근 가능한 이름으로 렌더링하는 마크업 예시를 포함하고 있습니다.

GlFormGroup을 사용할 때:

  • label 속성만 전달하면 label 값이 포함된 legend가 포함된 fieldset가 렌더링됩니다.
  • labellabel-for 속성을 모두 전달하면 labellabel-for ID와 동일한 양식 입력을 가리키는 label이 렌더링됩니다.

접근 가능한 이름이 있는 텍스트 입력

GlFormGroup를 사용할 때, label 속성만으로는 입력에 접근 가능한 이름을 제공할 수 없습니다. 입력에 접근 가능한 이름을 제공하려면 label-for 속성도 제공해야 합니다.

텍스트 입력 예시:

<!-- 레이블이 있는 입력 -->
<gl-form-group :label="__('Issue title')" label-for="issue-title">
  <gl-form-input id="issue-title" v-model="title" />
</gl-form-group>

<!-- 숨겨진 레이블이 있는 입력 -->
<gl-form-group :label="__('Issue title')" label-for="issue-title" label-sr-only>
  <gl-form-input id="issue-title" v-model="title" />
</gl-form-group>

textarea 예시:

<!-- 레이블이 있는 텍스트 영역 -->
<gl-form-group :label="__('Issue description')" label-for="issue-description">
  <gl-form-textarea id="issue-description" v-model="description" />
</gl-form-group>

<!-- 숨겨진 레이블이 있는 텍스트 영역 -->
<gl-form-group :label="__('Issue description')" label-for="issue-description" label-sr-only>
  <gl-form-textarea id="issue-description" v-model="description" />
</gl-form-group>

또는 일반적인 label 요소를 사용할 수 있습니다:

<!-- `label`을 사용하여 레이블이 있는 입력 -->
<label for="issue-title">{{ __('Issue title') }}</label>
<gl-form-input id="issue-title" v-model="title" />

<!-- `label`을 사용하여 숨겨진 레이블이 있는 입력 -->
<label for="issue-title" class="gl-sr-only">{{ __('Issue title') }}</label>
<gl-form-input id="issue-title" v-model="title" />

접근 가능한 이름이 있는 입력 항목 선택

접근 가능한 이름이 있는 선택 입력 예시:

<!-- 레이블이 있는 선택 입력 -->
<gl-form-group :label="__('Issue status')" label-for="issue-status">
  <gl-form-select id="issue-status" v-model="status" :options="options" />
</gl-form-group>

<!-- 숨은 레이블이 있는 선택 입력 -->
<gl-form-group :label="__('Issue status')" label-for="issue-status" label-sr-only>
  <gl-form-select id="issue-status" v-model="status" :options="options" />
</gl-form-group>

접근 가능한 이름이 있는 확인란 입력

단일 확인란:

<!-- 레이블이 있는 단일 확인란 -->
<gl-form-checkbox v-model="status" value="task-complete">
  {{ __('Task complete') }}
</gl-form-checkbox>

<!-- 숨은 레이블이 있는 단일 확인란 -->
<gl-form-checkbox v-model="status" value="task-complete">
  <span class="gl-sr-only">{{ __('Task complete') }}</span>
</gl-form-checkbox>

여러 확인란:

<!-- fieldset 안에 그룹화된 여러 레이블이 있는 확인란 -->
<gl-form-group :label="__('Task list')">
  <gl-form-checkbox value="task-1">{{ __('Task 1') }}</gl-form-checkbox>
  <gl-form-checkbox value="task-2">{{ __('Task 2') }}</gl-form-checkbox>
</gl-form-group>

<!-- 또는 -->
<gl-form-group :label="__('Task list')">
  <gl-form-checkbox-group v-model="selected" :options="options" />
</gl-form-group>

<!-- 숨은 범례가 있는 여러 레이블이 있는 확인란 -->
<gl-form-group :label="__('Task list')" label-sr-only>
  <gl-form-checkbox value="task-1">{{ __('Task 1') }}</gl-form-checkbox>
  <gl-form-checkbox value="task-2">{{ __('Task 2') }}</gl-form-checkbox>
</gl-form-group>

<!-- 또는 -->
<gl-form-group :label="__('Task list')" label-sr-only>
  <gl-form-checkbox-group v-model="selected" :options="options" />
</gl-form-group>

접근 가능한 이름이 있는 라디오 입력

단일 라디오 입력:

<!-- 레이블이 있는 단일 라디오 -->
<gl-form-radio v-model="status" value="opened">
  {{ __('Opened') }}
</gl-form-radio>

<!-- 숨은 레이블이 있는 단일 라디오 -->
<gl-form-radio v-model="status" value="opened">
  <span class="gl-sr-only">{{ __('Opened') }}</span>
</gl-form-radio>

여러 라디오 입력:

<!-- fieldset 안에 그룹화된 여러 레이블이 있는 라디오 입력 -->
<gl-form-group :label="__('Issue status')">
  <gl-form-radio value="opened">{{ __('Opened') }}</gl-form-radio>
  <gl-form-radio value="closed">{{ __('Closed') }}</gl-form-radio>
</gl-form-group>

<!-- 또는 -->
<gl-form-group :label="__('Issue status')">
  <gl-form-radio-group v-model="selected" :options="options" />
</gl-form-group>

<!-- 숨은 범례가 있는 여러 레이블이 있는 라디오 입력 -->
<gl-form-group :label="__('Issue status')" label-sr-only>
  <gl-form-radio value="opened">{{ __('Opened') }}</gl-form-radio>
  <gl-form-radio value="closed">{{ __('Closed') }}</gl-form-radio>
</gl-form-group>

<!-- 또는 -->
<gl-form-group :label="__('Issue status')" label-sr-only>
  <gl-form-radio-group v-model="selected" :options="options" />
</gl-form-group>

접근 가능한 이름이 있는 파일 입력

파일 입력 예시:

<!-- 레이블이 있는 파일 입력 -->
<label for="attach-file">{{ __('Attach a file') }}</label>
<input id="attach-file" type="file" />

<!-- 숨은 레이블이 있는 파일 입력 -->
<label for="attach-file" class="gl-sr-only">{{ __('Attach a file') }}</label>
<input id="attach-file" type="file" />

접근 가능한 이름이 있는 GlToggle 컴포넌트

GlToggle 예시:

<!-- 레이블이 있는 GlToggle -->
<gl-toggle v-model="notifications" :label="__('Notifications')" />

<!-- 숨은 레이블이 있는 GlToggle -->
<gl-toggle v-model="notifications" :label="__('Notifications')" label-position="hidden" />

접근 가능한 이름이 있는 GlFormCombobox 컴포넌트

GlFormCombobox 예시:

<!-- 레이블이 있는 GlFormCombobox -->
<gl-form-combobox :label-text="__('Key')" :token-list="$options.tokenList" />

접근 가능한 이름이 있는 이미지

이미지 예시:

<img :src="imagePath" :alt="__('이미지 설명')" />

<!-- SVG는 기본적으로 그래픽 역할(role)을 갖기 때문에 의미적으로 이미지인 경우 `role="img"`를 적용해야 합니다 -->
<svg role="img" :alt="__('이미지 설명')" />

<!-- 장식용 이미지, 스크린 리더에서 숨김 -->
<img :src="imagePath" :alt="" />

설명적 접근 가능 이름이 있는 버튼과 링크

버튼과 링크는 독립적으로 이해할 수 있는 충분히 설명적인 접근 가능 이름을 가져야 합니다.

<!-- 부적절한 예 -->
<gl-button @click="handleClick">{{ __('제출') }}</gl-button>

<gl-link :href="url">{{ __('페이지') }}</gl-link>

<!-- 적절한 예 -->
<gl-button @click="handleClick">{{ __('리뷰 제출') }}</gl-button>

<gl-link :href="url">{{ __("GitLab의 접근성 페이지") }}</gl-link>

버튼과 같은 스타일이 적용된 링크

링크는 GlButton을 사용하여 버튼과 같은 스타일을 적용할 수 있습니다.

 <gl-button :href="url">{{ __('버튼처럼 스타일이 적용된 링크') }}</gl-button>

역할(Role)

일반적으로 role 사용을 피하십시오. 대신 암시적으로 role을 갖는 의미론적 HTML 요소를 사용하십시오.

나쁜 예 좋은 예
<div role="button"> <button>
<div role="img"> <img>
<div role="link"> <a>
<div role="header"> <h1> ~ <h6>
<div role="textbox"> <input> 또는 <textarea>
<div role="article"> <article>
<div role="list"> <ol> 또는 <ul>
<div role="listitem"> <li>
<div role="table"> <table>
<div role="rowgroup"> <thead>, <tbody>, 또는 <tfoot>
<div role="row"> <tr>
<div role="columnheader"> <th>
<div role="cell"> <td>

키보드만 사용 가능한 지원

키보드 사용자는 페이지에서 자신이 어디에 있는지를 이해하기 위해 포커스 아웃라인을 필요로 합니다. 따라서, 요소가 상호 작용적이라면 다음을 확인해야 합니다:

  • 키보드 포커스를 받을 수 있어야 합니다.
  • 시각적 포커스 상태가 있어야 합니다.

a(GlLink) 및 button(GlButton)과 같은 의미있는 HTML을 사용하면 이러한 동작이 기본적으로 제공됩니다.

기억해야 할 점:

  • TabShift-Tab은 상호 작용 요소 간에만 이동해야 하며 정적 콘텐츠로 이동해서는 안 됩니다.
  • :hover 스타일을 추가할 때 대부분의 경우 마우스 사용자뿐만 아니라 키보드 사용자에게도 스타일이 적용되도록 :focus 스타일도 추가해야 합니다.
  • 상호 작용 요소의 outline을 제거하는 경우 box-shadow와 같은 다른 방법으로 시각적 포커스 상태를 유지해야 합니다.

자세한 내용은 Pajamas Keyboard-only 페이지를 참조하세요.

tabindex

tabindex를 사용하는 대신 tabindex=”0” 없이 사용하는 것을 선호합니다. 왜냐하면:

  • button(GlButton)과 같은 의미있는 HTML을 사용하면 암시적으로 tabindex="0"이 제공됩니다.
  • 탭 순서는 시각적 읽기 순서와 일치해야 하며 양수의 tabindex는 이를 방해합니다.

tabindex="0" 사용을 피하세요

divspan 태그 대신 상호 작용적인 요소를 사용하세요. 예시:

  • 요소를 클릭해야 하는 경우 button(GlButton)을 사용하세요.
  • 텍스트를 편집해야 하는 경우 input 또는 textarea를 사용하세요.

마크업이 의미론적으로 완료되면 CSS를 사용하여 원하는 시각적 상태로 업데이트하세요.

<!-- 나쁨 -->
<div role="button" tabindex="0" @click="expand">확장</div>

<!-- 좋음 -->
<gl-button class="gl-p-0!" category="tertiary" @click="expand">확장</gl-button>

상호 작용 요소에 tabindex="0" 사용하지 마세요

상호 작용 요소는 이미 탭에 접근할 수 있기 때문에 tabindex를 추가하는 것은 중복됩니다.

<!-- 나쁨 -->
<gl-link href="help" tabindex="0">도움말</gl-link>
<gl-button tabindex="0">제출</gl-button>

<!-- 좋음 -->
<gl-link href="help">도움말</gl-link>
<gl-button>제출</gl-button>

스크린 리더가 읽을 요소에 tabindex="0" 사용하지 마세요

스크린 리더는 탭에 접근할 수 없는 텍스트를 읽을 수 있습니다. tabindex="0"의 사용은 불필요하며 문제를 일으킬 수 있습니다. 왜냐하면 스크린 리더 사용자는 상호작용할 수 있는 것으로 기대하기 때문입니다.

<!-- 나쁨 -->
<p tabindex="0" :aria-label="message">{{ message }}</p>

<!-- 좋음 -->
<p>{{ message }}</p>

양수의 tabindex 사용을 피하세요

항상 tabindex="1" 이상을 사용하는 것을 피하세요.

아이콘

아이콘은 세 가지 유형으로 나눌 수 있습니다:

  • 정보를 전달하지 않는 장식용 아이콘
  • 의미를 전달하는 아이콘
  • 클릭 가능한 아이콘

정보를 전달하지 않는 아이콘

아이콘이 UI에서 제거되어도 사용자에게 정보가 손실되지 않을 때, 아이콘은 장식용입니다.

GitLab 내 대부분의 아이콘은 장식용이므로 GlIcon은 렌더링된 아이콘을 스크린 리더에서 자동으로 숨깁니다. 따라서 GlIconaria-hidden="true"를 추가할 필요가 없습니다.

<!-- 불필요함 — gl-icon은 렌더링된 아이콘을 스크린 리더에서 기본적으로 숨깁니다 -->
<gl-icon name="rocket" aria-hidden="true" />`

<!-- 좋음 -->
<gl-icon name="rocket" />`

정보를 전달하는 아이콘

아이콘이 UI에서 제거되면 사용자에게 정보가 손실되는 경우, 아이콘은 정보를 전달합니다.

예를 들어 Confidential 텍스트가 옆에 없이 기밀 문제라는 증거를 전하는 기밀 아이콘이 그 예입니다.

정보를 전달하는 아이콘은 스크린 리더 사용자에게도 정보가 전달되도록 액세스 가능한 이름을 가져야 합니다.

<!-- 나쁨 -->
<gl-icon name="eye-slash" />`

<!-- 좋음 -->
<gl-icon name="eye-slash" :aria-label="__('기밀 문제')" />`

클릭 가능한 아이콘

클릭 가능한 아이콘은 의미적으로 버튼이므로 액세스 가능한 이름과 함께 버튼으로 렌더링되어야 합니다.

<!-- 나쁨 -->
<gl-icon name="close" :aria-label="__('닫기')" @click="handleClick" />

<!-- 좋음 -->
<gl-button icon="close" category="tertiary" :aria-label="__('닫기')" @click="handleClick" />

툴팁

툴팁을 추가할 때 키보드 사용자가 툴팁을 볼 수 있도록 요소가 포커스를 받을 수 있는지 확인해야 합니다. 아이콘과 같은 정적인 요소의 경우, 이미 포커스를 받을 수 있는 버튼으로 감쌀 수 있으므로 별도로 tabindex=0을 추가할 필요가 없습니다.

다음 코드 스니펫은 툴팁이 있는 아이콘의 좋은 예입니다.

  • 이미 버튼이기 때문에 자동으로 포커스를 받을 수 있습니다.
  • 텍스트가 없는 버튼이기 때문에 액세스 가능한 이름과 함께 aria-label을 사용합니다.
  • 버튼 배경이 호버 시 회색으로 바뀌지 않도록 gl-hover-bg-transparent! 클래스를 사용할 수 있습니다.
  • 필요한 경우 버튼 패딩을 제거하기 위해 gl-p-0! 클래스를 사용할 수 있습니다.
<gl-button
  v-gl-tooltip
  class="gl-hover-bg-transparent! gl-p-0!"
  icon="warning"
  category="tertiary"
  :title="tooltipText"
NOTE:')"
/>

요소 숨김 방법

적절할 때 사용자로부터 요소를 숨긴다면 다음 표를 사용하세요.

시각적으로 숨김 스크린 리더로부터 숨김 시각적 및 스크린 리더로부터 숨김
.gl-sr-only aria-hidden="true" display: none, visibility: hidden, 또는 hidden 속성

스크린 리더로부터 장식적 이미지 숨김

스크린 리더 사용자에게 잡음을 줄이기 위해 장식적 이미지를 alt=""로 숨깁니다. 이미지가 img 요소가 아닌 경우(인라인 SVG 등) role="img"alt=""를 추가하여 숨길 수 있습니다.

gl-icon 컴포넌트는 기본적으로 아이콘을 스크린 리더로부터 숨기기 때문에 aria-hidden="true"가 불필요합니다.

<!-- 좋음 - 스크린 리더로부터 장식적 이미지 숨김 -->

<img src="decorative.jpg" alt="">

<svg role="img" alt="" />

<gl-icon name="epic" />

ARIA 사용 시기

의미 있는 HTML을 사용할 때는 ARIA가 필요하지 않습니다. 왜냐하면 이미 접근성이 내장되어 있기 때문입니다.

그러나 의미 있는 HTML과 동등한 UI 패턴이 없는 경우도 있습니다. 이에 대한 일반적인 예시로는 대화상자(모달) 및 탭이 있습니다. GitLab의 구체적인 예시로는 담당자 및 라벨 드롭다운이 있습니다. 이러한 위젯을 구축하는 데는 스크린 리더가 이해할 수 있도록 하기 위해 ARIA가 필요합니다. WCAG 준수 여부를 보장하기 위해서는 적절한 연구와 테스트가 수행되어야 합니다.