Navigation Patterns
Navigation is the skeleton of every application. Get it wrong and users are lost. Get it right and they never think about it. In this lesson you’ll prompt your way through three navigation patterns: expandable sidebar nav, accessible tab interface, and breadcrumb with pagination.
Sidebar navigation
A sidebar with collapsible sections is one of those components that looks simple but hides a lot of state. The key is to tell the AI exactly which ARIA attributes you want — otherwise you’ll get a pretty div that screen readers can’t parse.
Build a vertical sidebar nav with 3 collapsible sections.
Each section header is a <button> with aria-expanded.
Clicking toggles the child <ul> (hidden/visible) with a CSS transition.
Active item: font-weight 600, left 2px solid #2563eb border.
Style: max-width 240px, monochrome palette.
The resulting pattern uses hidden on the <ul> (not display:none in CSS) so it collapses cleanly. The chevron rotates via a CSS class on the parent <li>.
Tab interface
Tabs are deceptively tricky to get right. The ARIA spec requires role="tablist", role="tab", aria-selected, aria-controls, and role="tabpanel". Keyboard navigation must support arrow keys, Home, and End. If you don’t specify this upfront, most AI-generated tabs will be div-soup.
Create a tab interface with 4 tabs: Overview, Lessons, Resources, Discussion.
Use proper ARIA: role=tablist, role=tab, aria-selected, aria-controls, role=tabpanel.
Keyboard: ArrowLeft/ArrowRight moves focus and activates tabs. Home/End jump to ends.
Active tab: border-bottom 2px solid #111, font-weight 600.
Tab panels: only the active panel is visible.
Prompting tip: Always list the ARIA roles and keyboard behavior in the same prompt as the visual design. If you split them into separate prompts the second pass often breaks the first.
Breadcrumb + Pagination
Breadcrumbs and pagination are small components but they carry important semantic weight — aria-label="Breadcrumb" on the nav, aria-current="page" on the active crumb, and aria-label on each page button.
Build a breadcrumb: Courses / GSAP Animation / Scroll Fade.
Use <nav aria-label="Breadcrumb"> with an <ol>.
Last item gets aria-current="page".
Below it, a pagination row: Prev button, page numbers 1–5, Next button.
Clicking a page number marks it active (highlighted). Prev/Next update accordingly.
What the demo shows
The live demo has all three patterns on one page. The sidebar sections click-collapse with a CSS chevron rotation. The tabs are fully keyboard-navigable. The pagination updates active state with JS and disables Prev/Next at the boundaries. Open DevTools and inspect the ARIA attributes to see the full picture.
내비게이션 패턴은 사이트 아키텍처를 반영합니다. 메뉴 구조를 설명하면 Claude Code가 마크업, 스타일, 인터랙션을 처리합니다.
데스크톱 내비게이션
sticky nav 바를 만들어줘:
- 왼쪽: 로고 (텍스트 "ux dev", font-weight 700)
- 오른쪽: 링크 3개 (코스, 튜토리얼, 블로그)
- 스크롤 시 배경 흰색 + backdrop-filter blur(12px)
- 높이 56px, border-bottom 1px solid #e5e5e5
모바일 햄버거 메뉴
640px 이하에서:
- 링크 숨기기, 햄버거 아이콘 표시
- 클릭 시 드롭다운 메뉴 슬라이드 인
- ESC 또는 바깥 클릭으로 닫기
- aria-expanded, aria-controls 포함
드롭다운 메가메뉴
"코스" 링크에 마우스오버 시 드롭다운:
- 2열 그리드, 각 코스 카드 포함
- position absolute, 부드러운 opacity + translateY 전환
- 포커스 트랩: Tab으로 드롭다운 내 이동 가능
데모는 모바일 드로어와 접근성을 갖춘 완성된 nav를 보여줍니다.
/* Navigation Patterns — script.js
Sidebar expand/collapse, tab interface, and pagination.
*/
/* ── Sidebar Nav ── */
document.querySelectorAll('.section-header').forEach(btn => {
btn.addEventListener('click', () => {
const section = btn.closest('.nav-section');
const items = section.querySelector('.section-items');
const isOpen = section.classList.contains('is-open');
if (isOpen) {
section.classList.remove('is-open');
btn.setAttribute('aria-expanded', 'false');
items.hidden = true;
} else {
section.classList.add('is-open');
btn.setAttribute('aria-expanded', 'true');
items.hidden = false;
}
});
});
/* ── Tab Interface ── */
const tabList = document.querySelector('[role="tablist"]');
const tabs = Array.from(document.querySelectorAll('[role="tab"]'));
const panels = Array.from(document.querySelectorAll('[role="tabpanel"]'));
function activateTab(tab) {
tabs.forEach(t => {
t.classList.remove('is-active');
t.setAttribute('aria-selected', 'false');
t.setAttribute('tabindex', '-1');
});
panels.forEach(p => p.classList.add('is-hidden'));
tab.classList.add('is-active');
tab.setAttribute('aria-selected', 'true');
tab.removeAttribute('tabindex');
const panelId = tab.getAttribute('aria-controls');
document.getElementById(panelId).classList.remove('is-hidden');
}
tabs.forEach(tab => {
tab.addEventListener('click', () => activateTab(tab));
// Keyboard navigation (arrow keys per ARIA spec)
tab.addEventListener('keydown', e => {
let idx = tabs.indexOf(document.activeElement);
if (e.key === 'ArrowRight') {
e.preventDefault();
const next = tabs[(idx + 1) % tabs.length];
next.focus();
activateTab(next);
}
if (e.key === 'ArrowLeft') {
e.preventDefault();
const prev = tabs[(idx - 1 + tabs.length) % tabs.length];
prev.focus();
activateTab(prev);
}
if (e.key === 'Home') {
e.preventDefault();
tabs[0].focus();
activateTab(tabs[0]);
}
if (e.key === 'End') {
e.preventDefault();
tabs[tabs.length - 1].focus();
activateTab(tabs[tabs.length - 1]);
}
});
});
/* ── Pagination ── */
const pageNums = Array.from(document.querySelectorAll('.page-num'));
const prevBtn = document.querySelector('.prev-btn');
const nextBtn = document.querySelector('.next-btn');
let currentPage = 1;
function setPage(n) {
if (n < 1 || n > pageNums.length) return;
currentPage = n;
pageNums.forEach((btn, i) => {
const isActive = i + 1 === currentPage;
btn.classList.toggle('is-active', isActive);
btn.setAttribute('aria-current', isActive ? 'page' : 'false');
});
prevBtn.disabled = currentPage === 1;
nextBtn.disabled = currentPage === pageNums.length;
}
pageNums.forEach((btn, i) => {
btn.addEventListener('click', () => setPage(i + 1));
});
prevBtn.addEventListener('click', () => setPage(currentPage - 1));
nextBtn.addEventListener('click', () => setPage(currentPage + 1));
// Init
setPage(1);