ホバーカード
stimeo--hover-card
ホバーとフォーカスで開く紹介カード。中のリンクへカーソルを移しても消えません。
ホバーとフォーカスで開く、少し内容のあるカードです。ヒントと違ってリンクなどを入れられます。開くまでと閉じるまでにそれぞれ遅延を置けるので、カーソルが通り過ぎただけでは開きません。トリガーからカード本体へカーソルを移しても消えないので、中のリンクを押せます。Tab でカードの中のリンクへ入っても開いたままです。表示中は Esc で閉じられます。開いてもフォーカスは奪いません。中の内容はトリガーからも辿れることを前提にしているので、カードを開けない人が取り残されません。位置はこのデモの CSS で決めています。
キーボード操作
| キー | 動作 |
|---|---|
| Esc | 開いているカードを閉じる(document 全体で監視。フォーカス位置に依らない)。 |
<%# Markup for the hover_card demo.
Hover/focus on the link opens a rich supplementary card after a delay. The card
has no role="dialog" (it's purely supplementary), and its contents are assumed to
be reachable from the trigger's target too. The open/close delay, the hoverable
bridge, and Escape-to-dismiss are the library's. %>
<span class="hover-card" data-controller="stimeo--hover-card">
<a
class="hover-card__trigger"
href="#jane"
data-stimeo--hover-card-target="trigger"
aria-expanded="false"
aria-controls="hover-card-panel"
data-action="
mouseenter->stimeo--hover-card#open
mouseleave->stimeo--hover-card#close
focusin->stimeo--hover-card#open
focusout->stimeo--hover-card#close">
<%= t("components.hover_card.demo.trigger") %>
</a>
<%# The card has no role="dialog" (supplementary display, not an interrupting dialog). %>
<div
class="hover-card__panel"
id="hover-card-panel"
data-stimeo--hover-card-target="card"
data-action="
mouseenter->stimeo--hover-card#open
mouseleave->stimeo--hover-card#close
focusin->stimeo--hover-card#open
focusout->stimeo--hover-card#close"
hidden>
<p class="hover-card__name"><%= t("components.hover_card.demo.name") %></p>
<p class="hover-card__role"><%= t("components.hover_card.demo.role") %></p>
<a class="hover-card__link" href="#follow"><%= t("components.hover_card.demo.follow") %></a>
</div>
</span>
<%# Readout: what the component just reported. Purely visual — the component
already announces what a screen reader needs, so this line is aria-hidden to
keep it from being read twice. The value is a code fragment (API identifiers
and values), so only the label is translated. %>
<p class="demo-readout" aria-hidden="true">
<span><%= t("catalog.readout.events") %></span>
<span class="demo-readout__value" data-readout>—</span>
</p>
/*
* Presentation-only styles for the hover_card demo.
* The library opens/closes the card by toggling its hidden attribute. Placement
* is static — the consumer's CSS responsibility.
*/
.hover-card {
position: relative;
display: inline-block;
}
/* The theme-aware accent *text* token: the raw brand accent measures ~2.9:1 on the
light canvas, under the 4.5:1 floor. It resolves back to the brand value in dark. */
.hover-card__trigger {
color: var(--accent-700);
text-decoration: underline;
}
.hover-card__panel {
position: absolute;
top: calc(100% + 0.5rem);
left: 0;
z-index: 10;
width: 18rem;
display: flex;
flex-direction: column;
gap: 0.25rem;
padding: 1rem;
background: var(--surface, var(--surface-card));
border: 1px solid var(--border-strong);
border-radius: 0.5rem;
box-shadow: 0 8px 24px rgb(15 23 42 / 0.12);
}
/* Setting display: flex overrides the hidden attribute's default display:none, so
re-declare display:none while closed to honor the library's hidden toggle. */
.hover-card__panel[hidden] {
display: none;
}
.hover-card__name {
font-weight: 600;
}
.hover-card__role {
font-size: 0.875rem;
color: var(--color-text-muted);
}
.hover-card__link {
margin-top: 0.5rem;
color: var(--accent-700);
}
// hover_card opt-in positioning demo (consumer-side JS).
//
// The core controller (stimeo--hover-card) only handles open/close, delay, and
// dismiss. Viewport-edge flip/shift is optionally connected via the opt-in
// stimeo-ui/positioning module. We watch the card's hidden and, while open, track
// the trigger's coordinates.
import { attachPositioning } from 'stimeo-ui/positioning';
document.querySelectorAll('[data-controller~="stimeo--hover-card"]').forEach((root) => {
const trigger = root.querySelector('[data-stimeo--hover-card-target="trigger"]');
const card = root.querySelector('[data-stimeo--hover-card-target="card"]');
if (!trigger || !card) return;
let detach = null;
const sync = () => {
if (!card.hidden && !detach) {
detach = attachPositioning(trigger, card, {
placement: 'bottom-start',
offset: 8,
padding: 8,
});
} else if (card.hidden && detach) {
detach();
detach = null;
card.style.position = card.style.left = card.style.top = '';
}
};
new MutationObserver(sync).observe(card, { attributes: true, attributeFilter: ['hidden'] });
sync();
});
// Every move of the open state is reported, with a `reason` saying what drove it
// — a trigger, Escape, a click outside, or a call from your own code. A real app
// subscribes to mirror the state elsewhere or to send an analytics event; this
// demo writes the event name and its reason into the readout so both are visible
// while you interact. `reason: "api"` is the one to watch for: a close you asked
// for comes back to your own listener.
// The page chrome uses some of these controllers too (the nav, the tab strip),
// so everything below is scoped to the demo itself.
const demoStage = document.querySelector(".demo-stage") ?? document;
const stateReadout = demoStage.querySelector("[data-readout]");
const stateRoot = demoStage.querySelector("[data-controller~='stimeo--hover-card']");
for (const name of ["open", "close"]) {
stateRoot?.addEventListener(`stimeo--hover-card:${name}`, (event) => {
const reason = event.detail?.reason;
if (stateReadout) {
stateReadout.textContent = reason
? `stimeo--hover-card:${name} · reason: ${reason}`
: `stimeo--hover-card:${name}`;
}
});
}
これらのスタイルは共通のデザイントークン(ライト/ダーク両対応)を使います。共通スタイルも一緒にコピーし、ルート要素の data-theme を切り替えればダークになります。
このコンポーネントを動かすために HTML へ記述する data-* 属性です。ルート要素に下の data-controller を付け、その内側に各 target / value / action を配置します。
ルート要素に付与
data-controller="stimeo--hover-card"
ターゲット
| 名前 | 説明 | 属性 |
|---|---|---|
trigger
必須
|
ホバー/フォーカスでカードを開く要素(例: リンク)。aria-expanded で状態を反映し、フォーカスは奪わない。 |
data-stimeo--hover-card-target="trigger" |
card
必須
|
hidden と data-state で表示制御する非モーダルのポップオーバー内容。open/close を結ぶことでホバー可能な橋渡しになる。 |
data-stimeo--hover-card-target="card" |
値(Values)
| 名前 | 説明 | 属性 |
|---|---|---|
openDelay
|
ホバー/フォーカスで開くまでの待機ミリ秒。ちらつき防止(既定 300)。 | data-stimeo--hover-card-open-delay-value |
closeDelay
|
離脱/ブラーで閉じるまでの待機ミリ秒(フォーカスを再確認)。ちらつき防止(既定 200)。 | data-stimeo--hover-card-close-delay-value |
closeOnScroll
|
開いている間に window またはスクロール可能な祖先がスクロールしたときに閉じるか(既定 false)。 | data-stimeo--hover-card-close-on-scroll-value |
アクション
| 名前 | 説明 | アクション |
|---|---|---|
close
|
closeDelay 後に閉じる予約をする。保留中の open を取り消し、フォーカスがカード内なら中止する。 |
stimeo--hover-card#close |
open
|
openDelay 後(0 なら即時)にカードを開く。保留中の close を取り消す。 |
stimeo--hover-card#open |
イベント
| 名前 | 説明 | イベント |
|---|---|---|
close
|
カードを隠した直後に発火。detail の reason に、何がそうさせたかを載せる。 | stimeo--hover-card:close |
open
|
カードを表示した直後に発火。detail の reason に、何がそうさせたかを載せる。 | stimeo--hover-card:open |
状態フック
ライブラリが操作するのはこれらの ARIA / data 属性、カスタムプロパティだけです。見た目は利用側 CSS がこれらに反応して作ります([aria-selected] / [aria-expanded] / var(--stimeo--…) などのセレクタでフックします)。
| フック | 対象 | 意味 |
|---|---|---|
aria-expanded |
トリガー | 開いている間は "true"、閉じている間は "false"。 |
hidden |
カード | 開で属性なし、閉で付与。 |
data-state |
カード | "open" / "closed"。フェード遷移用の任意フック。 |