Skip to content

App screen shell

Splits a screen into top bar, scrolling body and tab bar so the bars stay put while the content moves.

Layout

Live preview

Source

<div class="dc-shell">
  <header class="dc-shell__top">
    <h1 class="dc-shell__title">일자리</h1>
    <button class="dc-shell__ico" type="button">
      <span aria-hidden="true">⋯</span>
      <span class="dc-sr">더보기</span>
    </button>
  </header>
  <main class="dc-shell__body">
    <p class="dc-shell__note">본문만 스크롤합니다. 위·아래 바는 제자리에 남습니다.</p>
    <a class="dc-shell__card" href="#">
      <span class="dc-shell__card-title">용접 보조 · 김해</span>
      <span class="dc-shell__card-meta">일당 160,000원 · 오늘 마감</span>
    </a>
    <a class="dc-shell__card" href="#">
      <span class="dc-shell__card-title">식품 포장 · 평택</span>
      <span class="dc-shell__card-meta">일당 130,000원 · 기숙사 있음</span>
    </a>
    <a class="dc-shell__card" href="#">
      <span class="dc-shell__card-title">물류 상하차 · 인천</span>
      <span class="dc-shell__card-meta">일당 140,000원 · 주 5일</span>
    </a>
    <a class="dc-shell__card" href="#">
      <span class="dc-shell__card-title">호텔 객실 정리 · 제주</span>
      <span class="dc-shell__card-meta">월급 2,450,000원 · 초보 가능</span>
    </a>
    <a class="dc-shell__card" href="#">
      <span class="dc-shell__card-title">농장 일손 · 나주</span>
      <span class="dc-shell__card-meta">일당 120,000원 · 차량 지원</span>
    </a>
  </main>
  <nav class="dc-shell__tabs" aria-label="주요 메뉴">
    <a class="dc-shell__tab" href="#" aria-current="page">
      <span class="dc-shell__tab-ico" aria-hidden="true">⌂</span>
      <span class="dc-shell__tab-lbl">홈</span>
    </a>
    <a class="dc-shell__tab" href="#">
      <span class="dc-shell__tab-ico" aria-hidden="true">⌕</span>
      <span class="dc-shell__tab-lbl">검색</span>
    </a>
    <a class="dc-shell__tab" href="#">
      <span class="dc-shell__tab-ico" aria-hidden="true">♡</span>
      <span class="dc-shell__tab-lbl">찜</span>
    </a>
    <a class="dc-shell__tab" href="#">
      <span class="dc-shell__tab-ico" aria-hidden="true">◉</span>
      <span class="dc-shell__tab-lbl">내 정보</span>
    </a>
  </nav>
</div>
.dc-shell {
  display: flex; flex-direction: column;
  height: 560px; overflow: hidden;
  background: var(--dc-bg);
  border: 1px solid var(--dc-line);
  border-radius: var(--dc-r-md);
  font-family: var(--dc-font);
}
.dc-shell__top {
  flex: 0 0 auto;
  display: flex; align-items: center; gap: var(--dc-2);
  padding: calc(var(--dc-safe-top) + var(--dc-2)) var(--dc-2) var(--dc-2) var(--dc-4);
  background: var(--dc-surface);
  border-bottom: 1px solid var(--dc-line);
}
.dc-shell__title {
  flex: 1 1 auto; min-width: 0;
  font-size: 19px; font-weight: 800; letter-spacing: -.02em;
  color: var(--dc-text);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.dc-shell__title--center { text-align: center; font-size: 16px; font-weight: 700; }
.dc-shell__ico {
  flex: 0 0 auto;
  display: grid; place-items: center;
  width: 44px; height: 44px;
  border: 0; border-radius: var(--dc-r-full);
  background: transparent; color: var(--dc-text);
  font-size: 20px; line-height: 1;
}
.dc-shell__ico:active { background: var(--dc-sunken); }
.dc-shell__body {
  flex: 1 1 auto; min-height: 0;
  overflow-y: auto; overscroll-behavior: contain;
  display: flex; flex-direction: column; gap: var(--dc-3);
  padding: var(--dc-4);
  background: var(--dc-bg);
}
.dc-shell__note { margin: 0; font-size: 12px; color: var(--dc-text-mute); }
.dc-shell__card {
  display: flex; flex-direction: column; gap: var(--dc-1);
  min-height: 72px; padding: var(--dc-4);
  border: 1px solid var(--dc-line); border-radius: var(--dc-r-md);
  background: var(--dc-surface); box-shadow: var(--dc-sh-1);
  text-decoration: none;
}
.dc-shell__card:active { background: var(--dc-sunken); }
.dc-shell__card-title { font-size: 15px; font-weight: 700; color: var(--dc-text); }
.dc-shell__card-meta { font-size: 13px; color: var(--dc-text-sub); }
.dc-shell__tabs {
  flex: 0 0 auto;
  display: flex; align-items: stretch;
  padding-bottom: var(--dc-safe-bottom);
  background: var(--dc-surface);
  border-top: 1px solid var(--dc-line);
}
.dc-shell__tab {
  flex: 1 1 0; 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-shell__tab:active { background: var(--dc-sunken); }
.dc-shell__tab[aria-current="page"] { color: var(--dc-brand-text); }
.dc-shell__tab-ico { font-size: 20px; line-height: 1; }
.dc-shell__tab-lbl { font-size: 11px; font-weight: 700; letter-spacing: -.01em; }
.dc-shell__foot {
  flex: 0 0 auto;
  padding: var(--dc-3) var(--dc-4) calc(var(--dc-safe-bottom) + var(--dc-3));
  background: var(--dc-surface);
  border-top: 1px solid var(--dc-line);
}
.dc-shell__btn {
  display: block; width: 100%; min-height: 48px;
  border: 0; border-radius: var(--dc-r-sm);
  background: var(--dc-brand); color: var(--dc-text-on);
  font-family: inherit; font-size: 16px; font-weight: 700;
}
.dc-shell__btn:active { background: var(--dc-brand-hi); }
.dc-shell__form { display: flex; flex-direction: column; gap: var(--dc-2); }
.dc-shell__label { font-size: 13px; font-weight: 700; color: var(--dc-text-sub); }
.dc-shell__input {
  width: 100%; min-height: 48px; padding: 0 var(--dc-3);
  border: 1px solid var(--dc-line-field); border-radius: var(--dc-r-sm);
  background: var(--dc-surface); color: var(--dc-text); font-size: 15px;
}

How to use it

  • Scroll the body, not the page — scrolling the whole screen drags the tab bar away with it
  • The top bar says where you are; the tab bar says where you can go. Don't mix the two
  • On single-purpose screens like forms, drop the tab bar and pin one action button instead

Watch out for

  • Without --dc-safe-top and --dc-safe-bottom the notch and home bar eat your content
  • Size the shell with dvh, not vh, or the bars jump when the address bar collapses
  • Icon-only buttons need a name in a dc-sr span

Next