Notification Badge
packages/ui/src/notification-badge.tsx
아이콘·버튼·메뉴 같은 UI 요소에 붙어 새로운 알림이나 읽지 않은 메시지 수를 나타낸다.
역할과 경계
- 사용자가 아직 처리하지 않은 것이 있다는 사실을 표시한다. 읽지 않은 메시지, 확인되지 않은 알림, 아직 보지 않은 변경사항이 그 대상이다.
- 상태와 무관한 보조 정보나 분류, 강조는 badge.md가 담당한다. 두 컴포넌트는 모양이 아니라 표시가 처리할 일을 가리키는지로 갈린다.
- 형태는 둘이며
count의 유무로 갈린다.count가 없으면 8px 원이고, 있으면 높이 18px 알약이다. 18px 은 12px 글자 상자 14px 에 위아래 2px 을 더한 값이며, 한 자리 수에서도 원을 유지하도록 최소 너비를 같은 18px 로 둔다. 원은 「있다」만 말하고 알약은 몇 개인지 말한다. - 크기 단계를 두지 않는다. 두 형태가 8px 과 18px 로 이미 작고, 필요한 단계는 붙는 아이콘 크기에 따라 갈리므로 실제 자리가 나온 뒤에 더한다.
- 자체 interaction 이나 state Property 를 갖지 않는다. 누르는 동작은 이 표시가 붙은 host 컨트롤이 갖는다.
Anatomy
- Container
- Count (Optional)
Notification Badge
└─ Count (Optional)
count 를 넘기지 않으면 Container 만 남아 원이 된다.
Properties
count:number. 없으면 점이고 있으면 개수다.0이하이면 아무것도 렌더하지 않는다max:99 | 999, 기본값99.count를 함께 넘길 때만 받는다tone:accent | neutral, 기본값accent.count를 함께 넘길 때만 받으며 점은accent로 고정한다stroke:boolean, 기본값true. 배경색과 같은 2px 링이다className: 붙는 위치를 받는 자리다. 컴포넌트는 위치를 소유하지 않는다render: 다른 요소로 렌더한다. 누르는 동작을 허용한다는 뜻은 아니다children은 받지 않는다. 내용은count가 정하므로 임의 요소가 들어올 자리가 없다
tone 과 max 는 개수 형태에서만 뜻을 가지므로 점에 함께 넘기면 타입에서 걸린다.
Behavior
count가0이하이면 아무것도 렌더하지 않는다. 그래서 화면 코드가unread > 0 &&을 쓰지 않아도 된다.count가max를 넘으면99+와999+로 표시한다. 표시 폭이 세 글자와 네 글자를 넘지 않는다.- 자체 interaction 이나 state 를 갖지 않는다. Hover·Focus·Active·Disabled 표현이 없고, 누르는 동작과 Focus 표시는 이 표시가 붙은 host 컨트롤이 갖는다.
색
- tone 은
accent와neutral둘이며 기본값은accent다. 알림 표시는 채운 형태만 있으므로 tone 마다 배경 하나와 글자 하나뿐이다. - badge.md의 「색」과 같은 프리미티브 단계 규칙을 쓴다. 배경은 600 단계이고 글자는
white다.
| tone | 배경 | 글자 |
|---|---|---|
accent (기본값) |
red-600 |
white |
neutral |
neutral-600 |
white |
oe-accent-background-bold-primarytoken 을 쓰지 않는다. 그 token 은 Light 에서red-600, Dark 에서red-400이어서 Dark 에서white글자의 대비가 무너진다. 이름은 이 자리를 가리키지만 값이 맞지 않는다.stroke는 배경색과 같은 2px 링이며 기본값은 켜짐이다. host 와 겹치는 자리에서 경계를 벌리는 용도이고, 겹치지 않는 자리에서는 끈다.
Specification
| 자리 | 값 |
|---|---|
| 점 지름 | 8px |
| 개수 높이 | 18px |
| 개수 최소 너비 | 18px |
| 개수 좌우 여백 | 4px |
| 글자 | 12px Medium |
| 모서리 | 완전한 원 |
| 링 | 2px oe-background-primary |
- 개수 높이 18px 은 12px 글자 상자 14px 에 위아래 2px 을 더한 값이다. 최소 너비를 같은 18px 로 두어 한 자리 수에서도 원을 유지한다.
- 수에
tabular-nums를 쓰지 않는다. 등폭 숫자는 열로 늘어놓은 수의 자리를 맞추는 장치인데 이 표시에는 열이 없고, 자릿수가 바뀌면 폭이 어차피 달라진다. 대신1의 advance 가3과 같은 7.5px 로 넓어져19처럼 섞인 수에서 자간이 벌어져 보인다. 비율 숫자에서1은 5.4px 다. 같은 자릿수에서 폭이 최대 2px 흔들리는 것은 감수한다. - 링 색은
oe-background-primary로 고정되어 있다. 다른 Surface 위에 놓는 자리에서는ring-*클래스로 그 Surface 색을 덮어써야 한다. 컴포넌트는 자기가 어느 Surface 위에 있는지 알지 못한다.
Guidelines
적절한 Shape 선택하기
둘은 무엇을 말하는지로 갈린다. 같은 자리에 둘 다 놓을 수 있으므로, 자리가 아니라 전달할 정보로 고른다.
| Shape | 제공하는 정보 | 쓰는 자리 |
|---|---|---|
dot |
처리할 것이 있다는 사실만 알린다. 수를 세게 만들지 않는다. | 공간이 좁거나 정확한 수가 중요하지 않은 자리다. tone 은 accent 하나뿐이다. |
count |
Count 를 함께 표시해 구체적인 알림 수나 상태 정보를 제공한다. | 세부 정보가 중요하고 충분한 공간이 있을 때다. max 로 표시 폭의 상한을 정한다. |
붙는 위치는 결합하는 요소에 맞춰 정하기
- 컴포넌트는 모양과 색과 수만 소유한다. 아이콘·버튼·메뉴 가운데 어디에, 어느 모서리에 달릴지는 화면 코드가
relative와absolute로 정한다. - 컴포넌트가 host 를 감싸는 구조를 들고 있으면 쓰려는 자리의 Layout 을 건드리게 된다. 그래서 위치를 소유하지 않고
className으로 받는다. - host 와 겹치는 자리에서는
stroke를 켜서 배경색 링으로 경계를 벌린다. 링이 없으면 아이콘의 선과 표시의 경계가 붙어 하나로 읽힌다.
핵심 상태가 있을 때만 표시하기
- 읽지 않은 메시지, 확인되지 않은 알림, 아직 보지 않은 변경사항처럼 사용자가 처리해야 할 상태가 있을 때만 표시한다.
- 사용자가 그 상호작용을 완료하면 사라진다. 확인한 뒤에도 표시가 남아 있으면 처리할 것이 남았다는 뜻으로 읽히므로, 표시 자체를 믿지 않게 된다.
- 상태와 무관한 장식이나 강조에는 쓰지 않는다. 그 자리는 badge.md가 담당한다.
최대 표시 수를 자리에 맞춰 정하기
max는 붙는 자리가 정한다. 아이콘 모서리처럼 좁은 자리는99, 목록 행처럼 넓은 자리는999다.- 넘으면
99+와999+로 표시한다. 값을 둘로 제한해 표시 폭이 세 글자와 네 글자로 고정되므로 좁은 자리에서 넘칠 일이 없다.
0은 표시하지 않기
count={0}이면 컴포넌트가 아무것도 렌더하지 않는다. 화면 코드가unread > 0 &&을 쓰지 않아도 된다.- 처리할 것이 없는데 표시가 남으면 사용자가 없는 알림을 찾게 된다.
접근 이름은 host 컨트롤이 담당하기
- 수와 뜻은 이 표시가 붙은 host 컨트롤의 접근 이름이 담는다. 아이콘 버튼이라면 그 버튼의 이름을 「알림 3개」나 「읽지 않은 메시지 3개」로 둔다.
- 컴포넌트는 수를 텍스트로 렌더할 뿐이고
aria-hidden이나role="status"를 두지 않는다. 자기가 어디에 붙는지 모르므로 「3」이 무엇의 3인지 말할 수 없다. - host 컨트롤의 이름에 수를 담지 않으면 스크린리더가 「3」만 읽는다. 붙이는 자리마다 이름을 함께 고친다.
남용하지 않기
- 중요한 액션을 유도할 때만 쓰고 그 수를 최소로 유지한다.
- 한 화면에 여러 개가 놓이면 시각적 피로를 주고 어느 것이 급한지 판단하는 것을 방해한다. 결국 전부 무시된다.
- 우선순위가 높은 자리에만 둔다. 나머지는 화면 안에서 다른 방식으로 알린다.
현재 정의하지 않은 항목
- 크기 단계. 붙는 아이콘 크기에 따라 8px·18px 외의 단계가 필요한지는 실제 자리가 나온 뒤에 본다.
- 링 색을 prop 으로 받을지. 지금은
oe-background-primary로 고정되어 있고 다른 Surface 에서는ring-*클래스로 덮어쓴다. 덮어쓰는 자리가 자주 나오면 Surface 색을 받는 prop 을 두는 편이 낫다.