GODRICH
#css애니메이션#버튼#로딩#전환효과#스크롤#대시보드#실전ui#디자인트렌드

아코디언 리스트 UI 9가지 — 체크리스트까지

아코디언 리스트는 목록 행을 클릭했을 때 그 자리에서 높이가 부드럽게 늘어나며 상세 내용이 펼쳐지는 UI로, 별도 페이지 이동 없이 리스트 화면 안에서 정보를 더 보여주고 싶을 때 씁니다.

자동 재생 · 눌러서 해당 항목으로 · 전부 한 zip

아홉 개는 인기순이 아니라 리스트 한 줄이 실제로 겪는 순서로 골랐습니다. 먼저 행 하나를 펼쳐 상세를 보는 가장 기본적인 동작(01)에서 시작해, 리스트를 그룹으로 묶어 스크롤을 돕는 동작(02), 아예 다른 화면처럼 전환되는 동작(03)으로 넘어갑니다. 그다음은 데이터가 더 필요할 때(04), 자주 쓰지 않는 부가 작업을 숨겨둘 때(05), 리스트 드래그 앤 드롭으로 순서 자체를 바꿀 때(06), 값을 그 자리에서 고칠 때(07), 지나간 일을 기록할 때(08), 마지막으로 할 일을 끝냈다고 표시할 때(09) 순서로 이어집니다. 06번 드래그 재정렬처럼 행을 손가락으로 다루는 쪽은 스와이프 삭제 리스트 UI에 따로 모았고, 펼칠 내용 자체가 없을 때 보여 줄 화면은 빈 화면 디자인에서 다룹니다.

01행 인라인 펼치기

리스트 ui 디자인에서 무엇이 보이나

주문 내역처럼 한 줄만 보이던 행을 클릭하면 화살표 아이콘이 90도로 돌면서 그 아래로 상세 정보가 나타납니다.

행 인라인 펼치기
새 창에서 열기

01 · 행 인라인 펼치기 — 측정 누적면적 19.034% · 움직인 프레임 14/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

grid-template-rows 0fr→1fraria-expandedcaret rotate transform

코드에서 볼 곳

.rowx-item__body {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows .3s cubic-bezier(.2,.8,.2,1);
}
.rowx-item.is-open .rowx-item__body { grid-template-rows: 1fr; }
.rowx-item__body-inner { overflow: hidden; }

height: auto는 애니메이션이 안 되지만 grid-template-rows는 0fr에서 1fr로 보간되기 때문에, 내용 길이를 몰라도 부드럽게 펼쳐집니다.

02그룹 리스트 고정 섹션 헤더

화면에서 일어나는 일

연락처 앱처럼 첫 글자로 묶인 리스트에서, 스크롤하는 동안 지금 보고 있는 그룹의 헤더 글자가 화면 위에 붙어 있다가 다음 그룹이 오면 자연스럽게 밀려 올라갑니다.

그룹 리스트 고정 섹션 헤더
새 창에서 열기

02 · 그룹 리스트 고정 섹션 헤더 — 측정 누적면적 25.533% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

position:sticky top:0그룹별 wrapper파스텔 톤

코드 읽는 법

.grp-group__header {
  position: sticky;
  top: 0;
  z-index: 1;
  background: #ff9ebb;
}

position: sticky는 부모 그룹 <li>의 높이 안에서만 붙어 있으므로, 그룹마다 감싸는 요소가 따로 있어야 다음 그룹이 헤더를 밀어낼 수 있습니다.

03리스트 → 상세 슬라이드 전환

어떤 움직임인가

목록 항목을 누르면 목록 패널이 왼쪽으로 밀려 나가고 상세 패널이 오른쪽에서 밀려 들어오는, 설정 화면에서 흔히 보는 전환입니다.

리스트 → 상세 슬라이드 전환
새 창에서 열기

03 · 리스트 → 상세 슬라이드 전환 — 측정 누적면적 6.846% · 움직인 프레임 17/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

두 패널 transform:translateXoverflow:hidden 컨테이너history 뒤로가기 대응

어디를 바꾸면 되나

.ltd-track {
  display: flex;
  width: 200%;
  transition: transform .3s cubic-bezier(.2,.8,.2,1);
}
.ltd-viewport.is-open .ltd-track { transform: translateX(-50%); }

목록과 상세를 나란히 둔 트랙 하나를 통째로 밀기 때문에, 두 패널을 각각 애니메이션하지 않아도 하나의 transform으로 끝납니다.

04더보기 버튼 리스트

무엇이 보이나

더보기 버튼을 누르면 잠깐 스피너가 돌고, 새 항목 세 개가 아래로 하나씩 순서를 두고 밀려 올라오며 나타납니다.

더보기 버튼 리스트
새 창에서 열기

04 · 더보기 버튼 리스트 — 측정 누적면적 43.192% · 움직인 프레임 13/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

stagger translateY+opacity버튼 로딩 상태 토글신뢰감 있는 밝은 톤

코드에서 볼 곳

