익스클루시브

Blocked Blur <mv-blocked-blur>

대화의 형태를 유지하는 뮤트.

문화적 레퍼런스

블랙 미러, “화이트 크리스마스”, 찰리 브루커 (Channel 4) (2014, 시리즈). 사람들은 현실에서 서로를 차단할 수 있습니다: 차단된 사람은 회색으로 흐려진 실루엣이 되고 목소리는 웅얼거리는 소음이 되어, 여전히 존재하지만 알아볼 수 없습니다. UI에서는 뮤트되거나 필터링된 메시지가 스레드 안에서 자리와 크기를 유지한 채 내용이 따로 보관된 차분한 실루엣이 되므로, 사용자가 한 번 보기나 뮤트 해제를 선택하기 전까지도 주변의 답글이 자연스럽게 이해됩니다.

작동 방식

대화의 형태를 유지하는 뮤트. 댓글 스레드, 피드 또는 채팅을 감싸면, 뮤트된 작성자(뮤트된 id 목록이나 isMuted 콜백과 대조되는 data-author)의 항목이 사라지는 대신 차분한 실루엣으로 바뀌어 주변의 답글이 계속 자연스럽게 이해됩니다. 아바타는 흐릿한 중립적 형상이 되고, 이름은 “Muted user”로 표시되며, 텍스트는 같은 길이의 부드러운 막대가 되고 항목은 정확한 크기를 유지합니다. 칩은 “Show once”와 “Unmute”를 제공합니다. 읽을 수 있는 노드는 분리되어 메모리에 보관되며 CSS로 흐리게만 처리하지 않으므로, 사용자가 요청하기 전까지 DOM이나 접근성 트리에 아무것도 남지 않습니다. “Show once”(취소 가능한 mv-reveal)는 내용을 제자리에 복원하며, 화면 밖으로 스크롤되거나 설정 가능한 타이머(호버나 포커스 중에는 일시 정지)가 끝나면 다시 숨깁니다. 뮤트된 메시지를 인용한 답글도 인용 부분을 실루엣으로 표시합니다(data-quote-author 또는 data-quote-of). 같은 메커니즘이 사유 레이블과 함께 콘텐츠 필터도 처리합니다: 뮤트한 키워드(“Hidden: contains a keyword you muted”), spoiler나 sensitive 같은 항목 플래그, 또는 사용자 정의 filter 콜백. 세 가지 변형: silhouette(레이아웃 유지), compact(연속된 항목마다 “2 messages from a muted user” 한 줄), hidden(제거되고 스레드 간격 표시만 남음).

카테고리데이터 표시
유형Web Component (<mv-blocked-blur>)
상태안정
키트신뢰와 개인정보 보호
Keywordsexclusive, culture, mute, block, blocked-user, muted-user, moderation, trust-and-safety, spoiler, sensitive-content, content-warning, keyword-filter, comments, thread, chat, feed, silhouette, placeholder, reveal

When to use

  • A forum, comment section or chat lets users mute or block people, and replies to their messages must still make sense
  • Spoilers, sensitive posts or muted keywords must be hidden until the user asks to see them, with the reason shown
  • A moderation or trust-and-safety feature needs content kept out of the page and the accessibility tree until an explicit reveal
  • A feed must keep its layout and scroll position stable while people are muted and unmuted

Avoid when

  • The content is a secret meant to be shown once and then destroyed for good → use Self-Destruct instead
  • The placeholders stand for content that is still loading, not content that is hidden on purpose → use Skeleton instead
  • The server can simply leave muted content out and nobody needs to see that it exists

설치

node scripts/add.mjs blocked-blur --out ./src/marvelous

