대시보드 컴포넌트 9종 — 화면 통째로 말고 이 부품만
대시보드 컴포넌트는 완성된 관리자 화면을 통째로 붙이지 않고 헤더·피드·캘린더처럼 화면 조각 하나만 그대로 옮겨 쓰는 UI 부품입니다. 아홉 개 중 하나는 Ctrl(⌘)+K로 여는 커맨드 팔레트인데, 방향키로 실제로 움직입니다.
자동 재생 · 눌러서 해당 항목으로 · 전부 한 zip
- 01 브레드크럼 + 페이지 헤더
- 02 액티비티 피드 타임라인
- 03 월간 캘린더 위젯
- 04 칸반 보드 미니 위젯
- 05 목표 진행률 스텝 카드
- 06 팀 멤버 리스트 (검색)
- 07 커맨드 팔레트 (⌘K)
- 08 태그 관리 입력 위젯
- 09 파일 업로드 드롭존
아홉 개는 화면에 들어오는 순서가 아니라 대시보드를 쓰는 사람이 실제로 거치는 순서대로 놓았습니다. 지금 어디에 있는지 알려주는 헤더에서 시작해 최근 활동을 확인하고, 일정(캘린더)과 작업(칸반)을 관리한 뒤 목표 진행률을 보고, 협업(팀·검색)과 정리(태그·파일)로 마무리합니다. 완성된 관리자 템플릿이 아니라 이 부품 하나만 떼어 내 프로젝트에 붙일 수 있도록, 데모마다 위젯 하나만 화면 가운데 두었습니다.
01브레드크럼 + 페이지 헤더
케밥(⋮) 버튼을 누르면 오른쪽 위 모서리를 기준으로 메뉴가 실제로 커지며 열리고, 바깥을 클릭하거나 Esc를 누르면 실제로 닫힙니다. 완성된 관리자 화면 없이 경로 표시와 액션 메뉴만 필요할 때 이 조각 하나만 붙이면 됩니다.
function close() {
wrap.classList.remove('is-open');
kebab.setAttribute('aria-expanded', 'false');
}
kebab.addEventListener('click', function (e) {
document.querySelector('.ph').classList.remove('is-demo');
e.stopPropagation();
var open = wrap.classList.toggle('is-open');
kebab.setAttribute('aria-expanded', String(open));
});
document.addEventListener('click', function (e) {
if (!e.target.closest('.ph__menu-wrap')) close();
});
02액티비티 피드 타임라인
'새 활동 보기'를 누르면 숨어 있던 항목이 실제로 위로 밀려 올라오며 나타나고, 종류별 아이콘은 완료·댓글·업로드를 각각 다른 색 원으로 구분합니다.
.af__item--new {
opacity: 0;
transform: translateY(10px);
transition: opacity $duration $easing, transform $duration $easing;
}
.af__item--new.af__item--shown { opacity: 1; transform: translateY(0); }
.af__item--new.is-demo { animation: af-in 2s $easing infinite; }
@keyframes af-in {
0% { opacity: 0; transform: translateY(10px); }
35% { opacity: 1; transform: translateY(0); }
80% { opacity: 1; transform: translateY(0); }
100% { opacity: 0; transform: translateY(10px); }
}
03월간 캘린더 위젯
화살표를 누르면 달이 실제로 넘어가며 그 달의 날짜 칸이 Date 객체로 다시 그려지고, 일정이 있는 날은 점으로, 오늘은 테두리로 표시됩니다.
document.querySelectorAll('.cal__nav').forEach(function (btn) {
btn.addEventListener('click', function () {
cur.m += Number(btn.dataset.dir);
if (cur.m < 0) { cur.m = 11; cur.y--; }
if (cur.m > 11) { cur.m = 0; cur.y++; }
build();
});
});
04칸반 보드 미니 위젯
카드의 화살표 버튼을 누르면 그 카드가 실제로 다음 칸(DOM)으로 옮겨가고, 마지막 칸에 닿으면 화살표 대신 체크 아이콘으로 바뀝니다. 드래그 라이브러리 없이 진행 단계 세 칸만 나눠 보여주고 싶을 때 쓰는 조각입니다.
@keyframes kb-move {
0% { transform: translateX(0) translateY(0); opacity: .9; }
45% { transform: translateX(96px) translateY(-3px); opacity: .9; }
90% { transform: translateX(192px) translateY(0); opacity: .9; }
96% { transform: translateX(192px) translateY(0); opacity: 0; }
100% { transform: translateX(0) translateY(0); opacity: 0; }
}
05목표 진행률 스텝 카드
'다음 단계' 버튼을 누르면 막대가 실제로 그만큼 채워지고 방금 넘긴 단계에 체크 아이콘이 남으며, 마지막 단계에서는 버튼이 사라집니다.
function apply() {
steps.forEach(function (s, i) { s.classList.toggle('is-active', i <= idx); });
fill.style.transform = 'scaleX(' + (Number(steps[idx].dataset.p) / 100) + ')';
pct.textContent = (idx + 1) + ' / ' + steps.length;
btn.hidden = idx === steps.length - 1;
}
btn.addEventListener('click', function () {
document.querySelector('.gt').classList.remove('is-demo');
idx = Math.min(idx + 1, steps.length - 1);
apply();
});
06팀 멤버 리스트 (검색)
이름을 입력하면 그 글자가 들어간 멤버만 실제로 남고 나머지는 실제로 숨겨지며, 온라인 상태 점은 계속 옅은 고리로 퍼졌다 사라집니다.
input.addEventListener('input', function () {
var q = input.value.trim().toLowerCase();
var shown = 0;
rows.forEach(function (r) {
var hit = r.dataset.name.toLowerCase().indexOf(q) !== -1;
r.hidden = !hit;
if (hit) shown++;
});
empty.hidden = shown !== 0;
});
07커맨드 팔레트 (⌘K)
버튼을 누르거나 Ctrl(⌘)+K를 누르면 패널이 실제로 화면 가운데서 커지며 열리고, 입력한 글자가 들어간 명령만 실제로 남으며 방향키·Enter로 고를 수 있습니다.
function filter() {
var q = input.value.trim().toLowerCase();
items.forEach(function (it) { it.hidden = q !== '' && it.dataset.kw.toLowerCase().indexOf(q) === -1; });
var vis = visibleItems();
empty.hidden = vis.length !== 0;
setActive(0, vis);
}
08태그 관리 입력 위젯
입력하고 추가를 누르면(또는 Enter) 새 태그 칩이 목록 끝에 바로 추가되고, 칩의 ×를 누르면 그 태그만 실제로 옅어지며 사라집니다.
function addTag() {
var v = input.value.trim();
if (!v) return;
var li = document.createElement('li');
li.className = 'tg__chip';
li.dataset.tag = v;
li.textContent = v;
var rm = document.createElement('button');
rm.type = 'button'; rm.className = 'tg__remove'; rm.textContent = '×';
rm.setAttribute('aria-label', v + ' 삭제');
li.appendChild(rm);
wireRemove(li);
list.appendChild(li);
input.value = '';
input.focus({ preventScroll: true });
}
09파일 업로드 드롭존
영역에 파일을 끌어다 놓거나 클릭해 고르면 진행률 막대가 실제로 0에서 끝까지 차오르고, 다 차면 체크 아이콘으로 바뀝니다.
function addFile(name) {
var li = document.createElement('li');
li.className = 'fu__file';
var nameEl = document.createElement('span'); nameEl.className = 'fu__file-name'; nameEl.textContent = name;
var track = document.createElement('span'); track.className = 'fu__track';
var fill = document.createElement('span'); fill.className = 'fu__fill';
track.appendChild(fill);
var rm = document.createElement('button'); rm.type = 'button'; rm.className = 'fu__remove';
rm.setAttribute('aria-label', name + ' 삭제'); rm.textContent = '×';
rm.addEventListener('click', function () { li.remove(); });
li.appendChild(nameEl); li.appendChild(track); li.appendChild(rm);
list.appendChild(li);
requestAnimationFrame(function () { fill.style.transform = 'scaleX(1)'; });
setTimeout(function () { li.classList.add('fu__file--done'); }, 900);
}
어디서 깨지는가 — 함정
아홉 개를 만들면서 진짜 걸린 함정은 케밥 메뉴처럼 화면 밖으로 펼쳐지는 요소였습니다. 데스크톱 480×300에서는 멀쩡했는데 320px 폭·200px 높이로 다시 재보니 드롭다운이 화면 절반을 스크롤 영역으로 만들었습니다 — overflow: hidden으로 눈에는 안 보여도 scrollHeight는 실제 레이아웃 크기를 그대로 잰다는 뜻이었고, 메뉴 항목 3개를 2개로 줄이고 시작 위치도 위로 조금 당기고 나서야 폰 화면 검사를 통과했습니다. 칸반 보드의 유령 카드도 비슷한 함정이었습니다 — 세 칸을 순간 이동하듯 옮기면 애니메이션이 측정 구간 대부분에서 멈춰 있어 '움직이지 않는 것'으로 잡혔고, 한 칸씩 멈춰 서는 대신 2초 동안 계속 미끄러지게 바꾸고 나서야 아홉 개 전부 실제로 움직이는 것으로 측정됐습니다. 번역 사전에서도 작은 함정이 있었습니다 — 칸반(04)과 목표 카드(05)가 똑같이 '완료'라는 글자를 쓰는데, 이건 항목마다 따로 채울 필요 없이 사전에 '완료' 한 줄만 채우면 두 항목이 동시에 통과된다는 걸 뒤늦게 알았고, 덕분에 나머지 예순 개 넘는 낱말도 항목별이 아니라 글자 값 하나 기준으로 정리했습니다. 압축 비밀번호 usrg4zqs로 여는 zip 안의 아홉 개 소스에도 이 수정이 그대로 들어가 있습니다.
접근성
아홉 개 전부 prefers-reduced-motion: reduce에서 장식용 자동 루프(케밥 메뉴 자동 열림, 활동 항목 미끄러짐, 오늘 날짜 고리, 유령 카드 이동, 진행률 체크 순서, 온라인 점 고리, 팔레트 열림, 새 태그 슬라이드, 업로드 진행률)를 전부 끄되 실제 상태 값은 그대로 둡니다. 케밥 메뉴(01)와 커맨드 팔레트(07)는 각각 role="menu"·role="dialog"와 aria-expanded·aria-modal을 갖추고 바깥 클릭·Esc로 닫히며, 캘린더(03)의 날짜 버튼은 aria-pressed로 선택 상태를 알립니다. 팀 리스트(06)의 검색 입력과 태그 관리(08)의 삭제 버튼은 각각 aria-label로 스크린 리더에 무엇을 검색·삭제하는지 알리며, 이 계산 방식은 MDN의 Element.scrollHeight 문서에 정리돼 있습니다.
다른 대시보드 소재는 대시보드 카테고리에 있고, 이 사이트가 무엇을 다루는지는 소개에 있습니다.
FAQ
완성된 관리자 대시보드가 아니라 이 부품 하나만 써도 되나요?
네, 이 아홉 개를 나눠서 만든 이유가 그것입니다. 각 데모는 관리자 화면 전체가 아니라 부품 하나만 화면 가운데 두었고, zip 안에도 그 부품 하나의 HTML·SCSS·JS만 들어 있어 자기 프로젝트의 헤더나 사이드바 자리에 바로 옮기면 됩니다.
React 버전도 실제로 상태가 바뀌나요?
네. 커맨드 팔레트의 열림·닫힘, 캘린더의 선택 날짜, 태그 목록처럼 상태가 있는 항목은 전부 useState로 들고 있고, 변수 3개(duration·easing·color)는 props로 받아 CSS 사용자 지정 속성으로 넘깁니다.
파일 업로드나 진행률 막대는 실제로 작동하나요, 장식인가요?
실제로 작동합니다. 파일 업로드(09)는 고른 파일 이름을 목록에 실제로 추가하고 진행률 막대를 0에서 끝까지 채우며, 목표 진행률(05) 버튼은 누를 때마다 실제로 다음 단계 값까지 막대를 채웁니다. 자동 재생 GIF에서 보이는 반복 움직임은 사람이 손대지 않았을 때만 도는 장식 루프이며, 대부분은 실제 조작을 시작하는 순간 바로 꺼지고 나머지도 prefers-reduced-motion 설정에서는 항상 꺼집니다.