07System log filtered by level chips
Info, warning, and error chips hold their pressed state with aria-pressed, rows outside the pressed levels drop out through hidden, and the chip color change snaps in steps(1, end).
Best fora service log panel on an admin screen, the event list in a deploy console
aria-pressedhiddensteps(1, end)

<main class="stage">
<section class="lg is-demo" id="lg" aria-label="System log">
<header class="lg__h">
<span class="lg__cursor" aria-hidden="true"></span>
<h2 class="lg__t">System log</h2>
<span class="lg__live" aria-live="polite" id="lg-live">Pick levels with the chips</span>
</header>
<div class="lg__chips" role="group" aria-label="Level filter">
<button class="lg__chip" type="button" data-k="info" aria-pressed="true">
<span class="lg__ci" aria-hidden="true"><svg viewBox="0 0 256 256" width="1em" height="1em" fill="currentColor"><g><path d="M224 128a96 96 0 1 1-96-96a96 96 0 0 1 96 96" opacity=".2"/><path d="M144 176a8 8 0 0 1-8 8a16 16 0 0 1-16-16v-40a8 8 0 0 1 0-16a16 16 0 0 1 16 16v40a8 8 0 0 1 8 8m88-48A104 104 0 1 1 128 24a104.11 104.11 0 0 1 104 104m-16 0a88 88 0 1 0-88 88a88.1 88.1 0 0 0 88-88m-92-32a12 12 0 1 0-12-12a12 12 0 0 0 12 12"/></g></svg></span>
Info 3
</button>
<button class="lg__chip" type="button" data-k="warn" aria-pressed="true">
<span class="lg__ci lg__ci--w" aria-hidden="true"><svg viewBox="0 0 256 256" width="1em" height="1em" fill="currentColor"><g><path d="M215.46 216H40.54c-12.62 0-20.54-13.21-14.41-23.91l87.46-151.87c6.3-11 22.52-11 28.82 0l87.46 151.87c6.13 10.7-1.79 23.91-14.41 23.91" opacity=".2"/><path d="M236.8 188.09L149.35 36.22a24.76 24.76 0 0 0-42.7 0L19.2 188.09a23.51 23.51 0 0 0 0 23.72A24.35 24.35 0 0 0 40.55 224h174.9a24.35 24.35 0 0 0 21.33-12.19a23.51 23.51 0 0 0 .02-23.72m-13.87 15.71a8.5 8.5 0 0 1-7.48 4.2H40.55a8.5 8.5 0 0 1-7.48-4.2a7.59 7.59 0 0 1 0-7.72l87.45-151.87a8.75 8.75 0 0 1 15 0l87.45 151.87a7.59 7.59 0 0 1-.04 7.72M120 144v-40a8 8 0 0 1 16 0v40a8 8 0 0 1-16 0m20 36a12 12 0 1 1-12-12a12 12 0 0 1 12 12"/></g></svg></span>
Warn 2
</button>
<button class="lg__chip" type="button" data-k="error" aria-pressed="true">
<span class="lg__ci lg__ci--e" aria-hidden="true"><svg viewBox="0 0 256 256" width="1em" height="1em" fill="currentColor"><g><path d="M224 128a96 96 0 1 1-96-96a96 96 0 0 1 96 96" opacity=".2"/><path d="M165.66 101.66L139.31 128l26.35 26.34a8 8 0 0 1-11.32 11.32L128 139.31l-26.34 26.35a8 8 0 0 1-11.32-11.32L116.69 128l-26.35-26.34a8 8 0 0 1 11.32-11.32L128 116.69l26.34-26.35a8 8 0 0 1 11.32 11.32M232 128A104 104 0 1 1 128 24a104.11 104.11 0 0 1 104 104m-16 0a88 88 0 1 0-88 88a88.1 88.1 0 0 0 88-88"/></g></svg></span>
Error 1
</button>
</div>
<ul class="lg__list" id="lg-list">
<li class="lg__row" data-k="info" data-row="1">
<div class="lg__fold"><p class="lg__line"><i class="lg__lv lg__lv--i" aria-hidden="true"><svg viewBox="0 0 256 256" width="1em" height="1em" fill="currentColor"><g><path d="M224 128a96 96 0 1 1-96-96a96 96 0 0 1 96 96" opacity=".2"/><path d="M144 176a8 8 0 0 1-8 8a16 16 0 0 1-16-16v-40a8 8 0 0 1 0-16a16 16 0 0 1 16 16v40a8 8 0 0 1 8 8m88-48A104 104 0 1 1 128 24a104.11 104.11 0 0 1 104 104m-16 0a88 88 0 1 0-88 88a88.1 88.1 0 0 0 88-88m-92-32a12 12 0 1 0-12-12a12 12 0 0 0 12 12"/></g></svg></i>Deploy 1.4.2 done <time class="lg__tm">09:12</time></p></div>
</li>
<li class="lg__row" data-k="warn" data-row="2">
<div class="lg__fold"><p class="lg__line"><i class="lg__lv lg__lv--w" aria-hidden="true"><svg viewBox="0 0 256 256" width="1em" height="1em" fill="currentColor"><g><path d="M215.46 216H40.54c-12.62 0-20.54-13.21-14.41-23.91l87.46-151.87c6.3-11 22.52-11 28.82 0l87.46 151.87c6.13 10.7-1.79 23.91-14.41 23.91" opacity=".2"/><path d="M236.8 188.09L149.35 36.22a24.76 24.76 0 0 0-42.7 0L19.2 188.09a23.51 23.51 0 0 0 0 23.72A24.35 24.35 0 0 0 40.55 224h174.9a24.35 24.35 0 0 0 21.33-12.19a23.51 23.51 0 0 0 .02-23.72m-13.87 15.71a8.5 8.5 0 0 1-7.48 4.2H40.55a8.5 8.5 0 0 1-7.48-4.2a7.59 7.59 0 0 1 0-7.72l87.45-151.87a8.75 8.75 0 0 1 15 0l87.45 151.87a7.59 7.59 0 0 1-.04 7.72M120 144v-40a8 8 0 0 1 16 0v40a8 8 0 0 1-16 0m20 36a12 12 0 1 1-12-12a12 12 0 0 1 12 12"/></g></svg></i>Cache latency 840ms <time class="lg__tm">09:15</time></p></div>
</li>
<li class="lg__row" data-k="info" data-row="3">
<div class="lg__fold"><p class="lg__line"><i class="lg__lv lg__lv--i" aria-hidden="true"><svg viewBox="0 0 256 256" width="1em" height="1em" fill="currentColor"><g><path d="M224 128a96 96 0 1 1-96-96a96 96 0 0 1 96 96" opacity=".2"/><path d="M144 176a8 8 0 0 1-8 8a16 16 0 0 1-16-16v-40a8 8 0 0 1 0-16a16 16 0 0 1 16 16v40a8 8 0 0 1 8 8m88-48A104 104 0 1 1 128 24a104.11 104.11 0 0 1 104 104m-16 0a88 88 0 1 0-88 88a88.1 88.1 0 0 0 88-88m-92-32a12 12 0 1 0-12-12a12 12 0 0 0 12 12"/></g></svg></i>Scheduled job registered <time class="lg__tm">09:18</time></p></div>
</li>
<li class="lg__row" data-k="error" data-row="4">
<div class="lg__fold"><p class="lg__line"><i class="lg__lv lg__lv--e" aria-hidden="true"><svg viewBox="0 0 256 256" width="1em" height="1em" fill="currentColor"><g><path d="M224 128a96 96 0 1 1-96-96a96 96 0 0 1 96 96" opacity=".2"/><path d="M165.66 101.66L139.31 128l26.35 26.34a8 8 0 0 1-11.32 11.32L128 139.31l-26.34 26.35a8 8 0 0 1-11.32-11.32L116.69 128l-26.35-26.34a8 8 0 0 1 11.32-11.32L128 116.69l26.34-26.35a8 8 0 0 1 11.32 11.32M232 128A104 104 0 1 1 128 24a104.11 104.11 0 0 1 104 104m-16 0a88 88 0 1 0-88 88a88.1 88.1 0 0 0 88-88"/></g></svg></i>Payment webhook failed (retry 1/3) <time class="lg__tm">09:21</time></p></div>
</li>
<li class="lg__row" data-k="warn" data-row="5">
<div class="lg__fold"><p class="lg__line"><i class="lg__lv lg__lv--w" aria-hidden="true"><svg viewBox="0 0 256 256" width="1em" height="1em" fill="currentColor"><g><path d="M215.46 216H40.54c-12.62 0-20.54-13.21-14.41-23.91l87.46-151.87c6.3-11 22.52-11 28.82 0l87.46 151.87c6.13 10.7-1.79 23.91-14.41 23.91" opacity=".2"/><path d="M236.8 188.09L149.35 36.22a24.76 24.76 0 0 0-42.7 0L19.2 188.09a23.51 23.51 0 0 0 0 23.72A24.35 24.35 0 0 0 40.55 224h174.9a24.35 24.35 0 0 0 21.33-12.19a23.51 23.51 0 0 0 .02-23.72m-13.87 15.71a8.5 8.5 0 0 1-7.48 4.2H40.55a8.5 8.5 0 0 1-7.48-4.2a7.59 7.59 0 0 1 0-7.72l87.45-151.87a8.75 8.75 0 0 1 15 0l87.45 151.87a7.59 7.59 0 0 1-.04 7.72M120 144v-40a8 8 0 0 1 16 0v40a8 8 0 0 1-16 0m20 36a12 12 0 1 1-12-12a12 12 0 0 1 12 12"/></g></svg></i>Disk free 12% <time class="lg__tm">09:24</time></p></div>
</li>
<li class="lg__row" data-k="info" data-row="6">
<div class="lg__fold"><p class="lg__line"><i class="lg__lv lg__lv--i" aria-hidden="true"><svg viewBox="0 0 256 256" width="1em" height="1em" fill="currentColor"><g><path d="M224 128a96 96 0 1 1-96-96a96 96 0 0 1 96 96" opacity=".2"/><path d="M144 176a8 8 0 0 1-8 8a16 16 0 0 1-16-16v-40a8 8 0 0 1 0-16a16 16 0 0 1 16 16v40a8 8 0 0 1 8 8m88-48A104 104 0 1 1 128 24a104.11 104.11 0 0 1 104 104m-16 0a88 88 0 1 0-88 88a88.1 88.1 0 0 0 88-88m-92-32a12 12 0 1 0-12-12a12 12 0 0 0 12 12"/></g></svg></i>Health check passed <time class="lg__tm">09:27</time></p></div>
</li>
</ul>
</section>
</main>@use "sass:color";
@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-quick;
$easing: $ease-spring;
$color: $subject-blue;
@include stage($stage-ink);
$line: color.mix($subject-cream, $stage-ink, 16%);
$soft: color.mix($subject-cream, $stage-ink, 8%);
.lg {
box-sizing: border-box;
width: min(304px, 92vw);
padding: $sp-3;
border: 1px solid $line;
border-radius: $r-card;
background: $stage-ink;
color: $subject-cream;
font-family: "Cascadia Code", Consolas, -apple-system, "Segoe UI", sans-serif;
font-size: 12px;
line-height: 1.3;
}
.lg__h {
display: flex;
align-items: center;
gap: $sp-1;
margin: 0 0 $sp-2;
}
// 터미널 커서 — 로그가 살아 있음을 알리는 깜빡임
.lg__cursor {
width: 7px;
height: 13px;
border-radius: $r-xs;
background: $subject-mint;
}
.lg__t { margin: 0; font-size: 12px; font-weight: 700; }
.lg__live {
margin-left: auto;
color: rgba(255, 255, 255, .72);
font-size: 10px;
white-space: nowrap;
}
.lg__chips {
display: flex;
gap: $sp-1;
margin: 0 0 $sp-2;
}
.lg__chip {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 0 $sp-2;
border: 1px solid $line;
border-radius: $r-pill;
background: $soft;
color: $subject-cream;
font-family: inherit;
font-size: 11px;
font-weight: 700;
line-height: 1.7;
cursor: pointer;
}
.lg__chip:focus-visible { outline: 2px solid $color; outline-offset: 2px; }
.lg__ci { display: inline-flex; width: 11px; height: 11px; color: $subject-sky; }
.lg__ci--w { color: $stage-yellow; }
.lg__ci--e { color: $error-soft; }
.lg__chip.is-off {
border-color: rgba(255, 255, 255, .14);
background: transparent;
color: rgba(255, 255, 255, .38);
}
.lg__list {
display: grid;
gap: $sp-1;
margin: 0;
padding: 0;
list-style: none;
}
.lg__row[hidden] { display: none; }
.lg__fold { display: grid; grid-template-rows: 1fr; }
.lg__line {
display: flex;
align-items: center;
gap: $sp-2;
margin: 0;
min-height: 0;
overflow: hidden;
padding: $sp-1 $sp-2;
border-radius: $r-xs;
background: rgba(255, 255, 255, .06);
color: rgba(255, 255, 255, .82);
font-size: 11px;
white-space: nowrap;
}
.lg__lv { display: inline-flex; flex: 0 0 auto; width: 12px; height: 12px; }
.lg__lv--i { color: $subject-sky; }
.lg__lv--w { color: $stage-yellow; }
.lg__lv--e { color: $error-soft; }
.lg__tm { margin-left: auto; color: rgba(255, 255, 255, .72); font-size: 10px; }
// ── 격자 미리보기 전용 2초 루프 ─────────────────────
// 칩 끄기와 줄 숨김은 grid 접힘으로 — 색이 바뀌는 순간은 steps 컷
@keyframes lg-warn-off {
0%, 24% { border-color: $line; background: $soft; color: $subject-cream; }
25%, 86% { border-color: rgba(255, 255, 255, .14); background: transparent; color: rgba(255, 255, 255, .38); }
87%, 100% { border-color: $line; background: $soft; color: $subject-cream; }
}
@keyframes lg-err-off {
0%, 54% { border-color: $line; background: $soft; color: $subject-cream; }
55%, 86% { border-color: rgba(255, 255, 255, .14); background: transparent; color: rgba(255, 255, 255, .38); }
87%, 100% { border-color: $line; background: $soft; color: $subject-cream; }
}
@keyframes lg-warn-fold {
0%, 24% { grid-template-rows: 1fr; }
31%, 86% { grid-template-rows: 0fr; }
87%, 100% { grid-template-rows: 1fr; }
}
@keyframes lg-err-fold {
0%, 54% { grid-template-rows: 1fr; }
61%, 86% { grid-template-rows: 0fr; }
87%, 100% { grid-template-rows: 1fr; }
}
@keyframes lg-chip-press-w {
0%, 24% { transform: scale(1); }
27% { transform: scale(.9); }
30%, 86% { transform: scale(1); }
87%, 100% { transform: scale(1); }
}
@keyframes lg-chip-press-e {
0%, 54% { transform: scale(1); }
57% { transform: scale(.9); }
60%, 100% { transform: scale(1); }
}
@keyframes lg-blink {
0%, 49% { opacity: 1; }
50%, 100% { opacity: 0; }
}
.lg.is-demo {
.lg__chip[data-k="warn"] { animation-name: lg-warn-off, lg-chip-press-w; }
.lg__chip[data-k="error"] { animation-name: lg-err-off, lg-chip-press-e; }
.lg__chip {
animation-duration: $dur-loop, $dur-loop;
animation-timing-function: steps(1, end), $easing;
animation-iteration-count: infinite, infinite;
}
.lg__row[data-k="warn"] .lg__fold { animation-name: lg-warn-fold; }
.lg__row[data-k="error"] .lg__fold { animation-name: lg-err-fold; }
.lg__row[data-k="warn"] .lg__fold,
.lg__row[data-k="error"] .lg__fold {
animation-duration: $dur-loop;
animation-timing-function: $easing;
animation-iteration-count: infinite;
}
.lg__cursor {
animation-name: lg-blink;
animation-duration: 1s;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
}
}
@media (max-width: 340px) {
.lg { padding: $sp-2; font-size: 11px; }
.lg__h { margin-bottom: $sp-1; }
.lg__chips { margin-bottom: $sp-1; }
.lg__chip { font-size: 10px; }
.lg__line { padding: 0 $sp-2; font-size: 10px; }
.lg__row[data-row="6"] { display: none; } // 좁은 화면에서는 마지막 줄부터 비운다
.lg__live { font-size: 9px; }
}
@media (prefers-reduced-motion: reduce) {
.lg.is-demo .lg__chip,
.lg.is-demo .lg__row .lg__fold,
.lg.is-demo .lg__cursor { animation: none !important; transform: none !important; opacity: 1 !important; }
// 움직임만 끄고 판정 결과는 남긴다 — 경고 칩은 꺼진 상태
.lg.is-demo .lg__chip[data-k="warn"] {
border-color: rgba(255, 255, 255, .14) !important;
background: transparent !important;
color: rgba(255, 255, 255, .38) !important;
}
.lg.is-demo .lg__row[data-k="warn"] .lg__fold { grid-template-rows: 0fr !important; }
}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);
}
.lg {
box-sizing: border-box;
width: min(304px, 92vw);
padding: 12px;
border: 1px solid rgb(23.5764705882%, 22.0862745098%, 22.9960784314%);
border-radius: 16px;
background: #17141a;
color: #fff7e6;
font-family: "Cascadia Code", Consolas, -apple-system, "Segoe UI", sans-serif;
font-size: 12px;
line-height: 1.3;
}
.lg__h {
display: flex;
align-items: center;
gap: 4px;
margin: 0 0 8px;
}
.lg__cursor {
width: 7px;
height: 13px;
border-radius: 4px;
background: #4cd4a6;
}
.lg__t {
margin: 0;
font-size: 12px;
font-weight: 700;
}
.lg__live {
margin-left: auto;
color: rgba(255, 255, 255, 0.72);
font-size: 10px;
white-space: nowrap;
}
.lg__chips {
display: flex;
gap: 4px;
margin: 0 0 8px;
}
.lg__chip {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 0 8px;
border: 1px solid rgb(23.5764705882%, 22.0862745098%, 22.9960784314%);
border-radius: 999px;
background: rgb(16.2980392157%, 14.9647058824%, 16.5960784314%);
color: #fff7e6;
font-family: inherit;
font-size: 11px;
font-weight: 700;
line-height: 1.7;
cursor: pointer;
}
.lg__chip:focus-visible {
outline: 2px solid #2f6df6;
outline-offset: 2px;
}
.lg__ci {
display: inline-flex;
width: 11px;
height: 11px;
color: #6ec8ff;
}
.lg__ci--w {
color: #ffd23f;
}
.lg__ci--e {
color: #ffe0d8;
}
.lg__chip.is-off {
border-color: rgba(255, 255, 255, 0.14);
background: transparent;
color: rgba(255, 255, 255, 0.38);
}
.lg__list {
display: grid;
gap: 4px;
margin: 0;
padding: 0;
list-style: none;
}
.lg__row[hidden] {
display: none;
}
.lg__fold {
display: grid;
grid-template-rows: 1fr;
}
.lg__line {
display: flex;
align-items: center;
gap: 8px;
margin: 0;
min-height: 0;
overflow: hidden;
padding: 4px 8px;
border-radius: 4px;
background: rgba(255, 255, 255, 0.06);
color: rgba(255, 255, 255, 0.82);
font-size: 11px;
white-space: nowrap;
}
.lg__lv {
display: inline-flex;
flex: 0 0 auto;
width: 12px;
height: 12px;
}
.lg__lv--i {
color: #6ec8ff;
}
.lg__lv--w {
color: #ffd23f;
}
.lg__lv--e {
color: #ffe0d8;
}
.lg__tm {
margin-left: auto;
color: rgba(255, 255, 255, 0.72);
font-size: 10px;
}
@keyframes lg-warn-off {
0%, 24% {
border-color: rgb(23.5764705882%, 22.0862745098%, 22.9960784314%);
background: rgb(16.2980392157%, 14.9647058824%, 16.5960784314%);
color: #fff7e6;
}
25%, 86% {
border-color: rgba(255, 255, 255, 0.14);
background: transparent;
color: rgba(255, 255, 255, 0.38);
}
87%, 100% {
border-color: rgb(23.5764705882%, 22.0862745098%, 22.9960784314%);
background: rgb(16.2980392157%, 14.9647058824%, 16.5960784314%);
color: #fff7e6;
}
}
@keyframes lg-err-off {
0%, 54% {
border-color: rgb(23.5764705882%, 22.0862745098%, 22.9960784314%);
background: rgb(16.2980392157%, 14.9647058824%, 16.5960784314%);
color: #fff7e6;
}
55%, 86% {
border-color: rgba(255, 255, 255, 0.14);
background: transparent;
color: rgba(255, 255, 255, 0.38);
}
87%, 100% {
border-color: rgb(23.5764705882%, 22.0862745098%, 22.9960784314%);
background: rgb(16.2980392157%, 14.9647058824%, 16.5960784314%);
color: #fff7e6;
}
}
@keyframes lg-warn-fold {
0%, 24% {
grid-template-rows: 1fr;
}
31%, 86% {
grid-template-rows: 0fr;
}
87%, 100% {
grid-template-rows: 1fr;
}
}
@keyframes lg-err-fold {
0%, 54% {
grid-template-rows: 1fr;
}
61%, 86% {
grid-template-rows: 0fr;
}
87%, 100% {
grid-template-rows: 1fr;
}
}
@keyframes lg-chip-press-w {
0%, 24% {
transform: scale(1);
}
27% {
transform: scale(0.9);
}
30%, 86% {
transform: scale(1);
}
87%, 100% {
transform: scale(1);
}
}
@keyframes lg-chip-press-e {
0%, 54% {
transform: scale(1);
}
57% {
transform: scale(0.9);
}
60%, 100% {
transform: scale(1);
}
}
@keyframes lg-blink {
0%, 49% {
opacity: 1;
}
50%, 100% {
opacity: 0;
}
}
.lg.is-demo .lg__chip[data-k=warn] {
animation-name: lg-warn-off, lg-chip-press-w;
}
.lg.is-demo .lg__chip[data-k=error] {
animation-name: lg-err-off, lg-chip-press-e;
}
.lg.is-demo .lg__chip {
animation-duration: 2s, 2s;
animation-timing-function: steps(1, end), cubic-bezier(0.2, 0.8, 0.2, 1);
animation-iteration-count: infinite, infinite;
}
.lg.is-demo .lg__row[data-k=warn] .lg__fold {
animation-name: lg-warn-fold;
}
.lg.is-demo .lg__row[data-k=error] .lg__fold {
animation-name: lg-err-fold;
}
.lg.is-demo .lg__row[data-k=warn] .lg__fold,
.lg.is-demo .lg__row[data-k=error] .lg__fold {
animation-duration: 2s;
animation-timing-function: cubic-bezier(0.2, 0.8, 0.2, 1);
animation-iteration-count: infinite;
}
.lg.is-demo .lg__cursor {
animation-name: lg-blink;
animation-duration: 1s;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
}
@media (max-width: 340px) {
.lg {
padding: 8px;
font-size: 11px;
}
.lg__h {
margin-bottom: 4px;
}
.lg__chips {
margin-bottom: 4px;
}
.lg__chip {
font-size: 10px;
}
.lg__line {
padding: 0 8px;
font-size: 10px;
}
.lg__row[data-row="6"] {
display: none;
}
.lg__live {
font-size: 9px;
}
}
@media (prefers-reduced-motion: reduce) {
.lg.is-demo .lg__chip,
.lg.is-demo .lg__row .lg__fold,
.lg.is-demo .lg__cursor {
animation: none !important;
transform: none !important;
opacity: 1 !important;
}
.lg.is-demo .lg__chip[data-k=warn] {
border-color: rgba(255, 255, 255, 0.14) !important;
background: transparent !important;
color: rgba(255, 255, 255, 0.38) !important;
}
.lg.is-demo .lg__row[data-k=warn] .lg__fold {
grid-template-rows: 0fr !important;
}
}// 실제 동작: 칩을 눌러 수준을 끄고 켠다 — 눌린 수준의 줄만 남고 나머지 줄은 hidden 로 화면에서 빠진다.
// 전부 꺼지면 아무 걸러짐이 없으므로 줄 전부를 다시 보여준다.
var lg = document.getElementById('lg');
var lgChips = [].slice.call(document.querySelectorAll('.lg__chip'));
var lgRows = [].slice.call(document.querySelectorAll('.lg__row'));
var lgLive = document.getElementById('lg-live');
function lgFilter() {
var on = lgChips.filter(function (c) { return c.getAttribute('aria-pressed') === 'true'; })
.map(function (c) { return c.getAttribute('data-k'); });
var showAll = on.length === 0 || on.length === lgChips.length;
lgRows.forEach(function (r) {
r.hidden = !(showAll || on.indexOf(r.getAttribute('data-k')) >= 0);
});
lgLive.textContent = showAll ? 'Showing every line' : on.length + ' levels only';
}
lgChips.forEach(function (c) {
c.addEventListener('click', function () {
lg.classList.remove('is-demo');
var now = c.getAttribute('aria-pressed') === 'true';
c.setAttribute('aria-pressed', now ? 'false' : 'true');
c.classList.toggle('is-off', now);
lgFilter();
});
});
lgFilter();import { useState } from "react";
import type { CSSProperties } from "react";
import styles from "./LogLevelFilter.module.scss";
export type LogLevelFilterProps = {
groupLabel?: string;
chipsLabel?: string;
heading?: string;
liveHint?: string;
showAllMsg?: string;
onlyMsg?: (n: number) => string;
chips?: { key: string; label: string }[];
rows?: { key: string; level: string; msg: string; tm: string }[];
demo?: boolean;
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
function InfoIcon() {
return (
<svg viewBox="0 0 256 256" width="1em" height="1em" fill="currentColor">
<g>
<path d="M224 128a96 96 0 1 1-96-96a96 96 0 0 1 96 96" opacity=".2" />
<path d="M144 176a8 8 0 0 1-8 8a16 16 0 0 1-16-16v-40a8 8 0 0 1 0-16a16 16 0 0 1 16 16v40a8 8 0 0 1 8 8m88-48A104 104 0 1 1 128 24a104.11 104.11 0 0 1 104 104m-16 0a88 88 0 1 0-88 88a88.1 88.1 0 0 0 88-88m-92-32a12 12 0 1 0-12-12a12 12 0 0 0 12 12" />
</g>
</svg>
);
}
function WarnIcon() {
return (
<svg viewBox="0 0 256 256" width="1em" height="1em" fill="currentColor">
<g>
<path d="M215.46 216H40.54c-12.62 0-20.54-13.21-14.41-23.91l87.46-151.87c6.3-11 22.52-11 28.82 0l87.46 151.87c6.13 10.7-1.79 23.91-14.41 23.91" opacity=".2" />
<path d="M236.8 188.09L149.35 36.22a24.76 24.76 0 0 0-42.7 0L19.2 188.09a23.51 23.51 0 0 0 0 23.72A24.35 24.35 0 0 0 40.55 224h174.9a24.35 24.35 0 0 0 21.33-12.19a23.51 23.51 0 0 0 .02-23.72m-13.87 15.71a8.5 8.5 0 0 1-7.48 4.2H40.55a8.5 8.5 0 0 1-7.48-4.2a7.59 7.59 0 0 1 0-7.72l87.45-151.87a8.75 8.75 0 0 1 15 0l87.45 151.87a7.59 7.59 0 0 1-.04 7.72M120 144v-40a8 8 0 0 1 16 0v40a8 8 0 0 1-16 0m20 36a12 12 0 1 1-12-12a12 12 0 0 1 12 12" />
</g>
</svg>
);
}
function ErrIcon() {
return (
<svg viewBox="0 0 256 256" width="1em" height="1em" fill="currentColor">
<g>
<path d="M224 128a96 96 0 1 1-96-96a96 96 0 0 1 96 96" opacity=".2" />
<path d="M165.66 101.66L139.31 128l26.35 26.34a8 8 0 0 1-11.32 11.32L128 139.31l-26.34 26.35a8 8 0 0 1-11.32-11.32L116.69 128l-26.35-26.34a8 8 0 0 1 11.32-11.32L128 116.69l26.34-26.35a8 8 0 0 1 11.32 11.32M232 128A104 104 0 1 1 128 24a104.11 104.11 0 0 1 104 104m-16 0a88 88 0 1 0-88 88a88.1 88.1 0 0 0 88-88" />
</g>
</svg>
);
}
const LEVEL_ICONS: Record<string, typeof InfoIcon> = { info: InfoIcon, warn: WarnIcon, error: ErrIcon };
const DEFAULT_CHIPS = [
{ key: "info", label: "Info 3" },
{ key: "warn", label: "Warn 2" },
{ key: "error", label: "Error 1" },
];
const DEFAULT_ROWS = [
{ key: "l1", level: "info", msg: "Deploy 1.4.2 done", tm: "09:12" },
{ key: "l2", level: "warn", msg: "Cache latency 840ms", tm: "09:15" },
{ key: "l3", level: "info", msg: "Scheduled job registered", tm: "09:18" },
{ key: "l4", level: "error", msg: "Payment webhook failed (retry 1/3)", tm: "09:21" },
{ key: "l5", level: "warn", msg: "Disk free 12%", tm: "09:24" },
{ key: "l6", level: "info", msg: "Health check passed", tm: "09:27" },
];
export default function LogLevelFilter({
groupLabel = "System log",
chipsLabel = "Level filter",
heading = "System log",
liveHint = "Pick levels with the chips",
showAllMsg = "Showing every line",
onlyMsg = (n) => `${n} levels only`,
chips = DEFAULT_CHIPS,
rows = DEFAULT_ROWS,
demo = true,
duration = 220,
easing = "cubic-bezier(.2,.8,.2,1)",
color = "#2f6df6",
}: LogLevelFilterProps) {
const [live, setLive] = useState(demo);
const [on, setOn] = useState<Record<string, boolean>>({ info: true, warn: true, error: true });
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
function toggle(k: string) {
setLive(false);
setOn((o) => ({ ...o, [k]: !o[k] }));
}
const onList = chips.filter((c) => on[c.key]).map((c) => c.key);
const showAll = onList.length === 0 || onList.length === chips.length;
const cls = [styles.lg, live ? styles.isDemo : ""].filter(Boolean).join(" ");
return (
<section className={cls} style={vars} aria-label={groupLabel}>
<header className={styles.h}>
<span className={styles.cursor} aria-hidden="true"></span>
<h2 className={styles.t}>{heading}</h2>
<span className={styles.live} aria-live="polite">{showAll ? showAllMsg : onlyMsg(onList.length)}</span>
</header>
<div className={styles.chips} role="group" aria-label={chipsLabel}>
{chips.map((c) => {
const Icon = LEVEL_ICONS[c.key] || InfoIcon;
return (
<button
key={c.key}
className={[styles.chip, styles[`c_${c.key}`], on[c.key] ? "" : styles.isOff].filter(Boolean).join(" ")}
type="button"
aria-pressed={on[c.key]}
onClick={() => toggle(c.key)}
>
<span className={[styles.ci, c.key === "warn" ? styles.ciW : c.key === "error" ? styles.ciE : ""].filter(Boolean).join(" ")} aria-hidden="true"><Icon /></span>
{c.label}
</button>
);
})}
</div>
<ul className={styles.list}>
{rows.map((r) => {
const Icon = LEVEL_ICONS[r.level] || InfoIcon;
return (
<li key={r.key} className={[styles.row, styles[`lv_${r.level}`]].filter(Boolean).join(" ")} hidden={!(showAll || onList.indexOf(r.level) >= 0)}>
<div className={styles.fold}>
<p className={styles.line}>
<i className={[styles.lv, r.level === "warn" ? styles.lvW : r.level === "error" ? styles.lvE : styles.lvI].filter(Boolean).join(" ")} aria-hidden="true"><Icon /></i>
{r.msg}
<time className={styles.tm}>{r.tm}</time>
</p>
</div>
</li>
);
})}
</ul>
</section>
);
}07 System log filtered by level chips — Measured area 29.574% · moving frames 9/23Original by GODRICH · SCSS and React ship in the zip · 9 Activity Feed UI Widgets — Sidebar-Ready Lists
The post this demo lives in
9 Activity Feed UI Widgets — Sidebar-Ready ListsAn activity feed ui is a list that keeps stacking in a screen corner, not a toast that flashes and fades. These nine Sid See all demos in this post