Marvelous UI MCP 서버를 사용하는 AI 에이전트: install_components({ slugs: ["blocked-blur"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).

복사되는 파일(의존성 포함): tokens/tokens.css, core/base.css, core/dom.js, core/element.js, core/motion.js, components/blocked-blur/blocked-blur.js, components/blocked-blur/blocked-blur.css.

사용법

빠른 시작, 동작하는 가장 작은 마크업:

<mv-blocked-blur muted="u_rick">
  <p data-author="u_kenji">Kenji: use night mode and a tripod.</p>
  <p data-author="u_rick">Rick: phone shots all look fake.</p>
</mv-blocked-blur>

기본 마크업입니다. 여기서 시작해 속성, data-*, CSS 변수로 커스터마이즈하세요:

<div id="bb-demo" style="width:min(100%,68rem);margin-inline:auto">
  <style>
    #bb-demo { font-size: .875rem }
    #bb-demo .bbd-bar { display:flex; flex-wrap:wrap; align-items:center; gap:.75rem 1.5rem; margin:0 0 1rem; padding:.75rem 1rem; border:1px solid var(--mv-border); border-radius:var(--mv-radius-xl); background:var(--mv-surface) }
    #bb-demo .bbd-field { display:flex; align-items:center; gap:.625rem }
    #bb-demo .bbd-k { color:var(--mv-fg-muted); font-size:.75rem; font-weight:600 }
    #bb-demo .bbd-list { display:flex; flex-wrap:wrap; gap:.375rem; align-items:center }
    #bb-demo .bbd-list .mv-badge { gap:.3rem }
    #bb-demo .bbd-list .mv-badge svg { width:.75rem; height:.75rem; opacity:.7 }
    #bb-demo .bbd-none { color:var(--mv-fg-subtle); font-size:.75rem }
    #bb-demo .bbd-log { flex-basis:100%; margin:0; min-height:1.1rem; color:var(--mv-fg-muted); font:500 .75rem/1.4 var(--mv-font-mono) }
    #bb-demo .bbd-log b { color:var(--mv-fg); font-weight:600 }
    #bb-demo .bbd-grid { display:grid; grid-template-columns:minmax(0,1fr) 21rem; gap:1.25rem; align-items:start }
    #bb-demo .bbd-card { border:1px solid var(--mv-border); border-radius:var(--mv-radius-xl); background:var(--mv-surface); overflow:hidden }
    #bb-demo .bbd-head { padding:1rem 1.25rem .875rem; border-bottom:1px solid var(--mv-border) }
    #bb-demo .bbd-head small { display:block; color:var(--mv-fg-muted); font-size:.75rem }
    #bb-demo .bbd-head h3 { margin:.25rem 0 0; font-size:1rem; letter-spacing:-.01em; line-height:1.35 }
    #bb-demo .bbd-thread { padding:.5rem 1.25rem 1rem }

    /* Comments */
    #bb-demo .bbd-c { display:grid; grid-template-columns:2rem minmax(0,1fr); gap:0 .75rem; padding-top:.875rem }
    #bb-demo .bbd-c > .mv-avatar { grid-row:span 2 }
    #bb-demo .bbd-c-main { min-width:0 }
    #bb-demo .bbd-c-head { display:flex; align-items:baseline; gap:.5rem; line-height:1.5 }
    #bb-demo .bbd-c-head b { font-weight:600; font-size:.8125rem }
    #bb-demo .bbd-c-head time { color:var(--mv-fg-subtle); font-size:.75rem }
    #bb-demo .bbd-c p { margin:.125rem 0 0; line-height:1.55; color:var(--mv-fg) }
    #bb-demo .bbd-c blockquote { margin:.375rem 0 .25rem; padding:.375rem .75rem; border-inline-start:3px solid var(--mv-border-strong); border-radius:0 var(--mv-radius-md) var(--mv-radius-md) 0; background:var(--mv-bg-subtle); color:var(--mv-fg-muted); font-size:.8125rem; line-height:1.5 }
    #bb-demo .bbd-foot { display:flex; align-items:center; gap:.25rem; margin:.25rem 0 0 -.5rem; min-height:1.75rem }
    #bb-demo .bbd-act { display:inline-flex; align-items:center; gap:.3rem; height:1.625rem; padding:0 .5rem; border:0; border-radius:var(--mv-radius-md); background:none; color:var(--mv-fg-muted); font:600 .75rem var(--mv-font-sans); cursor:pointer }
    #bb-demo .bbd-act:hover { background:var(--mv-bg-muted); color:var(--mv-fg) }
    #bb-demo .bbd-act:focus-visible { outline:none; box-shadow:var(--mv-focus-ring) }
    #bb-demo .bbd-act svg { width:.875rem; height:.875rem }
    #bb-demo .bbd-replies { margin-inline-start:-1.625rem; padding-inline-start:1.625rem; border-inline-start:2px solid var(--mv-border) }

    /* Chat */
    #bb-demo .bbd-chat { display:grid; gap:.625rem; padding:1rem }
    #bb-demo .bbd-msg { display:flex; align-items:flex-end; gap:.5rem; max-width:88% }
    #bb-demo .bbd-bubble { min-width:0; padding:.5rem .75rem; border-radius:1rem 1rem 1rem .375rem; background:var(--mv-bg-muted) }
    #bb-demo .bbd-bubble b { display:block; font-size:.6875rem; font-weight:600; color:var(--mv-fg-muted); line-height:1.4 }
    #bb-demo .bbd-bubble p { margin:0; line-height:1.45 }
    #bb-demo .bbd-msg[data-me] { margin-inline-start:auto; flex-direction:row-reverse }
    #bb-demo .bbd-msg[data-me] .bbd-bubble { border-radius:1rem 1rem .375rem 1rem; background:var(--mv-accent); color:var(--mv-fg-on-accent) }
    #bb-demo .bbd-day { justify-self:center; color:var(--mv-fg-subtle); font-size:.6875rem; font-weight:600; letter-spacing:.04em; text-transform:uppercase }
    #bb-demo .bbd-compose { display:flex; align-items:center; gap:.5rem; padding:.75rem 1rem; border-top:1px solid var(--mv-border); color:var(--mv-fg-subtle); font-size:.8125rem }
    #bb-demo .bbd-compose span { flex:1; padding:.5rem .75rem; border:1px solid var(--mv-border); border-radius:var(--mv-radius-full) }
    @media (max-width:56rem) { #bb-demo .bbd-grid { grid-template-columns:minmax(0,1fr) } }
  </style>

  <div class="bbd-bar">
    <div class="bbd-field">
      <span class="bbd-k" id="bbd-l-variant">Thread layout</span>
      <mv-segmented id="bbd-variant" aria-labelledby="bbd-l-variant" name="variant" value="silhouette">
        <button value="silhouette">Silhouette</button>
        <button value="compact">Compact</button>
        <button value="hidden">Hidden</button>
      </mv-segmented>
    </div>
    <div class="bbd-field">
      <span class="bbd-k">Muted</span>
      <span class="bbd-list" id="bbd-muted"></span>
    </div>
    <div class="bbd-field">
      <span class="bbd-k">Keyword</span>
      <span class="mv-badge" data-variant="outline" data-shape="pill">giveaway</span>
    </div>
    <label class="mv-choice" data-control="end">
      <input type="checkbox" role="switch" class="mv-switch" id="bbd-timer">
      <span class="mv-choice-text"><span class="mv-choice-title">Re-hide after 15 s</span></span>
    </label>
    <p class="bbd-log" id="bbd-log" aria-hidden="true">Rick Vance and Marcus Hill are muted. Their messages keep their place in the conversation.</p>
  </div>

  <div class="bbd-grid">
    <section class="bbd-card" aria-labelledby="bbd-post">
      <header class="bbd-head">
        <small>Night Sky Photography · posted by Ingrid Solberg · 142 comments</small>
        <h3 id="bbd-post">Phone or real camera for the northern lights? First trip to Tromsø in February</h3>
      </header>
      <mv-blocked-blur id="bbd-thread" class="bbd-thread" muted="u_rick u_marcus" keywords="giveaway">
        <article class="bbd-c" data-author="u_kenji" id="c-kenji">
          <span class="mv-avatar" data-size="sm" data-variant="tinted" style="--mv-avatar-hue:250" data-mute-part="avatar" role="img" aria-label="Kenji Watanabe"><span class="mv-avatar-fallback" aria-hidden="true">KW</span></span>
          <div class="bbd-c-main">
            <div class="bbd-c-head"><b data-mute-part="name">Kenji Watanabe</b><time datetime="2026-09-23T08:10">3h</time></div>
            <p data-mute-part="body">A recent phone is enough if it sits on a tripod. Use night mode, set the exposure to 3-5 seconds and turn the flash off. A stable base matters more than the sensor.</p>
            <div class="bbd-foot" data-mute-part="actions"><button class="bbd-act" type="button">Reply</button><button class="bbd-act" type="button" data-mute="u_kenji">Mute</button></div>
            <div class="bbd-replies">
              <article class="bbd-c" data-author="u_rick" id="c-rick">
                <span class="mv-avatar" data-size="sm" data-variant="tinted" style="--mv-avatar-hue:20" data-mute-part="avatar" role="img" aria-label="Rick Vance"><span class="mv-avatar-fallback" aria-hidden="true">RV</span></span>
                <div class="bbd-c-main">
                  <div class="bbd-c-head"><b data-mute-part="name">Rick Vance</b><time datetime="2026-09-23T08:42">3h</time></div>
                  <p data-mute-part="body">Phone aurora shots all look fake. If you can’t afford a real camera, stay home and watch a video of it instead.</p>
                  <div class="bbd-foot" data-mute-part="actions"><button class="bbd-act" type="button">Reply</button><button class="bbd-act" type="button" data-mute="u_rick">Mute</button></div>
                  <div class="bbd-replies">
                    <article class="bbd-c" data-author="u_amara" id="c-amara">
                      <span class="mv-avatar" data-size="sm" data-variant="tinted" style="--mv-avatar-hue:150" data-mute-part="avatar" role="img" aria-label="Amara Okafor"><span class="mv-avatar-fallback" aria-hidden="true">AO</span></span>
                      <div class="bbd-c-main">
                        <div class="bbd-c-head"><b data-mute-part="name">Amara Okafor</b><time datetime="2026-09-23T09:05">2h</time></div>
                        <blockquote data-quote-author="u_rick">If you can’t afford a real camera, stay home and watch a video of it instead.</blockquote>
                        <p data-mute-part="body">Hard disagree. My best shot from Abisko was taken on a phone with a $20 tripod. Kenji’s settings are exactly right.</p>
                        <div class="bbd-foot" data-mute-part="actions"><button class="bbd-act" type="button">Reply</button><button class="bbd-act" type="button" data-mute="u_amara">Mute</button></div>
                      </div>
                    </article>
                  </div>
                </div>
              </article>
              <article class="bbd-c" data-author="u_ingrid" id="c-ingrid">
                <span class="mv-avatar" data-size="sm" data-variant="tinted" style="--mv-avatar-hue:320" data-mute-part="avatar" role="img" aria-label="Ingrid Solberg"><span class="mv-avatar-fallback" aria-hidden="true">IS</span></span>
                <div class="bbd-c-main">
                  <div class="bbd-c-head"><b data-mute-part="name">Ingrid Solberg</b><time datetime="2026-09-23T09:30">2h</time></div>
                  <p data-mute-part="body">Thanks Kenji, ordering a mini tripod today. Any tips for keeping the battery alive at −15 °C?</p>
                  <div class="bbd-foot" data-mute-part="actions"><button class="bbd-act" type="button">Reply</button><button class="bbd-act" type="button" data-mute="u_ingrid">Mute</button></div>
                </div>
              </article>
            </div>
          </div>
        </article>
        <article class="bbd-c" data-author="u_lucas" id="c-lucas">
          <span class="mv-avatar" data-size="sm" data-variant="tinted" style="--mv-avatar-hue:60" data-mute-part="avatar" role="img" aria-label="Lucas Moreau"><span class="mv-avatar-fallback" aria-hidden="true">LM</span></span>
          <div class="bbd-c-main">
            <div class="bbd-c-head"><b data-mute-part="name">Lucas Moreau</b><time datetime="2026-09-23T10:02">1h</time></div>
            <p data-mute-part="body">Giveaway! The first 50 people who message me get my aurora presets pack for free. Link in my profile.</p>
            <div class="bbd-foot" data-mute-part="actions"><button class="bbd-act" type="button">Reply</button><button class="bbd-act" type="button" data-mute="u_lucas">Mute</button></div>
          </div>
        </article>
        <article class="bbd-c" data-author="u_priya" id="c-priya">
          <span class="mv-avatar" data-size="sm" data-variant="tinted" style="--mv-avatar-hue:200" data-mute-part="avatar" role="img" aria-label="Priya Raman"><span class="mv-avatar-fallback" aria-hidden="true">PR</span></span>
          <div class="bbd-c-main">
            <div class="bbd-c-head"><b data-mute-part="name">Priya Raman</b><time datetime="2026-09-23T10:40">48m</time></div>
            <p data-mute-part="body">Watch cloud cover more than the activity forecast. Clear skies at a Kp of 2 beat a Kp of 5 behind clouds every time.</p>
            <div class="bbd-foot" data-mute-part="actions"><button class="bbd-act" type="button">Reply</button><button class="bbd-act" type="button" data-mute="u_priya">Mute</button></div>
          </div>
        </article>
      </mv-blocked-blur>
    </section>

    <section class="bbd-card" aria-labelledby="bbd-chat-title">
      <header class="bbd-head">
        <small>Group chat · 6 members</small>
        <h3 id="bbd-chat-title">Book club · chapter 12</h3>
      </header>
      <mv-blocked-blur id="bbd-chat" class="bbd-chat" muted="u_rick u_marcus" variant="compact" flags="spoiler">
        <span class="bbd-day">Today</span>
        <div class="bbd-msg" data-author="u_sofia">
          <span class="mv-avatar" data-size="xs" data-variant="tinted" style="--mv-avatar-hue:280" data-mute-part="avatar" role="img" aria-label="Sofia Almeida"><span class="mv-avatar-fallback" aria-hidden="true">SA</span></span>
          <div class="bbd-bubble"><b data-mute-part="name">Sofia Almeida</b><p data-mute-part="body">Did everyone finish chapter 12?</p></div>
        </div>
        <div class="bbd-msg" data-author="u_marcus">
          <span class="mv-avatar" data-size="xs" data-variant="tinted" style="--mv-avatar-hue:30" data-mute-part="avatar" role="img" aria-label="Marcus Hill"><span class="mv-avatar-fallback" aria-hidden="true">MH</span></span>
          <div class="bbd-bubble"><b data-mute-part="name">Marcus Hill</b><p data-mute-part="body">This book is so overrated.</p></div>
        </div>
        <div class="bbd-msg" data-author="u_marcus">
          <span class="mv-avatar" data-size="xs" data-variant="tinted" style="--mv-avatar-hue:30" data-mute-part="avatar" role="img" aria-label="Marcus Hill"><span class="mv-avatar-fallback" aria-hidden="true">MH</span></span>
          <div class="bbd-bubble"><b data-mute-part="name">Marcus Hill</b><p data-mute-part="body">Whoever picked it clearly has no taste.</p></div>
        </div>
        <div class="bbd-msg" data-author="u_maya" data-flags="spoiler">
          <span class="mv-avatar" data-size="xs" data-variant="tinted" style="--mv-avatar-hue:170" data-mute-part="avatar" role="img" aria-label="Maya Cohen"><span class="mv-avatar-fallback" aria-hidden="true">MC</span></span>
          <div class="bbd-bubble"><b data-mute-part="name">Maya Cohen</b><p data-mute-part="body">I can’t believe the sister was the narrator all along.</p></div>
        </div>
        <div class="bbd-msg" data-author="u_me" data-me>
          <div class="bbd-bubble"><p data-mute-part="body">No spoilers please, I’m 30 pages behind!</p></div>
        </div>
        <div class="bbd-msg" data-author="u_sofia">
          <span class="mv-avatar" data-size="xs" data-variant="tinted" style="--mv-avatar-hue:280" data-mute-part="avatar" role="img" aria-label="Sofia Almeida"><span class="mv-avatar-fallback" aria-hidden="true">SA</span></span>
          <div class="bbd-bubble"><b data-mute-part="name">Sofia Almeida</b><p data-mute-part="body">Let’s meet Thursday at 7 PM at the café on Elm Street.</p></div>
        </div>
      </mv-blocked-blur>
      <div class="bbd-compose" aria-hidden="true"><span>Message the group…</span></div>
    </section>
  </div>

  <script type="module">
    const root = document.getElementById("bb-demo");
    const thread = document.getElementById("bbd-thread");
    const chat = document.getElementById("bbd-chat");
    await customElements.whenDefined("mv-blocked-blur");
    const names = { u_rick: "Rick Vance", u_marcus: "Marcus Hill", u_kenji: "Kenji Watanabe", u_amara: "Amara Okafor", u_ingrid: "Ingrid Solberg", u_lucas: "Lucas Moreau", u_priya: "Priya Raman" };
    const both = [thread, chat];
    const list = document.getElementById("bbd-muted");
    const log = document.getElementById("bbd-log");

    const renderMuted = () => {
      const ids = [...new Set(both.flatMap((el) => el.muted))];
      if (!ids.length) {
        list.replaceChildren(Object.assign(document.createElement("span"), { className: "bbd-none", textContent: "Nobody" }));
        return;
      }
      list.replaceChildren(...ids.map((id) => {
        const b = document.createElement("button");
        b.type = "button";
        b.className = "mv-badge";
        b.dataset.variant = "secondary";
        b.dataset.shape = "pill";
        b.setAttribute("aria-label", `Unmute ${names[id] ?? id}`);
        b.append(names[id] ?? id);
        const x = document.createElementNS("http://www.w3.org/2000/svg", "svg");
        x.setAttribute("viewBox", "0 0 24 24"); x.setAttribute("fill", "none"); x.setAttribute("stroke", "currentColor"); x.setAttribute("stroke-width", "2.4"); x.setAttribute("stroke-linecap", "round"); x.setAttribute("aria-hidden", "true");
        const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
        path.setAttribute("d", "M6 6l12 12M18 6 6 18");
        x.append(path);
        b.append(x);
        b.addEventListener("click", () => thread.unmute(id) || chat.unmute(id));
        return b;
      }));
    };
    renderMuted();

    const say = (event, text) => {
      const b = document.createElement("b");
      b.textContent = event;
      log.replaceChildren(b, ` · ${text}`);
    };
    const who = (d) => (d.author ? names[d.author] ?? d.author : d.reason === "flag" ? "a spoiler" : d.reason === "keyword" ? "a muted keyword" : "a filter");

    // One mute list for the whole app: muting in one place mutes everywhere.
    root.addEventListener("mv-mute", (e) => {
      const { author, items } = e.detail;
      say("mv-mute", `${names[author] ?? author} · ${items.length} message${items.length === 1 ? "" : "s"} silhouetted`);
      queueMicrotask(() => { both.forEach((el) => el.mute(author)); renderMuted(); });
    });
    root.addEventListener("mv-unmute", (e) => {
      const { author } = e.detail;
      say("mv-unmute", `${names[author] ?? author} is readable again`);
      queueMicrotask(() => { both.forEach((el) => el.unmute(author)); renderMuted(); });
    });
    root.addEventListener("mv-reveal", (e) => say("mv-reveal", `${e.detail.items.length} message${e.detail.items.length === 1 ? "" : "s"} from ${who(e.detail)}, shown once (${e.detail.trigger})`));
    root.addEventListener("mv-hide", (e) => say("mv-hide", `hidden again (${e.detail.cause})`));

    thread.addEventListener("click", (e) => {
      const btn = e.target.closest("[data-mute]");
      if (btn) thread.mute(btn.dataset.mute);
    });
    document.getElementById("bbd-variant").addEventListener("mv-change", (e) => { thread.variant = e.detail?.value ?? e.target.value; });
    document.getElementById("bbd-timer").addEventListener("change", (e) => {
      both.forEach((el) => { el.rehide = e.target.checked ? "scroll 15s" : "scroll"; });
    });
  </script>
</div>

API

Attributes

Name유형DefaultDescription
mutedstring (ids separated by spaces or commas)Muted author ids, matched against each item’s data-author. Kept in sync by mute() / unmute() when the attribute is present. The muted property accepts an array or a Set instead.
variantsilhouette | compact | hiddensilhouettesilhouette: each item keeps its size, with a neutral avatar, “Muted user” and bars of text. compact: consecutive hidden items (sibling items with no nested replies) collapse into one row, “2 messages from a muted user · Show once · Unmute”. hidden: the items are removed from view and leave a small “2 messages hidden” thread-gap marker. In compact and hidden, an item that has nested replies keeps its box and shows the row inside it, so the replies stay attached.
keywordsstring (comma-separated phrases)Muted keywords, matched on whole words in the item’s content (body and media parts, or all of its own text), ignoring case and accents. Matching items are hidden with the reason “Hidden: contains a keyword you muted”; their author stays visible.
flagsstring (comma-separated)Item flags to hide, matched against each item’s data-flags (e.g. flags="spoiler, sensitive"). Reason label: “Hidden: marked as spoiler”.
rehide"scroll" | time | "scroll <time>" | "never"scrollWhen a “Show once” reveal ends: scroll = once its content has been scrolled completely out of view; a time ("20s", "2m", "1500ms") = after that delay, paused while the content is hovered or has focus, with a thin draining line; both can be combined. Never while focus is inside the revealed content. Escape inside revealed content also hides it again.
itemCSS selector[data-author]Which descendants are items. Items can nest (replies inside comments).
quoteCSS selector[data-quote-author], [data-quote-of]Quotes of other messages. A quote is hidden as its own small silhouette when data-quote-author is muted, or when the item whose id is in data-quote-of is hidden for any reason (the quote takes the same reason).
hide-unmutebooleanRemoves the Unmute button (e.g. when unmuting only happens in the user’s settings).
data-authorstringOn each item: the author id.
data-mute-partavatar | name | body | media | <any other>On elements inside an item: what to hide and how. avatar → neutral blurred figure of the same size; name → “Muted user”; body (the default for an empty value) → bars of the same length and height; media → a soft block of the same size; any other value (actions, meta…) → blank space of the same size. The part element itself stays (with its classes and box); only its children and its aria-label, title, alt, href and role are stored away. Replaced elements (img, video, svg…) are swapped for a placeholder. Content filters (keywords, flags, filter) only hide body, media and quotes: the author stays visible. Without any part, all of the item’s own content (everything but nested items) becomes one silhouette.
data-flagsstringOn an item or a quote: flags matched by the flags attribute.
data-blocked-blur"hidden" | "revealed"Set by the component on hidden and revealed items and quotes (with data-blocked-blur-reason: author | keyword | flag | filter), for styling.

Properties

Name유형Description
mutedstring[]Muted author ids. Set an array, a Set or a string.
isMuted(item, authorId) => booleanOptional callback OR-ed with the muted ids (for a store or an API-driven block list). When it drives the mute, update your source in mv-unmute.
filter(item) => null | string | { reason, key? }Optional custom content filter. A string is the reason shown after “Hidden: ” (e.g. "reported by 3 people").
stringsPartial<Record<string, string>>Overrides for every visible text and announcement (e.g. { mutedUser: "Blocked user", authorSr: "Message from a blocked user, hidden.", fromMuted: "from a blocked user", unmute: "Unblock" }). Keys: mutedUser, showOnce, unmute, hide, hideLabel, hideManyLabel, shownOnce, fromMuted, fromMutedMany, keyword, flag ({flag}), filter, itemLabel, itemSr, authorSr, quoteAuthorLabel, quoteAuthorSr, quoteLabel, quoteSr, compactAuthor, compact, compactSr, gap, gapShown, gapSr, message, messages, announceShown, announceShownTimed, announceShownPlain, announceHidden, announceMuted, announceUnmuted. English defaults.
hiddenItemsElement[]Items currently hidden (read-only; revealed items and quotes excluded).
variant / item / quote / keywords / flags / rehide / hideUnmutereflectedMirror the attributes.

Methods

NameDescription
mute(authorId)Emits the cancelable mv-mute, then hides every item from that author. If focus was inside one of them (the app’s own Mute button), it moves to the Unmute button of its silhouette, which works as an undo. Returns false if cancelled or already muted.
unmute(authorId)Emits the cancelable mv-unmute, then restores every item from that author for good.
reveal(itemOrId)Shows a hidden item or quote once, as the Show once button does (emits mv-reveal).
hide(itemOrId?)Hides a revealed item again, or every revealed item without argument (emits mv-hide with cause "api").
refresh()Rebuilds every silhouette, re-measuring the content (after a font or layout change).

Events

NameDescription
mv-revealCancelable, before a Show once. detail: { items, author (null for content filters), authors, reason: "author" | "keyword" | "flag" | "filter", reasons, trigger: "button" | "api" }. preventDefault() keeps it hidden (e.g. an age check first).
mv-hideA revealed item was hidden again. detail: { items, cause: "scroll" | "timer" | "escape" | "button" | "api" }.
mv-unmuteCancelable, before an unmute from the chip or unmute(). detail: { author, items, trigger }. Persist the change here, or preventDefault() and handle it yourself (a confirmation, a settings page).
mv-muteCancelable, before mute(). detail: { author, items }.

Content structure

NameDescription
(content)Any thread, list, feed or chat markup. Items are found anywhere inside (the item selector), including items added later: new messages, infinite scroll, edits and data-author / data-flags changes are picked up by a MutationObserver.

CSS classes

NameDescription
mv-blocked-blur-veil / -linesBody silhouette: a grid stacking the blurred bars (one <i> per line, --_lw width) and the notice chip; keeps the measured width and height of the text.
mv-blocked-blur-avatar / -figure / -name / -media / -blankPlaceholders for the avatar, name, media and other parts.
mv-blocked-blur-noticeThe chip: -icon, -label (short visible label), a visually hidden sentence, -actions with .mv-blocked-blur-action buttons (data-mv-bb-action="show | unmute | hide").
mv-blocked-blur-rowCompact row or thread-gap marker (data-variant="compact | hidden", data-state="hidden | revealed", data-reason). Rendered as an <li> inside lists.
mv-blocked-blur-pill / -drain“Shown once · Hide” pill in the corner of a revealed silhouette, and the line that drains until the automatic re-hide (paused with data-paused).

CSS variables

NameDefaultDescription
--mv-blocked-blur-tonecolor-mix(fg 7%, surface)Background of the avatar and media placeholders.
--mv-blocked-blur-linecolor-mix(fg 14%, transparent)Color of the text bars.
--mv-blocked-blur-figurecolor-mix(fg 20%, transparent)Color of the blurred figure in the avatar placeholder.
--mv-blocked-blur-blur1.5pxSoftness of the bars and the figure.
--mv-blocked-blur-actionvar(--mv-accent)Color of the Show once button and of the draining line.
--mv-blocked-blur-pill-top / --mv-blocked-blur-pill-endvar(--mv-space-2)Position of the “Shown once” pill in the item’s corner (move it clear of your own corner menu).

Accessibility

Hidden content is really gone: its nodes are detached and kept in memory, and the aria-label, aria-labelledby, aria-describedby, title, alt, href and role of the item and its parts are stored away too, so a screen reader, find-in-page, copy or an extension can’t read what the user chose not to see. All the placeholder shapes (bars, figure, “Muted user”) are aria-hidden; each hidden item exposes one visually hidden sentence, “Message from a muted user, hidden.” (or the filter reason), followed by real “Show once” and “Unmute” buttons described by that sentence. Compact rows and gap markers do the same (“2 messages from a muted user, hidden.”). Show once restores the content where the silhouette was: in the silhouette variant the item keeps its measured size, so nothing around it moves; focus moves to the “Hide” button of the revealed item, and a polite status announces “Shown once. It hides again when you scroll away.” Re-hiding never happens while focus is inside the revealed content or while it is hovered (the timer pauses); Escape hides it again and puts focus back on Show once. Muting with the app’s own button inside an item moves focus to that silhouette’s Unmute button (an immediate undo); unmuting focuses the restored item. Mute, unmute and timer re-hides are announced politely. Keyboard: every control is a native <button> with a visible focus ring. Reduced motion (OS or data-motion="reduce"): the blur-in on reveal and the fade on hide become instant, and the draining timer line is not drawn (the timer still runs). Forced colors: bars and figures use GrayText, chips and rows get a CanvasText border, buttons use LinkText.

이 페이지는 AI로 번역되었습니다. 번역 문제 신고