btn.addEventListener('click', () => {
  list.classList.add('is-loading');
  setTimeout(() => {
    list.classList.remove('is-loading');
    list.classList.add('has-loaded');
  }, 500);
});

세 행은 처음부터 max-height: 0으로 접혀 있다가 .has-loaded가 붙으면 펼쳐지는데, 행마다 transition-delay를 조금씩 다르게 줘서 한 번에 뿅 나타나지 않고 순서대로 이어집니다.

05롱프레스 액션 메뉴

화면에서 일어나는 일

리스트 행을 0.5초 이상 누르고 있으면 즐겨찾기·보관·삭제 아이콘 메뉴가 튀어 오르듯 나타납니다.

롱프레스 액션 메뉴
새 창에서 열기

05 · 롱프레스 액션 메뉴 — 측정 누적면적 2.742% · 움직인 프레임 11/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

pointerdown 타이머 500msscale bounce keyframes키보드용 더보기 버튼 대체

코드 읽는 법

row.addEventListener('pointerdown', (e) => {
  if (e.target.closest('button')) return;
  timer = setTimeout(openMenu, 500);
});
['pointerup', 'pointerleave'].forEach((ev) =>
  row.addEventListener(ev, () => clearTimeout(timer))
);

손가락(마우스)을 떼면 타이머가 즉시 취소되므로, 스크롤하려고 살짝 누른 정도로는 메뉴가 열리지 않습니다. 마우스로 길게 누르기 힘든 사람을 위해 항상 보이는 "···" 버튼도 같은 메뉴를 엽니다.

06드롭 위치 표시 리스트 재정렬

어떤 움직임인가

여섯 점 손잡이를 잡고 끌면, 항목이 통째로 밀리는 대신 놓일 자리에 파란 빈 줄이 먼저 나타나 위치를 보여줍니다.

드롭 위치 표시 리스트 재정렬
새 창에서 열기

06 · 드롭 위치 표시 리스트 재정렬 — 측정 누적면적 19.851% · 움직인 프레임 16/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

drop-indicator 엘리먼트pointermove 중간 인덱스 계산화살표 키보드 이동

어디를 바꾸면 되나

handle.addEventListener('keydown', (e) => {
  if (!e.altKey || (e.key !== 'ArrowUp' && e.key !== 'ArrowDown')) return;
  e.preventDefault();
  const targetIdx = order().indexOf(item) + (e.key === 'ArrowUp' ? -1 : 1);
  list.insertBefore(item, order()[targetIdx]);
});

마우스 드래그가 어려운 사람도 손잡이에 포커스를 두고 Alt+화살표로 같은 결과를 만들 수 있어야 리스트 재정렬이 접근성을 갖춘 컴포넌트가 됩니다.

07리스트 인라인 편집

무엇이 보이나

이름 칸을 클릭하면 그 자리에서 바로 입력창으로 바뀌고, Enter를 누르거나 밖을 클릭하면 저장되며 연필 아이콘이 체크로 바뀝니다.

리스트 인라인 편집
새 창에서 열기

07 · 리스트 인라인 편집 — 측정 누적면적 6.801% · 움직인 프레임 12/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

contenteditable 대신 input 치환Escape 로 되돌리기신뢰감 있는 밝은 SaaS 톤

코드에서 볼 곳

input.addEventListener('click', () => { if (input.readOnly) startEdit(); });
input.addEventListener('keydown', (e) => {
  if (e.key === 'Enter') save();
  else if (e.key === 'Escape') cancel();
});
input.addEventListener('blur', () => { if (!input.readOnly) save(); });

contenteditable 대신 평범한 <input readonly>를 쓰면 값 검증이나 Escape 취소 같은 처리를 input의 표준 동작 그대로 가져갈 수 있어 코드가 짧아집니다.

08활동 내역 타임라인

화면에서 일어나는 일

세로선을 따라 아이콘 점이 찍힌 활동 기록이 위에서부터 하나씩 순서를 두고 옅게 떠오르며 나타납니다.

활동 내역 타임라인
새 창에서 열기

08 · 활동 내역 타임라인 — 측정 누적면적 6.644% · 움직인 프레임 10/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

stagger fade+translateY타임라인 세로선 + 아이콘 점파스텔 톤

코드 읽는 법

.tl-item {
  opacity: 0;
  transform: translateY(10px);
  animation: tl-in .6s ease-out forwards;
  animation-delay: calc(var(--n) * 140ms);
}
@keyframes tl-in { to { opacity: 1; transform: translateY(0); } }

각 항목에 --n 커스텀 속성으로 순번을 넘기고 calc()로 지연 시간을 계산하면, 항목이 몇 개든 반복문 없이 순서대로 떠오르는 효과를 만들 수 있습니다.

09체크리스트 취소선 완료

어떤 움직임인가

체크박스를 누르면 색이 차오르며 체크 표시가 튀어나오고, 글자엔 취소선이 왼쪽에서 오른쪽으로 그어지며 흐려집니다.

체크리스트 취소선 완료
새 창에서 열기

