要素の移送
stimeo--portal
要素をページの別の場所へ移します。切り取られる枠や重なりの制約から抜け出せます。
要素をページの別の場所、たとえば body の直下へ移します。祖先が中身を切り取っていたり、変形や重なりの順序を作っていたりすると、その中のオーバーレイは正しく出せません。移してしまえばその制約から抜けられます。元いた場所には目印を残すので、戻すときも同じ位置に戻ります。離れるときは、戻すか取り除くかを選べるので、行き場の無い要素がページに残りません。移した先はイベントとしても知らせます。位置の計算やフォーカスの世話はしないので、配置やダイアログの部品と組み合わせて使います。
カードは body の直下へ移されるので、この切り取られた枠を抜け出して画面の隅に浮きます。
用途: overflow: hidden や z-index / transform に阻まれるツールチップ/ポップオーバー/モーダルを <body> 直下へ逃がし、クリップや重なり順の問題を避けます。
キーボード操作
このコンポーネント自体はキーボード操作を持ちません。
<%# Portal demo: the card is a content target, so on connect the controller teleports it
out of the dashed (overflow:hidden) source box and into <body>, where demo.css floats
it at the viewport corner — visibly escaping the clipping ancestor. demo.js reflects
the mount destination into the status line. On disconnect (e.g. Turbo navigation) the
card is restored, leaving no orphan. The library only moves the node and sets
data-portaled; demo.css owns the look. %>
<div class="portal-demo">
<div
class="portal-demo__source"
data-controller="stimeo--portal"
data-stimeo--portal-to-value="body">
<p class="portal-demo__hint"><%= t("components.portal.demo.hint") %></p>
<div class="portal-demo__card" data-stimeo--portal-target="content">
<%= t("components.portal.demo.card") %>
</div>
</div>
<p
class="portal-demo__status"
role="status"
data-portal-status
data-mounted-label="<%= t("components.portal.demo.mounted") %>"></p>
<p class="portal-demo__note"><%= t("components.portal.demo.note") %></p>
</div>
/*
* Presentation-only styles for the portal demo. The library moves the card to <body>
* and sets data-portaled; this CSS clips the source box (to show the card escaping it)
* and floats the teleported card at the viewport corner.
*/
.portal-demo {
display: flex;
flex-direction: column;
gap: 0.75rem;
max-width: 28rem;
}
.portal-demo__source {
padding: 1rem;
border: 1px dashed var(--border);
border-radius: 0.5rem;
overflow: hidden;
}
.portal-demo__hint {
margin: 0;
color: var(--color-text-muted);
}
/* Teleported to <body>: pinned to the viewport corner, out of the clipped box. */
.portal-demo__card {
position: fixed;
right: 1rem;
bottom: 1rem;
z-index: 50;
max-width: 16rem;
padding: 0.75rem 1rem;
border-radius: 0.5rem;
background: var(--color-text);
color: var(--surface-page);
box-shadow: 0 10px 25px rgba(15, 23, 42, 0.25);
}
.portal-demo__status {
margin: 0;
color: var(--color-text-muted);
font-variant-numeric: tabular-nums;
}
.portal-demo__note {
margin: 0;
font-size: 0.85rem;
color: var(--color-text-muted);
}
// Portal demo (consumer-side JS).
//
// The controller teleports the card to <body> on connect; this JS only reflects where it
// landed into a status line. It both listens for the mount event and, in case connect
// already fired before this module ran, reports the current state once on load.
document.querySelectorAll(".portal-demo").forEach((root) => {
const status = root.querySelector("[data-portal-status]");
const source = root.querySelector('[data-controller="stimeo--portal"]');
if (!status || !source) return;
const label = status.dataset.mountedLabel ?? "Mounted into";
const report = (target) => {
status.textContent = `${label} <${(target?.tagName ?? "body").toLowerCase()}>`;
};
source.addEventListener("stimeo--portal:mount", (event) => report(event.detail.target));
// If the content has already left the source, the mount event fired before this ran.
if (!source.querySelector('[data-stimeo--portal-target="content"]')) {
report(document.body);
}
});
これらのスタイルは共通のデザイントークン(ライト/ダーク両対応)を使います。共通スタイルも一緒にコピーし、ルート要素の data-theme を切り替えればダークになります。
このコンポーネントを動かすために HTML へ記述する data-* 属性です。ルート要素に下の data-controller を付け、その内側に各 target / value / action を配置します。
ルート要素に付与
data-controller="stimeo--portal"
ターゲット
| 名前 | 説明 | 属性 |
|---|---|---|
content
|
移送する任意のノード。無指定ならコントローラ要素を移送する。 | data-stimeo--portal-target="content" |
値(Values)
| 名前 | 説明 | 属性 |
|---|---|---|
to
|
移送先の CSS セレクタ(既定 body)。解釈できない宣言(構文不正・空)は既定へ落とし、構文が有効で一致しない場合は移送しない。 |
data-stimeo--portal-to-value |
position
|
移送先での append(末尾)/ prepend(先頭)(既定 append)。 |
data-stimeo--portal-position-value |
restore
|
disconnect で原位置へ戻すか、否なら除去(既定 true)。 |
data-stimeo--portal-restore-value |
イベント
| 名前 | 説明 | イベント |
|---|---|---|
mount
|
宛先へ挿入した後に発火。detail.target を伴う。 |
stimeo--portal:mount |
unmount
|
原位置へ戻す / 除去したとき発火。 | stimeo--portal:unmount |
状態フック
ライブラリが操作するのはこれらの ARIA / data 属性、カスタムプロパティだけです。見た目は利用側 CSS がこれらに反応して作ります([aria-selected] / [aria-expanded] / var(--stimeo--…) などのセレクタでフックします)。
| フック | 対象 | 意味 |
|---|---|---|
data-portaled |
移送されたノード | 移送中に付与(true)。 |