前回の記事では、アンカーの定義や位置の指定の種類、スコープの定義、アンカーサイズの使用、フォールバックの指定など、Anchor Positioning の基本的な仕組みを紹介しました。
今回は実践編として、いくつか実例を取り上げながら Anchor Positioning の活用方法や、CSS の新たな表現の可能性を考えていきます。
前提
見出し「前提」本記事で紹介する、Anchor Positioning の機能は、W3C の仕様ではおもに「CSS Anchor Positioning Module Level 1」に属しており、草案(Working Draft)段階のため、今後、実装方法や機能に変更が発生する可能性があります。
1. ドロップダウンメニュー
見出し「1. ドロップダウンメニュー」まずは、ドロップダウンメニューの UI です。「File」などのメニュー項目を選択すると、子階層が表示されます。
ここでは、Popover API を使用して、暗黙的なアンカーを生成しています。説明のため簡略化していますが、以下に該当するコードを抜粋します。
以下のコードでは、メニューバーにおける適切な意味づけ(セマンティクス)や機能が十分に実装されていません。実際には、「Menu and Menubar Pattern | W3C」などを参考にした実装が望まれます。
<!-- HTML -->
<ul class="menubar">
<li>
<button type="button" popovertarget="menu-file">File</button>
<ul id="menu-file" class="menu" popover>
<li><button type="button">New</button></li>
<li>
<button type="button" popovertarget="menu-open">Open</button>
<ul id="menu-open" class="menu" popover>
<li><button type="button">foo.html</button></li>
<li><button type="button">bar.css</button></li>
<li><button type="button">baz.js</button></li>
</ul>
</li>
<li><button type="button">Save</button></li>
</ul>
</li>
<!-- ... -->
</ul>
<!-- CSS -->
<style>
.menubar {
display: grid;
justify-items: center;
}
.menu {
position-anchor: auto; /* for Firefox */
inset-block-start: calc(anchor(outside) + 4px);
inset-inline-start: anchor(inside);
display: flex;
flex-direction: column;
gap: 4px;
padding: 4px;
border: 1px solid currentColor;
&:not(:popover-open) {
display: none;
}
.menu & {
inset-block-start: anchor(inside);
inset-inline-start: anchor(outside);
}
}
.menu button {
&:hover {
background-color: hsl(180 76% 11%);
color: white;
}
}
@layer reset {
[popover] {
overflow: visible;
border: none;
}
ul {
margin: 0;
padding: 0;
list-style-type: none;
}
button {
border: none;
background: none;
font-size: 100%;
cursor: pointer;
}
}
</style>popovertarget 属性を指定した要素がアンカー要素になり、popover 属性を指定した要素と接続することが可能です。上記のデモでは、以下のように anchor() 関数を使用して位置を指定しています。
.menu {
inset-block-start: calc(anchor(outside) + 4px);
inset-inline-start: anchor(inside);
/* ... */
.menu & {
inset-block-start: anchor(inside);
inset-inline-start: anchor(outside);
}
}Popover API の注意点として、popover 属性を指定した要素(ポップオーバー)は、トップレイヤーに配置されるため、基準がビューポートになるということです。
たとえば、ポップオーバーの祖先要素に、overflow: clip などを指定してもクリッピングできません。また、position-try-fallbacks プロパティの判定基準もビューポートになります。
ちなみに、Firefox において、暗黙的なアンカーに anchor() 関数を使用しただけでは要素が正しい位置に配置されませんでしたが、position-anchor: auto を指定することで解消できます。この現象は position-area プロパティを使用したときには発生しません。
2. ツールチップ
見出し「2. ツールチップ」続いて、ツールチップの UI です。HTML における title 属性のように、対象要素にホバーもしくはフォーカスすると補足的な情報が表示されます。
以下に、HTML と CSS のコードを抜粋します。
<!-- HTML -->
<div class="toolbar">
<custom-tooltip class="toolbar-item">
<button type="button" class="toolbar-btn" data-toolbar="btn" aria-labelledby="align-left">
<svg width="40" height="40" viewBox="0 0 40 40">
<rect x="9" y="8" width="2" height="24"/>
<rect x="13" y="14" width="20" height="4"/>
<rect x="13" y="22" width="12" height="4"/>
</svg>
</button>
<p id="align-left" class="toolbar-tip" aria-hidden="true">Align left</p>
</custom-tooltip>
<!-- ... -->
</div>
<!-- CSS -->
<style>
.toolbar {
display: flex;
gap: 4px;
inline-size: fit-content;
margin-inline: auto;
}
.toolbar-item {
anchor-scope: all;
}
.toolbar-btn {
anchor-name: --toolbar-btn;
display: block;
}
.toolbar-tip {
position: absolute;
position-anchor: --toolbar-btn;
position-try-fallbacks: flip-block;
inset-block-start: calc(anchor(outside) + 8px);
justify-self: anchor-center;
margin: 0;
padding-block: 4px;
padding-inline: 8px;
border-radius: 4px;
background-color: black;
color: white;
&[aria-hidden="true"] {
display: none;
}
}
</style>まず、.toolbar-btn に anchor-name: --toolbar-btn を指定して、アンカー要素として定義しています。そして、.toolbar-tip に anchor() を指定して、アンカーと結合しています。また、アンカー要素は複数あるため、anchor-scope プロパティでスコープを定義しています。
.toolbar-item {
anchor-scope: all;
}
.toolbar-btn {
anchor-name: --toolbar-btn;
/* ... */
}
.toolbar-tip {
position: absolute;
position-anchor: --toolbar-btn;
position-try-fallbacks: flip-block;
inset-block-start: calc(anchor(outside) + 8px);
justify-self: anchor-center;
/* ... */
}ここから、ボタンにホバーもしくはフォーカスしたときにツールチップが表示されるように、JS で Web Components(Light DOM)を定義して完成です。
<!-- JS -->
<script>
class Tooltip extends HTMLElement {
#controller = new AbortController();
constructor() {
super();
}
connectedCallback() {
const { signal } = this.#controller;
const btn = this.querySelector('[data-toolbar="btn"]');
const tooltip = this.querySelector(`#${btn?.getAttribute('aria-labelledby')}`);
if (btn && tooltip) {
btn.addEventListener('mouseover', () => {
this.toggleTooltip(tooltip, true);
}, { signal });
btn.addEventListener('mouseout', () => {
this.toggleTooltip(tooltip, false);
}, { signal });
btn.addEventListener('focus', () => {
this.toggleTooltip(tooltip, true);
}, { signal });
btn.addEventListener('blur', () => {
this.toggleTooltip(tooltip, false);
}, { signal });
}
}
disconnectedCallback() {
this.#controller.abort();
}
// ツールチップを表示・非表示
toggleTooltip = (tooltip, isShow) => {
if (!tooltip) return;
tooltip.setAttribute('aria-hidden', (isShow ? 'false' : 'true'));
};
}
if (!customElements.get('custom-tooltip')) {
customElements.define('custom-tooltip', Tooltip);
}
</script>3. 追従する要素
見出し「3. 追従する要素」以前に、:has() 擬似クラスに関する記事で紹介した、ナビゲーションのリンクにホバーしたときに下線のスタイルが追従スタイルの改良版です。
上記の記事では、ナビ項目の幅が均等であることが前提条件でしたが、Anchor Positioning を使用することで任意の幅でも対応可能になります。
以下のデモでは、grid-template-columns のセレクトメニューを切り替えると、グリッドのスタイルが変更されます。各レイアウトで、カーソルに追従する要素の変化を確認してみてください。
このように、グリッドのレイアウトが変わっても、ナビ項目の横幅や位置にあわせて柔軟に変化させることが可能になります。
<!-- HTML -->
<nav class="nav">
<ul class="nav-list">
<li><a href="/" aria-current="page">Home</a></li>
<li><a href="/services/">Services</a></li>
<li><a href="/projects/">Projects</a></li>
<li><a href="/blog/">Blog</a></li>
<li><a href="/contact/">Contact</a></li>
</ul>
<span class="nav-chaser"></span>
</nav>
<!-- CSS -->
<style>
.nav {
position: relative;
overflow-x: auto;
}
.nav-list {
display: inline-grid;
grid-template-columns: repeat(5, auto);
list-style-type: none;
}
.nav-list a {
display: block;
padding: 20px;
color: currentColor;
text-align: center;
text-transform: uppercase;
text-decoration: none;
cursor: pointer;
}
/* 追従する要素 */
.nav {
--nav-target: --nav-current;
}
.nav-list a[aria-current] {
anchor-name: --nav-current;
}
.nav-chaser {
position: absolute;
position-anchor: var(--nav-target);
inset-inline: anchor(inside);
inset-block-end: anchor(inside);
display: block;
block-size: 4px;
background-color: teal;
transition: inset 0.4s ease;
pointer-events: none;
}
.nav:has(.nav-list a:is(:hover, :focus-visible)) {
--nav-target: --nav-item;
}
.nav-list a:is(:hover, :focus-visible) {
anchor-name: --nav-item;
}
</style>まず、初期状態で、カスタムプロパティ --nav-target に、aria-current 属性を持った要素のアンカー名である --nav-current を指定します。
追従する要素には、position-anchor: var(--nav-target) を指定します。これによって、初期状態に接続されるアンカー要素は、aria-current 属性を持った要素に指定した --nav-current になります。
.nav {
--nav-target: --nav-current;
}
.nav-list a[aria-current] {
anchor-name: --nav-current;
}
.nav-chaser {
position: absolute;
position-anchor: var(--nav-target);
inset-inline: anchor(inside);
inset-block-end: anchor(inside);
/* ... */
}そして、リンクにホバーやフォーカスしたときには、アンカー名 --nav-item を指定し、カスタムプロパティ --nav-target の値をそのアンカー名に変更します。ここでは、祖先要素 .nav のスタイルを変更するため :has() 擬似クラスを使用しています。
.nav-list a:is(:hover, :focus-visible) {
anchor-name: --nav-item;
}
.nav:has(a:is(:hover, :focus-visible)) {
--nav-target: --nav-item;
}このように、カスタムプロパティとアンカー名で、2 つのハイフン -- で始まる識別子(<dashed-ident>)が混在しているためまぎらわしいことが難点ですが、この方法で追従する要素の実装が可能になります。
ただ、2026 年 8 月現在では、Firefox ではアニメーションが有効にならないので、アニメーションが欠かせない場合、結局のところ JS による実装に頼らざるを得ないのかもしれません。
スクロールコンテナを包含ブロックにする
見出し「スクロールコンテナを包含ブロックにする」さきほどの例では、.nav に対して position: relative を指定していました。その理由として、この要素に対して overflow-x: auto を指定しており、領域に収まらないときに水平方向のスクロールを許可しているためです。
.nav {
/* ... */
position: relative;
overflow-x: auto;
}このように、Anchor Positioning で絶対配置する要素であったとしても、祖先要素がスクロールコンテナの場合には、包含ブロック(Containing Block)として定義する必要があります。
以下は、スクロールコンテナが包含ブロックではない場合(position: static)のデモです。ホバー時にアンカー要素に追従しますが、スクロールすると位置が変わってしまったり、追従要素がスクロールコンテナを突き抜けてしまいます。
なお、position: relative 以外にも、contain: content や scale: 1 といった形でも包含ブロックを定義できます。
裏を返すと、祖先要素に contain: content や scale: 1.5 などを指定していると、その要素が包含ブロックとなります。
4. 拡張リンク
見出し「4. 拡張リンク」カード UI では、リンクをコンポーネント全体に拡張する手法がよく使われます。
一般的には、カードを起点とするため、コンテナ要素に position: relative を指定して包含ブロックとして定義し、その内側に配置されたリンク要素の擬似要素に対して position: absolute を指定して、リンクの領域を広げます。
この手法はコンテナ要素とリンク要素の擬似要素の間に、別の包含ブロックを置けないという制約が存在しますが、Anchor Positioning を使用することで、この制約を回避できます。
以下のデモでは、リンク要素(「More」1)の上に「Widely」や「Newly」といったアイコンが絶対配置されていますが、リンク要素には position: relative を指定していません。つまり、リンクは包含ブロックではありません。
このように、拡張リンクの途中に、別の絶対配置の要素が存在する場合でも、HTML の構造を維持したまま実装することが可能です。以下にコードを抜粋します。
<!-- HTML -->
<div class="card-item">
<figure class="card-image">
<!-- ... -->
</figure>
<p class="card-desc">
contain-intrinsic-size
</p>
<a href="#" class="card-link">
More
<span class="card-icon">Widely</span>
</a>
</div>
<!-- CSS -->
<style>
/* アンカー要素(カード) */
.card-item {
anchor-name: --card-item;
anchor-scope: all;
padding: 16px;
border: 1px solid black;
border-radius: 8px;
max-inline-size: 200px;
}
/* アンカー要素(リンク) */
.card-link {
anchor-name: --card-link;
display: block;
padding-block: 4px;
border-radius: 100vmax;
background-color: hsl(180 100% 12%);
color: white;
text-align: center;
text-decoration: none;
&:hover {
background-color: hsl(180 100% 20%);
}
}
/* 拡張リンク */
.card-link::after {
content: '';
position: absolute;
position-anchor: --card-item;
position-area: center;
place-self: stretch;
display: block;
}
/* 画像 */
.card-image {
inline-size: 100%;
aspect-ratio: 1;
margin: 0;
object-fit: cover;
background-color: hsl(138 69% 38%);
}
/* アイコン */
.card-icon {
position: absolute;
position-anchor: --card-link;
inset-block-start: calc(anchor(inside) - 1em);
inset-inline-end: calc(anchor(inside) - 8px);
padding-inline: 8px;
border: 2px solid hsl(138 69% 38%);
border-radius: 100vmax;
background-color: white;
color: black;
}
</style>ここでは、カードとリンクの 2 つをアンカー要素として定義しています。拡張リンクはカード要素に接続し、アイコンはリンク要素に接続しています。
/* アンカー要素(カード) */
.card-item {
anchor-name: --card-item;
anchor-scope: all;
/* ... */
}
/* アンカー要素(リンク) */
.card-link {
anchor-name: --card-link;
/* ... */
}
/* 拡張リンク */
.card-link::after {
content: '';
position: absolute;
position-anchor: --card-item;
position-area: center;
place-self: stretch;
/* ... */
}
/* アイコン */
.card-icon {
position: absolute;
position-anchor: --card-link;
inset-block-start: calc(anchor(inside) - 1em);
inset-inline-end: calc(anchor(inside) - 8px);
/* ... */
}留意点として、Anchor Positioning をサポートしていない環境では、拡張リンクによる表示崩れの影響が大きいので、@supports 規則で調整しておくのが無難です。
@supports not (anchor-name: --card-item) {
.card-link {
position: relative;
}
.card-link::after {
display: none;
}
.card-icon {
inset-block-start: -1em;
inset-inline-end: -8px;
}
}5. タイムテーブル
見出し「5. タイムテーブル」前回の記事で作成したタイムテーブルのデモを改良します。
前回は、時間軸を 30 分ごとに区切って、要素を配置していました。今回は、時間軸を 60 分ごとに区切って構造をシンプルにし、その一方で、分単位で細かく指定できるようにします。
以下に該当するコードを抜粋します。
<!-- HTML -->
<div class="timetable">
<!-- アンカー要素(水平方向) -->
<div class="timetable-guide"></div>
<p id="stage-research" class="timetable-guide --stage" style="anchor-name: --stage-research;">Research Stage</p>
<p id="stage-design" class="timetable-guide --stage" style="anchor-name: --stage-design;">Design Stage</p>
<p id="stage-dev" class="timetable-guide --stage" style="anchor-name: --stage-dev;">Dev Stage</p>
<!-- アンカー要素(垂直方向) -->
<p class="timetable-guide --time" style="anchor-name: --time-10;">10:00</p>
<p class="timetable-guide --time" style="anchor-name: --time-11;">11:00</p>
<p class="timetable-guide --time" style="anchor-name: --time-12;">12:00</p>
<p class="timetable-guide --time" style="anchor-name: --time-13;">13:00</p>
<p class="timetable-guide --time" style="anchor-name: --time-14;">14:00</p>
<p class="timetable-guide --time" style="anchor-name: --time-15;">15:00</p>
<!-- 配置要素 -->
<p
class="timetable-item"
aria-describedby="stage-design"
style="
inset-inline: anchor(--stage-design inside);
inset-block-start: calc(anchor(--time-13 start) + anchor-size(--time-13 block) * calc(20 / 60));
inset-block-end: calc(anchor(--time-14 start) - anchor-size(--time-14 block) * calc(50 / 60) + 2px);
"
>
<span class="timetable-time">[13:20–14:50]</span>
<span class="timetable-name">Ockhams Razor</span>
</p>
<!-- ... -->
</div>
<!-- CSS -->
<style>
.timetable {
position: relative;
overflow-x: auto;
display: grid;
grid-template-columns: auto repeat(3, minmax(8em, 1fr));
gap: 2px;
max-inline-size: 600px;
padding: 2px;
color: white;
background-color: hsl(180 14% 56%);
text-align: center;
}
.timetable-guide {
padding: 8px;
background: hsl(180 20% 7%);
}
.timetable-guide.--time {
grid-column: 1;
min-block-size: 5em;
}
.timetable-item {
position: absolute;
display: flex;
flex-direction: column;
gap: 4px;
justify-content: center;
padding: 4px;
background-color: hsl(180 100% 12%);
}
@layer reset {
p {
margin: 0;
}
}
</style>ここでのポイントは、配置要素の style 属性で指定している、inset-block-start と inset-block-end の 2 つのプロパティです。calc(20 / 60) は 20 分、calc(50 / 60) は 50 分を意味します。
<p
class="timetable-item"
aria-describedby="stage-design"
style="
inset-inline: anchor(--stage-design inside);
inset-block-start: calc(anchor(--time-13 start) + anchor-size(--time-13 block) * calc(20 / 60));
inset-block-end: calc(anchor(--time-14 start) - anchor-size(--time-14 block) * calc(50 / 60) + 2px);
"
>
<span class="timetable-time">[13:20–14:50]</span>
<span class="timetable-name">Ockhams Razor</span>
</p>なお、ここではわかりやすく説明するために calc() 関数を使用していますが、実際のデモではテンプレート側(Astro)で計算済みの数値を指定しています。
以上で、グリッドの構成はシンプルになり、分単位での指定が可能になりました。
6. カルーセル(スライダー)
見出し「6. カルーセル(スライダー)」続いて、シンプルな機能のカルーセル(スライダー)のデモです。左右に配置されたボタンを選択すると、前後のスライドに移動します。
このデザインの特徴として、スライドの下にキャプションが配置されています。そのため、テキストの長さによって行数が変わり、スライドの高さが常に一定ではありません。
この状態で、左右のボタンを画像に対して上下センターに置きたい場合、従来の position: relative などを使用した方法では直感的に配置することができず、何らかの工夫が必要でしたが、Anchor Positioning を使用することで、よりシンプルにボタンを配置できます。
スライダーの機能(JS)は割愛しますが、以下に HTML と CSS のコードを抜粋します。
<!-- HTML -->
<image-slider class="slider">
<!-- スライダー -->
<div class="slider-track">
<figure class="slider-item">
<div class="slider-item-image"></div>
<figcaption class="slider-caption">Array findLast() and findLastIndex()</figcaption>
</figure>
<!-- ... -->
</div>
<!-- 前後ボタン -->
<button type="button" class="slider-btn --prev" aria-label="前のスライド">
<svg width="8" height="10" viewBox="0 0 8 10" class="slider-btn-svg">
<path d="M5.5,2 L1.5,5 L5.5,8" />
</svg>
</button>
<button type="button" class="slider-btn --next" aria-label="次のスライド">
<svg width="8" height="10" viewBox="0 0 8 10" class="slider-btn-svg">
<path d="M2.5,2 L6.5,5 L2.5,8" />
</svg>
</button>
</image-slider>
<!-- CSS -->
<style>
/* スライダー */
.slider {
anchor-scope: all;
display: block;
max-inline-size: 200px;
margin-inline: auto;
}
.slider-track {
anchor-name: --slider-track;
display: flex;
overflow-x: auto;
scrollbar-width: none;
scroll-snap-type: inline mandatory;
scroll-behavior: smooth;
}
.slider-item {
flex: 0 0 100%;
margin: 0;
margin-inline-start: 4px;
scroll-snap-align: start;
&:first-child {
margin-inline-start: 0;
}
}
.slider-item-image {
anchor-name: --slider-image;
inline-size: 100%;
block-size: auto;
aspect-ratio: 1;
object-fit: cover;
background-color: #1ea446;
}
.slider-caption {
margin-block-start: 8px;
}
/* ボタン */
.slider-btn {
position: absolute;
inset-block: anchor(--slider-image inside);
align-self: anchor-center;
display: grid;
place-items: center;
inline-size: 50px;
aspect-ratio: 1;
padding: 4px;
border: 2px solid currentColor;
border-radius: 50%;
background: none;
cursor: pointer;
&.--prev {
inset-inline-end: calc(anchor(--slider-track outside) + 20px);
}
&.--next {
inset-inline-start: calc(anchor(--slider-track outside) + 20px);
}
}
.slider-btn-svg {
inline-size: 18px;
block-size: auto;
}
.slider-btn-svg path {
fill: none;
stroke: currentColor;
stroke-linecap: round;
stroke-linejoin: round;
stroke-width: 1px;
}
</style>ここでのポイントは、水平方向の --slider-track と、垂直方向の --slider-image という、ボタンの配置の基準となるアンカーを 2 つ指定していることです。
.slider-track {
anchor-name: --slider-track;
}
.slider-item-image {
anchor-name: --slider-image;
}このとき、画像(.slider-item-image)が複数存在するため、アンカー名 --slider-image が重複しますが、最後に出現する画像がアンカー要素の対象となります。
そして、この 2 つのアンカー要素に対して前後ボタンを配置します。
.slider-btn {
position: absolute;
inset-block: anchor(--slider-image inside);
align-self: anchor-center;
/* ... */
&.--prev {
inset-inline-end: calc(anchor(--slider-track outside) + 20px);
}
&.--next {
inset-inline-start: calc(anchor(--slider-track outside) + 20px);
}
}なお、ページ内にスライダーが複数存在すると、アンカー名が競合してしまうので、コンポーネントのルートとなる要素に anchor-scope を指定しています。
.slider {
anchor-scope: all;
}7. ツアー
見出し「7. ツアー」ガイド付きツアーのデモです。「START」ボタンを選択するとツアーが開始されます。
Lorem ipsum, dolor sit amet consectetur adipisicing elit. Sequi, facilis accusamus itaque fugit quos officia minima eaque exercitationem sit nostrum 🫥 vero dolorum quaerat accusantium. Qui perferendis doloremque sequi reiciendis quisquam.
Step 1
ここでは、対象となる要素を囲うスポットライトの要素と、説明テキストを表示するポップオーバーを配置するために、Anchor Positioning を使用しています。
簡略化したコードを以下に抜粋します。JS はガイドのステップを進めるための最小限の機能だけを掲載しています。
<!-- HTML -->
<div class="container" data-tour>
<!-- コンテンツ -->
<div class="logo" style="anchor-name: --tour-1;"></div>
<p style="anchor-name: --tour-2;">
Lorem ipsum, dolor sit amet consectetur adipisicing elit.
Sequi, facilis accusamus itaque fugit quos officia minima eaque exercitationem sit nostrum
<span style="anchor-name: --tour-3;">🫥</span> vero dolorum quaerat accusantium.
Qui perferendis doloremque sequi reiciendis quisquam.
</p>
<!-- スポットライト -->
<div class="tour-guide"></div>
<!-- 説明テキスト -->
<div class="tour-guide-text">
<p>ガイドテキスト</p>
<button type="button" class="tour-guide-btn" data-tour-btn>Next</button>
</div>
</div>
<!-- CSS -->
<style>
/* コンテンツ */
.container {
--tour-item: --tour-1;
max-inline-size: 600px;
padding: 40px;
}
.logo {
block-size: 20px;
aspect-ratio: 3 / 1;
margin-block-end: 20px;
background-color: hsl(138 69% 38%);
}
/* スポットライト */
.tour-guide {
anchor-name: --tour-guide;
position: absolute;
position-anchor: var(--tour-item);
inset: calc(anchor(inside) - 4px);
border-radius: 8px;
box-shadow: 0 0 0 200vmax hsl(0 0% 0% / 80%);
}
/* 説明テキスト */
.tour-guide-text {
position: absolute;
position-anchor: --tour-guide;
position-area: end span-end;
position-try-fallbacks: flip-block, flip-inline, flip-block flip-inline, end span-all;
inline-size: 12em;
margin-block: 8px;
padding: 8px;
border-radius: 8px;
background-color: white;
}
.tour-guide-btn {
display: block;
margin-inline-start: auto;
}
@layer reset {
p {
margin: 0;
}
}
</style>
<!-- JS -->
<script>
const tour = document.querySelector('[data-tour]');
const btn = document.querySelector('[data-tour-btn]');
if (tour && btn) {
const max = 3;
let count = 1;
btn.addEventListener('click', () => {
count = (count >= max) ? 1 : count + 1;
tour.style.setProperty('--tour-item', `--tour-${count}`);
});
}
</script>まず、ガイドを表示したいコンテンツに anchor-name を指定しておきます。
<!-- コンテンツ -->
<div class="logo" style="anchor-name: --tour-1;"></div>
<p style="anchor-name: --tour-2;">
<!-- ... -->
<span style="anchor-name: --tour-3;">🫥</span>
<!-- ... -->
</p>続いて、スポットライトの要素を配置していきます。この要素は、先ほど指定した anchor-name を基準としますが、ステップごとに切り替える必要があるため、カスタムプロパティ --tour-item を経由します。まずは、初期値として --tour-1 を指定しています。
/* コンテンツ */
.container {
--tour-item: --tour-1;
/* ... */
}
/* スポットライト */
.tour-guide {
/* ... */
position: absolute;
position-anchor: var(--tour-item);
inset: calc(anchor(inside) - 4px);
border-radius: 8px;
box-shadow: 0 0 0 200vmax hsl(0 0% 0% / 80%);
}これで、anchor-name: --tour-1 に接続されるようになりました。なお、スポットライトの外側の黒背景(モーダル状態)は box-shadow プロパティで表現しています。
続いて、スポットライトをアンカー要素として指定しておきます。そして、その位置を基準に説明テキストを配置します。さらに、説明テキストには position-try-fallbacks プロパティを使用して、領域に収まらないときのフォールバックを指定します2。
/* スポットライト */
.tour-guide {
anchor-name: --tour-guide;
/* ... */
}
/* 説明テキスト */
.tour-guide-text {
position: absolute;
position-anchor: --tour-guide;
position-area: end span-end;
position-try-fallbacks: flip-block, flip-inline, flip-block flip-inline, end span-all;
/* ... */
}あとは JS 側で、「Next」ボタンを選択するごとに、カスタムプロパティ --tour-item の値を切り替えていきます。
const tour = document.querySelector('[data-tour]');
const btn = document.querySelector('[data-tour-btn]');
if (tour && btn) {
const max = 3;
let count = 1;
btn.addEventListener('click', () => {
count = (count >= max) ? 1 : count + 1;
tour.style.setProperty('--tour-item', `--tour-${count}`);
});
}8. ギターコード表
見出し「8. ギターコード表」ギターのコード(メジャーコードのみ)を表示するデモです。各コードを選択すると、押さえる弦(押弦)の位置が切り替わります。
詳細な(HTML や CSS の)コードは割愛しますが、考え方はタイムテーブルのデモと同じです。
まず、CSS でグリッドを生成してレイアウトを整え、水平方向にフレットの要素、垂直方向に弦の要素を配置し、固有の anchor-name を指定します。
<!-- フレット(水平方向) -->
<div style="anchor-name: --fret-1;"></div>
<div style="anchor-name: --fret-2;"></div>
<!-- ... -->
<div style="anchor-name: --fret-8;"></div>
<!-- 弦(垂直方向) -->
<div style="anchor-name: --string-1;"></div>
<div style="anchor-name: --string-2;"></div>
<!-- ... -->
<div style="anchor-name: --string-6;"></div>そして、6 つの弦それぞれに配置する要素を用意し、ギターのコードに応じて押弦のマークの位置を変えています。フレットの外に置かれる開放弦とミュート弦のマークは、Container Style Queries(@container style())で切り替えます。
<!-- HTML -->
<div data-chords="c">
<!-- 押弦 -->
<div class="chords-press --press-1"><div class="chords-press-item"></div></div>
<div class="chords-press --press-2"><div class="chords-press-item"></div></div>
<div class="chords-press --press-3"><div class="chords-press-item"></div></div>
<div class="chords-press --press-4"><div class="chords-press-item"></div></div>
<div class="chords-press --press-5"><div class="chords-press-item"></div></div>
<div class="chords-press --press-6"><div class="chords-press-item"></div></div>
</div>
<!-- CSS -->
<style>
/* 押弦の基本スタイル */
.chords-press {
--size: 12px;
--press: open;
&.--press-1 { --string: --string-1; }
&.--press-2 { --string: --string-2; }
&.--press-3 { --string: --string-3; }
&.--press-4 { --string: --string-4; }
&.--press-5 { --string: --string-5; }
&.--press-6 { --string: --string-6; }
}
.chords-press-item {
position: absolute;
inset-block-start: calc(anchor(var(--string) inside) - var(--size) / 2);
inset-inline-start: calc(anchor(var(--press) center) - var(--size) / 2);
inline-size: var(--size);
aspect-ratio: 1;
border: 1px solid white;
border-radius: 50%;
background-color: white;
}
/* 開放弦 */
@container style(--press: open) {
.chords-press-item {
background-color: transparent;
}
}
/* ミュート弦 */
@container style(--press: mute) {
.chords-press-item {
background-image: url('icon-mute.svg');
background-size: contain;
background-color: transparent;
border: none;
}
}
</style>あとは、ギターのコードごとのスタイルを用意して完成です。祖先要素に指定している data-chords 属性の値によって、スタイルを切り替えています。
/* C コード */
[data-chords="c"] .chords-press {
&.--press-1 { --press: open; }
&.--press-2 { --press: --fret-1; }
&.--press-3 { --press: open; }
&.--press-4 { --press: --fret-2; }
&.--press-5 { --press: --fret-3; }
&.--press-6 { --press: mute; }
}
/* D コード */
[data-chords="d"] .chords-press {
&.--press-1 { --press: --fret-2; }
&.--press-2 { --press: --fret-3; }
&.--press-3 { --press: --fret-2; }
&.--press-4 { --press: open; }
&.--press-5 { --press: mute; }
&.--press-6 { --press: mute; }
}
/* ... */Container Style Queries と組み合わせることで、より宣言的な指定が可能になりました。
9. すごろく
見出し「9. すごろく」ゴールにちょうどの目で止まることを目指す、簡易的なすごろくゲームです。
「ROLL」ボタンを選択すると、サイコロを振ることができます。「PLAYER SELECT」の項目では、プレイヤーの見た目を変更できます。ゴールにちょうど止まるとクリアですが、ゴールを通り過ぎた回数によってクリア条件が変わります。
ゲームの制御は JS によるものですが、コマが移動するときの位置の指定および、プレイヤー選択のスタイルに Anchor Positioning を使用しています。
ボード上のコマの配置については、ここまでのデモの応用ですが、すごろくのマスを CSS のグリッドで生成し、マスごとに anchor-name を指定しています。
<div class="sugoroku-step" style="anchor-name: --step-1;">1</div>
<div class="sugoroku-step" style="anchor-name: --step-2;">2</div>
<div class="sugoroku-step" style="anchor-name: --step-3;">3</div>
<div class="sugoroku-step" style="anchor-name: --step-4;">4</div>
<div class="sugoroku-step" style="anchor-name: --step-5;">5</div>
<div class="sugoroku-step" style="anchor-name: --step-6;">6</div>
<div class="sugoroku-step" style="anchor-name: --step-7;">7</div>
<div class="sugoroku-step" style="anchor-name: --step-8;">GOAL!</div>あとは、出た目に応じてコマに position-anchor を指定して、該当のマスに移動させます。
<div class="sugoroku-player" style="position-anchor: --step-3;">😀</div>プレイヤー選択時のスタイルも、追従する要素のデモの応用ですが、ホバー時には変化させないので、カスタムプロパティを経由する必要がなく、よりシンプルなコードになっています。ここでは、選択されている要素(input:checked)にアンカー名を指定しています。
.sugoroku-player-radio-label:has(input:checked) {
anchor-name: --checked-player;
}
.sugoroku-player-radio-chaser {
position: absolute;
position-anchor: --checked-player;
inset-block: anchor(inside);
inset-inline: anchor(inside);
/* ... */
}ちなみに、アニメーションには View Transition API を多用しています。そのため、環境によって描画上の不具合が発生することがあるかもしれません。
おわりに
見出し「おわりに」この記事では、Anchor Positioning の典型例である、ドロップダウンメニューやツールチップから、少し特殊なレイアウトまで、デモを作りながらその可能性を考えました。
やはり、実際に手を動かすと一気に理解が深まります。たとえば anchor() 関数の inside、outside のキーワードは思いのほか使い勝手がよかったり、水平軸と垂直軸で異なるアンカー要素と接続させることで、宣言的な指定方法を思いついたりと、新たな気づきが得られました。
一方で、ブラウザ間における表示の違いが気になることも多く、現時点では Firefox が顕著でした。この点については、今後 Interop などの取り組みをとおして、相互運用性が向上していくことが期待されます。
参考文献
見出し「参考文献」本記事の作成にあたり、以下のウェブページを参考にしました。
- Improving card patterns with anchor positioning | Thatemil(外部リンクを開く)
- Anchor Positioning | Roman Komarov(外部リンクを開く)
脚注
-
ここでは、説明しやすくする目的でラベルを「More」としてますが、ページ内でリンクのラベルが重複すると、スクリーンリーダーでリンク先が識別しづらくなる問題があります。詳しくは「アクセシビリティ通信 #1 テキストリンクの指定」参照。 ↩
-
position-try-fallbacksは、ブラウザの種類やステップの進む方向によって挙動が異なることがあり、指定方法が難しいと感じました。あくまでもフォールバックなので、完全に制御したいという考えを捨て、ある程度ブラウザに委ねたほうがよいのかもしれません。 ↩