05답글이 접히는 댓글 스레드 미니
답글 뭉치는 grid-template-rows 가 0fr 과 1fr 사이에서 접히고 펼쳐지며 토글에는 aria-expanded 가 붙고, 새 댓글은 아래에서 translateY 로 살짝 떠오릅니다.
쓰임블로그 옆의 짧은 댓글창, 사이드바의 최근 의견 위젯
grid-template-rowsaria-expandedtranslateY

<main class="stage">
<section class="ct is-demo" id="ct" aria-label="댓글 스레드">
<article class="ct__top">
<span class="ct__av" style="--a:#90c8b5">박</span>
<div class="ct__body">
<p class="ct__txt"><b>박서아</b> 새 배포 일정 어디서 볼 수 있나요?</p>
<time class="ct__tm">1시간 전</time>
</div>
</article>
<button class="ct__tg" type="button" id="ct-tg" aria-expanded="false" aria-controls="ct-re">
<span class="ct__caret" aria-hidden="true"><svg viewBox="0 0 256 256" width="1em" height="1em" fill="currentColor"><g><path d="m208 96l-80 80l-80-80Z" opacity=".2"/><path d="M215.39 92.94A8 8 0 0 0 208 88H48a8 8 0 0 0-5.66 13.66l80 80a8 8 0 0 0 11.32 0l80-80a8 8 0 0 0 1.73-8.72M128 164.69L67.31 104h121.38Z"/></g></svg></span>
답글 2개
</button>
<div class="ct__fold" id="ct-re">
<div class="ct__inner">
<ul class="ct__list" role="log" aria-live="polite" id="ct-list">
<li class="ct__rep" data-row="1">
<span class="ct__av ct__av--s" style="--a:#ff9ebb">이</span>
<p class="ct__txt"><b>이준</b> 릴리즈 노트 하단에 있어요</p>
<time class="ct__tm">50분 전</time>
</li>
<li class="ct__rep ct__rep--old" data-row="2">
<span class="ct__av ct__av--s" style="--a:#c3d6e2">최</span>
<p class="ct__txt"><b>최도윤</b> 대시보드 공지에도 올라왔습니다</p>
<time class="ct__tm">42분 전</time>
</li>
<li class="ct__rep ct__rep--new" data-row="new">
<div class="ct__rfold"><div class="ct__rcell">
<span class="ct__av ct__av--s" style="--a:#93a4ca">김</span>
<p class="ct__txt"><b>김하늘</b> 일정표 링크도 같이 공유드릴게요</p>
<time class="ct__tm">방금</time>
</div></div>
</li>
</ul>
<button class="ct__add" type="button" id="ct-add">댓글 달기</button>
</div>
</div>
</section>
</main>
<template id="ct-tpl"><li class="ct__rep is-in"><div class="ct__rfold"><div class="ct__rcell">
<span class="ct__av ct__av--s" style="--a:#b9a0ff">한</span>
<p class="ct__txt"><b>한별</b> 발표 자료도 곧 올릴게요</p>
<time class="ct__tm">방금</time>
</div></div></li></template>@use "sass:color";
@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-quick;
$easing: $ease-spring;
$color: $subject-blue;
@include stage($stage-paper);
$line: color.mix($subject-ink, $stage-paper, 14%);
$soft: color.mix($subject-blue, $stage-paper, 6%);
.ct {
box-sizing: border-box;
width: min(304px, 92vw);
padding: $sp-3;
border: 1px solid $line;
border-radius: $r-card;
background: $stage-paper;
color: $subject-ink;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
font-size: 12px;
line-height: 1.3;
}
// ── 원댓글 ────────────────────────────────────────
.ct__top {
display: grid;
grid-template-columns: auto 1fr;
gap: $sp-2;
padding: $sp-1 $sp-2;
border: 1px solid $line;
border-radius: $r-sm;
background: $soft;
}
.ct__av {
display: inline-grid;
place-items: center;
width: 20px;
height: 20px;
border-radius: 50%;
background: var(--a);
color: $subject-ink;
font-size: 11px;
font-weight: 700;
}
.ct__av--s { width: 16px; height: 16px; font-size: 10px; }
.ct__body { min-width: 0; }
.ct__txt { margin: 0; font-size: 12px; line-height: 1.35; }
.ct__txt b { font-weight: 700; }
.ct__tm { display: block; margin-top: 0; color: rgba(23, 20, 26, .72); font-size: 10px; }
// ── 답글 토글 ─────────────────────────────────────
.ct__tg {
display: inline-flex;
align-items: center;
gap: 4px;
margin: $sp-1 0 0 $sp-3;
padding: 0 $sp-2;
border: 0;
border-radius: $r-pill;
background: transparent;
color: $color;
font-family: inherit;
font-size: 11px;
font-weight: 700;
line-height: 1.7;
cursor: pointer;
}
.ct__tg:focus-visible { outline: 2px solid $color; outline-offset: 2px; }
.ct__caret { display: inline-flex; width: 11px; height: 11px; transform: rotate(-90deg); transition: transform $duration $easing; }
.ct__tg.is-open .ct__caret { transform: rotate(0deg); }
// ── 답글 뭉치 — grid-template-rows 로 접는다 ────────
.ct__fold {
display: grid;
grid-template-rows: 0fr;
margin-left: $sp-3;
transition: grid-template-rows $dur-base $easing;
}
.ct__fold.is-open { grid-template-rows: 1fr; }
.ct__inner { min-height: 0; overflow: hidden; }
.ct__list {
display: grid;
gap: $sp-1;
margin: $sp-1 0 0;
padding: 0;
list-style: none;
}
.ct__rep {
display: grid;
grid-template-columns: auto 1fr auto;
align-items: center;
gap: $sp-2;
padding: $sp-1 $sp-2;
border: 1px dashed $line;
border-radius: $r-sm;
background: $stage-paper;
}
.ct__rep .ct__tm { margin: 0; }
// 새 답글의 자리 — 접혀 있다가 자란다
.ct__rep--new, .ct__rep.is-in { display: block; }
.ct__rfold { display: grid; grid-template-rows: 1fr; }
.ct__rep--new .ct__rfold,
.ct__rep.is-in .ct__rfold { grid-template-rows: 0fr; }
.ct__rep.is-grow .ct__rfold { grid-template-rows: 1fr; transition: grid-template-rows $dur-base $easing; }
.ct__rcell {
display: grid;
grid-template-columns: auto 1fr auto;
align-items: center;
gap: $sp-2;
min-height: 0;
overflow: hidden;
}
// 사람이 눌러 넣은 답글: 아래에서 살짝 떠오른다 (is-in 동안만)
.ct__rep.is-in { border-style: solid; border-color: $color; }
.ct__rep.is-in .ct__rcell { animation: ct-rise $dur-base $ease-pop both; }
@keyframes ct-rise {
from { transform: translateY(8px); }
to { transform: translateY(0); }
}
.ct__add {
display: inline-flex;
align-items: center;
margin: $sp-2 0 0;
padding: $sp-1 $sp-3;
border: 1px solid $line;
border-radius: $r-pill;
background: $stage-paper;
color: $subject-ink;
font-family: inherit;
font-size: 12px;
font-weight: 700;
line-height: 1.6;
cursor: pointer;
}
.ct__add:focus-visible { outline: 2px solid $color; outline-offset: 2px; }
// ── 격자 미리보기 전용 2초 루프 ─────────────────────
@keyframes ct-fold-open {
0%, 14% { grid-template-rows: 0fr; }
22%, 86% { grid-template-rows: 1fr; }
87%, 100% { grid-template-rows: 0fr; }
}
@keyframes ct-caret-turn {
0%, 14% { transform: rotate(-90deg); }
19%, 86% { transform: rotate(0deg); }
87%, 100% { transform: rotate(-90deg); }
}
@keyframes ct-new-grow {
0%, 54% { grid-template-rows: 0fr; }
55% { grid-template-rows: 1fr; }
55.1%, 86% { grid-template-rows: 1fr; }
87%, 100% { grid-template-rows: 0fr; }
}
@keyframes ct-new-rise {
0%, 55% { transform: translateY(8px); }
62%, 86% { transform: translateY(0); }
87%, 100% { transform: translateY(8px); }
}
@keyframes ct-new-pop {
0%, 55% { transform: scale(.4); }
61%, 86% { transform: scale(1); }
87%, 100% { transform: scale(.4); }
}
@keyframes ct-add-breath {
0%, 54% { transform: translateY(0); }
59% { transform: translateY(-2px); }
64%, 86% { transform: translateY(0); }
87%, 100% { transform: translateY(0); }
}
.ct.is-demo {
.ct__fold {
animation-name: ct-fold-open;
animation-duration: $dur-loop;
animation-timing-function: $easing;
animation-iteration-count: infinite;
}
.ct__caret {
animation-name: ct-caret-turn;
animation-duration: $dur-loop;
animation-timing-function: $easing;
animation-iteration-count: infinite;
}
.ct__rep--new .ct__rfold {
animation-name: ct-new-grow;
animation-duration: $dur-loop;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
}
.ct__rep--new .ct__rcell {
animation-name: ct-new-rise;
animation-duration: $dur-loop;
animation-timing-function: $easing;
animation-iteration-count: infinite;
}
.ct__rep--new .ct__av {
animation-name: ct-new-pop;
animation-duration: $dur-loop;
animation-timing-function: $easing;
animation-iteration-count: infinite;
}
.ct__rep--new { border-style: solid; border-color: $color; }
.ct__add {
animation-name: ct-add-breath;
animation-duration: $dur-loop;
animation-timing-function: $easing;
animation-iteration-count: infinite;
}
}
@media (max-width: 340px) {
.ct { padding: $sp-2; font-size: 11px; }
.ct__txt { font-size: 11px; }
.ct__rep { padding: 0 $sp-2; }
.ct__rep--old { display: none; } // 좁은 화면에서는 오래된 답글부터 비운다
.ct__fold { margin-left: $sp-2; }
.ct__tg { margin-left: $sp-2; }
.ct__add { margin-top: $sp-1; padding: 0 $sp-3; }
}
@media (prefers-reduced-motion: reduce) {
.ct__fold { transition: none; }
.ct__caret { transition: none; }
.ct__rep.is-in .ct__rcell { animation: none !important; }
.ct.is-demo .ct__fold,
.ct.is-demo .ct__caret,
.ct.is-demo .ct__rep--new .ct__rfold,
.ct.is-demo .ct__rep--new .ct__rcell,
.ct.is-demo .ct__rep--new .ct__av,
.ct.is-demo .ct__add { animation: none !important; transform: none !important; }
// 움직임만 끄고 결과 상태는 남긴다 — 답글은 펼쳐진 채 새 답글까지
.ct.is-demo .ct__fold { grid-template-rows: 1fr !important; }
.ct.is-demo .ct__caret { transform: rotate(0deg) !important; }
.ct.is-demo .ct__rep--new .ct__rfold { grid-template-rows: 1fr !important; }
.ct.is-demo .ct__rep--new .ct__av { transform: scale(1) !important; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ffffff;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.ct {
box-sizing: border-box;
width: min(304px, 92vw);
padding: 12px;
border: 1px solid rgb(87.262745098%, 87.0980392157%, 87.4274509804%);
border-radius: 16px;
background: #ffffff;
color: #17141a;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
font-size: 12px;
line-height: 1.3;
}
.ct__top {
display: grid;
grid-template-columns: auto 1fr;
gap: 8px;
padding: 4px 8px;
border: 1px solid rgb(87.262745098%, 87.0980392157%, 87.4274509804%);
border-radius: 8px;
background: rgb(95.1058823529%, 96.5647058824%, 99.7882352941%);
}
.ct__av {
display: inline-grid;
place-items: center;
width: 20px;
height: 20px;
border-radius: 50%;
background: var(--a);
color: #17141a;
font-size: 11px;
font-weight: 700;
}
.ct__av--s {
width: 16px;
height: 16px;
font-size: 10px;
}
.ct__body {
min-width: 0;
}
.ct__txt {
margin: 0;
font-size: 12px;
line-height: 1.35;
}
.ct__txt b {
font-weight: 700;
}
.ct__tm {
display: block;
margin-top: 0;
color: rgba(23, 20, 26, 0.72);
font-size: 10px;
}
.ct__tg {
display: inline-flex;
align-items: center;
gap: 4px;
margin: 4px 0 0 12px;
padding: 0 8px;
border: 0;
border-radius: 999px;
background: transparent;
color: #2f6df6;
font-family: inherit;
font-size: 11px;
font-weight: 700;
line-height: 1.7;
cursor: pointer;
}
.ct__tg:focus-visible {
outline: 2px solid #2f6df6;
outline-offset: 2px;
}
.ct__caret {
display: inline-flex;
width: 11px;
height: 11px;
transform: rotate(-90deg);
transition: transform 220ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.ct__tg.is-open .ct__caret {
transform: rotate(0deg);
}
.ct__fold {
display: grid;
grid-template-rows: 0fr;
margin-left: 12px;
transition: grid-template-rows 300ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.ct__fold.is-open {
grid-template-rows: 1fr;
}
.ct__inner {
min-height: 0;
overflow: hidden;
}
.ct__list {
display: grid;
gap: 4px;
margin: 4px 0 0;
padding: 0;
list-style: none;
}
.ct__rep {
display: grid;
grid-template-columns: auto 1fr auto;
align-items: center;
gap: 8px;
padding: 4px 8px;
border: 1px dashed rgb(87.262745098%, 87.0980392157%, 87.4274509804%);
border-radius: 8px;
background: #ffffff;
}
.ct__rep .ct__tm {
margin: 0;
}
.ct__rep--new, .ct__rep.is-in {
display: block;
}
.ct__rfold {
display: grid;
grid-template-rows: 1fr;
}
.ct__rep--new .ct__rfold,
.ct__rep.is-in .ct__rfold {
grid-template-rows: 0fr;
}
.ct__rep.is-grow .ct__rfold {
grid-template-rows: 1fr;
transition: grid-template-rows 300ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.ct__rcell {
display: grid;
grid-template-columns: auto 1fr auto;
align-items: center;
gap: 8px;
min-height: 0;
overflow: hidden;
}
.ct__rep.is-in {
border-style: solid;
border-color: #2f6df6;
}
.ct__rep.is-in .ct__rcell {
animation: ct-rise 300ms cubic-bezier(0.2, 1.4, 0.4, 1) both;
}
@keyframes ct-rise {
from {
transform: translateY(8px);
}
to {
transform: translateY(0);
}
}
.ct__add {
display: inline-flex;
align-items: center;
margin: 8px 0 0;
padding: 4px 12px;
border: 1px solid rgb(87.262745098%, 87.0980392157%, 87.4274509804%);
border-radius: 999px;
background: #ffffff;
color: #17141a;
font-family: inherit;
font-size: 12px;
font-weight: 700;
line-height: 1.6;
cursor: pointer;
}
.ct__add:focus-visible {
outline: 2px solid #2f6df6;
outline-offset: 2px;
}
@keyframes ct-fold-open {
0%, 14% {
grid-template-rows: 0fr;
}
22%, 86% {
grid-template-rows: 1fr;
}
87%, 100% {
grid-template-rows: 0fr;
}
}
@keyframes ct-caret-turn {
0%, 14% {
transform: rotate(-90deg);
}
19%, 86% {
transform: rotate(0deg);
}
87%, 100% {
transform: rotate(-90deg);
}
}
@keyframes ct-new-grow {
0%, 54% {
grid-template-rows: 0fr;
}
55% {
grid-template-rows: 1fr;
}
55.1%, 86% {
grid-template-rows: 1fr;
}
87%, 100% {
grid-template-rows: 0fr;
}
}
@keyframes ct-new-rise {
0%, 55% {
transform: translateY(8px);
}
62%, 86% {
transform: translateY(0);
}
87%, 100% {
transform: translateY(8px);
}
}
@keyframes ct-new-pop {
0%, 55% {
transform: scale(0.4);
}
61%, 86% {
transform: scale(1);
}
87%, 100% {
transform: scale(0.4);
}
}
@keyframes ct-add-breath {
0%, 54% {
transform: translateY(0);
}
59% {
transform: translateY(-2px);
}
64%, 86% {
transform: translateY(0);
}
87%, 100% {
transform: translateY(0);
}
}
.ct.is-demo .ct__fold {
animation-name: ct-fold-open;
animation-duration: 2s;
animation-timing-function: cubic-bezier(0.2, 0.8, 0.2, 1);
animation-iteration-count: infinite;
}
.ct.is-demo .ct__caret {
animation-name: ct-caret-turn;
animation-duration: 2s;
animation-timing-function: cubic-bezier(0.2, 0.8, 0.2, 1);
animation-iteration-count: infinite;
}
.ct.is-demo .ct__rep--new .ct__rfold {
animation-name: ct-new-grow;
animation-duration: 2s;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
}
.ct.is-demo .ct__rep--new .ct__rcell {
animation-name: ct-new-rise;
animation-duration: 2s;
animation-timing-function: cubic-bezier(0.2, 0.8, 0.2, 1);
animation-iteration-count: infinite;
}
.ct.is-demo .ct__rep--new .ct__av {
animation-name: ct-new-pop;
animation-duration: 2s;
animation-timing-function: cubic-bezier(0.2, 0.8, 0.2, 1);
animation-iteration-count: infinite;
}
.ct.is-demo .ct__rep--new {
border-style: solid;
border-color: #2f6df6;
}
.ct.is-demo .ct__add {
animation-name: ct-add-breath;
animation-duration: 2s;
animation-timing-function: cubic-bezier(0.2, 0.8, 0.2, 1);
animation-iteration-count: infinite;
}
@media (max-width: 340px) {
.ct {
padding: 8px;
font-size: 11px;
}
.ct__txt {
font-size: 11px;
}
.ct__rep {
padding: 0 8px;
}
.ct__rep--old {
display: none;
}
.ct__fold {
margin-left: 8px;
}
.ct__tg {
margin-left: 8px;
}
.ct__add {
margin-top: 4px;
padding: 0 12px;
}
}
@media (prefers-reduced-motion: reduce) {
.ct__fold {
transition: none;
}
.ct__caret {
transition: none;
}
.ct__rep.is-in .ct__rcell {
animation: none !important;
}
.ct.is-demo .ct__fold,
.ct.is-demo .ct__caret,
.ct.is-demo .ct__rep--new .ct__rfold,
.ct.is-demo .ct__rep--new .ct__rcell,
.ct.is-demo .ct__rep--new .ct__av,
.ct.is-demo .ct__add {
animation: none !important;
transform: none !important;
}
.ct.is-demo .ct__fold {
grid-template-rows: 1fr !important;
}
.ct.is-demo .ct__caret {
transform: rotate(0deg) !important;
}
.ct.is-demo .ct__rep--new .ct__rfold {
grid-template-rows: 1fr !important;
}
.ct.is-demo .ct__rep--new .ct__av {
transform: scale(1) !important;
}
}// 실제 동작: 답글 2개 토글은 접힘·펼침을 바꾸고(aria-expanded),
// 댓글 달기는 답글 칸 맨 아래에 새 댓글을 자라나게 한다 (등장만).
var ct = document.getElementById('ct');
var ctTg = document.getElementById('ct-tg');
var ctRe = document.getElementById('ct-re');
var ctList = document.getElementById('ct-list');
function ctOpen(on) {
ctRe.classList.toggle('is-open', on);
ctTg.setAttribute('aria-expanded', on ? 'true' : 'false');
ctTg.classList.toggle('is-open', on);
}
ctTg.addEventListener('click', function () {
ct.classList.remove('is-demo');
ctOpen(!ctRe.classList.contains('is-open'));
});
document.getElementById('ct-add').addEventListener('click', function () {
ct.classList.remove('is-demo');
ctOpen(true);
var row = document.getElementById('ct-tpl').content.firstElementChild.cloneNode(true);
ctList.appendChild(row);
void row.offsetWidth; // 두 단계로 나눠야 grid 전환이 애니메이션으로 잡힌다
row.classList.add('is-grow'); // is-in 은 그대로 — 새 답글 표시(테두리)로 남긴다
});import { useEffect, useState } from "react";
import type { CSSProperties } from "react";
import styles from "./CommentThreadMini.module.scss";
export type CommentThreadMiniProps = {
groupLabel?: string;
topName?: string;
topText?: string;
topTm?: string;
repliesLabel?: string;
addLabel?: string;
nowLabel?: string;
replies?: { key: number; name: string; initial: string; av: string; text: string }[];
demo?: boolean;
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
function CaretIcon() {
return (
<svg viewBox="0 0 256 256" width="1em" height="1em" fill="currentColor">
<g>
<path d="m208 96l-80 80l-80-80Z" opacity=".2" />
<path d="M215.39 92.94A8 8 0 0 0 208 88H48a8 8 0 0 0-5.66 13.66l80 80a8 8 0 0 0 11.32 0l80-80a8 8 0 0 0 1.73-8.72M128 164.69L67.31 104h121.38Z" />
</g>
</svg>
);
}
const DEFAULT_REPLIES = [
{ key: 1, name: "Lee Jun", initial: "L", av: "#ff9ebb", text: " It's at the bottom of the release notes" },
{ key: 2, name: "Choi Do-yun", initial: "C", av: "#6ec8ff", text: " It's on the dashboard notice too" },
];
export default function CommentThreadMini({
groupLabel = "Comment thread",
topName = "Park Seo-a",
topText = " Where can I see the new release schedule?",
topTm = "1h ago",
repliesLabel = "2 replies",
addLabel = "Reply",
nowLabel = "now",
replies = DEFAULT_REPLIES,
demo = true,
duration = 220,
easing = "cubic-bezier(.2,.8,.2,1)",
color = "#2f6df6",
}: CommentThreadMiniProps) {
const [live, setLive] = useState(demo);
const [open, setOpen] = useState(false);
const [rows, setRows] = useState(replies);
const [grown, setGrown] = useState<number[]>([]);
// 방금 넣은 답글은 두 단계(shut → grow)로 렌더해야 grid 전환이 애니메이션으로 잡힌다
const last = rows[rows.length - 1];
useEffect(() => {
if (last && !grown.includes(last.key)) {
const raf = requestAnimationFrame(() => setGrown((g) => [...g, last.key]));
return () => cancelAnimationFrame(raf);
}
}, [rows, grown, last]);
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
function toggle() {
setLive(false);
setOpen((o) => !o);
}
function add() {
setLive(false);
setOpen(true);
setRows((r) => [...r, { key: Date.now(), name: "Han Byeol", initial: "H", av: "#b9a0ff", text: " I'll post the deck soon too" }]);
}
const cls = [styles.ct, live ? styles.isDemo : ""].filter(Boolean).join(" ");
return (
<section className={cls} style={vars} aria-label={groupLabel}>
<article className={styles.top}>
<span className={styles.av} style={{ background: "#4cd4a6" }}>P</span>
<div className={styles.body}>
<p className={styles.txt}><b>{topName}</b>{topText}</p>
<time className={styles.tm}>{topTm}</time>
</div>
</article>
<button
className={[styles.tg, open ? styles.tgOpen : ""].filter(Boolean).join(" ")}
type="button"
aria-expanded={open}
onClick={toggle}
>
<span className={styles.caret} aria-hidden="true"><CaretIcon /></span>
{repliesLabel}
</button>
<div className={[styles.fold, open ? styles.foldOpen : ""].filter(Boolean).join(" ")}>
<div className={styles.inner}>
<ul className={styles.list} role="log" aria-live="polite">
{rows.map((r, i) => {
const isNew = r === last && !grown.includes(r.key);
const rowCls = [
styles.rep,
i === 0 ? styles.rep1 : "",
i === 1 && rows.length > 2 ? styles.repOld : "",
!isNew ? styles.grow : "",
isNew ? styles.shut : "",
].filter(Boolean).join(" ");
return (
<li key={r.key} className={rowCls}>
<div className={styles.rfold}>
<div className={styles.rcell}>
<span className={styles.avS} style={{ background: r.av }}>{r.initial}</span>
<p className={styles.txt}><b>{r.name}</b>{r.text}</p>
<time className={styles.tm}>{r === last ? nowLabel : "50m ago"}</time>
</div>
</div>
</li>
);
})}
<li className={styles.repNew}>
<div className={styles.rfold}>
<div className={styles.rcell}>
<span className={styles.avS} style={{ background: "#2f6df6" }}>K</span>
<p className={styles.txt}><b>Kim Haneul</b> I'll share the schedule link too</p>
<time className={styles.tm}>{nowLabel}</time>
</div>
</div>
</li>
</ul>
<button className={styles.add} type="button" onClick={add}>{addLabel}</button>
</div>
</div>
</section>
);
}05 답글이 접히는 댓글 스레드 미니 — 측정 누적면적 16.678% · 움직인 프레임 7/23GODRICH 원작 · zip 에 SCSS·React 포함 · 알림 목록 ui 9가지 — 사이드바에 박히는 활동 피드와 이력 위젯
이 데모가 속한 글
알림 목록 ui 9가지 — 사이드바에 박히는 활동 피드와 이력 위젯알림 목록 ui는 떴다 사라지는 토스트가 아니라 화면 한구석에 자리를 잡고 계속 쌓이는 목록을 말합니다. 사이드바에 박히는 이 아홉 개 위젯은 읽음 표시, 고치기 전 값, 로그 수준, 오늘의 건수까지 전부 실제 상태 이 글의 데모 전부 보기


