비슷한 듯 다른 처리 방법들을 정리합니다.
- 1.
display: none - 2.
visibility: hidden - 3.
opacity: 0 - 4.
hiddenattribute - 5. 조건부 렌더링
- 6.
visually-hidden - 요약
1. display: none
- css box를 만들지 않는다.
- 렌더 트리에서도 제외되어 레이아웃 공간을 차지하지 않는다.
- 접근성 트리에 올라가지 않아 스크린리더에도 읽히지 않는다.
- 포커스 불가능
- 포인터 이벤트의 대상이 되지 않는다.
- 등록한 이벤트 리스너는 그대로 동작한다.
- 파일 업로드 버튼 등에서 사용되는 패턴
- DOM에는 남아있고, source html에도 들어온다.
- 속성을 변경(토글)하면 리플로우가 발생한다. (레이아웃을 재계산해야 하기 때문)
가운데 B에 display: none을 적용하면 B의 영역은 사라지고 A와 C가 붙는다.
2. visibility: hidden
- css box는 만들지만 화면에 그리지는 않는다.
- 크기와 위치까지 계산되므로 레이아웃 공간은 그대로 차지한다.
- 렌더 트리에 있기 때문에 Intersection Observer target으로 감지된다.
- 접근성 트리에서 제외되어 스크린리더에도 읽히지 않는다.
- 포커스 불가능
- 포인터 이벤트의 대상이 되지 않는다.
- 등록해둔 이벤트 리스너는 그대로 동작한다.
- 부모가
visibility: hidden이어도 자식 요소에visibility: visible을 주면 그 자식 요소만 노출된다. - 레이아웃은 이미 차지한 상태라 값을 토글하면 리플로우 없이 리페인트만 이루어진다.
transition이 가능하다. 이산 속성(<-> 연속 속성)이지만 한쪽 끝이visible이면 구간 전체를visible로 취급하는 규칙이 있어서, 페이드아웃이 끝난 뒤에 사라지게 만들 수 있다.
1번 예제
B는 안 보이지만 자리는 남아 있어서 A와 C가 붙지 않는다.
부모에 hidden을 주고 B에만 visible을 주면 B만 다시 나타난다.
마우스를 올리면 사라지는 트랜지션. visibility를 빼고 opacity만 두면 투명해진 뒤에도 B가 클릭을 가로챈다.
3. opacity: 0
- css box를 만들고 화면에도 그리며, 투명도만 0이다.
- 레이아웃 공간을 그대로 차지하고, Intersection Observer에도 잡힌다.
- 접근성 트리에 그대로 올라가 스크린리더가 읽는다.
- 포커스 가능. 탭 순서에 남아 있어 보이지 않는 곳으로 포커스가 이동한다.
- 포인터 이벤트의 대상이 된다. 클릭을 가로챌 수 있다.
pointer-events: none을 같이 줘야 막힌다.
- 새로운 스택 컨텍스트를 만들기 때문에
z-index기준이 바뀔 수 있다. - 레이아웃이 그대로라 토글해도 리플로우가 없다.
- 유일하게 중간값이 있는 연속 속성이라 트랜지션이 자연스럽다. 전환 효과에 자주 활용된다.
1번과 같은 배치. B는 투명해졌을 뿐 자리도 그대로고 글자도 그대로 읽힌다.
opacity: 0으로 투명하게 덮어놓은 두 번째 버튼은 클릭할 수 없지만, 덮개에 pointer-events: none을 적용한 세 번째 버튼에서는 클릭이 감지되는 것을 볼 수 있다.
4. hidden attribute
- HTML 전역 속성. 브라우저 기본 스타일시트의
[hidden] { display: none }으로 동작하므로 결과는 1번과 같다.- 박스 미생성, 접근성 트리 제외, 포커스/포인터 이벤트 불가
- 숨김을 CSS가 아니라 마크업의 의미로 표현한다는 차이가 있다.
- JS에서도
el.hidden = true로 클래스 조작 없이 토글할 수 있다.
- JS에서도
- 브라우저 기본 스타일시트라 작성자 CSS에 진다.
.card { display: flex }가 있으면<div class="card" hidden>은 숨겨지지 않는다.- 리셋에
[hidden] { display: none !important }를 넣어두는 것이 관행
hidden="until-found"로 Ctrl+F 검색과 scroll-to-text-fragment로는 찾을 수 있게 남겨둘 수 있다.- 요소가 찾아지면 자동으로 펼쳐진다.
until-found는display: none이 아니라content-visibility: hidden으로 동작한다. 박스는 만들되 내용을 건너뛰는 방식이라 높이는 0이 된다.- 아코디언·FAQ처럼 접어둔 내용이 검색에는 걸려야 하는 곳에 쓴다.
아래 두 예제에는 모두 hidden속성이 부여되었다.
두 번째만 .trap { display: flex }라는 작성자 CSS를 함께 받았는데, 기본 스타일시트를 이기면서 모든 박스가 노출되고 있다.
5. 조건부 렌더링
- 요소를 아예 만들지 않는다. DOM에 존재하지 않는다.
- SSR이라면 source html에도 들어가지 않아 크롤러가 읽을 수 없다. (당연히 조건이 false일 때에만 해당한다.)
- 보이면 안 되는 데이터를 감출 수 있는 유일한 방법이다.
- CSS 숨김은 전부 DOM에 남아 개발자 도구로 열람할 수 있다.
- 무거운 서브트리를 그리지 않아 초기 렌더 비용이 없다. 대신 토글할 때마다 마운트/언마운트 비용이 든다.
- 내부 상태가 사라진다. 입력값·스크롤 위치·포커스가 초기화된다.
- 트랜지션을 붙이려면 언마운트를 지연시켜야 해서 손이 더 간다.
- 렌더링 비용을 고려했을 때 요소가 무겁고 거의 노출될 일이 없다면 조건부 렌더링으로, 자주 토글하고 상태를 유지해야 하면 CSS 숨김으로 처리할 수 있다.
{open && <Panel />} // 조건부 렌더링 - 닫으면 DOM에서 사라지고 상태도 초기화
<Panel hidden={!open} /> // CSS 숨김 - DOM에 남아 입력값과 스크롤 위치가 보존됨6. visually-hidden
- 화면에서만 감추고 접근성 트리에는 남긴다.
- 아이콘 버튼의 레이블, 스킵 링크, 폼 컨트롤 설명처럼 눈으로는 맥락이 보이지만 스크린리더에는 말로 줘야 하는 것들에 쓴다.
display: none과visibility: hidden은 접근성 트리에서도 제외된다.- tailwindcss의
sr-only가 이 역할이다. 이름만 다르다.
- 크기를 0으로 만들면 일부 스크린리더가 읽지 않아 1px는 남겨둔다.
- 포커스되면 보여야 하는 경우(스킵 링크)는
:focus에서 해제한다. - 반대짝은
aria-hidden="true"- 화면에는 보이고 스크린리더만 무시한다. 장식용 아이콘에 사용한다.
- SEO 측면에서 이 속성(또는 위 방법 중 하나)을 이용해 키워드를 나열하면 구글 스팸 정책을 위반하게 된다.
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
border: 0;
}아래 두 버튼은 화면상 똑같이 ✕ 하나뿐이다. 오른쪽에만 visually-hidden으로 “닫기”라는 이름이 붙어 있어서, 스크린리더는 왼쪽을 “버튼”으로만 읽고 오른쪽을 “닫기 버튼”으로 읽는다.
아래 예제의 요소는 화면에 없지만, Tab 키를 눌러 포커스가 닿는 순간 나타난다.
스킵 링크가 이렇게 동작하는데, 스킵 링크는 키보드만으로 사이트를 탐색하는 사람들을 위해 쓰인다.
요약
| 레이아웃 차지 | 접근성 트리 | DOM | |
|---|---|---|---|
display: none | ✗ | ✗ | ○ |
visibility: hidden | ○ | ✗ | ○ |
opacity: 0 | ○ | ○ | ○ |
hidden 속성 | ✗ | ✗ | ○ |
| 조건부 렌더링 | ✗ | ✗ | ✗ |
visually-hidden | 거의 없음 | ○ | ○ |
Use Cases
- 사용자에게 보이면 안 되는 데이터인가 → 조건부 렌더링. CSS 숨김은 전부 DOM에 남는다.
- 스크린리더에는 읽혀야 하는가 →
visually-hidden - 자리를 비워둬야 하는가 →
visibility: hidden - 그 외 완전히 감추기 →
display: none
opacity: 0은 단독 숨김 수단이 아니다. 보이는 것만 없애고 클릭·포커스·낭독이 전부 남으므로, 전환 효과용으로 쓰고 숨김이 목적이면 visibility나 pointer-events: none을 같이 사용해야 한다.