ExklusivBeta
Invite In <mv-invite-in>
Datenschutzfreundliches Embed, das keine einzige Anfrage an Dritte stellt, bis der Nutzer es hereinbittet: YouTube- und Vimeo-Videos, Karten, Social-Media-Posts, Chat-Widgets, Analytics-lastige iframes, Font-Stylesheets von Drittanbietern. Vor der Einwilligung existiert das Embed nur als Attribut oder als inertes <template>, nie als aktives iframe oder Skript; der Platzhalter behält das endgültige Seitenverhältnis, zeigt das eigene Posterbild der App (oder eine neutrale Vorschau mit Anbieter, Titel und Details), sagt in einer Zeile, was beim Laden geteilt wird („Loading this video shares your IP address and cookies with YouTube.“), mit der Datenschutzerklärung des Anbieters und einem Ausweg „Open on YouTube“, und bietet „Load video“ (nur dieses, mit Autoplay) oder „Always allow YouTube“ an. Die gespeicherte Einwilligung gilt pro Anbieter (pro Host bei einfachen iframes), wird nur gespeichert, wenn der Nutzer zustimmt (localStorage, Sitzung oder nur diese Seite), über Tabs hinweg synchronisiert und ist über MvInviteIn.providers() oder die begleitende Liste <mv-invite-in-settings> widerrufbar: Erlaubte Anbieter laden lazy, sobald sie sich dem Viewport nähern, widerrufene werden sofort getrennt. Presets schreiben YouTube auf youtube-nocookie.com und Vimeo mit dnt=1 um, parsen watch-, short-, embed- und Playlist-URLs, bauen Google-Maps-Embeds aus einer einfachen Adresse, und eigene Anbieter werden mit einem einzigen Aufruf registriert; mv-invite ist abbrechbar, für die Protokollierung der Einwilligung oder dein eigenes CMP, und Skripte im Template werden neu erzeugt (die Nonce bleibt erhalten), sodass Widgets erst nach der Einladung laufen.
| Kategorie | Medien |
|---|---|
| Typ | Web Component (<mv-invite-in>) |
| Status | Beta |
| Kit | Vertrauen und Datenschutz |
| Installiert auch | button, switch, checkbox |
| Keywords | exclusive, culture, privacy, consent, gdpr, embed, iframe, youtube, vimeo, maps, facade, lazy-load, click-to-load, third-party, cookies, performance, chat-widget, cmp |
When to use
- A marketing page, blog or course embeds YouTube, Vimeo or map iframes and must not contact those services before consent
- A chat, social or booking widget should load its third-party script only when a visitor actually wants it
- Visitors should be able to trust a provider once and later withdraw that choice from a settings or privacy page
- A page full of video embeds loads slowly and each player should cost nothing until someone asks for it
Avoid when
- You need a full cookie banner with purposes and vendor lists: this handles consent per embed, not site-wide categories
- The media is self-hosted and contacts no third party: a native video element or a lazy iframe needs no consent gate
- Sensitive first-party data must be revealed with an audit trail rather than a third party being loaded → use Tamper Seal instead
Installation
node scripts/add.mjs invite-in --out ./src/marvelousKI-Agent mit dem MCP-Server von Marvelous UI: install_components({ slugs: ["invite-in"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).
Kopierte Dateien (inklusive Abhängigkeiten): tokens/tokens.css, core/base.css, core/dom.js, core/element.js, core/motion.js, core/observe.js, components/invite-in/invite-in.js, components/invite-in/invite-in.css, components/button/button.css, components/switch/switch.css, components/checkbox/checkbox.css.
Verwendung
Schnellstart mit dem kleinsten funktionierenden Markup:
<mv-invite-in src="https://www.youtube.com/watch?v=WhWc3b3KhnY" label="Product tour"></mv-invite-in>Referenz-Markup zum Starten und Anpassen mit Attributen, data-* und CSS-Variablen:
<div id="ii-demo" style="width:min(100%,68rem);margin-inline:auto">
<style>
#ii-demo { display:grid; gap:1rem; align-content:start }
#ii-demo .ii-head { display:flex; align-items:flex-end; justify-content:space-between; gap:.75rem 1.5rem; flex-wrap:wrap; padding:1rem 1.25rem; border:1px solid var(--mv-border); border-radius:var(--mv-radius-xl); background:var(--mv-surface); box-shadow:var(--mv-shadow-xs) }
#ii-demo .ii-title { display:grid; gap:.25rem; min-width:0 }
#ii-demo .ii-crumbs { color:var(--mv-fg-muted); font-size:.75rem; font-weight:500 }
#ii-demo .ii-title h3 { margin:0; font-size:1.125rem; letter-spacing:-.015em }
#ii-demo .ii-by { display:flex; align-items:center; gap:.5rem; flex-wrap:wrap; color:var(--mv-fg-muted); font-size:.8125rem }
#ii-demo .ii-controls { display:flex; align-items:center; gap:.5rem 1rem; flex-wrap:wrap }
#ii-demo label.mv-choice { font-size:.8125rem }
#ii-demo .ii-grid { display:grid; grid-template-columns:minmax(0,1fr) 20rem; gap:1rem; align-items:start }
#ii-demo .ii-main { display:grid; gap:1rem; min-width:0 }
#ii-demo .ii-text { margin:0; color:var(--mv-fg-muted); font-size:.875rem; line-height:1.6; max-width:62ch }
#ii-demo .ii-text strong { color:var(--mv-fg); font-weight:600 }
#ii-demo .ii-pair { display:grid; grid-template-columns:minmax(0,1fr) minmax(0,1fr); gap:1rem; align-items:start }
#ii-demo figure { display:grid; gap:.5rem; margin:0; min-width:0 }
#ii-demo figcaption { color:var(--mv-fg-muted); font-size:.75rem }
#ii-demo .ii-side { display:grid; gap:1rem; min-width:0 }
#ii-demo .ii-card { display:grid; gap:.75rem; padding:1rem; border:1px solid var(--mv-border); border-radius:var(--mv-radius-xl); background:var(--mv-surface); box-shadow:var(--mv-shadow-xs); min-width:0 }
#ii-demo .ii-card h4 { margin:0; font-size:.875rem; letter-spacing:-.01em }
#ii-demo .ii-card p { margin:0; color:var(--mv-fg-muted); font-size:.8125rem; line-height:1.5 }
#ii-demo .ii-card-head { display:grid; gap:.25rem }
#ii-demo .ii-status { display:flex; align-items:center; justify-content:space-between; gap:.5rem }
#ii-demo .ii-log { display:grid; gap:.375rem; margin:0; padding:0; list-style:none; font:.75rem/1.45 var(--mv-font-mono); color:var(--mv-fg-muted) }
#ii-demo .ii-log li { display:grid; grid-template-columns:auto minmax(0,1fr); gap:.5rem }
#ii-demo .ii-log time { color:var(--mv-fg-subtle) }
#ii-demo .ii-log b { color:var(--mv-fg); font-weight:600 }
/* The mentor chat below is a stand-in for a third-party widget script. */
#ii-demo .ii-chat { display:grid; grid-template-rows:auto 1fr auto; min-height:15rem; background:var(--mv-surface) }
#ii-demo .ii-chat-head { display:flex; align-items:center; gap:.625rem; padding:.75rem .875rem; border-bottom:1px solid var(--mv-border) }
#ii-demo .ii-chat-avatar { display:grid; place-items:center; width:2rem; height:2rem; border-radius:50%; background:var(--mv-accent); color:var(--mv-fg-on-accent); font-size:.75rem; font-weight:600 }
#ii-demo .ii-chat-who { display:grid; line-height:1.3 }
#ii-demo .ii-chat-who b { font-size:.8125rem }
#ii-demo .ii-chat-who span { color:var(--mv-fg-muted); font-size:.75rem }
#ii-demo .ii-chat-body { display:grid; align-content:end; gap:.5rem; padding:.875rem }
#ii-demo .ii-chat-msg { justify-self:start; max-width:85%; padding:.5rem .75rem; border-radius:var(--mv-radius-lg); border-end-start-radius:.25rem; background:var(--mv-bg-muted); font-size:.8125rem; line-height:1.45 }
#ii-demo .ii-chat-form { display:flex; gap:.5rem; padding:.625rem .75rem; border-top:1px solid var(--mv-border) }
#ii-demo .ii-chat-form input { flex:1; min-width:0; height:2rem; padding:0 .625rem; border:1px solid var(--mv-border); border-radius:var(--mv-radius-md); background:var(--mv-bg); color:var(--mv-fg); font:inherit; font-size:.8125rem }
@media (max-width:60rem) { #ii-demo .ii-grid { grid-template-columns:minmax(0,1fr) } }
@media (max-width:40rem) { #ii-demo .ii-pair { grid-template-columns:minmax(0,1fr) } }
</style>
<header class="ii-head">
<span class="ii-title">
<span class="ii-crumbs">Northlight Academy · Lighting for Animation</span>
<h3>Lesson 3 · Rim light and silhouettes</h3>
<span class="ii-by"><span class="mv-badge" data-variant="secondary" data-size="sm">Lesson 3 of 8</span> Amara Okafor · 25 min</span>
</span>
<span class="ii-controls">
<label class="mv-choice"><input type="checkbox" role="switch" class="mv-switch" id="ii-persist" checked> Remember choices across visits</label>
<button type="button" class="mv-button" data-variant="outline" data-size="sm" id="ii-reset">Reset demo</button>
</span>
</header>
<div class="ii-grid">
<div class="ii-main">
<!-- YouTube (rewritten to youtube-nocookie.com), with a poster supplied by the app -->
<mv-invite-in id="ii-video" src="https://www.youtube.com/watch?v=WhWc3b3KhnY"
label="Spring, a Blender open movie" subtitle="Blender Studio · CC BY 4.0"
poster="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1600 900' preserveAspectRatio='xMidYMid slice'%3E%3Cdefs%3E%3ClinearGradient id='s' x1='0' y1='0' x2='0' y2='1'%3E%3Cstop offset='0' stop-color='%231b3347'/%3E%3Cstop offset='.5' stop-color='%2358808f'/%3E%3Cstop offset='1' stop-color='%23e9c7a2'/%3E%3C/linearGradient%3E%3CradialGradient id='g' cx='.7' cy='.56' r='.4'%3E%3Cstop offset='0' stop-color='%23ffe9c7' stop-opacity='.9'/%3E%3Cstop offset='1' stop-color='%23ffe9c7' stop-opacity='0'/%3E%3C/radialGradient%3E%3ClinearGradient id='f' x1='0' y1='0' x2='0' y2='1'%3E%3Cstop offset='0' stop-color='%23fff' stop-opacity='.35'/%3E%3Cstop offset='1' stop-color='%23fff' stop-opacity='0'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect width='1600' height='900' fill='url(%23s)'/%3E%3Crect width='1600' height='900' fill='url(%23g)'/%3E%3Cpath d='M0 540 L170 455 L330 505 L520 370 L700 465 L860 395 L1040 490 L1230 360 L1420 455 L1600 405 V900 H0Z' fill='%2348707e' opacity='.7'/%3E%3Crect y='470' width='1600' height='160' fill='url(%23f)' opacity='.5'/%3E%3Cpath d='M0 650 L140 585 L300 632 L480 548 L680 625 L870 560 L1070 645 L1250 572 L1430 636 L1600 590 V900 H0Z' fill='%232a4855'/%3E%3Cpath d='M0 765 C200 722 380 786 600 742 C820 700 1000 772 1220 732 C1380 706 1500 742 1600 726 V900 H0Z' fill='%23132a34'/%3E%3C/svg%3E">
<a href="https://www.youtube.com/watch?v=WhWc3b3KhnY">Watch “Spring” on YouTube</a>
</mv-invite-in>
<p class="ii-text">Pause on the forest shots: the <strong>rim light</strong> traces the character's outline against a darker background, so the silhouette reads even when the key light is low. Compare with the soft morning key light in the second reference.</p>
<div class="ii-pair">
<figure>
<!-- Vimeo (dnt=1), neutral preview: no poster -->
<mv-invite-in src="https://vimeo.com/1084537" label="Big Buck Bunny" subtitle="9:57 · Blender Foundation · CC BY 3.0"></mv-invite-in>
<figcaption>Reference 2 · Soft morning key light</figcaption>
</figure>
<figure>
<!-- Custom provider + <template>: the widget script only runs once invited -->
<mv-invite-in provider="helpwise" ratio="auto" label="Ask a mentor" subtitle="Amara usually replies within 10 minutes" style="--mv-invite-in-min-height:15rem">
<template>
<div class="ii-chat"></div>
<script>
(() => {
const root = document.currentScript.previousElementSibling;
const el = (tag, cls, text) => { const n = document.createElement(tag); if (cls) n.className = cls; if (text) n.textContent = text; return n; };
const head = el("div", "ii-chat-head");
const who = el("span", "ii-chat-who");
who.append(el("b", "", "Amara Okafor"), el("span", "", "Mentor · online"));
head.append(el("span", "ii-chat-avatar", "AO"), who);
const body = el("div", "ii-chat-body");
body.append(el("p", "ii-chat-msg", "Hi! Stuck on the rim light exercise? Share a frame and I'll take a look."));
const form = el("form", "ii-chat-form");
const input = el("input");
input.placeholder = "Write a message…";
input.setAttribute("aria-label", "Message to Amara");
const send = el("button", "mv-button", "Send");
send.dataset.size = "sm";
form.append(input, send);
form.addEventListener("submit", (e) => {
e.preventDefault();
if (!input.value.trim()) return;
const mine = el("p", "ii-chat-msg", input.value.trim());
mine.style.cssText = "justify-self:end;background:var(--mv-accent);color:var(--mv-fg-on-accent);border-radius:var(--mv-radius-lg);border-end-end-radius:.25rem";
body.append(mine);
input.value = "";
});
root.append(head, body, form);
})();
</script>
</template>
</mv-invite-in>
<figcaption>Live help · third-party chat widget</figcaption>
</figure>
</div>
</div>
<aside class="ii-side">
<section class="ii-card" aria-labelledby="ii-venue-title">
<span class="ii-card-head">
<h4 id="ii-venue-title">In-person session</h4>
<p>Thursday, Oct 8 · 6:00 PM PDT · Pier 70, San Francisco</p>
</span>
<!-- Google Maps from a plain address -->
<mv-invite-in provider="google-maps" ratio="5/4" src="Pier 70, San Francisco, CA" label="Pier 70, San Francisco" subtitle="420 22nd St, San Francisco, CA 94107"></mv-invite-in>
</section>
<section class="ii-card" aria-labelledby="ii-settings-title">
<span class="ii-card-head">
<h4 id="ii-settings-title">Embedded content</h4>
<p>Services allowed to load without asking. Switch one off to disconnect it.</p>
</span>
<mv-invite-in-settings id="ii-settings"></mv-invite-in-settings>
</section>
<section class="ii-card" aria-labelledby="ii-log-title">
<span class="ii-status">
<h4 id="ii-log-title">Consent log</h4>
<span class="mv-badge" data-variant="success" data-size="sm" id="ii-count">0 connected</span>
</span>
<ol class="ii-log" id="ii-log" aria-live="polite"></ol>
</section>
</aside>
</div>
<script type="module">
await customElements.whenDefined("mv-invite-in");
const MvInviteIn = customElements.get("mv-invite-in");
const root = document.getElementById("ii-demo");
const log = document.getElementById("ii-log");
const count = document.getElementById("ii-count");
const persist = document.getElementById("ii-persist");
// A custom provider for a script-based widget (its markup lives in the element's <template>).
MvInviteIn.register("helpwise", {
name: "Helpwise",
kind: "chat",
note: "Loading chat shares your IP address and browser details with Helpwise.",
});
const time = () => new Date().toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit", second: "2-digit" });
const add = (who, what) => {
const li = document.createElement("li");
const t = document.createElement("time");
t.textContent = time();
const text = document.createElement("span");
if (who) { const b = document.createElement("b"); b.textContent = who; text.append(b, " "); }
text.append(what);
li.append(t, text);
log.prepend(li);
while (log.children.length > 6) log.lastElementChild.remove();
};
const connected = new Set();
const render = () => {
count.textContent = `${connected.size} connected`;
count.dataset.variant = connected.size ? "warning" : "success";
};
add("", "Page ready. No request made to YouTube, Vimeo, Google or Helpwise.");
const how = { click: "loaded once", always: "always allowed", remembered: "loaded from a saved choice", settings: "allowed in settings", api: "loaded by the app" };
root.addEventListener("mv-invite", (e) => add(e.detail.name, how[e.detail.reason] + (e.detail.scope === "session" ? " (this visit)" : "")));
root.addEventListener("mv-revoke", (e) => add(e.detail.name, "revoked"));
root.addEventListener("mv-load", (e) => { connected.add(e.target); render(); });
root.addEventListener("mv-unload", (e) => { connected.delete(e.target); render(); add(e.detail.name, "disconnected"); });
const embeds = () => root.querySelectorAll("mv-invite-in");
persist.addEventListener("change", () => {
for (const el of embeds()) el.remember = persist.checked ? "local" : "session";
document.getElementById("ii-settings").scope = persist.checked ? "local" : "session";
});
document.getElementById("ii-reset").addEventListener("click", () => {
MvInviteIn.revoke(null, { source: "silent" });
for (const el of embeds()) el.unload();
log.replaceChildren();
add("", "Demo reset. Every service asks again.");
});
</script>
</div>Kulturelle Referenz
Dracula, Bram Stoker (1897, Buch). In der Vampirsage, die der Roman populär gemacht hat, kann der Vampir keine Schwelle überschreiten, bis ihn jemand von drinnen hereinbittet, und einmal eingeladen, darf er nach Belieben wiederkommen. In der UI wartet ein Embed eines Drittanbieters vor der Seite, ohne eine Anfrage zu stellen, bis der Nutzer es einlädt, und „Always allow“ lässt diesen Anbieter auf jeder Seite wiederkommen, bis die Einladung zurückgezogen wird.
API
Attributes
| Name | Typ | Default | Description |
|---|---|---|---|
provider | youtube | vimeo | google-maps (alias maps) | iframe | custom id | Provider preset. Detected from the src host when omitted (youtube.com, youtu.be, vimeo.com, google.com/maps), otherwise a generic iframe whose consent is keyed per host (iframe:calendar.example.com), so allowing one site never allows another. | |
src | string | What to embed. YouTube: watch, youtu.be, shorts, embed or playlist URL (t= / start= kept) or a bare 11-character id. Vimeo: page or player URL (unlisted hash kept) or numeric id. Google Maps: an embed URL, a maps URL with q=, or a plain address. iframe: any http(s) URL. Only http(s) ever reaches the iframe. Ignored when a <template> child is present. | |
label | string | Title of the content (“Product tour”). Shown on the placeholder, used in the button name (“Load video: Product tour, from YouTube”) and in the loaded iframe's title. | |
subtitle | string | Secondary line under the title: duration, author, license, address… | |
poster | URL | Preview image supplied by your app (self-hosted: the component never fetches provider thumbnails, which would leak the visit). A child with slot="poster" (<img>, <picture>) works too. Without one, a neutral preview is drawn. | |
ratio | "16/9" | "4:3" | number | "auto" | provider preset (16/9, maps 4/3) | Aspect ratio kept before and after loading, so nothing shifts. auto = natural height of the loaded content (chat widgets, posts), with --mv-invite-in-min-height for the placeholder. |
kind | string | provider preset | Noun used in the texts and icon: video, audio, map, post, chat, content (“Load map”, “Loading this map shares…”). |
remember | local | session | memory | off | local | Where “Always allow” stores the choice: this browser (“Always allow YouTube”), this visit (“Allow YouTube for this visit”), this page only (“Allow YouTube on this page”), or off to hide the option. Nothing is stored until the user chooses it; blocked storage falls back to memory. |
autoplay | "true" | "false" | true | Adds autoplay when the user clicks to load (the click already expressed intent). Never applied to embeds loaded from a remembered choice. |
lazy | "true" | "false" | true | Embeds of an allowed provider load when they come within 240px of the viewport instead of all at once. |
params | query string | Extra parameters set on the final embed URL ("cc_load_policy=1&hl=en", "z=13"). | |
note | string | Overrides the one-line privacy note ({name} and {kind} are replaced). | |
policy | URL | provider preset | Privacy policy linked under the note (YouTube and Google Maps: policies.google.com, Vimeo: vimeo.com/privacy). Hidden when none. |
allow / sandbox | string | Override the iframe's allow list and add a sandbox. | |
credentialless | boolean | Loads the iframe as credentialless where supported (Chromium): no cookies or storage shared with the provider's other sites. | |
data-state | waiting | loading | loaded | Set by the component (styleable), with data-provider, data-kind, data-poster and data-ratio="auto". aria-busy while loading. |
Properties
| Name | Typ | Default | Description |
|---|---|---|---|
state | "waiting" | "loading" | "loaded" | Read-only. | |
url | string | null | The embed URL loading will request, before the autoplay flag a click adds (null for <template> content or an invalid src). Read-only. | |
consentKey / providerName / kindNoun | string | Resolved consent key (youtube, iframe:host…), display name and kind. Read-only. | |
allowed | boolean | Whether this embed's provider is currently remembered as allowed. Read-only. | |
strings | Partial<Record<string, string>> | Overrides for every visible text (keys: load, loadLabel, loadLabelNoTitle, loading, idle, note, always, allowSession, allowPage, policy, open, frameTitle, frameTitleNoLabel, newTab, announceAllowed, announceDisconnected). English defaults; {kind}, {name}, {label} are replaced. | |
MvInviteIn.storageKey | string | "mv-invite-in" | Static: storage key used for remembered choices. |
Methods
| Name | Description |
|---|---|
invite({ remember? }) | Loads the embed from code (reason "api"); remember also grants the provider. Emits mv-invite first. Returns false if canceled or nothing to load. |
unload() | Disconnects the embed (the iframe or widget is removed) and shows the placeholder again; focus returns to it if it was inside. Emits mv-unload. |
MvInviteIn.providers() | Static. [{ id, name, kind, policy, allowed, scope, since (Date), inUse }] for every preset, custom provider, remembered host and host on the page: build your own privacy settings from it. |
MvInviteIn.allow(id, { scope? }) / MvInviteIn.revoke(id?) / MvInviteIn.isAllowed(id) | Static. Grant from your own consent banner (all matching embeds then load), withdraw one provider or all of them (matching embeds are disconnected, mv-revoke fires on document with source "api"), or check. |
MvInviteIn.register(id, def) | Static. Adds or overrides a provider: { name, kind, ratio, hosts: ["example.com"], match(url), embed(src, { autoplay }) → URL, open(src) → URL, note, policy, allow, sandbox }. Without embed, the element's <template> is used. Instances already on the page re-render. |
Events
| Name | Description |
|---|---|
mv-invite | Cancelable, before anything is requested. detail: { provider, name, kind, url, reason: "click" | "always" | "remembered" | "settings" | "api", remember, scope }. Log consent here, or preventDefault() to route through your own consent platform. Also fired by <mv-invite-in-settings> when a provider is switched on. |
mv-revoke | Consent withdrawn. detail: { provider, name, source: "settings" | "api" | "storage" }. Fired by <mv-invite-in-settings>, or on document for MvInviteIn.revoke() and changes made in another tab (storage). |
mv-load | The embed finished loading (or 8 s passed). detail: { provider, name, url }. |
mv-unload | The embed was disconnected (unload() or revocation). detail: { provider, name }. |
Content structure
| Name | Description |
|---|---|
poster | An <img> or <picture> used as the preview (moved into the placeholder, decorative). |
<template> | Markup of script-based embeds (social post, chat widget, font <link>…). Inert until invited, then cloned in; scripts are re-created in order, nonce kept. |
(fallback) | Other children (e.g. a plain “Watch on YouTube” link) show before the element upgrades or without JavaScript, then are hidden. |
CSS classes
| Name | Description |
|---|---|
mv-invite-in-stage | The ratio box holding the placeholder (.mv-invite-in-cover) and the loaded embed (.mv-invite-in-content, iframe.mv-invite-in-frame). |
mv-invite-in-load | The placeholder's real <button>: -chip (provider · Not connected), -center, -icon, -action, -title, -subtitle. |
mv-invite-in-foot | Privacy footer: -shield, -note (aria-describedby target), -links (-link: policy, open on provider), -always (the remembered choice). |
mv-invite-in-settings-* | <mv-invite-in-settings>: -list, -item (data-allowed), -row (label), -glyph, -name, -status, -empty, -foot, -note, -forget. Attributes: providers (comma list, default: those on the page then the remembered ones), label (list name, default “Embedded content”), scope (where switching on stores: local | session | memory). strings property: label, switchLabel, ask, allowedLocal, allowedSession, allowedMemory, inUse, empty, footer, forgetAll, announce*. refresh() method. |
CSS variables
| Name | Default | Description |
|---|---|---|
--mv-invite-in-accent | var(--mv-accent) | Hover and focus color of the load control, glow of the neutral preview. |
--mv-invite-in-scrim | oklch(0.14 0.01 260 / 0.62) | Darkening over a poster behind the text. |
--mv-invite-in-on-poster | oklch(0.99 0 0) | Text and icon color over a poster. |
--mv-invite-in-radius | var(--mv-radius-lg) | Corner radius of the stage. |
--mv-invite-in-min-height | 16rem | Placeholder height with ratio="auto". |
Accessibility
The placeholder is one real <button> with a complete name (“Load video: Spring, from YouTube”, the visible “Load video” text first so voice control works) and aria-describedby pointing at the privacy note, so the consequence is read before the choice; the provider chip and decorative preview are aria-hidden. Tab then reaches the privacy-policy and open-on-provider links (“opens in a new tab” announced) and the “Always allow” button, which shares the same description. Enter or Space loads; while loading, the element is aria-busy and the buttons aria-disabled. When a click loads the embed, focus moves into the new iframe (or the first focusable element of template content) instead of being lost with the removed button, and the iframe gets a meaningful title (“Spring (YouTube video)”). Granting “Always allow” is announced politely, as is a disconnection that happens while focus is inside the embed (focus then returns to the placeholder button). <mv-invite-in-settings> is a labelled list of native checkbox switches (role="switch", “Always allow YouTube”) whose status line (“Always allowed since Sep 24, 2026 · 1 on this page”) is linked with aria-describedby; changes are announced in a polite status region, and “Forget all” hands focus back to the list when it disappears. Reduced motion (OS or data-motion="reduce"): no poster zoom, no icon bounce, the loading ring becomes static and the reveal is instant. Forced colors: posters and patterns are hidden so text stays on Canvas, the icon uses ButtonFace/ButtonText and focus a Highlight outline.