Skip to content

Bottom tab bar

For an app with five or fewer top-level destinations that people should reach from anywhere in one tap.

Navigation

Live preview

Source

<nav class="dc-tb" aria-label="주요 메뉴">
  <ul class="dc-tb__list">
    <li>
      <a class="dc-tb__item" href="#" aria-current="page">
        <span class="dc-tb__ico" aria-hidden="true">⌂</span>
        <span class="dc-tb__lbl">홈</span>
      </a>
    </li>
    <li>
      <a class="dc-tb__item" href="#">
        <span class="dc-tb__ico" aria-hidden="true">⌕</span>
        <span class="dc-tb__lbl">검색</span>
      </a>
    </li>
    <li>
      <a class="dc-tb__item" href="#">
        <span class="dc-tb__ico" aria-hidden="true">+</span>
        <span class="dc-tb__lbl">등록</span>
      </a>
    </li>
    <li>
      <a class="dc-tb__item" href="#">
        <span class="dc-tb__ico" aria-hidden="true">♡</span>
        <span class="dc-tb__lbl">찜</span>
        <span class="dc-tb__dot" aria-hidden="true"></span>
        <span class="dc-sr">새 소식 있음</span>
      </a>
    </li>
    <li>
      <a class="dc-tb__item" href="#">
        <span class="dc-tb__ico" aria-hidden="true">◉</span>
        <span class="dc-tb__lbl">내 정보</span>
      </a>
    </li>
  </ul>
</nav>
.dc-tb {
  padding-bottom: var(--dc-safe-bottom);
  background: var(--dc-surface);
  border-top: 1px solid var(--dc-line);
  font-family: var(--dc-font);
}
.dc-tb__list { display: flex; align-items: stretch; }
.dc-tb__list > li { flex: 1 1 0; min-width: 0; display: flex; }
.dc-tb__item {
  position: relative;
  flex: 1 1 auto; min-width: 0;
  display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: 3px;
  min-height: 56px; padding: var(--dc-2) var(--dc-1);
  color: var(--dc-text-mute); text-decoration: none;
  transition: color var(--dc-fast), background var(--dc-fast);
}
.dc-tb__item:active { background: var(--dc-sunken); }
.dc-tb__item[aria-current="page"] { color: var(--dc-brand-text); }
.dc-tb__ico { font-size: 20px; line-height: 1; }
.dc-tb__lbl {
  font-size: 11px; font-weight: 700; letter-spacing: -.01em;
  white-space: nowrap; max-width: 100%;
  overflow: hidden; text-overflow: ellipsis;
}
.dc-tb__dot {
  position: absolute; top: 9px; right: calc(50% - 16px);
  width: 8px; height: 8px; border-radius: var(--dc-r-full);
  background: var(--dc-danger);
  box-shadow: 0 0 0 2px var(--dc-surface);
}
.dc-tb__badge {
  position: absolute; top: 5px; right: calc(50% - 22px);
  min-width: 18px; height: 18px; padding: 0 5px;
  border-radius: var(--dc-r-full);
  background: var(--dc-danger); color: var(--dc-text-on);
  font-size: 11px; font-weight: 700; line-height: 18px; text-align: center;
  box-shadow: 0 0 0 2px var(--dc-surface);
}
.dc-tb__item--raised { justify-content: flex-start; padding-top: var(--dc-1); }
.dc-tb__raise {
  display: grid; place-items: center;
  width: 46px; height: 46px; margin-top: -14px;
  border-radius: var(--dc-r-full);
  background: var(--dc-brand); color: var(--dc-text-on);
  font-size: 24px; line-height: 1;
  box-shadow: var(--dc-sh-2);
}

How to use it

  • Keep it to 3–5 slots; a sixth destination belongs inside the profile tab, not here
  • Keep the text labels — icons alone are not legible to everyone
  • Mark the active tab with aria-current="page", not colour alone
  • Tapping the active tab again should scroll that tab back to the top

Watch out for

  • Without --dc-safe-bottom in the padding, the home indicator covers the tabs
  • Five slots means 78px each; shorten any label that does not fit on one line
  • A numeric badge is visual only — repeat it in a dc-sr span for screen readers

Next