Exclusivebeta
Blocked Blur — <mv-blocked-blur>
Muting that keeps the shape of the conversation. Wrap a comment thread, feed or chat: items from muted authors (data-author matched against the muted ids or an isMuted callback) turn into calm silhouettes instead of vanishing, so the replies around them still make sense. The avatar becomes a neutral blurred figure, the name reads “Muted user”, the text becomes soft bars of the same length and the item keeps its exact size; a chip offers “Show once” and “Unmute”. The readable nodes are detached and kept in memory, never just blurred with CSS, so nothing is left in the DOM or the accessibility tree until the user asks. “Show once” (cancelable mv-reveal) restores the content in place, and it hides again when scrolled out of view or after a configurable timer that pauses while hovered or focused. Replies that quote a muted message show the quote as a silhouette too (data-quote-author or data-quote-of). The same mechanism handles content filters, with a reason label: muted keywords (“Hidden: contains a keyword you muted”), item flags such as spoiler or sensitive, or a custom filter callback. Three variants: silhouette (keeps the layout), compact (a one-line “2 messages from a muted user” row per run) and hidden (removed, leaving a thread-gap marker).
| Category | Data display |
|---|---|
| Type | Web Component (<mv-blocked-blur>) |
| Status | beta |
| Keywords | exclusive, 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
Install
node scripts/add.mjs blocked-blur --out ./src/marvelousAI agent with the Marvelous UI MCP server: install_components({ slugs: ["blocked-blur"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).
Files copied (dependencies included): 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.
Usage
Canonical markup — start from it and customize with attributes, data-* and CSS variables:
<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>Cultural reference
Black Mirror, “White Christmas” — Charlie Brooker (Channel 4) (2014, series). People can block each other in real life: the blocked person turns into a grey, blurred silhouette whose voice is muffled noise, still present but unreadable. In the UI, muted or filtered messages keep their place and size in a thread as calm silhouettes whose content is stored away, so replies around them still make sense, until the user chooses to show them once or unmute.
API
Attributes
| Name | Type | Default | Description |
|---|---|---|---|
muted | string (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. | |
variant | silhouette | compact | hidden | silhouette | silhouette: 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. |
keywords | string (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. | |
flags | string (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" | scroll | When 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. |
item | CSS selector | [data-author] | Which descendants are items. Items can nest (replies inside comments). |
quote | CSS 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-unmute | boolean | Removes the Unmute button (e.g. when unmuting only happens in the user’s settings). | |
data-author | string | On each item: the author id. | |
data-mute-part | avatar | 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-flags | string | On 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 | Type | Description |
|---|---|---|
muted | string[] | Muted author ids. Set an array, a Set or a string. |
isMuted | (item, authorId) => boolean | Optional 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"). |
strings | Partial<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. |
hiddenItems | Element[] | Items currently hidden (read-only; revealed items and quotes excluded). |
variant / item / quote / keywords / flags / rehide / hideUnmute | reflected | Mirror the attributes. |
Methods
| Name | Description |
|---|---|
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
| Name | Description |
|---|---|
mv-reveal | Cancelable, 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-hide | A revealed item was hidden again. detail: { items, cause: "scroll" | "timer" | "escape" | "button" | "api" }. |
mv-unmute | Cancelable, 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-mute | Cancelable, before mute(). detail: { author, items }. |
Content structure
| Name | Description |
|---|---|
(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
| Name | Description |
|---|---|
mv-blocked-blur-veil / -lines | Body 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 / -blank | Placeholders for the avatar, name, media and other parts. |
mv-blocked-blur-notice | The 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-row | Compact 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
| Name | Default | Description |
|---|---|---|
--mv-blocked-blur-tone | color-mix(fg 7%, surface) | Background of the avatar and media placeholders. |
--mv-blocked-blur-line | color-mix(fg 14%, transparent) | Color of the text bars. |
--mv-blocked-blur-figure | color-mix(fg 20%, transparent) | Color of the blurred figure in the avatar placeholder. |
--mv-blocked-blur-blur | 1.5px | Softness of the bars and the figure. |
--mv-blocked-blur-action | var(--mv-accent) | Color of the Show once button and of the draining line. |
--mv-blocked-blur-pill-top / --mv-blocked-blur-pill-end | var(--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.