익스클루시브

Invite In <mv-invite-in>

사용자가 불러들이기 전까지 제3자에게 요청을 하나도 보내지 않는 개인정보 우선 임베드입니다. YouTube 및 Vimeo 동영상, 지도, 소셜 게시물, 채팅 위젯, 분석 코드가 많은 iframe, 제3자 폰트 스타일시트에 적합합니다.

문화적 레퍼런스

드라큘라, 브램 스토커 (1897, 책). 이 소설이 대중화한 흡혈귀 전승에서 흡혈귀는 안에 있는 누군가가 초대하기 전까지 문턱을 넘을 수 없으며, 한번 초대받으면 언제든 다시 돌아올 수 있습니다. UI에서는 서드파티 임베드가 사용자가 초대할 때까지 아무 요청도 보내지 않고 페이지 밖에서 기다리며, “Always allow”를 선택하면 초대가 철회될 때까지 해당 제공자가 모든 페이지에 다시 나타날 수 있습니다.

작동 방식

사용자가 초대하기 전까지 제3자에게 요청을 전혀 보내지 않는 개인정보 우선 임베드: YouTube 및 Vimeo 동영상, 지도, 소셜 게시물, 채팅 위젯, 분석 코드가 많은 iframe, 제3자 글꼴 스타일시트. 동의 전에는 임베드가 속성이나 비활성 <template>으로만 존재하며, 실제로 동작하는 iframe이나 스크립트로는 절대 존재하지 않습니다. 플레이스홀더는 최종 종횡비를 유지하고, 앱 자체의 포스터(또는 제공자, 제목, 세부 정보가 담긴 중립적인 미리보기)를 표시하며, 로드하면 무엇이 공유되는지 한 줄로 알려 주고(“Loading this video shares your IP address and cookies with YouTube.”), 제공자의 개인정보 처리방침과 “Open on YouTube” 우회 경로를 함께 제공하며, “Load video”(이 동영상만, 자동 재생 포함) 또는 “Always allow YouTube”를 제안합니다. 기억된 동의는 제공자별(일반 iframe은 호스트별)로 관리되며, 사용자가 동의한 경우에만 저장되고(localStorage, 세션 또는 이 페이지), 탭 간에 동기화되며, MvInviteIn.providers() 또는 함께 제공되는 <mv-invite-in-settings> 목록에서 철회할 수 있습니다. 허용된 제공자는 뷰포트에 가까워질 때 지연 로드되고, 철회된 제공자는 즉시 연결이 끊깁니다. 프리셋은 YouTube를 youtube-nocookie.com으로, Vimeo를 dnt=1로 다시 쓰고, watch, short, embed, playlist URL을 파싱하며, 일반 주소로 Google Maps 임베드를 만들고, 사용자 정의 제공자는 한 번의 호출로 등록됩니다. mv-invite는 동의 기록이나 자체 CMP를 위해 취소할 수 있으며, 템플릿 스크립트는 다시 생성되므로(nonce 유지) 위젯은 초대된 뒤에 한 번만 실행됩니다.

카테고리미디어
유형Web Component (<mv-invite-in>)
상태안정
키트신뢰와 개인정보 보호
함께 설치되는 항목button, checkbox, switch
Keywordsexclusive, 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

설치

node scripts/add.mjs invite-in --out ./src/marvelous

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

복사되는 파일(의존성 포함): tokens/tokens.css, core/base.css, components/button/button.css, components/checkbox/checkbox.css, components/switch/switch.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.

사용법

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

<mv-invite-in src="https://www.youtube.com/watch?v=WhWc3b3KhnY" label="Product tour"></mv-invite-in>

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

<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="parleydesk" 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("parleydesk", {
      name: "Parleydesk",
      kind: "chat",
      note: "Loading chat shares your IP address and browser details with Parleydesk.",
    });

    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 Parleydesk.");

    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>

API

Attributes

Name유형DefaultDescription
provideryoutube | vimeo | google-maps (alias maps) | iframe | custom idProvider 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.
srcstringWhat 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.
labelstringTitle 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.
subtitlestringSecondary line under the title: duration, author, license, address…
posterURLPreview 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.
kindstringprovider presetNoun used in the texts and icon: video, audio, map, post, chat, content (“Load map”, “Loading this map shares…”).
rememberlocal | session | memory | offlocalWhere “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"trueAdds autoplay when the user clicks to load (the click already expressed intent). Never applied to embeds loaded from a remembered choice.
lazy"true" | "false"trueEmbeds of an allowed provider load when they come within 240px of the viewport instead of all at once.
paramsquery stringExtra parameters set on the final embed URL ("cc_load_policy=1&hl=en", "z=13").
notestringOverrides the one-line privacy note ({name} and {kind} are replaced).
policyURLprovider presetPrivacy policy linked under the note (YouTube and Google Maps: policies.google.com, Vimeo: vimeo.com/privacy). Hidden when none.
allow / sandboxstringOverride the iframe's allow list and add a sandbox.
credentiallessbooleanLoads the iframe as credentialless where supported (Chromium): no cookies or storage shared with the provider's other sites.
data-statewaiting | loading | loadedSet by the component (styleable), with data-provider, data-kind, data-poster and data-ratio="auto". aria-busy while loading.

Properties

Name유형DefaultDescription
state"waiting" | "loading" | "loaded"Read-only.
urlstring | nullThe embed URL loading will request, before the autoplay flag a click adds (null for <template> content or an invalid src). Read-only.
consentKey / providerName / kindNounstringResolved consent key (youtube, iframe:host…), display name and kind. Read-only.
allowedbooleanWhether this embed's provider is currently remembered as allowed. Read-only.
stringsPartial<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.storageKeystring"mv-invite-in"Static: storage key used for remembered choices.

Methods

NameDescription
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

NameDescription
mv-inviteCancelable, 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-revokeConsent 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-loadThe embed finished loading (or 8 s passed). detail: { provider, name, url }.
mv-unloadThe embed was disconnected (unload() or revocation). detail: { provider, name }.

Content structure

NameDescription
posterAn <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

NameDescription
mv-invite-in-stageThe 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-loadThe placeholder's real <button>: -chip (provider · Not connected), -center, -icon, -action, -title, -subtitle.
mv-invite-in-footPrivacy 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

NameDefaultDescription
--mv-invite-in-accentvar(--mv-accent)Hover and focus color of the load control, glow of the neutral preview.
--mv-invite-in-scrimoklch(0.14 0.01 260 / 0.62)Darkening over a poster behind the text.
--mv-invite-in-on-posteroklch(0.99 0 0)Text and icon color over a poster.
--mv-invite-in-radiusvar(--mv-radius-lg)Corner radius of the stage.
--mv-invite-in-min-height16remPlaceholder 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.

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