09 · 체크리스트 취소선 완료 — 측정 누적면적 0.621% · 움직인 프레임 13/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

scale(1.15) 체크scaleX 취소선캔디 파스텔 톤

어디를 바꾸면 되나

.ck-item__text::after {
  content: "";
  position: absolute; inset: 50% 0 auto 0; height: 2px;
  background: #ff9ebb;
  transform: scaleX(0);
  transform-origin: left;
  transition: transform .3s;
}
.ck-item.is-checked .ck-item__text::after { transform: scaleX(1); }

text-decoration-line은 브라우저마다 애니메이션이 매끄럽지 않아서, 대신 가짜 밑줄 하나를 scaleX(0)에서 scaleX(1)로 늘리는 방식을 씁니다. 왼쪽을 원점(transform-origin: left)으로 고정해야 줄이 왼쪽부터 그어집니다.

아코디언 리스트, 어디서 깨지는가

01번 행 펼치기를 만들면서 실제로 겪은 함정은 grid-template-rows를 auto로 바로 애니메이션하려다 실패한 경우입니다 — auto는 시작값도 끝값도 아니라서 브라우저가 보간할 두 숫자가 없고, 그냥 순간적으로 튀어 버립니다. 반드시 0fr과 1fr처럼 그리드 비율값끼리 애니메이션해야 부드럽게 펼쳐집니다. 두 번째로 04번 더보기 버튼에서는 새 행을 처음부터 display: none으로 숨겼다가 보이는 순간 클래스를 바꾸면, 브라우저가 레이아웃을 계산하기도 전에 전환이 끝나 버려 애니메이션 없이 뿅 나타났습니다 — max-height: 0처럼 실제로 렌더링은 되지만 크기만 0인 상태로 시작해야 전환이 눈에 보입니다. 같은 높이 애니메이션 함정은 카드가 격자 안에서 자리를 바꾸는 포트폴리오 갤러리 UI에서도 똑같은 원리로 튀어나옵니다. 이 아홉 개 파일의 압축 비밀번호는 pw5ht9pc이고, 압축을 풀면 vanilla·react 폴더에서 같은 아홉 가지 동작을 나란히 비교해 볼 수 있습니다.

다른 리스트·표 주제는 실전 UI 카테고리에, 스와이프·정렬·페이지네이션처럼 리스트가 다루는 다른 아홉 가지는 스와이프 삭제 리스트 UI 9가지에 정리돼 있고, 이 사이트가 무엇을 다루는지는 소개에 있습니다. grid-template-rows가 fr 단위를 어떻게 보간하는지는 MDN CSS grid-template-rows 문서에, position: sticky의 동작 범위는 MDN position 문서에 정리돼 있습니다.

접근성 — 아홉 개가 reduced-motion에서 하는 일

시스템에서 동작 줄이기를 켜면 아홉 개 데모 모두 transition과 animation을 끄고 최종 상태만 바로 보여줍니다. 01·03번처럼 높이나 위치가 바뀌는 항목은 펼침·전환 상태로 곧장 이동하고, 05·09번처럼 튀어 오르는 느낌이 핵심인 항목은 그 느낌만 빠지고 결과(메뉴 열림, 체크 완료)는 그대로 남습니다. 06번 재정렬과 07번 인라인 편집은 원래도 키보드만으로 전부 조작되므로 동작 자체는 줄어들지 않습니다.

FAQ

아코디언 리스트를 만들 때 아코디언 요소를 써도 되나요?

<details>·<summary>는 열고 닫기 자체는 기본 제공하지만 높이 애니메이션은 직접 넣어야 하고 브라우저마다 기본 스타일이 달라, 아홉 개 데모에서는 버튼과 grid-template-rows로 더 세밀하게 제어했습니다. 접근성이 중요하면 <details> 위에 커스텀 스타일을 얹는 방법도 있습니다. 행을 열어 상세로 들어갔을 때 지금 위치를 표시하는 문제는 브레드크럼 UI가 답을 들고 있습니다.

리스트 항목이 몇 백 개로 많아지면 체크 리스트 ui 코드도 그대로 써도 되나요?

01·02·08번처럼 화면에 보이는 항목마다 애니메이션이 걸리는 경우 항목이 아주 많아지면 가상 스크롤(화면에 보이는 만큼만 렌더링) 같은 별도 최적화가 필요합니다. zip의 코드는 한 페이지에 보이는 수십 개 규모를 기준으로 만들었습니다.

롱프레스 메뉴는 모바일에서도 그대로 동작하나요?

pointerdown·pointerup 이벤트는 마우스·터치·펜을 모두 같은 코드로 처리하므로 그대로 동작합니다. 다만 모바일 브라우저는 길게 누르면 자체 컨텍스트 메뉴나 텍스트 선택이 뜰 수 있어, 데모 코드는 touch-action: manipulation으로 그 충돌을 막습니다.

압축 비밀번호를 입력하세요

비밀번호는 이 글 본문 안에 있습니다. 읽다 보면 나옵니다.