03Chuyển slide từ list sang chi tiết
Bấm vào một mục sẽ trượt panel danh sách sang trái và panel chi tiết trượt vào từ phải; nút quay lại sẽ đảo ngược.
Dùng choĐi vào menu con trong cài đặt, mở email từ hộp thư
두 패널 transform:translateXoverflow:hidden 컨테이너history 뒤로가기 대응

<main class="stage">
<div class="ltd-viewport is-demo">
<div class="ltd-track">
<ul class="ltd-list" aria-label="Cài đặt">
<li>
<button class="ltd-list__row" type="button">
<span>Cài đặt thông báo</span>
<svg viewBox="0 0 256 256" aria-hidden="true"><g fill="currentColor"><path d="m176 128l-80 80V48Z" opacity=".2"/><path d="m181.66 122.34l-80-80A8 8 0 0 0 88 48v160a8 8 0 0 0 13.66 5.66l80-80a8 8 0 0 0 0-11.32M104 188.69V67.31L164.69 128Z"/></g></svg>
</button>
</li>
<li>
<button class="ltd-list__row" type="button">
<span>Quyền riêng tư</span>
<svg viewBox="0 0 256 256" aria-hidden="true"><g fill="currentColor"><path d="m176 128l-80 80V48Z" opacity=".2"/><path d="m181.66 122.34l-80-80A8 8 0 0 0 88 48v160a8 8 0 0 0 13.66 5.66l80-80a8 8 0 0 0 0-11.32M104 188.69V67.31L164.69 128Z"/></g></svg>
</button>
</li>
</ul>
<div class="ltd-detail">
<button class="ltd-detail__back" type="button">
<svg viewBox="0 0 256 256" aria-hidden="true"><g fill="currentColor"><path d="M112 56v144l-72-72Z" opacity=".2"/><path d="M216 120h-96V56a8 8 0 0 0-13.66-5.66l-72 72a8 8 0 0 0 0 11.32l72 72A8 8 0 0 0 120 200v-64h96a8 8 0 0 0 0-16m-112 60.69L51.31 128L104 75.31Z"/></g></svg>
Danh sách
</button>
<h2>Cài đặt thông báo</h2>
<p>Bật/tắt thông báo push, email, tin nhắn theo từng mục.</p>
</div>
</div>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-base; // 패널 슬라이드 시간
$easing: $ease-spring;
$color: $subject-blue;
@include stage($stage-ink);
.ltd-viewport {
width: min(260px, 80vw);
height: 168px;
overflow: hidden;
border-radius: $r-card;
background: $stage-paper;
position: relative;
}
.ltd-track {
display: flex;
width: 200%;
height: 100%;
transition: transform $duration $easing;
}
.ltd-viewport.is-open .ltd-track { transform: translateX(-50%); }
.ltd-list, .ltd-detail {
width: 50%;
flex: 0 0 auto;
box-sizing: border-box;
padding: $sp-4;
}
.ltd-list { list-style: none; margin: 0; display: flex; flex-direction: column; gap: $sp-2; }
.ltd-list__row {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
border: 0;
background: rgba(23, 20, 26, .04);
border-radius: $r-sm;
padding: $sp-3;
font-size: 13px;
color: $stage-ink;
cursor: pointer;
font-family: inherit;
}
.ltd-list__row:focus-visible { outline: 2px solid $color; outline-offset: 2px; }
.ltd-list__row svg { width: 14px; height: 14px; color: $color; }
.ltd-detail { display: flex; flex-direction: column; gap: $sp-2; }
.ltd-detail h2 { margin: 0; font-size: 15px; color: $stage-ink; }
.ltd-detail p { margin: 0; font-size: 12px; color: rgba(23, 20, 26, .65); }
.ltd-detail__back {
display: inline-flex;
align-items: center;
gap: $sp-1;
align-self: flex-start;
border: 0;
background: none;
color: $subject-blue-deep; // 페이퍼 패널 위 파란 글자 — $subject-blue 는 4.25:1
font-weight: 700;
font-size: 13px;
cursor: pointer;
padding: $sp-1 0;
font-family: inherit;
}
.ltd-detail__back:focus-visible { outline: 2px solid $subject-blue-deep; outline-offset: 2px; }
.ltd-detail__back svg { width: 14px; height: 14px; }
// 데모 강제 루프 — track 을 keyframes 로 자동 왕복
.ltd-viewport.is-demo .ltd-track { animation: ltd-loop $dur-loop $easing infinite; }
@keyframes ltd-loop {
0%, 10% { transform: translateX(0); }
45%, 65% { transform: translateX(-50%); }
95%, 100% { transform: translateX(0); }
}
@media (prefers-reduced-motion: reduce) {
.ltd-track { transition: none; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #17141a;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.ltd-viewport {
width: min(260px, 80vw);
height: 168px;
overflow: hidden;
border-radius: 16px;
background: #ffffff;
position: relative;
}
.ltd-track {
display: flex;
width: 200%;
height: 100%;
transition: transform 300ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.ltd-viewport.is-open .ltd-track {
transform: translateX(-50%);
}
.ltd-list, .ltd-detail {
width: 50%;
flex: 0 0 auto;
box-sizing: border-box;
padding: 16px;
}
.ltd-list {
list-style: none;
margin: 0;
display: flex;
flex-direction: column;
gap: 8px;
}
.ltd-list__row {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
border: 0;
background: rgba(23, 20, 26, 0.04);
border-radius: 8px;
padding: 12px;
font-size: 13px;
color: #17141a;
cursor: pointer;
font-family: inherit;
}
.ltd-list__row:focus-visible {
outline: 2px solid #2f6df6;
outline-offset: 2px;
}
.ltd-list__row svg {
width: 14px;
height: 14px;
color: #2f6df6;
}
.ltd-detail {
display: flex;
flex-direction: column;
gap: 8px;
}
.ltd-detail h2 {
margin: 0;
font-size: 15px;
color: #17141a;
}
.ltd-detail p {
margin: 0;
font-size: 12px;
color: rgba(23, 20, 26, 0.65);
}
.ltd-detail__back {
display: inline-flex;
align-items: center;
gap: 4px;
align-self: flex-start;
border: 0;
background: none;
color: #0a4fe8;
font-weight: 700;
font-size: 13px;
cursor: pointer;
padding: 4px 0;
font-family: inherit;
}
.ltd-detail__back:focus-visible {
outline: 2px solid #0a4fe8;
outline-offset: 2px;
}
.ltd-detail__back svg {
width: 14px;
height: 14px;
}
.ltd-viewport.is-demo .ltd-track {
animation: ltd-loop 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
@keyframes ltd-loop {
0%, 10% {
transform: translateX(0);
}
45%, 65% {
transform: translateX(-50%);
}
95%, 100% {
transform: translateX(0);
}
}
@media (prefers-reduced-motion: reduce) {
.ltd-track {
transition: none;
}
}(function () {
var view = document.querySelector('.ltd-viewport');
function open() { view.classList.remove('is-demo'); view.classList.add('is-open'); view.querySelector('.ltd-detail__back').focus({ preventScroll: true }); }
function close() { view.classList.remove('is-demo'); view.classList.remove('is-open'); view.querySelector('.ltd-list__row').focus({ preventScroll: true }); }
view.querySelectorAll('.ltd-list__row').forEach(function (row) {
row.addEventListener('click', open);
});
view.querySelector('.ltd-detail__back').addEventListener('click', close);
view.addEventListener('keydown', function (e) {
if (e.key === 'Escape' && view.classList.contains('is-open')) close();
});
})();import { useRef, useState, type CSSProperties } from "react";
import styles from "./ListToDetailSlideTransition.module.scss";
export type ListRow = { title: string; heading: string; body: string };
export type ListToDetailSlideTransitionProps = {
rows: ListRow[];
/** 패널 슬라이드 시간 (기본 300ms) */
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
/** 목록 패널과 상세 패널을 나란히 두고 track 전체를 translateX 로 밀어 전환한다 */
export default function ListToDetailSlideTransition({
rows,
duration = 300,
easing = "cubic-bezier(.2,.8,.2,1)",
color = "#2f6df6",
}: ListToDetailSlideTransitionProps) {
const [open, setOpen] = useState<ListRow | null>(null);
const backRef = useRef<HTMLButtonElement>(null);
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<div
className={styles.viewport}
style={vars}
onKeyDown={(e: any) => { if (e.key === "Escape" && open) setOpen(null); }}
>
<div className={styles.track} style={{ transform: open ? "translateX(-50%)" : undefined }}>
<ul className={styles.list}>
{rows.map((row) => (
<li key={row.title}>
<button className={styles.row} type="button" onClick={() => setOpen(row)}>
<span>{row.title}</span>
<svg viewBox="0 0 256 256" aria-hidden="true"><g fill="currentColor"><path d="m176 128l-80 80V48Z" opacity=".2" /><path d="m181.66 122.34l-80-80A8 8 0 0 0 88 48v160a8 8 0 0 0 13.66 5.66l80-80a8 8 0 0 0 0-11.32M104 188.69V67.31L164.69 128Z" /></g></svg>
</button>
</li>
))}
</ul>
<div className={styles.detail}>
<button ref={backRef} className={styles.back} type="button" onClick={() => setOpen(null)}>
<svg viewBox="0 0 256 256" aria-hidden="true"><g fill="currentColor"><path d="M112 56v144l-72-72Z" opacity=".2" /><path d="M216 120h-96V56a8 8 0 0 0-13.66-5.66l-72 72a8 8 0 0 0 0 11.32l72 72A8 8 0 0 0 120 200v-64h96a8 8 0 0 0 0-16m-112 60.69L51.31 128L104 75.31Z" /></g></svg>
목록
</button>
{open && <><h2>{open.heading}</h2><p>{open.body}</p></>}
</div>
</div>
</div>
);
}03 Chuyển slide từ list sang chi tiết — Diện tích đo được 6.846% · khung hình chuyển động 17/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · 9 mẫu List UI: accordion list css và checklist
Bài viết chứa demo này
9 mẫu List UI: accordion list css và checklistAccordion list css là kiểu danh sách mà khi bạn bấm vào một dòng, chiều cao của nó tự giãn ra để lộ nội dung chi tiết — Xem tất cả demo trong bài này
Nếu bạn thích hiệu ứng này - demo tương tự
Giao diện danh sách · bảng07 Danh sách skeleton shimmer9 mẫu List UI: swipe to delete css và phân trang
Giao diện nhập liệu01 Tự chuyển sang ô kế tiếpOTP Input UI: 9 tình huống kèm validation
Giao diện đăng nhập · xác thực01 Thẻ đăng nhập kiểu kính mờ (glassmorphism)9 mẫu form đăng ký & đăng nhập copy-paste được