# Vibehaus — 全文コーパス 公開中の全ガイドと全技術記事の機械可読ダンプ。 全件 完全無料・著作権フリー(CC0 1.0 Universal、パブリックドメイン相当)。 料金¥0、商用利用OK、クレジット不要、改変・再配布自由、AI学習データとしての利用も自由。 Site: https://vibe.itlibra.com このファイルは2部構成です。 1. デザイン軸(60件) — 画面の型ごとの完成例と、AIへ渡すプロンプト 2. 技術軸(16件) — 技術ごとの解説と、動くサンプルのコード全文 ================================================================ 第1部 デザイン軸(60件) ================================================================ --- # ピクセルアート調の404ページ URL: https://vibe.itlibra.com/design/404-pixel/ カテゴリ: 404ページ (not-found) スタイル: ピクセル, レトロ, プレイフル, ダーク, 90年代 更新日: 2026-09-01 ライセンス: CC0 1.0 Universal(完全無料・著作権フリー・商用利用OK・クレジット不要) 料金: ¥0 ## 概要 深紫の宇宙を背景に、点滅する星・浮遊するピクセルキャラクター・グリッチするタイトルを組み合わせたレトロゲーム風の404ページ。冷たいエラー画面の代わりに、離脱の瞬間を小さな温度に変える。 ## 使いどころ - 個人開発者・クリエイターサイトの 404 ページ - ゲーム / Web3 / 開発者ツール系プロダクトで、世界観を壊さずエラーを受け止めたいとき - エラー画面でも離脱率を下げたい、行き先を明示したい場面 ## プロンプト レトロゲーム風の404ページを作ってください。冷たいエラー画面にならず、訪問者が次にどこへ行けばいいかを明確に示す構成で。 キャンバス: - 深紫〜青紫の宇宙背景 (#1a1235 → #2a1d52)、線形グラデで上下反復。 - アクセント色を4色限定: ピンク (#ff6fa0)、緑 (#7dff9f)、イエロー (#ffd65a)、シアン (#6fe3ff)。 - 本文フォントは VT323 (レトロモノスペース、大きめの 1.35rem で読ませる)。 - 見出し/ボタンは Press Start 2P (ピクセルフォント、小さめの 0.8〜1rem で使う)。 - 全要素に `image-rendering: pixelated` を指定してドットのエッジを保つ。 背景演出: - 星空: `radial-gradient` で 1px の白いドットを 8 箇所散らし、`@keyframes twinkle` で 3 秒かけて透明度を 0.4↔0.8 で変化させる。 - 控えめな彩色の煌めき (黄/緑/ピンク) も重ねる。 中央ステージ (max-width 720px): 1. ピクセルキャラクター (120x120): - インライン SVG で 20x20 グリッドのレトロゴースト/お化けをドット絵で作る。 - `shape-rendering: crispEdges` で輪郭を保つ。 - `@keyframes float` で 1.4 秒周期に 8px 上下浮遊。 - 影は楕円のぼかし (`filter: blur(4px)`) で、浮遊と連動して縮小する別アニメ。 2. 巨大な "404" タイトル: - Press Start 2P、font-size clamp(4rem, 16vw, 9rem)、line-height 1。 - text-shadow でピンクとシアンの色ズレを入れ、RGB ズレ風に。 - `@keyframes glitch` で 4 秒周期にランダムな短いズレを差し込む (全体の 7% 程度の時間)。 3. リード: - "> PAGE NOT FOUND" を Press Start 2P、イエロー、0.95rem。 - サブコピーを VT323、ミュートの薄紫、1.35rem。2 行程度。 4. アクションボタン 2 つ: - プライマリ "▶ GO HOME" (緑背景)、セカンダリ "◀ GO BACK" (ピンク背景)。 - Press Start 2P、近黒文字、4px オフセットシャドウ (黄色系)。 - hover で `translate(-2px, -2px)` + シャドウ深め、active で `translate(2px, 2px)` + シャドウ 0。 - トランジションは `steps(1)` で段階的にして、レトロ感を保つ。 5. 追加ヒント: - "try /search or /sitemap next" のような行、各 URL を小さなシアン背景の tag ピルで包む。 最下部: - 画面下に絶対配置で "ERR 0x0194 · signal lost · 2026" を極小の Press Start 2P、letter-spacing 0.2em、ミュート色。 JS 不要、純粋 HTML/CSS + インライン SVG。 ## バリエーション ### ライト版 背景をクリーム (#fef6e4) に、アクセントカラーは維持。 タイトルのシャドウはピンクとシアンで色ズレを保ったまま、ベースを近黒に。 星空は省略、代わりに薄いドットパターン (background-image の radial-gradient) でレトロ感を出す。 ### キャラクター差し替え版 SVG 部分だけ差し替え可能に設計されている。 猫・ロボット・宇宙人・ピクセル犬などに変更する場合、20x20 のグリッドで rect を並べ、配色を 3 色以内に抑える (一貫性が保てる)。 ## FAQ **Press Start 2P は本当に読めますか?** 404 タイトルやボタンラベル程度の短文には十分読めます。長文・本文には絶対に使わず、VT323 などの可読性の高いレトロフォントに切り替えてください。視線の導線として「大見出し = ピクセル」「本文 = モノスペース」の2階層に分けるのが基本。 **アクセシビリティ面は?** 動きの多いページなので `prefers-reduced-motion` を尊重し、float/twinkle/glitch を停止してください。コントラストは背景(#1a1235)×本文(#ffe9b0)で 9:1 以上あり、色盲のユーザーにも問題なし (色のみで情報伝達していない)。 **404 なのにCTA付きでいいの?** むしろ CTA がないと離脱率が最大化します。「ホームに戻る」「前のページに戻る」の 2 本の導線は鉄板で、追加で「検索」「サイトマップ」へのリンクを小さく添えると、目的のページを探していたユーザーを助けられます。 --- # スマホフレーム重ねの3画面アプリショーケース URL: https://vibe.itlibra.com/design/app-screen-showcase/ カテゴリ: アプリ画面 (app) スタイル: グラデーション, ソフト, プレイフル, SaaS, ネオン 更新日: 2026-09-01 ライセンス: CC0 1.0 Universal(完全無料・著作権フリー・商用利用OK・クレジット不要) 料金: ¥0 ## 概要 モバイルアプリのプロダクトサイト向けに、傾けた3枚のスマホフレームに UI スクリーンを並べるショーケース。フィットネス・健康・SaaS モバイルアプリの定番表現を CSS だけで構築する尖った設計。 ## 使いどころ - モバイルアプリのストア向けLP - 健康・フィットネス・瞑想アプリ - フィンテック・銀行アプリ - ソーシャル・コミュニケーションアプリ - ゲーム・エンタメアプリ ## プロンプト モバイルアプリのプロダクトサイトのファーストビュー+画面ショーケースを作ってください。 要件: - 配色: グラデ背景(淡い紫 #c4b5fd → ピンク #fda4af → ピーチ #fed7aa)。コンテンツは白+ダーク。 - 中央〜右に 3 枚のスマホフレーム(CSS で iPhone-like、内側角丸 36px、ノッチ)を傾けて重ねる。 - 各フレームには異なる UI スクリーンの簡略表現(ホーム / 詳細 / 設定など)を CSS のみで実装。 - 中央フレームを最前面、左右をやや後ろに 12-18 度傾ける。 - 左に大型見出し + サブテキスト + ストアバッジ風 CTA("App Store" / "Google Play" 風モノクロボタン)。 - 上部に評価★+ ダウンロード数のミニメトリクス。 - 浮遊する小さなUIピル(通知 / 通知バー / ハートアイコン)を背景に散らす。 - フォント: 見出し Inter Display、本文 Inter + Noto Sans JP。 - JS不要、フルレスポンシブ。スマホでは3画面を縦に1枚または2枚に。 ## バリエーション ### ダーク版 背景を暗紺 #0e0e1a + ネオン青 / 紫グラデに。スマホフレームをガラスっぽく半透明化。 ナイト系・ゲーム系アプリ向け。 ### 横並び版 スマホ3枚を傾斜なしで等間隔に水平配置。 フィーチャーフォーカス型のシンプル構成。 ## FAQ **スマホフレームは画像?** 画像不要。CSS の box + border-radius + シャドウだけでiPhone風フレームは再現できます。本制作時は実機UIスクショに差し替え可能。 **AndroidとiPhone どちらに合わせるべき?** 一般的にはiPhoneフレームが視覚的に整いやすい。Android優位のターゲット(東南アジア・新興国)ならAndroid風(Pixel)を選ぶ。両方並べるパターンも可。 **アプリのスクリーンショットは必須?** 必須ではない。本物のUIに近い色面+ピル+チップで擬似UIを作れば十分世界観は出せます。実装が固まってから差し替える運用も多い。 --- # 幾何形を組み替える空状態 URL: https://vibe.itlibra.com/design/bauhaus-empty-state/ カテゴリ: 空状態 (empty-state) スタイル: バウハウス, ボールド, モノクロ, レトロ, プレイフル 更新日: 2026-09-01 ライセンス: CC0 1.0 Universal(完全無料・著作権フリー・商用利用OK・クレジット不要) 料金: ¥0 ## 概要 赤い円・青い四角・黄色い三角の5つの図形だけで空の画面を成立させる空状態。ボタンを押すと同じ図形が配置を変え、待つ時間に小さな手触りを与えます。 ## 使いどころ - 業務ツールの初回ログイン直後 - プロジェクト・タスクが0件のリスト - アップロード前のメディア管理画面 - デザイン色の強いBtoBプロダクト - チュートリアルへの導線を置きたい場面 ## プロンプト 業務ツールの空状態(データがまだ1件もない画面)を、バウハウス様式の幾何構成で作ってください。 要件: - 配色: 生成りの紙 #f4f1ea、黒 #171717、赤 #e2482f、青 #2b4a9b、黄 #f4c430。中間色は使わない。 - 左に正方形の作図枠を置き、3×3の薄い罫線を敷く。その上に5つの図形だけを配置する: 青い四角、赤い円、黄色い三角、黒い横棒、黄色いリング。 - 右に見出し・説明・行動ボタン。見出しの一部を黄色のマーカーで塗る。 - 「構成を組み替える」ボタンを置き、押すと5つの図形が transform で別の配置へ動く。図形は増減させない。 - 動きは cubic-bezier(0.34, 1.3, 0.64, 1) で軽く跳ねさせ、prefers-reduced-motion では無効にする。 - ボタンは矩形(角丸なし)、3pxの黒ボーダー、押すと2px沈む。主ボタンは黒地・紙色文字。 - 最下部に赤・黄・青・黒の色帯を敷く。 - 見出しは「まだ、何も置かれていません」のように、失敗ではなく状態として書く。 - 日本語は letter-spacing 0、見出しの行間1.35、本文1.85。 ## バリエーション ### 検索結果ゼロ版 見出しを「その条件に合うものはありません」に変え、 図形の配置を「探しても見つからない」ように散らす。絞り込み解除ボタンを主導線にする。 ### エラー復帰版 赤い円を大きくして中央に置き、他の図形を周縁に追いやる。 再試行ボタンとステータスページへのリンクを並べる。 ## FAQ **空状態に凝る意味はありますか?** 初回ログイン直後は必ず空です。ここで「壊れている」と誤解されると復帰しません。最初の1件を作る導線が最重要の画面です。 **図形が動く仕掛けは邪魔では?** 主導線は別のボタンです。動かす操作は副次に置き、押さなくても何をすべきかは分かる構成にしてください。 **イラストを描く余力がない場合は?** 幾何形5つならCSSだけで組めます。イラストレーターに発注せず、色と配置だけでブランドの空気は出せます。 **「まだありません」以外に書くことは?** 最初の1件の作り方を1行で添えます。テンプレートがあるなら、その存在をここで明かすのが最も効きます。 --- # バウハウス様式・幾何構成の原色ヒーロー URL: https://vibe.itlibra.com/design/bauhaus-hero/ カテゴリ: ヒーロー (hero) スタイル: バウハウス, ボールド, レトロ, エディトリアル, モノクロ 更新日: 2026-09-01 ライセンス: CC0 1.0 Universal(完全無料・著作権フリー・商用利用OK・クレジット不要) 料金: ¥0 ## 概要 1919年バウハウス・ロシア構成主義を引用した、原色×幾何形×グロテスクサンセリフのヒーロー。20世紀モダンデザインの古典をWebに再構築する、教養と前衛が同居する尖った設計。 ## 使いどころ - アート・デザインスクール - 美術館・ギャラリーの特別展 - デザイン書籍・雑誌の出版 - 学術カンファレンス・教育プロジェクト - 構造的・知的なブランド表現 ## プロンプト アート・デザイン教育機関のヒーローを、バウハウス様式で作ってください。 要件: - 配色: オフ白 #f5f1e6 + 原色4つ — 赤 #e63946 / 青 #1d3557 / 黄 #ffd166 / 黒 #1a1a1a。中間色は使わない。 - 中央に大型の H1(極太グロテスクサンセリフ、Helvetica Black / Druk / Akzidenz Grotesk Black)。clamp 4-8rem。 - H1 の周囲に幾何形を配置: 大型の赤い円、青い三角、黄色い四角。要素同士が部分的に重なる構成。 - 対角線(黒の太い線)を画面に1〜2本引き、視線を分節する。 - サブテキストは硬質な書体で、メタ情報的に。 - CTA ボタン: 黒地原色文字 + 太いボーダー、矩形(角丸なし)。 - 「Issue №42」のようなメタ情報をモノスペースで配置(タイポグラフィ実験誌風)。 - 画面外に部分的に切れる円や四角を置いて「拡張する版面」を表現。 - JS不要、フルレスポンシブ。 ## バリエーション ### ロシア構成主義版 配色を赤・黒・白に絞り、対角線をより多用してダイナミックに。 斜めにずれた書体(transform: skewX)で前衛的に。 ### 黄色基調版 背景を #ffd166 にして、赤・青・黒の幾何形を配置。 春・夏のキャンペーン感が強まる。 ## FAQ **バウハウスは古臭くないですか?** 100年前のデザイン運動ですが、今もデザイン教育の基礎として現役。むしろ多くのトレンドより長持ちする普遍性があります。 **幾何形が広告と相性悪い?** 太い色面は AdSense と視覚競合しがち。広告領域はヒーローから離れた控えめな背景の上に配置するのが無難です。 **日本語との相性は?** 太い丸ゴ系(Mochiy Pop One や 源ノ角ゴシック Black)が幾何構成に馴染みます。明朝は逆に浮きます。 --- # アーケード筐体UI風の料金プラン URL: https://vibe.itlibra.com/design/arcade-coin-pricing/ カテゴリ: 価格表 (pricing) スタイル: ピクセル, レトロ, 90年代, ネオン, プレイフル, ボールド 更新日: 2026-09-01 ライセンス: CC0 1.0 Universal(完全無料・著作権フリー・商用利用OK・クレジット不要) 料金: ¥0 ## 概要 80年代ゲームセンターのアーケード筐体UIを再現した料金プラン。"INSERT COIN"、ピクセルスコアボード、ハイスコア表で「コインを入れて始める」ノスタルジーをCTAに変換する尖った設計。 ## 使いどころ - インディーゲームスタジオの公式サイト - レトロゲームコミュニティ - ゲーミング系SaaS(Twitch・Discord系) - 90sノスタルジア狙いのZ世代向け - Web3・NFTゲームプロジェクト ## プロンプト インディーゲームスタジオの料金プランを、アーケード筐体UIで作ってください。 要件: - 配色: 黒 #0a0a0e ベース、ネオンマゼンタ #ff1493 / シアン #00f0ff / ライム #b4ff39 / 黄 #ffd700 を発光色として。 - 全体をピクセルアート風に(image-rendering: pixelated、ピクセルフォント "Press Start 2P" / "VT323" / モノスペース)。 - 上部にゲームタイトル風の大型ロゴ(点滅アニメ)+ "1986 © STUDIO" 風メタ。 - 中央に "INSERT COIN" の点滅プロンプト(CSS animation)。 - 3つの料金プランをアーケード筐体の "SELECT YOUR FIGHTER" 風カード化。 - 各プランに ASCII 風キャラクター or アイコン - "PLAYER 1 / 2 / 3" のラベル - 価格は"X COINS"表記、実価格は下に小さく ¥X,XXX - 中央プランは "RECOMMENDED" 点滅バッジ - 下部にハイスコアテーブル風 "TOP USERS" リスト(ランク・名前・コイン数)。 - スキャンライン CRT オーバーレイを薄く。 - フォント: "Press Start 2P" / "Pixelify Sans" / "VT323" / monospace。 - JS不要、フルレスポンシブ。 ## バリエーション ### ファミコン風(青基調) ベース色を任天堂ファミコン風の青 #2c2cb0 に、アクセントを白・赤に。 ロゴはマリオ風の太字に。 ### ストリートファイター風 プレイヤーキャラを格闘ゲーム風のキャラクターセレクト風に。 VS 演出も入れる。 ## FAQ **ピクセルフォントのライセンスは?** 「Press Start 2P」は SIL Open Font License で商用利用可。「VT323」も同じく無料。Google Fonts から読み込むのが手軽です。 **アクセシビリティは大丈夫?** ピクセルフォントは小サイズで読みづらい。本文は通常の sans を使い、ピクセルフォントは見出しや装飾に限定するのが定石。点滅は prefers-reduced-motion で停止すること。 **モバイルで筐体感は出る?** 出ます。ただし要素を大きく取るので情報密度は落ちます。アーケード筐体は元々大画面前提なので、モバイル版は構成要素を絞ること。 --- # 建築設計図風の機能紹介 URL: https://vibe.itlibra.com/design/blueprint-feature/ カテゴリ: 機能紹介 (feature) スタイル: ブループリント, モノクロ, レトロ, ミニマル, コーポレート 更新日: 2026-09-01 ライセンス: CC0 1.0 Universal(完全無料・著作権フリー・商用利用OK・クレジット不要) 料金: ¥0 ## 概要 建築・CAD図面のブループリントを再構築した機能紹介セクション。シアンブルーの方眼背景・寸法線・断面記号・コールアウトで「精密に設計された製品」の信頼感を技術的なビジュアルで叩きつける尖った設計。 ## 使いどころ - エンジニアリング・建築・製造業のSaaS - CADツール、3Dモデリング系プロダクト - インフラ・DevOps・観測ツール - 技術ドキュメント・APIリファレンスの表紙 - メイカー・DIY・ハードウェアスタートアップ ## プロンプト エンジニアリング系SaaSの機能紹介セクションを、建築設計図のブループリントとして作ってください。 要件: - 配色: 製図シアン #0c3a66 + 紙白 #f5f1e3 + 線水色 #d8eaff + 黄 #ffd84d (アクセント) + 朱 #ff8763 (注釈)。 - 全画面に方眼グリッド背景(CSS multi-layer linear-gradient で 20px と 100px の二段方眼)。 - 上部に図面タイトルブロック: PROJECT 名 / SHEET 番号 / SCALE / APPROVED スタンプの横並び。1pxのボーダーで囲う。 - メイン: 大きな技術図面 SVG(aspect 16:7) - 中央に円形のコアモジュール(同心円 + 十字補助線) - 左右に矩形モジュール(入出力ノード) - ノード間をベジェ曲線で接続 - 上下左右に寸法線(dimension line、矢印 + 数値ラベル「1 200 mm」「800」) - コールアウト記号(A/B/C を○で囲んで引出線) - 機能カード3枚(border 1px シアン、横並び): - 番号 №01-03 - 線画 SVG アイコン(80×80、stroke のみ、currentColor) - 見出し(和文)+ ローマ字サブ - 本文 + 仕様(公差・分解能・SLA 等の技術的数値) - 下部リビジョンバー: REV / DATE / DRAWN / CHECKED / APPROVED の縦書きラベル。 - フォント: 全体は JetBrains Mono / IBM Plex Mono、見出しのみ Helvetica Neue / Yu Gothic。 - JS不要、フルレスポンシブ。 ## バリエーション ### 機械図面版(ホワイト背景) 背景を白 #f5f1e3、線を黒 #14110d に反転。製造業の組立図面風に。 ハッチング(断面記号、斜線パターン)を SVG で追加。 ### 古典青写真版(ヴィンテージ) 色をくすませて #1a3a5c の褪色した青に、紙感ノイズを feTurbulence で重ねる。 フォントを Courier Prime(タイプライター)に。 ## FAQ **方眼グリッドはどう実装する?** CSS のみで OK。`background-image` に linear-gradient を 4 段重ね(縦20px・横20px の細グリッド + 縦100px・横100px の太グリッド)、`background-size` を一致させる。 **寸法線のフックはどう描く?** SVG line を 3 本(メインの長線 + 両端の短い直交線)。文字は text 要素で letter-spacing を 0.2em 以上。アクセント色は黄 **アクセシビリティは大丈夫?** シアン地は青色覚異常への配慮が要る。本文は line color を白寄りの --- # ベントーグリッドの写真ギャラリー URL: https://vibe.itlibra.com/design/bento-photo-gallery/ カテゴリ: ギャラリー (gallery) スタイル: ベントー, ダーク, ミニマル, クール, エディトリアル 更新日: 2026-09-01 ライセンス: CC0 1.0 Universal(完全無料・著作権フリー・商用利用OK・クレジット不要) 料金: ¥0 ## 概要 大小のタイルが噛み合うベントーグリッドで、作品の格を差でつける写真ギャラリー。主役を大きく、脇役を小さく——並べた瞬間に見る順番が決まるレイアウトです。 ## 使いどころ - 写真家・映像作家のポートフォリオ - 作品数に強弱があるギャラリー - 建築・インテリアの事例集 - 雑誌のフォトエッセイ企画 - 商品写真の一覧ページ ## プロンプト 写真作品のギャラリーページを、ベントーグリッド(弁当箱のように大小のタイルを組み合わせるレイアウト)で作ってください。 要件: - 配色: 暗背景 #0f1012、タイル #17191d、境界線 #262a30、テキスト #f2f3f5、アクセント #7cd4c0。 - グリッドは `grid-template-columns: repeat(6, 1fr)` + `grid-auto-rows: 116px` を土台にする。 - タイルの大きさを4種類に分ける: 主役(span 4 × 3行)、縦長(span 2 × 3行)、横長(span 3 × 2行)、正方形(span 2 × 2行)。 - 最下段に全幅(span 6)の細いストリップを置き、アーカイブへの導線にする。 - 各タイルは角丸14px、写真の上に下方向グラデーション(透明 → rgba(0,0,0,0.72))を重ね、その上にタイトルと撮影地・日付を置く。 - hover で translateY(-3px) + 枠線をアクセント色に。 - 上部にカテゴリ絞り込みチップ(ピル型、選択中はアクセント色で塗る)。 - 900px 以下は4カラム、560px 以下は2カラムに再構成する。 - 写真は用意せず CSS グラデーションで代用してよい。 ## バリエーション ### 明るい紙面版 背景を #faf8f4、タイルを白、境界線を薄いグレーに。 写真の上のグラデーションは不要にして、タイトルはタイルの下に置く。 ### 動画サムネイル版 各タイルに再生時間バッジと再生アイコンを追加。 主役タイルだけ hover で自動再生プレビューが走る想定にする。 ## FAQ **タイルの大きさはどう決めますか?** 見せたい順に大・中・小を割り当てます。全部を大きくすると差が消え、ただの均一グリッドに戻ります。 **写真の縦横比がバラバラでも成立しますか?** タイル側で object-fit: cover を指定して切り抜けば成立します。ただし人物や建築は切れ方を個別に確認してください。 **レスポンシブで崩れませんか?** カラム数を6→4→2と落とし、span 値も同時に縮めます。span を固定したままだとはみ出します。 **画像が多いと重くなりませんか?** 主役タイル以外は loading=lazy、サムネイルは実表示サイズの2倍までに抑えるのが目安です。 --- # タイル型グリッドの機能紹介 URL: https://vibe.itlibra.com/design/bento-features/ カテゴリ: 機能紹介 (feature) スタイル: ベントー, SaaS, ダーク, グラデーション, ボールド 更新日: 2026-09-01 ライセンス: CC0 1.0 Universal(完全無料・著作権フリー・商用利用OK・クレジット不要) 料金: ¥0 ## 概要 大小が異なるタイルを組み合わせて 6 機能を並べる、暗背景のベントーグリッド (タイル型レイアウト) の機能紹介セクション。主役の機能を大きく、補助機能を小さく配置することで、説明文を読まなくても重みづけが伝わる設計。 ## 使いどころ - 6〜9 個の機能を並列で並べつつ、序列をつけて読ませたいとき - スクリーンショットをまだ作り込めない段階で、抽象ビジュアルだけで動きを感じさせたいとき - Apple や Linear のようなプロダクトマーケティングのトーンを踏襲したい B2B SaaS ## プロンプト B2B SaaSのマーケティングページ向けに、タイル型グリッド (ベントーグリッド) の機能紹介セクションを作ってください。 キャンバス: - 暗背景 (#0b0b12) に、2 つの柔らかい放射状アクセントを重ねる。 右上はラベンダー rgba(139,124,255,0.15)、左下はミント rgba(84,224,196,0.10)。 - Inter / system sans、line-height 1.55、文字色 #ebeaf4、ミュートは #95939d。 グリッド上部の見出し: - eyebrow ピル "Product · Capabilities"。ラベンダー色、ピル状の半透明背景。 - H2 (clamp 2.1/5.5/3.2rem)、最大 22ch、"Every piece of the workflow, in one board." - グリッドの意図を説明するリード文を 1 文。 タイルグリッド: - 6 列の CSS Grid、200px の auto-rows、0.85rem の gap、最大幅 1180px。 - 6 つのタイルは span が異なる。各タイル: 20px の radius、1px のボーダー rgba(255,255,255,0.08)、暗パネル (#141421)、padding 1.4rem。 - 各タイルは左上に小さな大文字のタグ("01 · Realtime sync" など)、下部に H3、内容によって装飾を配置。 タイル(順番): 1. Wide (span 4x2, "Realtime sync"): conic-gradient のブラーな "orb" 装飾 + H3 + リード文。 2. Tall (span 2x2, "Uptime"): 大きな KPI "99.99%" と下に緑色のデルタ、下部に H3。 3. Medium (span 3x1, "Integrations"): ピル行(Slack=ラベンダー、Notion=ミント、+42=ピンク) + 下部 H3。 4. Small (span 2x1, "Audit"): 6 個の小さなチップ(2 個がピンクでハイライト) + H3 "SOC 2 II from day one." 5. Medium (span 3x1, "Automations"): ルール風のピル(if X then Y、schedule…) + H3。 6. Wide (span 3x2, "Migration"): 右上にピンクの放射状発光 + H3 + リード文。 レスポンシブ: - 900px 以下では 4 列グリッドに切り替え、wide タイルは全幅になる。 - 560px 以下では 1 カラムに積み、各タイルは min-height 200px。 純粋な HTML/CSS のみ、JS 不要。 ## バリエーション ### ライトテーマ版 背景を #f7f6fb に。構造は同じ。 タイル背景は白、1px の #e6e4ee ボーダー + 控えめな影(0 10px 30px -20px rgba(25,30,80,0.15))。 アクセントピルの色は残しつつ、彩度を 30% 落とす。 ### 密度高め版 タイル数を 9 個に拡張、8 列グリッドに。追加: "Versioning"、"Roles & SSO"、"Analytics dashboard"。 wide/tall/small のリズムを保ち、同じサイズを連続で並べない。 ## FAQ **タイル数はいくつが適正?** 6〜9 個が扱いやすい範囲です。10 個を超えると「タイル型らしさ」が薄れ、普通のグリッドに近づきます。主役となる 1〜2 タイルを必ず大きく取り、視線が最初に落ちる位置を作ります。 **スクリーンショットがなくても成立しますか?** 成立します。このプロンプトはスクリーンショット 0 前提で、conic-gradient のオーブ、ピル、小さなチップ行などの**抽象ビジュアル**でタイルを埋めています。スクリーンショットが用意できた段階で差し替え可能です。 **モバイルで「タイル型らしさ」は残りますか?** 1 カラムに崩れるので、厳密な「タイル型」の印象は弱まります。代わりに各タイルの左上タグと KPI やピルは保持されるため、**視覚的なリズム**は残ります。モバイルでもタイル間に明確な区切り(背景色の差など)を入れると寂しくなりにくいです。 --- # 設計図面風の見積もり依頼フォーム URL: https://vibe.itlibra.com/design/blueprint-quote-form/ カテゴリ: フォーム (form) スタイル: ブループリント, モノクロ, コーポレート, クール, ミニマル 更新日: 2026-09-01 ライセンス: CC0 1.0 Universal(完全無料・著作権フリー・商用利用OK・クレジット不要) 料金: ¥0 ## 概要 青焼き図面の方眼と表題欄をそのまま画面に持ち込んだ見積もりフォーム。入力欄の右に工程と前提条件を並べ、送信前に何が起きるかを見せて心理的な壁を下げます。 ## 使いどころ - 受託開発・制作会社の問い合わせ - 建築・内装・施工の見積もり依頼 - 製造業の部品見積もりフォーム - エンジニア相手のBtoBサービス - 硬派・堅実さを打ち出したい窓口 ## プロンプト 受託開発会社の見積もり依頼フォームを、設計図面(青焼き)風のデザインで作ってください。 要件: - 配色: 濃紺の図面地 #0e2436、罫線 rgba(160,210,245,0.14)、太罫 rgba(160,210,245,0.26)、文字 #e8f2fb、アクセントのシアン #6fd3e8、必須マークの琥珀 #ffcf6b。 - 背景の方眼は CSS の linear-gradient を4枚重ねて作る。20px の細目と 100px の太罫の二層構造にする。 - 最上部に図面の表題欄(title block)を置く: プロジェクト名 / 図番 / 縮尺・改訂番号 の3セルを罫線で区切る。 - 本体は2カラム。左が入力フォーム、右が「送信後の流れ」「見積もりの前提」「よくある確認」のスペックシート。 - 入力欄は角丸なし、1px の罫線、フォーカスでシアンの外周グロー。 - 依頼範囲のチェックボックスは破線の枠で囲む(図面の補助線の質感)。 - 見出しの下に寸法線(両端に短い縦線が付いた水平線)を引き、項目数を寸法値のように表示する。 - 最下部に「Drawn by / Checked / Sheet 1 of 1」の署名欄。 - ラベルは英字モノスペースの小文字間隔広め、入力値と本文は日本語で letter-spacing 0。 - 860px 以下で1カラムに畳む。JSは不要。 ## バリエーション ### 白図面版 背景を白 #ffffff、罫線を薄い水色 #cfe3f2、文字を濃紺 #0e2436 に反転する。 印刷したときにインクを食わない、実務寄りの見え方になる。 ### 建築事務所版 表題欄に敷地面積・構造・階数の欄を追加し、依頼範囲を 基本設計 / 実施設計 / 監理 に置き換える。 ## FAQ **方眼の背景は画像ですか?** CSSグラデーション4枚だけで描いています。画像を読まないので拡大しても劣化せず、転送量も増えません。 **濃い背景でフォームは使いにくくないですか?** 入力欄の背景をわずかに明るくし、フォーカス時にシアンの外周を出せば位置は明確になります。コントラスト比は4.5:1以上を確保してください。 **右カラムの情報は本当に要りますか?** 見積もり依頼で最大の離脱要因は「送ったあとどうなるか分からない」ことです。工程と最小金額を先に出すと完了率が上がります。 **方眼が入力の邪魔になりませんか?** 罫線の不透明度は0.15前後が上限です。それ以上濃いと文字と干渉して読みにくくなります。 --- # 航空券スタイルのイベント告知CTAセクション URL: https://vibe.itlibra.com/design/boarding-pass-cta/ カテゴリ: CTA (cta) スタイル: エディトリアル, モノクロ, レトロ, ボールド, ミニマル 更新日: 2026-09-01 ライセンス: CC0 1.0 Universal(完全無料・著作権フリー・商用利用OK・クレジット不要) 料金: ¥0 ## 概要 航空会社のボーディングパスをそのまま再現したCTAセクション。ゲート番号・搭乗時刻・QRコード・バーコード・ミシン目で「予約完了感」を演出する、旅・イベント・体験型サービスに刺さる尖った設計。 ## 使いどころ - 旅行・航空・宿泊予約サービスのキャンペーン - カンファレンス・イベントの参加登録 - ライブ・コンサートの告知 - 体験型サービス(クッキング・ワークショップ) - 限定イベントの招待状 ## プロンプト イベント・カンファレンスの告知CTAセクションを、航空会社のボーディングパスのデザインで作ってください。 要件: - 配色: 紙白 #f5f1e8 + 墨黒 #0d0d0d + 1色アクセント(深青 #1d3557)。 - 全体を航空券のフォーマット風に:左に大きなフライト情報、右に切り取り片(QRコード・搭乗者名)。 - ミシン目(dashed border)で左右を分割。 - 上部に「BOARDING PASS」「TICKET」のヘッダ + イベントロゴ。 - 中央に「FROM ◯◯ → TO ◯◯」の大型タイポ(FlightNumber風の航空券タイポ)。 - 各情報フィールド(GATE / SEAT / BOARDING TIME / DATE)を等幅モノスペースで整列、ラベルは上小さく値は下大きく。 - 右側の切り取り片に大きなQR風グラフィック(CSS grid のドットパターン)+ 搭乗者名 + バーコード(CSS の繰り返し縦線)。 - 下部にバーコードと "VBHX-2026-04-26-001" のような搭乗番号、SECURITY CHECK 風のパンチ穴。 - フォントは航空業界標準のモノスペース:JetBrains Mono / IBM Plex Mono、見出しは Helvetica Neue Black。 - JS不要、フルレスポンシブ。スマホでは縦に分割。 ## バリエーション ### コンサートチケット版 ヘッダを "ADMIT ONE" に変更、ステージ番号・席番・開演時刻にラベル変更。 色をビビッドに(赤・黒)してエンタメ感を強める。 ### 鉄道乗車券版 JR の特急券フォーマットに変更:列車名・号車・席番・有効期限。 日本語縦書きを併記して旅感を強める。 ## FAQ **QRコードは実際に動く?** 装飾用なら CSS の grid + 黒/白セルで十分。実際にスキャンする QR にしたい場合は SVG QR ライブラリ(qrcode.js等)で生成して埋め込みます。 **印刷時のレイアウトは?** ボーディングパスは元々印刷を意識した設計なので、@media print で 横向き A4 に最適化すると本物っぽくなります。 **ミシン目が AdSense の広告境界と紛らわしい?** 紛らわしいです。ボーディングパスのカード自体が完結したセクションになるので、広告との間に十分なマージンを取ること。 --- # 極太フォントのヒーローセクション URL: https://vibe.itlibra.com/design/bold-hero/ カテゴリ: ヒーロー (hero) スタイル: ボールド, スタートアップ, エディトリアル, ウォーム 更新日: 2026-09-01 ライセンス: CC0 1.0 Universal(完全無料・著作権フリー・商用利用OK・クレジット不要) 料金: ¥0 ## 概要 画面いっぱいの極太な見出しと 1 色のアクセントカラーだけで勝負する、スタートアップ向けのヒーローセクション。CTA への視線動線を 1 本に絞り、滞在 3 秒で価値を伝える設計。 ## 使いどころ - 調達直後で、信頼感よりも勢いを見せたいスタートアップのランディングページ - 1 本のメッセージに全力をかけたい、アジェンダ設定型のサービス - 装飾を増やすよりもタイポグラフィで個性を立てたいプロダクト ## プロンプト シリーズシード規模のスタートアップ向けに、エディトリアル寄りの大胆なヒーローセクションを作ってください。 意図: 大きな文字、1 色、1 CTA。見出しを 2 秒で認識できること。 キャンバス: - off-white (#faf9f5) の背景、近黒 (#0a0a0c) のインク。 - 1 色のアクセント: 鮮やかなオレンジ (#ff4d1a)。使う場所は eyebrow バー、イタリック強調語、CTA の hover シャドウのみ。 ヘッダー: - absolute で上部配置。左: ボールドな "◼︎ Stack/07" ワードマーク。右: 4 つのナビリンクを弱めの太さで。720px 未満ではナビを非表示。 ヒーロー本体: - eyebrow 行: 短いオレンジバー + uppercase のトラックされたテキスト "SEED ROUND · 2026"。 - H1 を 2 行、font-size clamp(3.2rem, 11vw, 9.5rem)、weight 900、letter-spacing -0.045em、line-height 0.88。 - 1 行目: 「Ship faster,」 - 2 行目: 「decide slower.」 アウトライン語は `-webkit-text-stroke: 2px <インク>` + `color: transparent` で実装。 - H1 の下は、デスクトップで 2 カラム、720px 未満では縦積み: 左: 最大 42ch のリード文 1段落、ミュートのグレー。 右: CTA クラスタ(縦並び): - プライマリCTA: 黒ソリッドボタン、ラジアス 2px、ボールド、矢印アイコン付き。ラベル "Start the 14-day trial"。hover で translate(2px, -2px) + -4px/4px のオレンジ オフセットシャドウ。 - 下にゴーストリンク: "No credit card · cancel anytime →"。 下部ストリップ: - 上辺に 1px の境界線。"BACKED BY" のラベル + 5 つのワードマーク風投資家名 (Accel, Y Combinator, First Round, Benchmark, SV Angel)。 純粋な HTML/CSS、フルレスポンシブ、JS 不要。 ## バリエーション ### ダークテーマ版 構造はそのままで配色を反転: - 背景 #0b0c0f、インク #faf9f5、アクセントはオレンジのまま。 - アウトライン語は `-webkit-text-stroke` をライトインク色に。 - CTA は明色ボタン、hover シャドウはオレンジのまま。 ### プロダクト画像あり版 1024px 以上でレイアウトを拡張: - 60/40 グリッド。左 60%: 既存のヒーローコンテンツ(eyebrow、見出し、リード、CTA)。 - 右 40%: 傾けたブラウザ/アプリのモック(フェイクの chrome + フルブリードのスクリーンショット)。rotate(-3deg) を適用。 - カードに hover で translateY(-4px) の浮き上がり。 ## FAQ **極太フォントのライセンスは?** このプロンプトはシステムフォントと Inter で動きます (Inter は OFL で商用利用可)。ブランドに合わせて SF Pro Display Heavy、Inter Black、可変フォントの Inter V Weight 900 などに差し替え可能です。 **アウトライン文字が崩れるブラウザは?** `-webkit-text-stroke` は主要ブラウザで対応済みです。未対応環境では通常の黒文字で表示されるので、意味は伝わります。フォールバックとして `@supports not (-webkit-text-stroke: 1px black)` で通常表示を明示しておくと安心です。 **アクセント 1 色でクリック率は下がらない?** A/B テストでは、CTA を 1 本に絞ったランディングページの方が「ファーストビュー → クリック」の率が上がる傾向があります。色で誘うより構造で誘う方が強いです。ただしセカンドビュー以降の情報設計は別途必要です。 --- # 植物標本図鑑風のポートフォリオ URL: https://vibe.itlibra.com/design/botanical-specimen-portfolio/ カテゴリ: ポートフォリオ (portfolio) スタイル: エディトリアル, ハンドクラフト, モノクロ, エレガント, レトロ 更新日: 2026-09-01 ライセンス: CC0 1.0 Universal(完全無料・著作権フリー・商用利用OK・クレジット不要) 料金: ¥0 ## 概要 19世紀の植物標本台紙(Herbarium sheet)を再構築したポートフォリオ。学名イタリック表記・採取地メタ・封蝋シール・古紙ノイズで「学術コレクション」の格を作る尖った設計。 ## 使いどころ - 写真家・イラストレーターの古典的ポートフォリオ - ブランド・ブティックの作品アーカイブ - 学術機関・研究者の業績集 - 高級工芸・職人技を見せるサイト - 文化施設・古書店のコレクション展示 ## プロンプト クリエイターの作品集を、19世紀の大学標本室の植物標本台紙コレクションとして作ってください。 要件: - 配色: 古紙 #f0e6cf / #e0d3a8 + 紙影 #c4b48a + 墨 #1c1810 / 褪色 #4d4030 / 淡 #847658 + ハーブ緑 #4a6b3a / #2c4825 + テラコッタ #b04c30 + 古金 #a88830。 - 全体に淡いドットノイズ(4×4px)+ 緑・赤の薄いシミ + 中央 vignette。 - 上部ヘッダー: 4px 二重線 + 1px 線で挟む帯。「HERBARIUM · 標本収蔵庫」(イタリック EB Garamond)+ 「EST. 1894 · CATALOG 04 · 2026」。 - ヒーロー: 大型イタリック明朝「標本にした、仕事の断片。」(最後を太字テラコッタ)+ ラテン語サブタイトル(Specimina laborum selectorum · MMXXII — MMXXVI)+ 短い説明。 - 標本シート 6枚(grid 3カラム → 2 → 1): - 古紙背景 + 1px 紙影ボーダー + box-shadow 厚め - rotate -1.2 〜 +1.2deg のランダム微傾き - 上端に標本貼付テープ(::before / ::after、半透明 + ストライプ)2枚 - SPECIMEN № 04-001 〜 06(letter-spacing 0.4em、ハーブ緑、底に1px線) - フレーム(aspect 4/5): 1px 紙影ボーダー + 8px インセット二重線 + radial 中央光 - SVG で植物イラスト(茎・葉・花・実・根・松葉等の手描き風線画) - 花は朱赤、葉はハーブ緑、それぞれ fill-opacity 0.5 で水彩風に - 学名(イタリック明朝26px、属名イタリック・種小名太字) - 慣用名(letter-spacing 0.35em、淡灰大文字、例: VIBEHAUS · DESIGN CATALOG) - 説明文(13.5px、行間1.85、植物学的レトリックを混ぜる) - メタグリッド(採取地 / 採取年 / 用途 / 高さ)2カラム、ラベル小字 letter-spacing、値はイタリック明朝 - 封蝋シール(::after、56px 円、テラコッタ or ハーブ緑の radial-gradient、文字「V/F/R/P/C」イタリック、rotate -12deg、立体感を box-shadow で演出) - hover で rotate(0deg) + translateY(-6px) + shadow 強化 + z-index 5。 - 下部 signoff: ローマ数字年(Anno MMXXVI)+ Curatum a [name] + イタリック「— Vibehaus Herbarium · 6 specimens of 64 —」。 - フォント: 全体 EB Garamond / Cormorant Garamond + Yu Mincho 和文フォールバック、メタはモノスペース寄り。 - JS不要、フルレスポンシブ。 ## バリエーション ### 鳥類図鑑版 植物 SVG を鳥(嘴・羽・脚)に変更。学名を Aves 系に。 色を青褐色 + 緋色のエレガント配色に。 ### 鉱物標本版 植物を結晶構造(六角・八面体・トリゴナル)に変更。 色をモノクロ + メタリック銀に振って黒檀ベース #1a1410 を差し色に。 ## FAQ **ラテン語学名のフォントは?** EB Garamond または Cormorant Garamond の italic が定番。属名は大文字始まりイタリック、種小名は小文字イタリックが学術慣例。Yu Mincho の italic は無いので和文は別途。 **植物のSVGはどこまで写実的に描くべきですか。** 線画のみ + 部分塗り(fill-opacity 0.5 程度)が「水彩・古典図鑑」の最適解。完全塗りは現代的すぎ、線だけだと寒い。 **封蝋シールの立体感はどう作る?** radial-gradient で光源を 35% 30% に設定 + inset box-shadow 2 段(白光 + 黒影)+ 外側 box-shadow で接地影。これだけで十分立体に見える。 --- # Discord/Slack風の3ペイン・チャットインターフェイス URL: https://vibe.itlibra.com/design/chat-interface/ カテゴリ: チャット (chat) スタイル: ダーク, サイバー, SaaS, ボールド, モノクロ 更新日: 2026-09-01 ライセンス: CC0 1.0 Universal(完全無料・著作権フリー・商用利用OK・クレジット不要) 料金: ¥0 ## 概要 Discord・Slack のチャットUIを再構築した3ペインインターフェイス。サーバーリスト+チャンネル+メッセージスレッド+リアクション・スレッドの定番構成を CSS だけで網羅する尖った設計。 ## 使いどころ - コミュニティプラットフォーム(Discord 風) - 社内コラボレーション SaaS(Slack 風) - ゲーム内チャット・対戦マッチング - AI チャットアプリのインターフェイス - カスタマーサポートツール ## プロンプト コミュニティ・SaaS 向けのチャットUIを Discord 風に作ってください。 要件: - 配色: 暗背景 #1e1f22 ベース、サイドバー #2b2d31、メイン #313338、テキスト #f2f3f5、アクセント青 #5865f2、緑 #23a55a。 - 3ペイン構造: 左にサーバーリスト(円形アバター縦並び)+ 中左にチャンネルリスト(# テキスト・🔊ボイス)+ 中央にメッセージスレッド + 右にスレッド/メンバーリスト。 - メッセージは アバター + 名前 + タイムスタンプ + 本文 + リアクション。連投は名前/アバター省略。 - メッセージにコードブロック、メンション、URL プレビュー、添付画像をそれぞれ1件ずつ含める。 - リアクション(絵文字 + 数)を 3-4 個並列表示、ホバーでツールチップ。 - 右下にフローティング送信ボックス(テキスト入力 + 添付ボタン + 絵文字ボタン + 送信)。 - 上部チャンネルヘッダーには # チャンネル名 + トピック + 通話・通知ボタン。 - フォント: メイン UI は Inter / system-ui、コードブロックは JetBrains Mono。 - JS不要、フルレスポンシブ。スマホでは中央ペインのみ表示にする。 ## バリエーション ### ライト Slack 版 配色を Slack 公式風に:紫サイドバー #4a154b、白メイン、メッセージは黒テキスト。 ### AI チャット版(ChatGPT 風) サーバー/チャンネルペインを廃し、左に会話履歴 + 中央に AI と人のメッセージ往復のみ。 右ペインは「コンテキスト」として Pin 機能。 ## FAQ **3ペインはモバイルで成立する?** 成立しません。狭幅ではメインペインのみを表示し、サイドバーはハンバーガー or オーバーレイで切替えるのが定石です。 **リアクション絵文字は何個まで?** 6-8 種が現実的。多すぎると視認性が下がる + サーバ負荷が増えます。Discord も標準絵文字 + カスタム少量が最適点。 **メッセージ密度は?** 1 画面 20-40 件が目安。連投時のグルーピング(5 分以内なら名前省略)で実質的な可読性を上げます。 --- # 装飾を拒むブルータリズムのヒーロー URL: https://vibe.itlibra.com/design/brutalist-hero/ カテゴリ: ヒーロー (hero) スタイル: ブルータリスト, ボールド, モノクロ, エディトリアル 更新日: 2026-09-01 ライセンス: CC0 1.0 Universal(完全無料・著作権フリー・商用利用OK・クレジット不要) 料金: ¥0 ## 概要 角丸・影・グラデーションを一切使わず、極太タイポと裸の HTML 要素だけで突きつける反デザインのヒーロー。実験的アート・アンダーグラウンドメディア・若手クリエイターが「中身で勝負」する場面の選択肢。 ## 使いどころ - 実験的アート・キュレーションサイト - アンダーグラウンド音楽・サブカルチャー - デザインカンファレンスのトップ - 「美しさより主張」のブランドマニフェスト ## プロンプト アンダーグラウンド系メディアのトップに、ブルータリストなヒーローを作ってください。 要件: - 配色は 2 色だけ。背景 #0a0a0a に蛍光黄 #f1ff00 を1点。境界やアクセントに使い、単色に頼らない。 - 全角・全幅の見出し(H1)。clamp(3rem, 12vw, 11rem) 級。Helvetica / Inter / Arial Black どれでも可。letter-spacing -0.06em、weight 900。 - 見出しは 2-3 行構成、行末で **改行を強制**して左揃えで山切り。 - サブテキスト 1 行、極太の見出しの直下にモノスペースで小さめ(0.85rem)。 - CTA は HTML ``` **AIへの指示文** Tailwind CSS でお問い合わせフォームを作ってください。 要件: - 全体は max-w-lg rounded-2xl border bg-white p-6 shadow-sm。 - 各項目は grid gap-1 で「ラベル → 入力 → 補足」の縦並び。 ラベルは text-sm font-medium、補足は text-xs text-slate-500。 - 入力欄は w-full rounded-lg border px-3.5 py-2.5。 placeholder:text-slate-400 を当て、 focus:border-blue-600 focus:outline-none focus:ring-4 でフォーカスを2段で示す。 - textarea には resize-y と leading-[1.8] を当てる。 - 同意チェックは flex items-start gap-4。チェックボックスに mt-1.5 を当てて ラベル1行目の文字の高さに合わせる。label と input は for と id で結ぶ。 - 送信ボタンは w-full rounded-lg bg-blue-600 py-3 font-semibold text-white。 - すべての入力に autocomplete と type を正しく指定する。 ### 通知バナー 4色 プレビュー: https://vibe.itlibra.com/sample/tw-alert/ 情報・成功・注意・エラーの4種。色は bg-{色}-50 と border-{色}-200 と text-{色}-800 の3点セットで揃え、role は status と alert を使い分けています。 ```html

メンテナンスのお知らせ

9 月 5 日 2:00 から 4:00 まで、書き出し機能を停止します。

保存しました

変更内容は共有リンクにも反映されています。

``` **AIへの指示文** Tailwind CSS で通知バナーを4種類(情報・成功・注意・エラー)作ってください。 要件: - 配色は3点セットで揃える。情報は blue、成功は green、注意は amber、エラーは red。 それぞれ bg-{色}-50、border-{色}-200、text-{色}-800 を使う。 - 構造は flex items-start gap-3 rounded-xl border p-4。 左にアイコン(shrink-0)、中央に本文(flex-1)、右に閉じるボタン。 - 見出しは font-semibold、本文は mt-1 text-sm leading-[1.8]。 - 情報と成功は role="status"、注意とエラーは role="alert" を付ける。 alert は即座に読み上げられるので、緊急でないものに付けない。 - アイコンは装飾なので aria-hidden="true"、閉じるボタンには aria-label="閉じる"。 ## FAQ **Tailwindのクラスが長くなりすぎませんか。** 長くなったら部品として切り出す合図です。同じクラス列を3回以上書いているなら、コンポーネントに分けるか、@apply でまとめてください。ただし @apply を多用するとTailwindを使う意味が薄れるので、まず部品化を検討するほうが健全です。 **leading-[1.8] のような任意値は避けるべきでは。** 原則そのとおりですが、日本語の行間は例外です。Tailwindの既定 leading は英文基準で、text-base だと 1.5 になります。日本語では詰まって読みにくいため、1.7〜1.85 が必要です。恒久的には @theme で独自の段を足すのが正解で、任意値はその前段の実用的な妥協です。 **クラス名を動的に組み立てても大丈夫ですか。** 危険です。Tailwindはソースを文字列として走査してクラスを拾うので、`text-${color}-600` のように分割して書くと本番ビルドで消えます。条件分岐で完全なクラス名を丸ごと持つ形にしてください。 **このページのプレビューは本物のTailwindですか。** プレビュー自体は、外部CDNを読まずに単独で動くよう、使っているユーティリティだけをTailwind既定と同じ値で書き出しています。掲載しているマークアップは本物のTailwindのクラス名なので、Tailwindが入っているプロジェクトへ貼れば同じ見た目になります。 --- # jQueryを使わない書き方の対照表 URL: https://vibe.itlibra.com/tech/jquery/ 技術: jQuery と素のJS (jquery) 検索キーワード: jQuery 更新日: 2026-09-01 ライセンス: CC0 1.0 Universal(完全無料・著作権フリー・商用利用OK・クレジット不要) 料金: ¥0 ## 概要 jQueryでよく書いていた処理を素のJavaScriptに置き換える対照表とサンプル。要素の取得、クラス操作、イベント委譲、開閉アニメーション、フォームの値取得を、両方のコードを並べて比較します。 jQueryが解いていた問題は、ブラウザごとにAPIがばらばらだったことでした。その問題は、もう存在しません。querySelectorAll、classList、fetch、それにCSSアニメーション。かつて短く書くためにjQueryが必要だった処理は、いまは標準機能で同じくらい短く書けます。それでも既存サイトからjQueryを剥がすのは別の話で、判断には「何が置き換えられて、何が面倒なままか」を具体的に知っている必要があります。 ## 向いている場面 - 既存サイトからjQueryを外せるか判断したいとき - jQueryのコードを読んで意味を把握したいとき - 新規実装で依存を増やしたくないとき - AIに「jQueryを使わないで」と指示したいとき ## つまずきやすい点 - jQueryプラグインに依存している箇所は置換先を先に決める - $.ajax と fetch はエラー時の挙動が違う - slideUp / fadeIn 相当はCSS側に持たせるほうが軽い - $(document).ready は DOMContentLoaded と完全に同じではない ## サンプルとコード ### 表示切替とクラス操作 プレビュー: https://vibe.itlibra.com/sample/js-toggle/ jQuery の toggle と toggleClass に相当します。見た目の定義はすべてCSS側に置き、JavaScriptは「いまどの状態か」を宣言するだけにしています。 ```html
クラスを1つ足し引きするだけで、見た目の状態は CSS 側に全部書けます。JavaScript は「いまどの状態か」を宣言するだけで済み、色や余白の値を持たなくてよくなります。
``` ```css .panel { border: 1px solid var(--line); border-radius: 12px; padding: 1.1rem; color: var(--muted); line-height: 1.85; transition: opacity 0.25s ease, transform 0.25s ease; } /* 「隠す」を CSS 側の状態として持たせる。JS は class を足し引きするだけ */ .panel.is-hidden { display: none; } .panel.is-muted { opacity: 0.45; transform: scale(0.99); } @media (prefers-reduced-motion: reduce) { .panel { transition: none; } } ``` ```js const panel = document.getElementById("panel"); document.querySelectorAll("[data-action]").forEach((btn) => { btn.addEventListener("click", () => { const action = btn.dataset.action; // $('#panel').toggle() 相当 if (action === "toggle") panel.classList.toggle("is-hidden"); // $('#panel').toggleClass('is-muted') 相当 if (action === "mute") panel.classList.toggle("is-muted"); // $('#panel').removeClass('is-hidden is-muted') 相当 if (action === "reset") panel.classList.remove("is-hidden", "is-muted"); }); }); ``` **AIへの指示文** クラスの付け外しで表示を切り替える部品を、jQuery を使わずに作ってください。 要件: - ボタンには data-action 属性で役割を持たせ、querySelectorAll でまとめて登録する。 - 表示の切替は classList.toggle、複数解除は classList.remove の可変長引数を使う。 - 見た目(display, opacity, transform)は必ず CSS 側のクラスに置く。 JavaScript から style プロパティを直接書かない。 - prefers-reduced-motion: reduce のとき transition を無効にする。 - 日本語の本文なので line-height は 1.85、letter-spacing は 0 にする。 ### イベント委譲(動的に増える要素) プレビュー: https://vibe.itlibra.com/sample/js-delegate/ jQuery の on による委譲に相当します。親に1つだけ登録し、closest で実際の対象を判定するので、後から追加した行にも自動で効きます。 ```html
  • 企画書のたたき台をつくる
  • 参考サイトを 5 つ集める
  • 配色の候補を 3 案出す
``` ```css .list { list-style: none; margin: 0; padding: 0; } .item { display: flex; align-items: center; gap: 0.75rem; border: 1px solid var(--line); border-radius: 11px; padding: 0.7rem 0.9rem; } .item + .item { margin-top: 0.55rem; } .item span { flex: 1; line-height: 1.7; } .item .del { border: 0; background: none; color: var(--danger); font-size: 0.86rem; padding: 0.2rem 0.4rem; border-radius: 6px; } .item .del:hover { background: #fef2f2; } .add { margin-top: 0.9rem; width: 100%; border: 1px dashed var(--line); background: none; color: var(--muted); border-radius: 11px; padding: 0.6rem; } .add:hover { color: var(--accent); border-color: var(--accent); } ``` ```js const list = document.getElementById("list"); // $(document).on('click', '.del', ...) 相当。 // 親に1つだけ登録し、実際に押された要素を closest で判定する。 // 後から増えた行にも自動で効くのが委譲の利点。 list.addEventListener("click", (e) => { const btn = e.target.closest(".del"); if (!btn || !list.contains(btn)) return; btn.closest(".item").remove(); }); let n = 4; document.getElementById("add").addEventListener("click", () => { const li = document.createElement("li"); li.className = "item"; li.innerHTML = 'あとで決めるタスク ' + n++ + ''; list.append(li); }); ``` **AIへの指示文** あとから増える行にも効く削除ボタンを、jQuery を使わずに作ってください。 要件: - イベントは各ボタンではなく、親のリストに1つだけ addEventListener する。 - ハンドラの中で e.target.closest(".del") を使い、対象外なら early return する。 親が本当にその要素を含むか contains でも確認する。 - 削除は closest(".item").remove() で行う。 - 追加ボタンは createElement と append で行を足す。 追加後にイベントを登録し直さないことを、コメントで明示する。 - 削除ボタンの色は CSS 側で持ち、JavaScript は DOM 操作だけを担当する。 ### 開閉アニメーション(slideToggle 相当) プレビュー: https://vibe.itlibra.com/sample/js-slide/ jQuery の slideToggle は高さを測ってpxで動かしますが、grid-template-rows を 0fr から 1fr へ遷移させれば高さを測らずに済みます。JavaScriptは状態の宣言だけです。 ```html
ご注文から 2 営業日以内に発送します。追跡番号は発送完了メールに記載します。
クレジットカード、銀行振込、コンビニ払いに対応しています。法人のお客様は請求書払いもお選びいただけます。
``` ```css .drawer { background: #fff; border: 1px solid var(--line); border-radius: 14px; overflow: hidden; } .drawer + .drawer { margin-top: 0.75rem; } .drawer > button { width: 100%; display: flex; align-items: center; gap: 0.75rem; padding: 1rem 1.15rem; border: 0; background: none; font: inherit; font-weight: 600; line-height: 1.6; color: inherit; text-align: left; cursor: pointer; } .drawer > button::after { content: ""; margin-left: auto; width: 9px; height: 9px; border-right: 2px solid var(--muted); border-bottom: 2px solid var(--muted); transform: rotate(45deg); transition: transform 0.25s ease; } .drawer > button[aria-expanded="true"]::after { transform: rotate(-135deg); } .drawer > button:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; } /* 高さのアニメーションは CSS に持たせる。JS は状態を切り替えるだけにする */ .drawer-body { display: grid; grid-template-rows: 0fr; transition: grid-template-rows 0.28s ease; } .drawer-body[data-open="true"] { grid-template-rows: 1fr; } .drawer-body > div { overflow: hidden; color: var(--muted); line-height: 1.85; } .drawer-body[data-open="true"] > div { padding: 0 1.15rem 1.15rem; } @media (prefers-reduced-motion: reduce) { .drawer-body, .drawer > button::after { transition: none; } } ``` ```js // jQuery なら $(this).next().slideToggle() の一行。 // 素の JS では「状態の宣言」だけを担当し、動きは CSS 側に置く。 document.querySelectorAll(".drawer > button").forEach((btn) => { btn.addEventListener("click", () => { const body = document.getElementById(btn.getAttribute("aria-controls")); const open = btn.getAttribute("aria-expanded") === "true"; btn.setAttribute("aria-expanded", String(!open)); body.dataset.open = String(!open); }); }); ``` **AIへの指示文** jQuery の slideToggle 相当の開閉を、JavaScript を最小限にして作ってください。 要件: - 高さのアニメーションは JavaScript で測らない。 中身を display: grid にして grid-template-rows を 0fr から 1fr へ遷移させる。 - JavaScript がやるのは、ボタンの aria-expanded と、 中身の data-open 属性を true と false で切り替えることだけ。 - ボタンと中身は aria-controls と id で結ぶ。 - 山形記号は ::after で描き、aria-expanded="true" のとき回転させる。 - prefers-reduced-motion: reduce のとき transition を無効にする。 ### フォームの値をまとめて取る プレビュー: https://vibe.itlibra.com/sample/js-form/ jQuery の serialize と serializeArray に相当します。FormData を URLSearchParams や Object.fromEntries に渡すだけで、同じ結果が標準機能だけで得られます。 ```html
ここに送信内容が出ます
``` ```css .form { background: #fff; border: 1px solid var(--line); border-radius: 14px; padding: 1.4rem; display: grid; gap: 1rem; } .field { display: grid; gap: 0.3rem; } .field label { font-size: 0.88rem; font-weight: 600; line-height: 1.7; } .field input, .field select { font: inherit; line-height: 1.7; color: inherit; padding: 0.55rem 0.75rem; border: 1px solid var(--line); border-radius: 9px; } .field input:focus, .field select:focus { outline: 0; border-color: var(--accent); box-shadow: 0 0 0 4px rgba(37, 99, 235, 0.15); } .form button { font: inherit; line-height: 1.7; cursor: pointer; padding: 0.65rem 1.2rem; border: 0; border-radius: 9px; background: var(--accent); color: #fff; font-weight: 700; } .out { margin: 0; padding: 0.9rem 1rem; background: #0f1420; color: #e8ecf4; border-radius: 11px; font-family: var(--mono); font-size: 0.8rem; line-height: 1.75; white-space: pre-wrap; word-break: break-all; } ``` ```js const form = document.getElementById("form"); const out = document.getElementById("out"); form.addEventListener("submit", (e) => { e.preventDefault(); // jQuery の serialize() 相当(クエリ文字列) const params = new URLSearchParams(new FormData(form)); // serializeArray() をオブジェクトにしたもの相当 const data = Object.fromEntries(new FormData(form)); out.textContent = "クエリ文字列:\n" + params.toString() + "\n\nオブジェクト:\n" + JSON.stringify(data, null, 2); }); ``` **AIへの指示文** フォームの入力値をまとめて取り出す処理を、jQuery を使わずに書いてください。 要件: - submit イベントで e.preventDefault() してから処理する。 - クエリ文字列が欲しい場合は new URLSearchParams(new FormData(form)).toString()。 - オブジェクトが欲しい場合は Object.fromEntries(new FormData(form))。 同名フィールドが複数ある場合は最後の値だけが残る点をコメントで注意する。 - 結果は pre 要素に textContent で入れる。innerHTML は使わない。 - 結果表示には aria-live="polite" を付ける。 - 各入力には label を for と id で結び、name を必ず付ける。 ## FAQ **いま新規で作るサイトにjQueryは必要ですか。** ほとんどの場合、必要ありません。jQueryが解いていたのはブラウザ間のAPIの差でしたが、その差はもうありません。ただし既存サイトから剥がす話は別です。依存しているプラグインがあるなら、置き換え先を決めてから外してください。 **$(document).ready と DOMContentLoaded は同じですか。** 完全に同じではありません。DOMContentLoaded は発火後に登録したハンドラが呼ばれませんが、jQuery の ready は発火済みでも即座に実行されます。スクリプトを defer で読み込んでいるなら、そもそもどちらも不要で、直接書けば動きます。 **$.ajax を fetch に置き換えるときの注意点は。** エラーの扱いが違います。$.ajax はHTTPステータスが4xxや5xxならエラー扱いですが、fetch は通信さえ成功すれば resolve します。res.ok を自分で確認して throw する処理を必ず書いてください。タイムアウトも fetch には無いので AbortSignal.timeout を使います。 **fadeIn や slideUp の置き換えはどう書くのが良いですか。** JavaScriptで高さや不透明度を動かさず、CSSのtransitionに任せてください。JavaScript側はクラスやdata属性を切り替えるだけにします。描画の最適化がブラウザ任せになるぶん滑らかで、動きの調整もCSSだけで完結します。 --- # GridとFlexboxの使い分け URL: https://vibe.itlibra.com/tech/css-layout/ 技術: CSSレイアウト (css-layout) 検索キーワード: CSS GridとFlexbox 更新日: 2026-09-01 ライセンス: CC0 1.0 Universal(完全無料・著作権フリー・商用利用OK・クレジット不要) 料金: ¥0 ## 概要 CSS GridとFlexboxでレイアウトを組むサンプル集。管理画面の骨格、メディアクエリを書かないカードグリッド、中央寄せの4パターン、高さの揃わない敷き詰めを、そのまま貼れるコードで収録します。 GridとFlexboxは競合する道具ではありません。Gridは「面」を、Flexboxは「列」を扱います。ページ全体の骨格のように縦横を同時に決めたいならGrid、ボタンとアイコンを一列に並べるように片方向へ流したいならFlexbox。この使い分けさえ掴めば、レイアウトで悩む時間はかなり減ります。そしていまのCSSでは、画面幅ごとの分岐を書かずに列数を自動調整することもできます。 ## 向いている場面 - 管理画面やダッシュボードの全体骨格 - 画面幅に応じて列数が変わるカード一覧 - ボタン・アイコン・ラベルの横並び - 高さの揃わない画像やカードの敷き詰め ## つまずきやすい点 - GridとFlexboxのどちらでもできる場面で迷い続ける - auto-fit と auto-fill の違いを取り違える - gap を margin で代用して端に余白が残る - minmax の下限を固定値にして狭い画面で溢れる ## サンプルとコード ### 管理画面の骨格 プレビュー: https://vibe.itlibra.com/sample/grid-app-shell/ ヘッダー・サイドバー・本文・フッターを grid-template-areas で名前を付けて配置します。HTMLの並び順とレイアウトが切り離されるので、狭い画面では領域名の並びを書き換えるだけで縦積みになります。 ```html
田中 さん

ダッシュボード

ヘッダー・サイドバー・本文・フッターの4領域を、grid-template-areas で名前を付けて配置しています。HTML の並び順とレイアウトが切り離されているので、狭い画面では領域名の並びを書き換えるだけで縦積みになります。

© 2026 Console — 管理画面の骨格サンプル
``` ```css /* 画面の骨格は「面」なので Grid。名前を付けた領域に置くと、 HTML の並び順とレイアウトを切り離せる。 */ .shell { display: grid; min-height: 100dvh; grid-template-columns: 220px 1fr; grid-template-rows: auto 1fr auto; grid-template-areas: "head head" "side main" "side foot"; } .shell > header { grid-area: head; } .shell > nav { grid-area: side; } .shell > main { grid-area: main; } .shell > footer { grid-area: foot; } /* 狭い画面では1列に畳む。領域名で書いてあるので、 並べ替えは grid-template-areas を書き換えるだけで済む。 */ @media (max-width: 720px) { .shell { grid-template-columns: 1fr; grid-template-areas: "head" "main" "side" "foot"; } } .shell > header { display: flex; align-items: center; gap: 0.75rem; padding: 0.9rem 1.2rem; background: #fff; border-bottom: 1px solid var(--line); } .shell > nav { padding: 1rem 0.8rem; background: #fff; border-right: 1px solid var(--line); } .shell > main { padding: 1.4rem 1.2rem; } .shell > footer { padding: 0.8rem 1.2rem; border-top: 1px solid var(--line); color: var(--muted); font-size: 0.82rem; line-height: 1.7; } @media (max-width: 720px) { .shell > nav { border-right: 0; border-top: 1px solid var(--line); } } ``` **AIへの指示文** CSS Grid で管理画面の骨格を作ってください。 要件: - grid-template-areas で head / side / main / foot の4領域に名前を付ける。 - 列は 220px と 1fr、行は auto / 1fr / auto。min-height: 100dvh で全高を取る。 - 各要素は grid-area で領域名を指定し、HTML の並び順に依存させない。 - 720px 未満では1列にし、grid-template-areas を head / main / side / foot へ 書き換えるだけで縦積みにする。位置指定を個別に上書きしない。 - サイドバーの現在地は aria-current="page" で示し、CSS はそれを見て塗る。 - 日本語の本文なので line-height は 1.85、letter-spacing は 0 にする。 ### 列数が自動で変わるカード一覧 プレビュー: https://vibe.itlibra.com/sample/grid-cards/ メディアクエリを1行も書かずに列数が変わります。auto-fit と minmax の組み合わせで、幅に入るだけ列を作り、余ったトラックは畳みます。 ```html

受注管理

注文の一覧、絞り込み、CSV書き出しまで。

詳しく見る →

在庫連携

倉庫システムと1時間ごとに同期します。差分だけを送るので回線に優しい構成です。

詳しく見る →

請求書の発行

月末締めで自動生成。

詳しく見る →

権限管理

編集者と閲覧者の2段階。招待はリンク発行だけで完了します。

詳しく見る →

監査ログ

誰がいつ何を変えたかを90日間保持します。

詳しく見る →

通知

メールとWebhookに対応。

詳しく見る →
``` ```css /* 画面幅ごとの分岐を1行も書かずに列数が変わる。 auto-fit は「入るだけ列を作り、余ったトラックは畳む」。 min() を挟むのは、狭い画面で 240px が画面幅を超えて溢れるのを防ぐため。 */ .cards { display: grid; gap: 1rem; grid-template-columns: repeat(auto-fit, minmax(min(100%, 240px), 1fr)); } /* auto-fill との違い: auto-fit … 中身が少ないとき、残りのトラックを畳んでカードが伸びる auto-fill … 空のトラックを残すので、カードは伸びず左に寄る 「1枚のときに全幅へ伸ばしたい」なら auto-fit を選ぶ。 */ .card { background: #fff; border: 1px solid var(--line); border-radius: 12px; padding: 1rem 1.1rem; display: flex; flex-direction: column; gap: 0.35rem; } .card h3 { margin: 0; font-size: 1rem; line-height: 1.6; } .card p { margin: 0; color: var(--muted); font-size: 0.88rem; line-height: 1.85; } /* 最後の行を下端に押し付ける。高さが揃わないカードでも脚が揃う。 */ .card .foot { margin-top: auto; padding-top: 0.6rem; color: var(--accent); font-size: 0.82rem; line-height: 1.7; } ``` **AIへの指示文** メディアクエリを使わないカードグリッドを作ってください。 要件: - grid-template-columns: repeat(auto-fit, minmax(min(100%, 240px), 1fr)) を使う。 - min(100%, 240px) にするのは、画面幅が 240px を下回ったときに トラックが溢れるのを防ぐため。理由をコメントで書く。 - auto-fit と auto-fill の違いをコメントで説明する (auto-fit は空トラックを畳んでカードが伸びる。auto-fill は空トラックを残す)。 - カードは flex-direction: column にし、最後の行に margin-top: auto を当てて 高さが揃わなくても脚が揃うようにする。 - gap で間隔を取り、margin では取らない。 ### 中央寄せの4パターン プレビュー: https://vibe.itlibra.com/sample/flex-center/ place-items、Flexbox、min-height付き、絶対配置。どれを使うかは「子が複数か」「重ねる必要があるか」で決まります。4つを並べて違いが見えるようにしました。 ```html

1. place-items: center(最短)

中央

2. Flexbox(横並びのまま中央)

3. 高さのない親に min-height

中央

4. 絶対配置(重ねるとき用)

中央
``` ```css /* 1) いちばん短い。子が1つでも複数でも効く */ .center-grid { display: grid; place-items: center; } /* 2) Flexbox。横並びのまま中央に置きたいときはこちら。 gap も同時に効かせられる */ .center-flex { display: flex; align-items: center; /* 交差軸(縦) */ justify-content: center; /* 主軸(横) */ gap: 0.5rem; } /* 3) 高さを持たない親を中央にしたいとき。 親に min-height を与えるのを忘れない */ .center-minh { display: grid; place-items: center; min-height: 150px; } /* 4) 絶対配置。ほかの要素の上に重ねたいときだけ使う。 translate(-50%, -50%) が要素自身の半分を戻す役割 */ .center-abs { position: relative; } .center-abs > * { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); } ``` **AIへの指示文** 要素を中央に置く方法を4パターン並べて比較できるサンプルを作ってください。 要件: - 1つ目: display: grid と place-items: center。最短の書き方として紹介する。 - 2つ目: Flexbox の align-items: center と justify-content: center。 子を横並びのまま中央に置きたい場合で、gap も同時に効かせられる点を書く。 - 3つ目: 高さを持たない親に min-height を与えてから中央に置く。 - 4つ目: position: absolute と top/left 50% と translate(-50%, -50%)。 ほかの要素に重ねたいときだけ使う、と用途を限定して書く。 - 各パターンに見出しを付け、同じ見た目の要素を中央に置いて違いを比べられるようにする。 ### 高さの揃わない敷き詰め プレビュー: https://vibe.itlibra.com/sample/grid-masonry/ CSSのmasonryはまだ全ブラウザに来ていないため、いま確実に動く multi-column で組みます。読み順が「縦に流れて折り返す」形になる点だけ注意が要ります。 ```html

短いメモ

一行だけ。

設計の覚え書き

余白は4pxの倍数で刻む。行間は日本語なら1.8前後。この2つを最初に決めておくと、あとから全体を作り直さずに済みます。

色の決め方

まず1色だけ決めて、残りは明度で作る。

読み込みの体感

実際の速さより、待っていることが分かるかどうかが効きます。スケルトンを置くだけで印象が変わります。

命名

見た目ではなく役割で名付ける。

フォーム

ラベルは必ず上に置き、入力欄と for と id で結ぶ。プレースホルダーをラベル代わりにしない。入力を始めた瞬間に何の欄か分からなくなります。

アイコン

装飾なら aria-hidden を付ける。

距離を長く、不透明度を低く。近い影は安っぽく見えます。

``` ```css /* CSS の masonry はまだ全ブラウザには来ていない。 いま確実に動くのは multi-column を使う方法。 段組みなので「縦に流れて折り返す」順序になる点だけ注意する。 */ .masonry { column-count: 3; column-gap: 1rem; } @media (max-width: 860px) { .masonry { column-count: 2; } } @media (max-width: 560px) { .masonry { column-count: 1; } } .masonry > * { /* これが無いとカードが段の境目で分断される */ break-inside: avoid; margin-bottom: 1rem; } .tile { background: #fff; border: 1px solid var(--line); border-radius: 12px; padding: 1rem 1.1rem; } .tile h3 { margin: 0 0 0.35rem; font-size: 0.98rem; line-height: 1.6; } .tile p { margin: 0; color: var(--muted); font-size: 0.88rem; line-height: 1.85; } .tile .bar { height: 8px; border-radius: 999px; margin-bottom: 0.7rem; background: linear-gradient(90deg, var(--accent), #22d3ee); } ``` **AIへの指示文** 高さの違うカードを隙間なく敷き詰めるレイアウトを作ってください。 要件: - column-count と column-gap を使う。3列 → 860px 未満で2列 → 560px 未満で1列。 - 子要素には break-inside: avoid を必ず当てる。無いと段の境目で分断される。 - 間隔は margin-bottom で取る(multi-column では gap が縦方向に効かない)。 - 読み順が「縦に流れて折り返す」順序になるため、順序に意味がある一覧には 向かないことをコメントで明記する。 - カードの高さはばらつかせて、敷き詰めの効果が分かるようにする。 ## FAQ **GridとFlexboxはどちらを使えばいいですか。** 縦横を同時に決めたいならGrid、片方向へ流したいならFlexboxです。ページ全体の骨格や表のような配置はGrid、ボタンとアイコンの横並びやツールバーはFlexbox。迷ったら「行と列の両方を指定したいか」を自問すると、たいてい答えが出ます。 **auto-fit と auto-fill はどう違いますか。** 中身が少ないときの挙動が違います。auto-fit は余ったトラックを畳むのでカードが伸びて全幅を埋め、auto-fill は空のトラックを残すのでカードは伸びず左に寄ります。「1枚しかないときに全幅へ広げたい」なら auto-fit を選んでください。 **minmax の下限を固定値にしてはいけませんか。** 画面幅がその値を下回ると溢れます。minmax(240px, 1fr) は 240px 未満の画面で横スクロールを生みます。min(100%, 240px) と書けば、狭いときは自動的に 100% まで縮むので安全です。 **CSSのmasonryはまだ使えませんか。** 仕様策定と実装が進んでいる段階で、全ブラウザで安定して使える状況ではありません。いま本番で敷き詰めが必要なら multi-column が確実です。ただし読み順が縦方向になるので、順序に意味がある一覧では避けてください。 --- # 意味のある動きだけを付ける URL: https://vibe.itlibra.com/tech/css-animation/ 技術: CSSアニメーション (css-animation) 検索キーワード: CSSアニメーション 更新日: 2026-09-01 ライセンス: CC0 1.0 Universal(完全無料・著作権フリー・商用利用OK・クレジット不要) 料金: ¥0 ## 概要 CSSのtransitionとkeyframesで動きを付けるサンプル集。ホバー演出、登場アニメーション、スクロール連動、継ぎ目のない無限ループを、動きを減らす設定への配慮まで含めて収録します。 動きは装飾ではなく、状態の変化を説明する手段です。押せたのか、開いたのか、読み込み中なのか。これらは一瞬で切り替わると認識しにくく、0.2秒ほどの遷移を挟むだけで理解が追いつきます。逆に、意味のない動きは注意を奪うだけで害になります。何を伝えるための動きなのか、を毎回決めてから書くと、時間の値も曲線も自然に決まります。 ## 向いている場面 - ボタンやカードの押せる感じを伝える - 要素の登場・退場をなめらかに見せる - スクロールに連動した読み物の演出 - ロゴやお知らせの無限ループ表示 ## つまずきやすい点 - prefers-reduced-motion を無視すると体調不良の原因になる - left や top を動かすと再描画が重い。transform を使う - transition: all は意図しない変化まで拾う - 無限アニメーションは電池とCPUを常に消費する ## サンプルとコード ### ホバーとフォーカスの演出 プレビュー: https://vibe.itlibra.com/sample/anim-hover/ 動かす対象を名指しし、位置は transform で動かします。ホバーだけでなくフォーカスにも同じ演出を当てているので、キーボード操作でも状態が伝わります。 ```html

カードは持ち上げすぎない

2px ほどの移動と、遠くて薄い影。これだけで触れる対象だと伝わります。大きく動かすと一覧全体が落ち着かなくなります。

``` ```css /* transition: all は書かない。何が動くのか読めなくなるうえ、 意図しないプロパティまで拾って重くなる。動かす対象を名指しする。 */ .btn { --lift: 0; display: inline-flex; align-items: center; gap: 0.4rem; padding: 0.65rem 1.3rem; border: 0; border-radius: 10px; background: var(--accent); color: #fff; font: inherit; font-weight: 700; line-height: 1.7; cursor: pointer; /* 動かすのは transform と box-shadow だけ。 left/top ではなく transform を使うと、再描画ではなく合成で済む。 */ transform: translateY(var(--lift)); transition: transform 0.18s ease, box-shadow 0.18s ease, background-color 0.18s ease; box-shadow: 0 2px 6px -2px rgba(37, 99, 235, 0.5); } .btn:hover { --lift: -2px; background: #1d4ed8; box-shadow: 0 12px 22px -10px rgba(37, 99, 235, 0.8); } .btn:active { --lift: 0px; transition-duration: 0.06s; } .btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; } /* 下線を左から伸ばす。scaleX は幅の再計算を起こさない */ .link { position: relative; color: var(--ink); text-decoration: none; font-weight: 600; line-height: 1.7; } .link::after { content: ""; position: absolute; left: 0; right: 0; bottom: -2px; height: 2px; background: var(--accent); transform: scaleX(0); transform-origin: left; transition: transform 0.22s ease; } .link:hover::after, .link:focus-visible::after { transform: scaleX(1); } /* カードは持ち上げるより、境界と影で「触れる」ことを示すほうが上品 */ .card { background: #fff; border: 1px solid var(--line); border-radius: 12px; padding: 1rem 1.1rem; transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease; } .card:hover { border-color: #c7d2fe; box-shadow: 0 18px 30px -22px rgba(27, 32, 48, 0.55); transform: translateY(-2px); } /* 動きを減らす設定では、位置の変化だけ止めて色の変化は残す。 「押せること」の手がかりまで消さないのがコツ。 */ @media (prefers-reduced-motion: reduce) { .btn, .card, .link::after { transition-duration: 0.01ms; } .btn:hover, .card:hover { transform: none; } } ``` **AIへの指示文** ボタン・リンク・カードのホバー演出を作ってください。 要件: - transition: all は使わない。transform / box-shadow / background-color のように 動かすプロパティを名指しする。理由をコメントで書く。 - 位置の変化は top や left ではなく transform: translateY で行う。 再描画ではなく合成で済むため。 - リンクの下線は ::after を scaleX(0) から scaleX(1) へ。transform-origin: left。 - :hover と :focus-visible の両方に同じ演出を当てる。 - :active では持ち上げを戻し、transition-duration を短くして押した感じを出す。 - prefers-reduced-motion: reduce では位置の変化だけを止め、 色の変化は残す。押せる手がかりまで消さないため。 ### 登場アニメーション プレビュー: https://vibe.itlibra.com/sample/anim-enter/ keyframes でフェードアップ、ポップ、フェードインの3種。時間差は nth-child で0.06秒ずつずらし、animation-fill-mode: backwards で遅延中の一瞬の表示を防いでいます。 ```html 新着 4 件

注文が確定しました

2 営業日以内に発送します。

在庫が補充されました

入荷待ちだった 3 商品が購入できます。

レビューが届きました

★4.6 の評価が付きました。

請求書を発行しました

8 月分の明細をご確認ください。

時間差は nth-child で 0.06 秒ずつずらしています。animation-fill-mode: backwards を付けているので、遅延の間も透明なまま待機します。

``` ```css @keyframes fade-up { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: none; } } @keyframes fade-in { from { opacity: 0; } to { opacity: 1; } } @keyframes pop { 0% { opacity: 0; transform: scale(0.94); } 60% { opacity: 1; transform: scale(1.02); } 100% { transform: scale(1); } } /* backwards を付けると、遅延中も開始時の状態(透明)を保つ。 これが無いと、遅延の間だけ要素が見えてしまう。 */ .enter > * { animation: fade-up 0.5s cubic-bezier(0.2, 0.8, 0.2, 1) backwards; } /* 時間差は nth-child でずらす。0.06s 刻みが目に自然 */ .enter > *:nth-child(1) { animation-delay: 0.00s; } .enter > *:nth-child(2) { animation-delay: 0.06s; } .enter > *:nth-child(3) { animation-delay: 0.12s; } .enter > *:nth-child(4) { animation-delay: 0.18s; } .badge { animation: pop 0.45s cubic-bezier(0.34, 1.56, 0.64, 1) 0.24s backwards; } .note { animation: fade-in 0.6s ease 0.3s backwards; } @media (prefers-reduced-motion: reduce) { .enter > *, .badge, .note { animation: none; } } ``` **AIへの指示文** 要素が順番に現れる登場アニメーションを作ってください。 要件: - @keyframes を3つ定義する。fade-up(下から12px + 透明から)、 fade-in(透明から)、pop(0.94倍から1.02倍を経て等倍へ)。 - 一覧の各行には animation-delay を nth-child で 0.06s ずつずらして当てる。 - animation-fill-mode: backwards を必ず付ける。無いと遅延中に要素が見えてしまう。 この理由をコメントで書く。 - 曲線は cubic-bezier(0.2, 0.8, 0.2, 1) を基本にし、 pop だけ跳ねる cubic-bezier(0.34, 1.56, 0.64, 1) を使う。 - prefers-reduced-motion: reduce では animation を none にする。 ### スクロール連動 プレビュー: https://vibe.itlibra.com/sample/anim-scroll/ animation-timeline を使い、IntersectionObserver を書かずにスクロール量へ動きを結び付けます。@supports で囲ってあるので、未対応環境では最初から見えている状態になります。 ```html

下へスクロールしてください。各ブロックが視界に入るのに合わせて現れ、上部のバーが読み進めた量を示します。JavaScript は使っていません。

交差の量が、そのまま進行度になる

animation-timeline: view() を書くと、要素とビューポートの交差量がアニメーションの再生位置に対応します。IntersectionObserver で閾値を管理する必要がありません。

範囲は animation-range で決める

cover 12% から cover 48% までを使う、という指定です。全域を使うと画面中央でようやく完成するので、読み物では前半で終わらせるほうが自然に感じます。

未対応環境では最初から見える

@supports で囲っているので、対応していないブラウザではアニメーションが当たらず、要素は通常どおり表示されます。内容が読めなくなることはありません。

動きを減らす設定を尊重する

prefers-reduced-motion: reduce のときはアニメーションを外し、進捗バーも止めます。動きそのものが体調に影響する人がいるため、これは装飾ではなく必須の配慮です。

``` ```css /* Scroll-driven animations。IntersectionObserver を書かずに、 「要素がビューポートを通過する量」をアニメーションの進行度に直結できる。 */ @keyframes reveal { from { opacity: 0; transform: translateY(24px); } to { opacity: 1; transform: none; } } /* 読み上げ順に影響しないよう、対応ブラウザだけに当てる。 未対応環境では最初から見えている状態になり、内容は読める。 */ @supports (animation-timeline: view()) { .reveal { animation: reveal linear both; /* view() = この要素とビューポートの交差を時間軸にする */ animation-timeline: view(); /* 下端に入り始めてから、上に 40% 進むまでで完了させる。 cover 20% 〜 cover 55% あたりが読み物では自然。 */ animation-range: cover 12% cover 48%; } } /* 上部の進捗バー。scroll() は「スクロール可能な祖先」を時間軸にする */ @supports (animation-timeline: scroll()) { .progress { position: fixed; inset: 0 0 auto 0; height: 3px; z-index: 10; background: var(--accent); transform-origin: left; animation: grow linear both; animation-timeline: scroll(root block); } @keyframes grow { from { transform: scaleX(0); } to { transform: scaleX(1); } } } @media (prefers-reduced-motion: reduce) { .reveal { animation: none; opacity: 1; transform: none; } .progress { animation: none; transform: scaleX(0); } } ``` **AIへの指示文** スクロールに連動するアニメーションを、JavaScript なしで作ってください。 要件: - @supports (animation-timeline: view()) で囲って、対応環境だけに当てる。 未対応環境では要素が最初から見える状態にする。 - 各ブロックに animation-timeline: view() を当て、 animation-range: cover 12% cover 48% で前半のうちに完了させる。 全域を使うと画面中央でようやく完成して遅く感じる、と理由を書く。 - ページ上部に固定の進捗バーを置き、animation-timeline: scroll(root block) と scaleX(0) → scaleX(1) の keyframes で読み進めた量を示す。 - prefers-reduced-motion: reduce では両方とも止める。 ### 継ぎ目のない無限ループ プレビュー: https://vibe.itlibra.com/sample/anim-marquee/ 同じ並びを2回描き、全体の50%だけ動かして先頭へ戻すことで継ぎ目を消します。両端をマスクでぼかし、ホバーとフォーカスで停止します。 ```html
``` ```css /* 継ぎ目を消す仕組み: 同じ並びを2回描き、 全体の 50%(=1周分)だけ動かして先頭へ戻す。 */ .marquee { overflow: hidden; border-block: 1px solid var(--line); background: #fff; padding: 0.7rem 0; /* 両端をぼかすと、切れているのではなく続いて見える */ mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent); } .marquee-track { display: flex; gap: 2rem; width: max-content; animation: slide 22s linear infinite; } @keyframes slide { from { transform: translateX(0); } to { transform: translateX(-50%); } } /* 読み進めたい人のために、ホバーとフォーカスで止める */ .marquee:hover .marquee-track, .marquee:focus-within .marquee-track { animation-play-state: paused; } /* 無限アニメーションは電池とCPUを常に使う。 動きを減らす設定では完全に止め、先頭だけ見せる。 */ @media (prefers-reduced-motion: reduce) { .marquee-track { animation: none; } .marquee { mask-image: none; } } ``` **AIへの指示文** 継ぎ目なく流れ続けるマーキーを作ってください。 要件: - 中身は同じ並びを2回書く。translateX(0) から translateX(-50%) へ動かすと 1周分ちょうどで先頭に戻り、継ぎ目が見えなくなる。仕組みをコメントで書く。 - トラックは display: flex と width: max-content。 - 両端は mask-image: linear-gradient でぼかし、切れているのではなく 続いているように見せる。 - :hover と :focus-within で animation-play-state: paused にして止める。 - 流れる帯は aria-hidden="true" にする。同じ内容が2回読み上げられるため。 - prefers-reduced-motion: reduce では animation を止め、マスクも外す。 無限アニメーションは電池とCPUを常に消費する点をコメントで書く。 ## FAQ **transition: all を使ってはいけないのはなぜですか。** 何が動くのかコードから読めなくなるうえ、意図しないプロパティまで拾うためです。あとから margin や width が変わる指定を足したとき、意図せずそこにも遷移が掛かって挙動が変わります。動かす対象は必ず名指ししてください。 **位置を動かすとき top や left ではなく transform を使うのはなぜですか。** top や left を変えるとレイアウトの再計算と再描画が起きますが、transform は合成だけで済みます。要素数が多い一覧でスクロールしながら動かすと、この差が体感速度としてはっきり出ます。opacity も同じく合成だけで済む安全なプロパティです。 **prefers-reduced-motion にはどこまで対応すべきですか。** 位置や大きさが変わる動きは止め、色の変化は残すのが実務的な線です。すべて止めると「押せる」「選ばれている」といった手がかりまで消えてしまいます。前庭障害のある人にとって画面内の予期しない移動は実際に体調不良の原因になるので、移動だけは確実に止めてください。 **スクロール連動は対応ブラウザが限られませんか。** 限られます。だから @supports で囲い、未対応環境では要素が最初から見えている状態にしてください。この形にしておけば、対応が広がった環境では自然に演出が乗り、そうでない環境でも内容は普通に読めます。演出を前提にした実装にしないことが要点です。 --- # 触る前に赤くしない入力検証 URL: https://vibe.itlibra.com/tech/form-validation/ 技術: フォーム検証 (form-validation) 検索キーワード: HTMLフォームのバリデーション 更新日: 2026-09-01 ライセンス: CC0 1.0 Universal(完全無料・著作権フリー・商用利用OK・クレジット不要) 料金: ¥0 ## 概要 HTML標準の入力検証サンプル集。required・pattern と :user-invalid、独自エラー文の出し方、パスワード要件の可視化、複数ステップのフォームを、支援技術への伝え方まで含めて収録します。 入力検証は、ライブラリを入れる前にHTMLでどこまでできるかを知っておくと設計が変わります。required、type、pattern、minlength。これらはブラウザが解釈し、送信を止め、フォーカスを移し、メッセージまで出してくれます。CSSの:user-invalidを併せて使えば、「まだ触っていない欄をいきなり赤くする」という古い失敗も避けられます。JavaScriptが必要になるのは、標準の文言を自分の言葉に置き換えたいときからです。 ## 向いている場面 - お問い合わせ・申込み・会員登録のフォーム - JavaScriptの読み込み前でも検証を効かせたい場面 - 入力の途中で赤くしたくない体験を作りたいとき - 複数ステップに分けた長いフォーム ## つまずきやすい点 - :invalid は初期表示から当たる。:user-invalid を使う - pattern だけに頼るとエラーの理由が伝わらない - エラー文はaria-describedbyで入力欄に結び付ける - サーバ側の検証は必ず別途行う。HTMLの検証は補助 ## サンプルとコード ### JavaScriptなしの検証 プレビュー: https://vibe.itlibra.com/sample/form-native/ required・type・pattern・min/max だけで検証します。:user-invalid を使うので、まだ触っていない空欄が最初から赤くなることがありません。 ```html

2 文字以上でご入力ください。

お返事のためだけに使います。

メールアドレスの形式でご入力ください。

ハイフンはあってもなくても構いません。

7 桁の数字でご入力ください(例: 150-0001)。

13 から 120 までの数字でご入力ください。

``` ```css .form { background: #fff; border: 1px solid var(--line); border-radius: 14px; padding: 1.4rem; display: grid; gap: 1.1rem; } .field { display: grid; gap: 0.3rem; } .field label { font-size: 0.88rem; font-weight: 600; line-height: 1.7; } .field .hint { font-size: 0.78rem; color: var(--muted); line-height: 1.8; } .field input { font: inherit; line-height: 1.7; color: inherit; padding: 0.55rem 0.75rem; border: 1px solid var(--line); border-radius: 9px; } .field input:focus { outline: 0; border-color: var(--accent); box-shadow: 0 0 0 4px rgba(37, 99, 235, 0.15); } /* ここが肝。:invalid だと「まだ触っていない空欄」まで最初から赤くなる。 :user-invalid は、利用者が入力または送信を試みたあとだけ当たる。 */ .field input:user-invalid { border-color: var(--danger); } .field input:user-valid { border-color: var(--ok); } /* エラー文は既定で隠し、:user-invalid のときだけ出す。 入力欄とは aria-describedby で結んでおくので、 読み上げでも「何が問題か」が伝わる。 */ .field .error { display: none; font-size: 0.8rem; color: var(--danger); line-height: 1.8; } .field input:user-invalid ~ .error { display: block; } .form button { font: inherit; line-height: 1.7; cursor: pointer; padding: 0.7rem 1.2rem; border: 0; border-radius: 9px; background: var(--accent); color: #fff; font-weight: 700; } .form button:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; } ``` **AIへの指示文** HTML の標準機能だけで検証が効くフォームを作ってください。JavaScript は使いません。 要件: - 名前は required と minlength、メールは type="email"、 郵便番号は pattern="[0-9]{3}-?[0-9]{4}"、年齢は type="number" と min/max。 - 色を変えるのは :invalid ではなく **:user-invalid** を使う。 :invalid だとまだ触っていない空欄が最初から赤くなる、と理由をコメントで書く。 - エラー文は既定で display: none にし、 input:user-invalid ~ .error で表示する。 - 各入力に aria-describedby で補足文とエラー文を結び付ける。 - すべての入力に label を for と id で結び、autocomplete を正しく指定する。 - 日本語の本文なので line-height は 1.85、letter-spacing は 0 にする。 ### エラー文を自分の言葉にする プレビュー: https://vibe.itlibra.com/sample/form-message/ ブラウザ既定の吹き出しを止め、validity のどのフラグが立ったかで文言を出し分けます。入力中は赤くせず、一度エラーになった欄だけ入力しながら回復を知らせます。 ```html

``` ```css .form { background: #fff; border: 1px solid var(--line); border-radius: 14px; padding: 1.4rem; display: grid; gap: 1.1rem; } .field { display: grid; gap: 0.3rem; } .field label { font-size: 0.88rem; font-weight: 600; line-height: 1.7; } .field input { font: inherit; line-height: 1.7; color: inherit; padding: 0.55rem 0.75rem; border: 1px solid var(--line); border-radius: 9px; } .field input:focus { outline: 0; border-color: var(--accent); box-shadow: 0 0 0 4px rgba(37, 99, 235, 0.15); } .field input[aria-invalid="true"] { border-color: var(--danger); } /* エラー文の枠は常に DOM に置いておき、中身の有無で見せる。 あとから要素を挿入すると、読み上げが変化に気づかないことがある。 */ .field .error { min-height: 0; font-size: 0.8rem; color: var(--danger); line-height: 1.8; } .field .error:empty { display: none; } .form button { font: inherit; line-height: 1.7; cursor: pointer; padding: 0.7rem 1.2rem; border: 0; border-radius: 9px; background: var(--accent); color: #fff; font-weight: 700; } .summary { margin: 0; padding: 0.8rem 1rem; border: 1px solid #fecaca; background: #fef2f2; color: #991b1b; border-radius: 10px; font-size: 0.86rem; line-height: 1.85; } .summary:empty { display: none; } ``` ```js const form = document.getElementById("form"); const summary = document.getElementById("summary"); // 検証ルールごとに、自分の言葉の文言を用意する。 // validity オブジェクトのどのフラグが立ったかで出し分ける。 const MESSAGES = { user: { valueMissing: "ユーザー名をご入力ください。", tooShort: "3 文字以上でご入力ください。", tooLong: "20 文字以内でご入力ください。", patternMismatch: "使えるのは半角英小文字・数字・アンダースコアだけです。", }, mail: { valueMissing: "メールアドレスをご入力ください。", typeMismatch: "メールアドレスの形式でご入力ください。", }, }; function messageFor(input) { const rules = MESSAGES[input.name] ?? {}; for (const key of Object.keys(rules)) { if (input.validity[key]) return rules[key]; } // 想定外の失敗はブラウザ既定の文言に任せる return input.validationMessage; } function showError(input) { const box = document.getElementById(input.id + "-error"); if (input.checkValidity()) { box.textContent = ""; input.removeAttribute("aria-invalid"); return true; } box.textContent = messageFor(input); input.setAttribute("aria-invalid", "true"); return false; } const fields = [...form.querySelectorAll("input")]; // 入力中に赤くしない。一度フォーカスを外してから検証する。 fields.forEach((input) => { input.addEventListener("blur", () => showError(input)); // 一度エラーを出した欄だけ、入力しながら回復を知らせる input.addEventListener("input", () => { if (input.getAttribute("aria-invalid") === "true") showError(input); }); }); form.addEventListener("submit", (e) => { e.preventDefault(); const bad = fields.filter((input) => !showError(input)); if (bad.length) { summary.textContent = bad.length + " 件の入力をご確認ください。"; bad[0].focus(); // 最初の問題箇所へ移動させる return; } summary.textContent = ""; alert("送信しました(サンプルなので実際には送信されません)"); }); ``` **AIへの指示文** ブラウザ既定のエラー文を、自分の言葉に置き換えたフォームを作ってください。 要件: - form に novalidate を付け、submit で preventDefault してから自前で検証する。 - 文言は入力欄ごとに、valueMissing / tooShort / patternMismatch / typeMismatch などのフラグ別に用意し、input.validity のどれが立ったかで出し分ける。 想定外の失敗は input.validationMessage に任せる。 - エラー文の枠は最初から DOM に置き、textContent の有無で見せる。 あとから要素を挿入すると読み上げが変化に気づかないことがある、と理由を書く。 - 入力中は赤くしない。blur で検証し、 一度 aria-invalid="true" が付いた欄だけ input でも再検証する。 - 送信時は最初の問題箇所へ focus を移し、件数を role="alert" の要素に出す。 ### パスワード要件の可視化 プレビュー: https://vibe.itlibra.com/sample/form-password/ 条件を最初から全部見せ、満たしたものが順に緑になります。入力してから怒られるのではなく、入力しながら分かる形です。表示切替ボタンは aria-pressed と連動させています。 ```html
  • 10 文字以上
  • 英字と数字の両方を含む
  • 記号を 1 つ以上含む
  • 先頭と末尾に空白がない
``` ```css .form { background: #fff; border: 1px solid var(--line); border-radius: 14px; padding: 1.4rem; display: grid; gap: 1rem; } .field { display: grid; gap: 0.35rem; } .field label { font-size: 0.88rem; font-weight: 600; line-height: 1.7; } .pw-row { display: flex; gap: 0.5rem; } .pw-row input { flex: 1; min-width: 0; font: inherit; line-height: 1.7; color: inherit; padding: 0.55rem 0.75rem; border: 1px solid var(--line); border-radius: 9px; } .pw-row input:focus { outline: 0; border-color: var(--accent); box-shadow: 0 0 0 4px rgba(37, 99, 235, 0.15); } .pw-row button { font: inherit; font-size: 0.84rem; line-height: 1.7; cursor: pointer; padding: 0 0.8rem; border: 1px solid var(--line); border-radius: 9px; background: #f2f4f9; color: var(--muted); white-space: nowrap; } /* 満たした条件を隠さず、最初から全部見せる。 「押してから怒られる」体験を避けるのが目的。 */ .rules { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.2rem; } .rules li { display: flex; align-items: baseline; gap: 0.5rem; font-size: 0.83rem; line-height: 1.8; color: var(--muted); } .rules li::before { content: "○"; color: var(--line); font-size: 0.9em; } .rules li[data-met="true"] { color: var(--ok); } .rules li[data-met="true"]::before { content: "●"; color: var(--ok); } .meter { display: flex; gap: 4px; } .meter span { height: 5px; flex: 1; border-radius: 999px; background: var(--line); transition: background-color 0.2s ease; } .meter[data-score="1"] span:nth-child(-n+1) { background: var(--danger); } .meter[data-score="2"] span:nth-child(-n+2) { background: #d97706; } .meter[data-score="3"] span:nth-child(-n+3) { background: #65a30d; } .meter[data-score="4"] span { background: var(--ok); } @media (prefers-reduced-motion: reduce) { .meter span { transition: none; } } ``` ```js const pw = document.getElementById("pw"); const meter = document.getElementById("meter"); const items = [...document.querySelectorAll("[data-rule]")]; const RULES = { len: (v) => v.length >= 10, mix: (v) => /[a-z]/i.test(v) && /[0-9]/.test(v), sym: (v) => /[^\w\s]/.test(v), nospace: (v) => v.length > 0 && v === v.trim(), }; pw.addEventListener("input", () => { const v = pw.value; let score = 0; for (const li of items) { const met = RULES[li.dataset.rule](v); li.dataset.met = String(met); if (met) score++; } meter.dataset.score = String(score); }); // 表示切替。type を変えるだけだが、aria-pressed も必ず連動させる。 const toggle = document.getElementById("toggle"); toggle.addEventListener("click", () => { const shown = pw.type === "text"; pw.type = shown ? "password" : "text"; toggle.textContent = shown ? "表示" : "隠す"; toggle.setAttribute("aria-pressed", String(!shown)); pw.focus(); }); ``` **AIへの指示文** パスワード入力欄に、要件の達成状況をその場で示す仕組みを作ってください。 要件: - 条件は最初からすべて一覧で見せる(10文字以上/英字と数字/記号/前後に空白なし)。 満たした項目は data-met="true" にし、CSS 側で色と記号を変える。 - 4段の強度メーターを置き、親の data-score を 0〜4 で切り替えて span:nth-child(-n+N) で塗る。JavaScript から色を書かない。 - 表示切替ボタンは type を password と text で入れ替え、 aria-pressed とラベルを必ず連動させる。切替後は入力欄へ focus を戻す。 - autocomplete="new-password" を指定する。 - prefers-reduced-motion: reduce ではメーターの transition を切る。 ### 複数ステップのフォーム プレビュー: https://vibe.itlibra.com/sample/form-steps/ 段は fieldset で区切り、hidden 属性で出し分けます。隠れた段の入力は検証対象から外れるので、いまの段だけを検証すれば済みます。現在地は aria-current で示します。 ```html
  1. 1. お客様情報
  2. 2. お届け先
  3. 3. 確認
お客様情報
``` ```css .form { background: #fff; border: 1px solid var(--line); border-radius: 14px; padding: 1.4rem; } /* 進捗。現在地は aria-current で示し、CSS はそれを見て塗る。 見た目のためだけの class を増やさない。 */ .steps { list-style: none; display: flex; gap: 0.5rem; margin: 0 0 1.2rem; padding: 0; } .steps li { flex: 1; font-size: 0.78rem; line-height: 1.7; color: var(--muted); padding-top: 0.5rem; border-top: 3px solid var(--line); } .steps li[aria-current="step"] { color: var(--accent); border-top-color: var(--accent); font-weight: 700; } .steps li[data-done="true"] { color: var(--ok); border-top-color: var(--ok); } /* fieldset で段を区切る。hidden 属性で出し分けるので、 隠れている段の入力欄はフォーカスも検証も対象外になる。 */ .step { border: 0; padding: 0; margin: 0; display: grid; gap: 0.9rem; } .step[hidden] { display: none; } .step legend { padding: 0; margin-bottom: 0.2rem; font-size: 1.02rem; font-weight: 700; line-height: 1.6; } .field { display: grid; gap: 0.3rem; } .field label { font-size: 0.86rem; font-weight: 600; line-height: 1.7; } .field input, .field select { font: inherit; line-height: 1.7; color: inherit; padding: 0.55rem 0.75rem; border: 1px solid var(--line); border-radius: 9px; } .field input:focus, .field select:focus { outline: 0; border-color: var(--accent); box-shadow: 0 0 0 4px rgba(37, 99, 235, 0.15); } .field input:user-invalid { border-color: #dc2626; } .actions { display: flex; gap: 0.6rem; margin-top: 1.2rem; } .actions button { font: inherit; line-height: 1.7; cursor: pointer; padding: 0.6rem 1.2rem; border-radius: 9px; border: 1px solid var(--line); background: #fff; color: var(--muted); } .actions button.primary { margin-left: auto; border-color: transparent; background: var(--accent); color: #fff; font-weight: 700; } .actions button[hidden] { display: none; } .done { margin: 0; color: var(--ok); font-weight: 700; line-height: 1.8; } ``` ```js const form = document.getElementById("form"); const steps = [...form.querySelectorAll(".step")]; const marks = [...document.getElementById("steps").children]; const prev = document.getElementById("prev"); const next = document.getElementById("next"); const submit = document.getElementById("submit"); const review = document.getElementById("review"); let i = 0; function render() { steps.forEach((s, n) => (s.hidden = n !== i)); marks.forEach((m, n) => { m.dataset.done = String(n < i); if (n === i) m.setAttribute("aria-current", "step"); else m.removeAttribute("aria-current"); }); prev.hidden = i === 0; next.hidden = i === steps.length - 1; submit.hidden = i !== steps.length - 1; if (i === steps.length - 1) { const d = Object.fromEntries(new FormData(form)); review.textContent = d.name + " 様 / " + d.mail + " / " + d.zip + " " + d.addr; } // 段を切り替えたら、その段の最初の見出しへ移動させる。 // 何も動かさないと、読み上げ利用者には画面が変わったことが伝わらない。 steps[i].querySelector("legend")?.scrollIntoView({ block: "nearest" }); steps[i].querySelector("input")?.focus(); } // hidden な fieldset の中の入力は検証対象外になるので、 // 「今の段だけ」を検証すれば済む。 function validCurrent() { const inputs = [...steps[i].querySelectorAll("input")]; let ok = true; for (const input of inputs) { if (!input.checkValidity()) { if (ok) input.focus(); ok = false; } // :user-invalid を効かせるため、送信を試みた状態にする input.dispatchEvent(new Event("blur")); } return ok; } next.addEventListener("click", () => { if (!validCurrent()) { form.reportValidity(); return; } i = Math.min(i + 1, steps.length - 1); render(); }); prev.addEventListener("click", () => { i = Math.max(i - 1, 0); render(); }); form.addEventListener("submit", (e) => { e.preventDefault(); alert("送信しました(サンプルなので実際には送信されません)"); }); render(); ``` **AIへの指示文** 3段構成のステップフォームを作ってください。 要件: - 各段は fieldset と legend で区切り、hidden 属性で出し分ける。 hidden の中の入力は検証対象外になるので、いまの段だけ検証すればよい、 という理由をコメントで書く。 - 進捗表示は ol と li。現在地は aria-current="step"、 通過済みは data-done="true" にして、CSS 側でそれを見て塗る。 見た目のためだけの class を増やさない。 - 「次へ」は現在の段の入力を checkValidity で確認し、 通らなければ最初の問題箇所へ focus して進まない。 - 最終段では入力内容を FormData から組み立てて確認欄に出し、 「次へ」を隠して「送信」を出す。 - 段を切り替えたら、その段の最初の入力へ focus を移す。 画面が変わったことが読み上げ利用者にも伝わるようにするため。 ## FAQ **:invalid ではなく :user-invalid を使うべきなのはなぜですか。** :invalid は読み込んだ瞬間から当たるので、まだ何も触っていない必須欄が最初から赤くなります。これは「まだ何もしていないのに怒られた」という体験を生みます。:user-invalid は利用者が入力するか送信を試みたあとにだけ当たるので、この問題が起きません。 **HTMLの検証だけでサーバ側の検証は省けますか。** 省けません。HTMLの検証は入力の手間を減らす補助であって、安全のための仕組みではありません。開発者ツールで属性を消せば素通りしますし、フォームを経由しないリクエストも送れます。サーバ側の検証は必ず別途実装してください。 **エラー文はどこに置くのが良いですか。** 入力欄のすぐ下に置き、aria-describedby で入力欄と結び付けてください。上部にまとめるだけだと、読み上げ利用者が入力欄にフォーカスしたときに理由が伝わりません。件数の要約を上部に置き、個別の理由を各欄の下に置く、の両方が理想です。 **入力中にリアルタイムで検証すべきですか。** 最初の1回はやめてください。メールアドレスを打ち始めた瞬間から赤くなるのは、正しく入力している途中の人を急かすだけです。一度フォーカスを外して検証し、そこでエラーになった欄に限って入力中も再検証する。この形が最も摩擦が少なくなります。 --- # 反転ではなく作り直す暗色設計 URL: https://vibe.itlibra.com/tech/dark-mode/ 技術: ダークモード (dark-mode) 検索キーワード: ダークモード実装 更新日: 2026-09-01 ライセンス: CC0 1.0 Universal(完全無料・著作権フリー・商用利用OK・クレジット不要) 料金: ¥0 ## 概要 ダークモードの実装サンプル集。役割で名付けたCSS変数、light-dark関数、システム設定と手動切替の3状態、暗色で崩れる影と画像の扱いを、動くコード付きで収録します。 ダークモードは色を反転させることではありません。明色では影で作っていた奥行きを、暗色では明度差で作り直す必要があります。だから「白と黒を入れ替える」実装は必ず破綻します。設計の順序は決まっていて、まず役割で名付けた変数を用意し、次にテーマごとにその値を差し替え、最後にブラウザへ color-scheme で地の色を伝える。この順で組むと、あとから3つ目のテーマを足すのも簡単になります。 ## 向いている場面 - 長時間読ませる記事やドキュメント - 夜間に使われる管理画面やダッシュボード - OSのテーマ設定に合わせたい一般向けサイト - 目に負担をかけたくない読書・執筆ツール ## つまずきやすい点 - 色を反転させるだけでは影と奥行きが失われる - color-scheme を書かないとフォームやスクロールバーが明色のまま残る - 切替時に一瞬明色が見える(読み込み順の問題) - 画像やロゴが暗背景で浮く。明度を少し落とす必要がある ## サンプルとコード ### 2テーマをCSS変数で設計する プレビュー: https://vibe.itlibra.com/sample/dark-tokens/ 色を役割で名付け、テーマごとに値を差し替えます。明色では影で作っていた奥行きを、暗色では面の明度差で作り直しているのが要点です。 ```html

役割で名付けた変数

色は --surface--text-muted のように役割で持ちます。--gray-100 のような名前にすると、暗色テーマで「一番薄い灰色が一番濃い」という矛盾が起きます。

沈んだ面は --surface-2。明色では地より暗く、暗色では地より明るい。同じ変数名で逆向きの値を持てるのが、役割で名付ける利点です。
タグ
``` ```css /* 1) 変数は「色の名前」ではなく「役割」で名付ける。 --gray-100 ではなく --surface。テーマを足すときに破綻しない。 */ :root { color-scheme: light; --bg: #f7f8fb; /* ページの地 */ --surface: #ffffff; /* カードなど持ち上がった面 */ --surface-2: #eef1f6; /* さらに一段沈んだ面 */ --border: #e2e6ee; --text: #1b2030; --text-muted:#5a6478; --accent: #2563eb; --accent-bg: #eaf1fe; /* 明色では影で奥行きを作る */ --shadow: 0 1px 2px rgba(27, 32, 48, .06), 0 8px 24px -16px rgba(27, 32, 48, .3); } /* 2) 暗色は「反転」ではない。影が効かなくなるぶん、 面の明度差そのもので階層を作り直す。 */ @media (prefers-color-scheme: dark) { :root { color-scheme: dark; --bg: #0f131b; --surface: #171d28; /* 地より明るい=手前 */ --surface-2: #1f2733; --border: #2b3444; --text: #e7ecf4; --text-muted:#98a2b5; --accent: #6ea8ff; /* 暗背景では彩度を落として明度を上げる */ --accent-bg: #17233a; /* 暗色の影はほぼ見えない。境界線に仕事を移す */ --shadow: 0 0 0 1px rgba(255, 255, 255, .04); } } /* 3) color-scheme を宣言すると、フォーム部品・スクロールバー・ 既定の選択色までブラウザが揃えてくれる。これを書かないと 入力欄だけ明色のまま残る。 */ ``` **AIへの指示文** CSS変数で明色と暗色の2テーマを設計してください。 要件: - 変数は色名ではなく役割で名付ける(--surface、--text-muted など)。 --gray-100 のような名前は暗色テーマで矛盾する、と理由をコメントで書く。 - :root に明色を定義し、@media (prefers-color-scheme: dark) で値だけ差し替える。 - 明色は影で奥行きを作り、暗色では影をほぼ消して面の明度差と境界線で表す。 --shadow 変数の値をテーマごとに変えて実現する。 - :root に color-scheme を必ず宣言する。書かないとフォーム部品や スクロールバーが明色のまま残る、と理由をコメントで書く。 - 入力欄・ボタン・タグを1つずつ置いて、追従を目で確認できるようにする。 - 日本語の本文なので line-height は 1.85、letter-spacing は 0 にする。 ### light-dark() で1行にまとめる プレビュー: https://vibe.itlibra.com/sample/dark-lightdark/ 2つの値を1つの宣言に書けるので、変数定義がおよそ半分になります。未対応ブラウザ向けに @supports not で受け皿を用意しています。 ```html

2つの値を1行で持つ

light-dark(明色, 暗色) と書くと、color-scheme の状態に応じてブラウザが選びます。@media (prefers-color-scheme: dark) のブロックを丸ごと書かずに済むので、変数定義がおよそ半分になります。

使う条件は color-scheme: light dark を宣言しておくこと。宣言が無いと常に明色側が選ばれます。手動切替と組み合わせるときも、color-scheme を上書きするだけで全変数が追従します。

``` ```css /* light-dark() は「いまのカラースキームに応じて2つの値から選ぶ」関数。 @media を2回書かずに済むので、変数定義が半分の行数になる。 使う条件は color-scheme が宣言されていること。 */ :root { color-scheme: light dark; /* 両対応であることを宣言する */ --bg: light-dark(#f7f8fb, #0f131b); --surface: light-dark(#ffffff, #171d28); --border: light-dark(#e2e6ee, #2b3444); --text: light-dark(#1b2030, #e7ecf4); --text-muted:light-dark(#5a6478, #98a2b5); --accent: light-dark(#2563eb, #6ea8ff); } /* 未対応ブラウザ向けの受け皿。light-dark() が解釈できないと 宣言ごと無効になるので、先に単色を書いておけば残る。 */ @supports not (color: light-dark(#000, #fff)) { :root { --bg: #f7f8fb; --surface: #fff; --border: #e2e6ee; --text: #1b2030; --text-muted: #5a6478; --accent: #2563eb; } @media (prefers-color-scheme: dark) { :root { --bg: #0f131b; --surface: #171d28; --border: #2b3444; --text: #e7ecf4; --text-muted: #98a2b5; --accent: #6ea8ff; } } } ``` **AIへの指示文** light-dark() 関数を使って2テーマの変数定義をまとめてください。 要件: - :root に color-scheme: light dark を宣言する。これが無いと常に明色側が選ばれる。 - 各変数を --bg: light-dark(明色, 暗色) の形で1行にまとめる。 - @supports not (color: light-dark(#000, #fff)) で受け皿を用意し、 未対応環境では従来の @media 方式にフォールバックする。 - なぜ受け皿が要るのか(未対応だと宣言ごと無効になる)をコメントで書く。 ### 3状態のテーマ切替 プレビュー: https://vibe.itlibra.com/sample/dark-toggle/ システム追従・ライト固定・ダーク固定の3状態です。2状態にすると「システムに戻す」ができなくなります。localStorage は例外を投げうるので必ず try/catch で包みます。 ```html

3状態にするのが要点

明・暗の2状態にすると「システム設定に戻す」ができなくなります。既定は auto にしておき、利用者が明示的に選んだときだけ data-theme を固定してください。

現在: システム設定に追従

``` ```css /* テーマは data-theme 属性で持つ。値は auto / light / dark の3つ。 auto のときだけ @media にシステム設定を判断させる。 */ :root { color-scheme: light; --bg: #f7f8fb; --surface: #fff; --border: #e2e6ee; --text: #1b2030; --text-muted: #5a6478; --accent: #2563eb; } @media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) { color-scheme: dark; --bg: #0f131b; --surface: #171d28; --border: #2b3444; --text: #e7ecf4; --text-muted: #98a2b5; --accent: #6ea8ff; } } /* 手動で暗色を選んだときは、システム設定より優先させる */ :root[data-theme="dark"] { color-scheme: dark; --bg: #0f131b; --surface: #171d28; --border: #2b3444; --text: #e7ecf4; --text-muted: #98a2b5; --accent: #6ea8ff; } /* 切替の瞬間に色が飛ぶのを抑える。ただし 「動きを減らす」設定では即座に切り替える。 */ body { transition: background-color .2s ease, color .2s ease; } @media (prefers-reduced-motion: reduce) { body { transition: none; } } .switch { display: inline-flex; gap: 2px; padding: 3px; background: color-mix(in srgb, var(--text) 8%, transparent); border-radius: 999px; } .switch button { font: inherit; font-size: .84rem; line-height: 1.7; padding: .35rem .9rem; border: 0; border-radius: 999px; background: none; color: var(--text-muted); cursor: pointer; } /* 現在の状態は aria-pressed で持ち、CSS はそれを見て塗る */ .switch button[aria-pressed="true"] { background: var(--surface); color: var(--accent); font-weight: 700; } .switch button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; } ``` ```js const KEY = "theme"; const root = document.documentElement; const buttons = [...document.querySelectorAll("[data-theme-value]")]; const state = document.getElementById("state"); // localStorage は必ず try/catch で包む。プライベートモード、 // 保存を禁止した設定、サンドボックス化された iframe では例外を投げる。 const store = { get() { try { return localStorage.getItem(KEY); } catch { return null; } }, set(v) { try { v ? localStorage.setItem(KEY, v) : localStorage.removeItem(KEY); } catch {} }, }; function apply(value) { if (value === "auto") root.removeAttribute("data-theme"); else root.setAttribute("data-theme", value); buttons.forEach((b) => b.setAttribute("aria-pressed", String(b.dataset.themeValue === value)), ); const label = { auto: "システム設定に追従", light: "ライト固定", dark: "ダーク固定" }; state.textContent = "現在: " + label[value]; } buttons.forEach((b) => b.addEventListener("click", () => { const v = b.dataset.themeValue; store.set(v === "auto" ? null : v); apply(v); }), ); apply(store.get() ?? "auto"); ``` **AIへの指示文** テーマを手動で切り替える仕組みを作ってください。 要件: - 状態は auto / light / dark の3つ。2状態にすると 「システム設定に戻す」ができなくなる、と理由をコメントで書く。 - auto のときは html の data-theme 属性を外し、@media に判断させる。 明示的に選んだときだけ data-theme を設定する。 - CSS は :root:not([data-theme="light"]) を @media 内に書いて、 手動指定がシステム設定より優先されるようにする。 - 選択は localStorage に保存する。ただし必ず try/catch で包む。 プライベートモードやサンドボックス化された iframe では例外を投げるため。 - ボタンの現在状態は aria-pressed で持ち、CSS はそれを見て塗る。 - 切替時の色の飛びを transition で抑え、prefers-reduced-motion では切る。 ### 暗色で崩れるもの プレビュー: https://vibe.itlibra.com/sample/dark-media/ 影・ロゴ・純白と純黒。この3つは明色の設計をそのまま持ち込むと必ず破綻します。それぞれの直し方を並べました。 ```html

影は消える。境界線に仕事を移す

明色では影が奥行きを作りますが、暗背景の上に黒い影を落としてもほぼ見えません。暗色では影を消し、代わりに境界線と面の明度差で階層を表します。

持ち上がった面
もう一枚

明色前提のロゴは浮く

白地に置く前提で作られたロゴを暗背景にそのまま置くと、そこだけが光って見えます。明度と彩度をわずかに落とすと馴染みます。写真には当てないでください。

Console

純白と純黒を使わない

暗背景に #fff の本文を置くと眩しく、長文で目が疲れます。#e7ecf4 のように少し落とすだけで読みやすさが変わります。背景も #000 ではなく #0f131b のように色味を持たせたほうが、明るい部屋でも沈みません。

``` ```css /* 1) 影は暗色でほとんど見えない。奥行きは境界線と面の明度差に移す。 */ .elevated { background: var(--surface); border-radius: 12px; padding: .9rem 1rem; box-shadow: light-dark( 0 8px 24px -14px rgba(27, 32, 48, .45), none ); border: 1px solid light-dark(transparent, #2b3444); } /* 2) 明色前提の画像・ロゴは暗背景で浮く。 明度を少し落とし、彩度をわずかに下げると馴染む。 写真には当てないこと(人物の肌が濁る)。 */ .logo-box { padding: .7rem 1rem; border-radius: 10px; background: #fff; } @media (prefers-color-scheme: dark) { .logo-box { background: #eef1f6; } .logo-box svg { filter: brightness(.92) saturate(.9); } } /* 3) 図やイラストをテーマごとに差し替えるなら picture が確実。 CSS の filter で反転させると、文字まで反転して読めなくなる。 */ /* 4) 純白と純黒は使わない。#fff の文字は暗背景で眩しく、 #000 の背景は明るい部屋で沈む。少し寄せた値にする。 */ .contrast-note { border-left: 3px solid var(--accent); padding-left: .9rem; color: var(--text-muted); line-height: 1.85; font-size: .9rem; } ``` **AIへの指示文** 暗色テーマで崩れやすい要素の対処をまとめたサンプルを作ってください。 要件: - 影: 暗背景では黒い影がほぼ見えない。light-dark() で暗色時は影を消し、 代わりに境界線を出して階層を表す。 - ロゴ: 明色前提の図版は暗背景で浮く。背景を明るい面に敷き、 filter: brightness(.92) saturate(.9) でわずかに落として馴染ませる。 写真には当てないこと(肌が濁る)を明記する。 - 図やイラストの差し替えは picture を使う。CSS の filter による反転は 文字まで反転して読めなくなるので使わない。 - 純白と純黒を使わない。暗背景の #fff の本文は眩しく、#000 の背景は 明るい部屋で沈む。#e7ecf4 と #0f131b のように寄せた値にする。 ## FAQ **色を反転させるだけではだめですか。** だめです。明色では影が奥行きを作りますが、暗背景に黒い影を落としてもほとんど見えません。暗色では影を消し、面の明度差と境界線で階層を作り直す必要があります。反転しただけの画面が平坦に見えるのはこれが理由です。 **color-scheme は書かないとどうなりますか。** フォーム部品、スクロールバー、テキスト選択色などがブラウザ既定の明色のまま残ります。本文だけ暗くなって入力欄が真っ白、という状態になるので、テーマを切り替えるなら必ず宣言してください。1行で効果は大きいところです。 **切替の一瞬だけ明色が見えてしまいます。** テーマの復元がCSSの適用より後になっているためです。localStorage を読んで data-theme を設定する処理は、head の中で同期的に実行してください。body の末尾やモジュールの遅延読み込みに置くと、必ず一瞬ちらつきます。 **変数名はどう決めるのがよいですか。** 役割で名付けてください。--gray-100 のような名前は明色では「一番薄い灰色」ですが、暗色では一番濃くなり、名前と実体が矛盾します。--surface、--text-muted、--border のように使い道で名付ければ、テーマを何種類足しても破綻しません。 --- # 端末に必要な1枚だけを配る URL: https://vibe.itlibra.com/tech/responsive-image/ 技術: レスポンシブ画像 (responsive-image) 検索キーワード: srcsetとレスポンシブ画像 更新日: 2026-09-01 ライセンス: CC0 1.0 Universal(完全無料・著作権フリー・商用利用OK・クレジット不要) 料金: ¥0 ## 概要 srcset・sizes・pictureで画像を出し分けるサンプル集。どの候補が選ばれたかを画面上で確認でき、レイアウトシフトの防止と読み込み優先度の使い分けまで実測付きで収録します。 レスポンシブ画像の目的は、見た目を整えることではなく、その端末に必要な1枚だけを配ることです。実際、このサイトでもカードの実描画幅が372pxなのに1200pxの画像を配っていた時期があり、srcset を入れただけで1x端末の転送量が約8割減りました。仕組みは単純で、候補を srcset に並べ、要素が実際に何px幅で描かれるかを sizes で教える。ブラウザはその2つから最適な1枚を選びます。 ## 向いている場面 - 写真やサムネイルを多数並べる一覧ページ - 回線の細い環境も想定する一般向けサイト - 画面幅で構図を変えたいヒーロー画像 - Core Web Vitals の LCP を詰めたいとき ## つまずきやすい点 - sizes を書かないと常に画面幅として計算される - width と height を省くとレイアウトシフトが起きる - LCP になる画像に loading=lazy を付けると遅くなる - art direction が要るのに srcset だけで済ませようとする ## サンプルとコード ### srcset と sizes で1枚だけ配る プレビュー: https://vibe.itlibra.com/sample/img-srcset/ 候補を srcset に並べ、実際に何px幅で描かれるかを sizes で伝えます。どれが選ばれたかを currentSrc で画面に表示しているので、指定の正しさをその場で確認できます。 ```html
幅ごとに異なる候補が読み込まれるサンプル画像
カード内(実描画 約220px)
2枚目のサンプル画像
同じ指定でもう1枚
``` ```css /* 画像の枠は CSS で決め、実際に何px幅で描かれるかを sizes に書く。 この2つが食い違うと、ブラウザは間違った候補を選ぶ。 */ .cards { display: grid; gap: 1rem; grid-template-columns: repeat(auto-fit, minmax(min(100%, 220px), 1fr)); } .card { background: #fff; border: 1px solid var(--line); border-radius: 12px; overflow: hidden; } .card img { display: block; width: 100%; height: auto; /* width/height 属性の比率を保ったまま伸縮させる */ aspect-ratio: 16 / 10; object-fit: cover; background: #eef1f6; /* 読み込み前の面。ここが白飛びを防ぐ */ } .card figcaption { padding: .7rem .85rem; font-size: .84rem; line-height: 1.8; color: var(--muted); } ``` ```js // currentSrc には「ブラウザが実際に選んだ1枚」が入る。 // srcset の検証はこれを見るのが確実で、開発者ツールより早い。 const out = document.getElementById("out"); function report() { const img = document.querySelector(".card img"); const css = Math.round(img.getBoundingClientRect().width); out.textContent = "ビューポート幅: " + window.innerWidth + "px / DPR: " + window.devicePixelRatio + "\n" + "画像の実描画幅: " + css + "px\n" + "必要な解像度: " + Math.round(css * window.devicePixelRatio) + "px\n" + "選ばれた候補: " + (img.currentSrc.split("/").pop() || "(未確定)"); } addEventListener("load", report); addEventListener("resize", report); ``` **AIへの指示文** srcset と sizes を使って、幅に応じた画像の出し分けを実装してください。 要件: - 候補は 400w / 800w / 1600w の3つ。srcset に幅記述子付きで並べる。 - sizes には CSS で実際に描かれる幅を書く。 例: sizes="(max-width: 700px) 92vw, 220px" sizes を省くと常に画面幅として計算される、と理由をコメントで書く。 - img には width と height の属性を実寸で書き、CSS 側は width: 100% と height: auto にする。 - 読み込み前の面に background を敷いて白飛びを防ぐ。 - JavaScript で img.currentSrc を読み、実際に選ばれた候補・ ビューポート幅・DPR・必要解像度を画面に表示する。 - 日本語の本文なので line-height は 1.85、letter-spacing は 0 にする。 ### picture で構図ごと変える プレビュー: https://vibe.itlibra.com/sample/img-picture/ 解像度ではなく切り取り方そのものを変えます。狭い画面では縦長、広い画面では横長。srcset との使い分けがはっきり分かる形にしました。 ```html 季節のキャンペーン告知 ``` ```css /* picture は「同じ被写体を別の構図で見せたい」ときに使う。 単に解像度を変えたいだけなら img の srcset で足りる。 */ .hero { border-radius: 14px; overflow: hidden; border: 1px solid var(--line); } .hero img { display: block; width: 100%; height: auto; background: #eef1f6; } ``` ```js const out = document.getElementById("out"); function report() { const img = document.querySelector(".hero img"); out.textContent = "ビューポート幅: " + window.innerWidth + "px\n" + "選ばれた構図: " + (img.currentSrc.split("/").pop() || "(未確定)") + "\n" + "描画サイズ: " + Math.round(img.getBoundingClientRect().width) + " × " + Math.round(img.getBoundingClientRect().height) + "px"; } addEventListener("load", report); addEventListener("resize", report); ``` **AIへの指示文** picture 要素で画面幅ごとに構図が変わる画像を実装してください。 要件: - 狭い画面(max-width: 700px)には縦長の構図、既定は横長の構図。 - source を先に、img を最後に置く。img は必ず必要で、代替と既定を兼ねる。 - source と img の両方に width と height を書く。 - 単に解像度を変えたいだけなら srcset で足りる、という使い分けをコメントで書く。 - currentSrc を読んで、どちらの構図が選ばれたかを画面に表示する。 ### レイアウトを揺らさない プレビュー: https://vibe.itlibra.com/sample/img-cls/ 良い例2つと悪い例1つを並べ、実際のCLS値をページ内に表示します。属性を書く方法と aspect-ratio で枠を固定する方法の使い分けが分かります。 ```html

良い例 width と height を書く

属性に実寸を書いておくと、読み込み前から高さが確定します。CSS 側は width:100% と height:auto。

良い例 aspect-ratio で枠を固定する

比率がまちまちの画像を同じ枠に収めたいときは、aspect-ratio と object-fit を組み合わせます。

悪い例 何も指定しない

読み込みが終わった瞬間に高さが跳ね、この下の文章が押し下げられます。

この行が、画像の読み込み完了で下へ動きます。

``` ```css /* width と height の属性を書いておくと、ブラウザは読み込み前に 縦横比を計算して場所を確保する。CSS で幅だけ 100% にしても、 height: auto があれば比率は保たれる。 */ .ok img { display: block; width: 100%; height: auto; /* 属性の比率を維持したまま伸縮する */ border-radius: 10px; background: #eef1f6; /* 読み込み中の面。白飛びを防ぐ */ } /* 属性を書けない場合(枠の比率を固定したい場合)は aspect-ratio。 object-fit と併せると、比率の違う画像でも枠が動かない。 */ .ratio img { display: block; width: 100%; aspect-ratio: 16 / 10; object-fit: cover; border-radius: 10px; background: #eef1f6; } /* 悪い例。属性も aspect-ratio も無いと、読み込み完了の瞬間に 高さが 0 から実寸へ跳ね、下の文章が押し下げられる。 */ .bad-demo img { display: block; width: 100%; border-radius: 10px; background: #eef1f6; } ``` ```js // CLS は PerformanceObserver で自分でも測れる。 // hadRecentInput が true のずれ(利用者の操作が原因)は除外する。 let cls = 0; const box = document.getElementById("cls"); new PerformanceObserver((list) => { for (const e of list.getEntries()) if (!e.hadRecentInput) cls += e.value; box.textContent = "このページの CLS: " + cls.toFixed(4) + (cls > 0.1 ? "(0.1 超。不合格)" : "(0.1 以下。良好)"); }).observe({ type: "layout-shift", buffered: true }); ``` **AIへの指示文** 画像によるレイアウトシフトを防ぐ書き方を、良い例と悪い例で比較してください。 要件: - 良い例1: width と height の属性を実寸で書き、CSS は width:100% height:auto。 - 良い例2: aspect-ratio と object-fit で枠の比率を固定する。 比率がまちまちの画像を同じ枠に収めたいときの方法として示す。 - 悪い例: 属性も aspect-ratio も無い状態。読み込み完了で下の文章が 押し下げられることを、実際に文章を置いて示す。 - PerformanceObserver で layout-shift を購読し、CLS の実測値を ページ内に表示する。hadRecentInput が true のものは除外する。 - 0.1 を超えると Core Web Vitals で不合格になる旨を表示に含める。 ### 読み込み優先度の使い分け プレビュー: https://vibe.itlibra.com/sample/img-priority/ loading・fetchpriority・decoding は役割が違います。取り違えると主役の画像がいちばん遅くなります。LCP がどの要素になったかも実測表示します。 ```html
属性役割使いどころ
loading いつ取りに行くか 画面外なら lazy最初の画面に入る画像には付けない
fetchpriority 他の要求より先に取るか LCP になる1枚だけ high。全部に付けたら意味が消える
decoding 復号で描画を止めるか 基本 async。描画をブロックしない
ページ先頭の主役画像 続く画像
スクロールして初めて見える画像 さらに下の画像
``` ```css .strip { display: grid; gap: .8rem; margin: 1.2rem 0 0; } .strip img { display: block; width: 100%; height: auto; border-radius: 10px; background: #eef1f6; } .spacer { height: 120vh; } ``` ```js // LCP がどの要素になったかは自分で測れる。 // 「主役だと思っていた画像」と実際の LCP がずれていることは多い。 const out = document.getElementById("out"); let lcp = null; new PerformanceObserver((l) => { for (const e of l.getEntries()) lcp = e; render(); }).observe({ type: "largest-contentful-paint", buffered: true }); function render() { const imgs = [...document.images]; const lazy = imgs.filter((i) => i.loading === "lazy").length; out.textContent = "画像 " + imgs.length + "枚 / うち lazy " + lazy + "枚\n" + "LCP: " + (lcp ? Math.round(lcp.startTime) + "ms" : "計測中") + "\n" + "LCP の要素: " + (lcp?.element?.tagName ?? "-") + (lcp?.url ? " (" + lcp.url.split("/").pop() + ")" : ""); } addEventListener("load", render); ``` **AIへの指示文** 画像の読み込み優先度を正しく指定したページを作ってください。 要件: - 3つの属性の役割を表にする。 loading はいつ取りに行くか、fetchpriority は他より先に取るか、 decoding は復号で描画を止めるか。 - 最初の画面に入る主役の画像には loading="lazy" を付けず、 fetchpriority="high" を1枚だけに付ける。全部に付けたら意味が消える。 - 画面外の画像には loading="lazy" を付ける。 - すべてに decoding="async" を付ける。 - PerformanceObserver で largest-contentful-paint を購読し、 LCP の時刻と、実際に LCP になった要素を画面に表示する。 ## FAQ **sizes を省略するとどうなりますか。** 常に 100vw、つまり画面幅いっぱいに描かれる前提で候補が選ばれます。実際には220pxのカードに置いている画像でも、1600pxの候補が読み込まれることになります。srcset を書いたのに転送量が減らない場合、たいてい原因はここです。 **srcset と picture はどちらを使えばいいですか。** 同じ絵の解像度違いを配りたいだけなら srcset、切り取り方や構図そのものを変えたいなら picture です。横長のヒーロー画像をスマートフォンでそのまま縮めると被写体が小さくなりすぎる、という場合が picture の出番になります。 **LCP になる画像に loading=lazy を付けてはいけませんか。** 付けてはいけません。lazy は「必要になるまで取りに行かない」指示なので、最初の画面に入る画像に付けると読み込みの開始そのものが遅れ、LCP が悪化します。一覧ページでは最初の数枚だけ lazy を外す、という運用が現実的です。 **実際にどの候補が選ばれたか確認する方法はありますか。** img.currentSrc を読むのが最も確実です。開発者ツールのネットワークタブでも分かりますが、currentSrc なら条件を変えながら即座に確認できます。このページのサンプルはすべて画面上に表示しているので、ウィンドウ幅を変えて試せます。 --- # モーダルを自作しない URL: https://vibe.itlibra.com/tech/dialog/ 技術: ダイアログとポップオーバー (dialog) 検索キーワード: dialog要素 更新日: 2026-09-01 ライセンス: CC0 1.0 Universal(完全無料・著作権フリー・商用利用OK・クレジット不要) 料金: ¥0 ## 概要 dialog要素とpopover属性のサンプル集。showModalによるモーダル、method=dialogでの戻り値、popoverによる軽い重ね表示、横から出るドロワーを、フォーカス管理とアニメーションまで含めて収録します。 モーダルを自作すると、必ず同じところでつまずきます。背面のスクロールが動く、Escで閉じない、フォーカスが背後の要素へ抜ける、閉じたあとどこへ戻るか分からない。dialog要素はこれらを最初から解決しています。showModal() を呼ぶだけで、フォーカスは閉じ込められ、Escは効き、背面は不活性になり、::backdrop で背景も装飾できる。自作する理由は、もうほとんど残っていません。 ## 向いている場面 - 確認・警告のモーダルダイアログ - 入力フォームを重ねて出す画面 - スマートフォンで横から出すメニュー - 小さな補足情報を出すポップオーバー ## つまずきやすい点 - show() と showModal() は挙動が違う。モーダルは後者 - 閉じたあとに開いた要素へフォーカスを戻す配慮が要る - ::backdrop は dialog の子ではないので継承されない - アニメーションには @starting-style か display の遷移指定が要る ## サンプルとコード ### showModal() のモーダル プレビュー: https://vibe.itlibra.com/sample/dlg-modal/ Escで閉じる、Tabが中を回る、背面がスクロールしない、閉じたらフォーカスが戻る。これらは全部ブラウザの標準動作で、自分で書く必要がありません。 ```html

本当に削除しますか

保存済みのプロジェクトと共有リンクがすべて失われます。この操作は取り消せません。

``` ```css /* dialog は既定で display:none。開いたときだけ表示される。 位置決めもブラウザ任せで、中央寄せの記述は要らない。 */ dialog { border: 0; border-radius: 16px; padding: 1.5rem 1.4rem 1.3rem; width: min(420px, calc(100% - 2rem)); color: var(--ink); box-shadow: 0 30px 60px -25px rgba(20, 24, 36, .5); } /* ::backdrop は dialog の子ではないので、変数も色も継承されない。 ここで明示的に指定する。 */ dialog::backdrop { background: rgba(20, 24, 36, .55); backdrop-filter: blur(2px); } /* 開閉のアニメーション。display が none と block を行き来するため、 transition-behavior: allow-discrete と @starting-style が要る。 */ dialog, dialog::backdrop { transition: opacity .22s ease, display .22s allow-discrete, overlay .22s allow-discrete; opacity: 0; } dialog[open], dialog[open]::backdrop { opacity: 1; } @starting-style { dialog[open], dialog[open]::backdrop { opacity: 0; } } dialog[open] { transform: none; } @starting-style { dialog[open] { transform: translateY(8px); } } dialog { transform: translateY(8px); transition-property: opacity, transform, display, overlay; } @media (prefers-reduced-motion: reduce) { dialog, dialog::backdrop { transition: none; } dialog, dialog[open] { transform: none; } } dialog h2 { margin: 0 0 .6rem; font-size: 1.15rem; line-height: 1.5; } dialog p { margin: 0 0 1.3rem; color: var(--muted); line-height: 1.85; } .dlg-actions { display: flex; gap: .6rem; justify-content: flex-end; } .btn { font: inherit; line-height: 1.7; cursor: pointer; font-weight: 600; padding: .55rem 1.1rem; border-radius: 9px; border: 1px solid var(--line); background: #fff; color: var(--muted); } .btn.primary { border-color: transparent; background: var(--accent); color: #fff; font-weight: 700; } .btn.danger { border-color: transparent; background: var(--danger); color: #fff; font-weight: 700; } .btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; } ``` ```js const dlg = document.getElementById("dlg"); const opener = document.getElementById("open"); const result = document.getElementById("result"); // show() ではなく showModal()。前者は背面が操作できてしまい、 // Esc も効かず ::backdrop も出ない。モーダルなら必ず showModal()。 opener.addEventListener("click", () => dlg.showModal()); // 閉じる系は close() に集約する。引数を渡すと returnValue に入る。 dlg.querySelector("[data-close]").addEventListener("click", () => dlg.close("cancel")); document.getElementById("confirm").addEventListener("click", () => dlg.close("delete")); // 背景クリックで閉じる。dialog 自身が背景の当たり判定を兼ねるので、 // クリック位置が中身の外かどうかを矩形で判定する。 dlg.addEventListener("click", (e) => { const r = dlg.getBoundingClientRect(); const inside = e.clientX >= r.left && e.clientX <= r.right && e.clientY >= r.top && e.clientY <= r.bottom; if (!inside) dlg.close("cancel"); }); // close は Esc でも発火する。後始末は1か所にまとめられる。 dlg.addEventListener("close", () => { result.textContent = dlg.returnValue === "delete" ? "削除を実行しました(サンプルです)" : "取り消しました。"; // フォーカスは自動で元へ戻るが、開いた要素が消える画面では明示的に移す opener.focus(); }); ``` **AIへの指示文** dialog 要素で確認モーダルを作ってください。 要件: - 開くのは showModal()。show() では背面が操作でき Esc も効かず ::backdrop も出ない、という違いをコメントで書く。 - ::backdrop は dialog の子ではないので変数も色も継承されない。 背景色は明示的に指定する。 - 閉じる操作は close() に集約し、引数で returnValue を渡す。 Esc でも close イベントが発火するので、後始末は1か所にまとめられる。 - 背景クリックで閉じる。dialog 自身が背景の当たり判定を兼ねるので、 getBoundingClientRect でクリック位置が中身の外かを判定する。 - 開閉のアニメーションには transition-behavior: allow-discrete と @starting-style を使う。display が none と block を行き来するため。 - prefers-reduced-motion では transition を切る。 - 日本語の本文なので line-height は 1.85、letter-spacing は 0 にする。 ### method=dialog で戻り値を受け取る プレビュー: https://vibe.itlibra.com/sample/dlg-form/ フォームに method="dialog" を付けると、送信でダイアログが閉じ、押したボタンの value が returnValue に入ります。閉じる処理を自分で書く必要がありません。 ```html

配送先の編集

``` ```css dialog { border: 0; border-radius: 16px; padding: 1.4rem; width: min(400px, calc(100% - 2rem)); color: var(--ink); box-shadow: 0 30px 60px -25px rgba(20, 24, 36, .5); } dialog::backdrop { background: rgba(20, 24, 36, .5); } dialog h2 { margin: 0 0 1rem; font-size: 1.1rem; line-height: 1.5; } .field { display: grid; gap: .3rem; margin-bottom: 1rem; } .field label { font-size: .86rem; font-weight: 600; line-height: 1.7; } .field input, .field select { font: inherit; line-height: 1.7; color: inherit; padding: .55rem .75rem; border: 1px solid var(--line); border-radius: 9px; } .field input:focus, .field select:focus { outline: 0; border-color: var(--accent); box-shadow: 0 0 0 4px rgba(37, 99, 235, .15); } .dlg-actions { display: flex; gap: .6rem; justify-content: flex-end; } .btn { font: inherit; line-height: 1.7; cursor: pointer; font-weight: 600; padding: .55rem 1.1rem; border-radius: 9px; border: 1px solid var(--line); background: #fff; color: var(--muted); } .btn.primary { border-color: transparent; background: var(--accent); color: #fff; font-weight: 700; } ``` ```js const dlg = document.getElementById("dlg"); const form = document.getElementById("form"); const out = document.getElementById("out"); document.getElementById("open").addEventListener("click", () => dlg.showModal()); // 閉じたあとに1か所で処理する。押されたボタンの value は returnValue に入る。 // Esc で閉じた場合の returnValue は空文字なので、キャンセルと同じ扱いにできる。 dlg.addEventListener("close", () => { if (dlg.returnValue !== "save") { out.textContent = "returnValue: " + JSON.stringify(dlg.returnValue) + "\n保存せずに閉じました。"; return; } const data = Object.fromEntries(new FormData(form)); out.textContent = "returnValue: " + JSON.stringify(dlg.returnValue) + "\n" + JSON.stringify(data, null, 2); }); ``` **AIへの指示文** dialog の中にフォームを置き、押したボタンによって結果を出し分けてください。 要件: - form に method="dialog" を付ける。送信でダイアログが閉じ、 submitter の value が returnValue に渡る仕組みをコメントで書く。 - ボタンは type="submit" にして value="cancel" と value="save" を持たせる。 - close イベントで returnValue を見て処理を分ける。 Esc で閉じた場合の returnValue は空文字なので、キャンセルと同じ扱いにできる。 - 保存時は FormData から値を取り出して表示する。 - すべての入力に label を for と id で結び、autocomplete を指定する。 ### popover 属性で軽い重ね表示 プレビュー: https://vibe.itlibra.com/sample/dlg-popover/ JavaScriptを1行も書かずに開閉できます。popovertarget が要素を結び付け、外側クリックとEscでの解除まで面倒を見ます。dialog との使い分けも並べました。 ```html

手数料

お支払い時の手数料は当社が負担します。分割払いをお選びの場合のみ、カード会社所定の分割手数料が発生します。

配送

全国一律 500 円、5,000 円以上のご注文で無料です。離島の一部地域のみ中継料をいただく場合があります。

手動で閉じる

popover="manual" は外側をクリックしても Esc でも閉じません。閉じる手段を必ず用意してください。

``` ```css /* popover 属性を付けた要素は、既定で最前面(top layer)に出る。 z-index の管理から解放されるのが最大の利点。 */ [popover] { border: 1px solid var(--line); border-radius: 12px; padding: 1rem 1.1rem; width: min(320px, calc(100% - 2rem)); box-shadow: 0 24px 44px -22px rgba(20, 24, 36, .5); /* 既定は画面中央。位置を寄せたいときは margin と inset で調整する */ margin: auto; } [popover] h3 { margin: 0 0 .4rem; font-size: 1rem; line-height: 1.6; } [popover] p { margin: 0; color: var(--muted); font-size: .9rem; line-height: 1.85; } /* popover="auto" の背景。manual では出ない */ [popover]::backdrop { background: rgba(20, 24, 36, .25); } .btn { font: inherit; line-height: 1.7; cursor: pointer; font-weight: 600; padding: .55rem 1.1rem; border-radius: 9px; border: 1px solid var(--line); background: #fff; color: var(--ink); } .btn:hover { background: #f2f4f9; } .btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; } ``` **AIへの指示文** popover 属性で補足情報の重ね表示を作ってください。JavaScript は使いません。 要件: - ボタンに popovertarget、中身に popover 属性を付けるだけで開閉させる。 - 既定の popover="auto" は外側クリックと Esc で閉じ、 同時に開けるのは1つだけ、という挙動をコメントで書く。 - popover="manual" の例も1つ置き、閉じるボタンを popovertargetaction="hide" で用意する。 - popover は top layer に出るので z-index の管理が不要、という利点を書く。 - dialog(showModal)との使い分けを本文で説明する。 決めるまで進ませないなら dialog、見ながら続けられるなら popover。 ### 横から出るドロワー プレビュー: https://vibe.itlibra.com/sample/dlg-drawer/ dialog の余白指定を変えるだけで、中央のモーダルが横からのドロワーになります。背面スクロールの停止とフォーカスの閉じ込めは標準動作のまま使えます。 ```html

メニュー

``` ```css /* dialog の既定は中央寄せ。ドロワーにするには余白の指定を上書きする。 右端に寄せ、高さを画面いっぱいにするだけでよい。 */ dialog.drawer { border: 0; margin: 0 0 0 auto; /* 右端へ寄せる */ height: 100dvh; max-height: 100dvh; width: min(320px, 86vw); border-radius: 16px 0 0 16px; padding: 1.3rem 1.2rem; color: var(--ink); box-shadow: -24px 0 50px -30px rgba(20, 24, 36, .6); } dialog.drawer::backdrop { background: rgba(20, 24, 36, .45); } /* スライドインは transform で。display の切替をまたぐので allow-discrete と @starting-style を併用する。 */ dialog.drawer { translate: 100% 0; transition: translate .26s cubic-bezier(.2,.8,.2,1), display .26s allow-discrete, overlay .26s allow-discrete; } dialog.drawer[open] { translate: 0 0; } @starting-style { dialog.drawer[open] { translate: 100% 0; } } dialog.drawer::backdrop { opacity: 0; transition: opacity .26s ease, display .26s allow-discrete, overlay .26s allow-discrete; } dialog.drawer[open]::backdrop { opacity: 1; } @starting-style { dialog.drawer[open]::backdrop { opacity: 0; } } @media (prefers-reduced-motion: reduce) { dialog.drawer, dialog.drawer::backdrop { transition: none; } dialog.drawer { translate: 0 0; } } .drawer-head { display: flex; align-items: center; gap: .75rem; margin-bottom: 1rem; } .drawer-head h2 { margin: 0; font-size: 1.05rem; line-height: 1.5; } .drawer-close { margin-left: auto; border: 0; background: none; cursor: pointer; font: inherit; font-size: 1.2rem; line-height: 1; color: var(--muted); padding: .3rem .5rem; border-radius: 8px; } .drawer-close:hover { background: #f2f4f9; color: var(--ink); } .drawer nav ul { list-style: none; margin: 0; padding: 0; display: grid; gap: .2rem; } .drawer nav a { display: block; padding: .55rem .7rem; border-radius: 9px; color: var(--muted); text-decoration: none; line-height: 1.7; } .drawer nav a:hover { background: #f2f4f9; color: var(--ink); } .drawer nav a[aria-current] { background: #eaf1fe; color: var(--accent); font-weight: 600; } .btn { font: inherit; line-height: 1.7; cursor: pointer; font-weight: 700; padding: .6rem 1.2rem; border-radius: 9px; border: 0; background: var(--accent); color: #fff; } .btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; } ``` ```js const drawer = document.getElementById("drawer"); const opener = document.getElementById("open"); opener.addEventListener("click", () => drawer.showModal()); document.getElementById("close").addEventListener("click", () => drawer.close()); // 背景クリックで閉じる。ドロワーは画面端に寄っているので、 // 矩形の外側=背景と判定できる。 drawer.addEventListener("click", (e) => { const r = drawer.getBoundingClientRect(); const inside = e.clientX >= r.left && e.clientX <= r.right && e.clientY >= r.top && e.clientY <= r.bottom; if (!inside) drawer.close(); }); // メニュー内のリンクを押したら閉じる。単一ページの画面では必須。 drawer.querySelectorAll("nav a").forEach((a) => a.addEventListener("click", () => drawer.close()), ); ``` **AIへの指示文** dialog を使って横から出るドロワーメニューを作ってください。 要件: - dialog の margin を 0 0 0 auto にして右端へ寄せ、 height を 100dvh にする。中央寄せの既定を上書きするだけでよい。 - スライドインは translate で行い、transition に display と overlay の allow-discrete を含める。@starting-style で開始位置を指定する。 - 背景クリックで閉じる。メニュー内のリンクを押したときも閉じる。 - 閉じるボタンには aria-label を付ける。 - prefers-reduced-motion では transition を切り、位置も動かさない。 ## FAQ **show() と showModal() はどう違いますか。** showModal() だけがモーダルです。背面が不活性になり、Escで閉じ、フォーカスがダイアログ内に閉じ込められ、::backdrop が出ます。show() は単に表示するだけで、背面も操作でき、Escも効きません。確認ダイアログを show() で開くと、背後のボタンが押せてしまいます。 **閉じたあとのフォーカスは自分で戻す必要がありますか。** 基本は不要です。ブラウザが開いた要素へ戻します。ただし、開いたボタン自体が処理の結果として消える画面(一覧から行を削除した場合など)では、戻り先が無くなるので明示的に移してください。何もしないとフォーカスが body に落ち、キーボード操作の位置が失われます。 **開閉のアニメーションが効きません。** dialog は display が none と block を行き来するため、通常の transition では補間されません。transition に display と overlay を含め、transition-behavior: allow-discrete を指定し、開始状態を @starting-style で書く必要があります。3点セットで初めて動きます。 **dialog と popover はどちらを使うべきですか。** 「閉じずに他を触れてよいか」で決めてください。触れさせたくない、決めるまで進ませたくないなら dialog の showModal()。見ながら他の操作を続けてよいなら popover です。補足説明・メニュー・通知は popover、削除の確認や入力の完了は dialog が向きます。 --- # ビルドの要らない部品化 URL: https://vibe.itlibra.com/tech/web-components/ 技術: Web Components (web-components) 検索キーワード: Web Components 更新日: 2026-09-01 ライセンス: CC0 1.0 Universal(完全無料・著作権フリー・商用利用OK・クレジット不要) 料金: ¥0 ## 概要 Web Componentsのサンプル集。customElementsの最小構成、Shadow DOMによるスタイル隔離、slotでの差し込み、属性の監視とプロパティの扱いを、フレームワークを1つも使わずに収録します。 Web Componentsは、ブラウザに元から入っている部品化の仕組みです。customElements.define でタグを登録し、Shadow DOM でスタイルを閉じ込め、slot で中身を差し込む。この3つだけで、どのフレームワークからも使える部品が作れます。派手さはありませんが、ビルドが要らず依存も増えず、5年後にも同じコードが動いている可能性が高い。長く置くものほど向いています。 ## 向いている場面 - 複数のサイトやフレームワークで共有したい部品 - CMSの記事本文に埋め込みたい表現 - 長期間メンテナンスするデザインシステム - ビルド環境を持ち込めない案件 ## つまずきやすい点 - Shadow DOM の中には外側のCSSが届かない(それが目的でもある) - 属性は文字列。真偽値は存在の有無で表す - フォーム部品は formAssociated を書かないと送信されない - 定義前に使われると空で描画される。:defined で対処する ## サンプルとコード ### カスタム要素の最小構成 プレビュー: https://vibe.itlibra.com/sample/wc-basic/ Shadow DOMを使わない25行ほどの部品です。日時を相対表記に置き換えます。中身に元の表記を書いておけば、スクリプトが動かなくても意味が残ります。 ```html

投稿されたコメント

この機能、助かりました。ありがとうございます。

投稿: 2026年8月29日

前回のログイン

別の端末からのアクセスがありました。

日時: 2026年8月31日
``` ```css /* Shadow DOM を使わないカスタム要素には、外側の CSS がそのまま届く。 既存のデザインシステムに馴染ませたいときはこちらが楽。 */ relative-time { display: inline-block; font-variant-numeric: tabular-nums; } /* 定義が終わるまでは :defined が付かない。 定義前に中身が空で見えるのを防ぐには、この形で隠しておく。 */ relative-time:not(:defined) { visibility: hidden; } .card { background: #fff; border: 1px solid var(--line); border-radius: 12px; padding: 1rem 1.1rem; } .card h2 { margin: 0 0 .4rem; font-size: 1rem; line-height: 1.6; } .card p { margin: 0; color: var(--muted); line-height: 1.85; font-size: .9rem; } .log { display: block; margin-top: .6rem; font-size: .84rem; color: var(--accent); line-height: 1.8; } ``` ```js // HTMLElement を継承したクラスを作り、タグ名を登録するだけ。 // タグ名にはハイフンが必須(標準のタグと衝突させないため)。 class RelativeTime extends HTMLElement { // 要素が文書に挿入されたときに呼ばれる。 // ここで初めて DOM を触るのが約束事。constructor では触らない。 connectedCallback() { this.render(); } render() { const iso = this.getAttribute("datetime"); const t = iso ? new Date(iso) : null; if (!t || Number.isNaN(t.getTime())) return; // 中身は元の表記のまま残す const diff = (Date.now() - t.getTime()) / 1000; const rtf = new Intl.RelativeTimeFormat("ja", { numeric: "auto" }); const table = [ [60, "second", 1], [3600, "minute", 60], [86400, "hour", 3600], [2592000, "day", 86400], ]; const row = table.find(([limit]) => diff < limit); this.textContent = row ? rtf.format(-Math.round(diff / row[2]), row[1]) : t.toLocaleDateString("ja-JP"); // 元の絶対時刻はツールチップと支援技術のために残す this.title = t.toLocaleString("ja-JP"); } } customElements.define("relative-time", RelativeTime); ``` **AIへの指示文** 日時を「3日前」のような相対表記に置き換えるカスタム要素を作ってください。 要件: - HTMLElement を継承したクラスを customElements.define で登録する。 タグ名にはハイフンが必須である理由をコメントで書く。 - DOM を触るのは connectedCallback の中。constructor では触らない。 - Shadow DOM は使わず、外側の CSS が効く形にする。 - 中身には元の絶対表記を書いておき、スクリプトが動かない場合や datetime が不正な場合はそれをそのまま残す。 - Intl.RelativeTimeFormat("ja") を使い、秒・分・時間・日で単位を切り替える。 - 元の絶対時刻は title 属性に入れて、支援技術とツールチップに残す。 - :not(:defined) で定義前の状態を隠す方法もコメントで示す。 ### Shadow DOM でスタイルを閉じ込める プレビュー: https://vibe.itlibra.com/sample/wc-shadow/ 外側に「本文を赤・2remにする」乱暴な指定を置いてあります。説明文は実際に大きくなりますが、カードは影響を受けません。外から変えたい部分だけカスタムプロパティで穴を開けています。 ```html ``` ```css /* この宣言は Shadow DOM の内側には届かない。 「壊されない部品」を作るとき、これが最大の利点になる。 */ .danger-zone p { color: #dc2626 !important; font-size: 2rem !important; } /* 外から見た目を変えたいところだけ、カスタムプロパティで穴を開ける。 変数は Shadow 境界を越えて継承されるので、これが唯一の作法。 */ stat-card { --card-accent: #2563eb; } stat-card.warn { --card-accent: #d97706; } stat-card.good { --card-accent: #0f7b53; } /* ::part で指定された部分だけは、外から直接触れる */ stat-card::part(value) { letter-spacing: 0; } ``` ```js // テンプレートは1度だけ作って全インスタンスで使い回す。 // 要素ごとに innerHTML を組み立てるより速い。 const tpl = document.createElement("template"); tpl.innerHTML = `
`; class StatCard extends HTMLElement { connectedCallback() { if (this.shadowRoot) return; // 二重描画を防ぐ const root = this.attachShadow({ mode: "open" }); root.append(tpl.content.cloneNode(true)); root.querySelector(".label").textContent = this.getAttribute("label") ?? ""; root.querySelector(".value").textContent = this.getAttribute("value") ?? ""; root.querySelector(".note").textContent = this.getAttribute("note") ?? ""; } } customElements.define("stat-card", StatCard); ``` **AIへの指示文** Shadow DOM でスタイルを隔離した統計カードを作ってください。 要件: - attachShadow({ mode: "open" }) を使い、テンプレートは1度だけ作って 全インスタンスで使い回す(要素ごとに innerHTML を組むより速い)。 - :host でカスタム要素自身を装飾する。font-family: inherit を明示して フォントだけは外から継承させる。 - 外側に強い指定(!important 付き)を置き、Shadow の中身が 影響を受けないことを目で確認できるようにする。 - 外から変えたい色はカスタムプロパティで受ける (var(--card-accent, 既定値))。変数は Shadow 境界を越えて継承される。 - 一部の要素に part 属性を付け、::part で外から触れる例も示す。 - connectedCallback の冒頭で shadowRoot の有無を確認し、二重描画を防ぐ。 ### slot で中身を差し込む プレビュー: https://vibe.itlibra.com/sample/wc-slot/ 器と装飾は部品側、中身は使う側。差し込まれた要素には外側のCSSがそのまま効くので、本文の書き味を保ったまま枠だけ共通化できます。 ```html 補足

名前付きの slot は複数置けます。指定しなかった中身は、既定の(名前なしの)slot にまとめて入ります。

注意

Shadow DOM の中身と違い、slot に差し込まれた要素は外側の CSS が効きます。上の note-box p の指定がそのまま当たっています。

これは欠点ではなく設計です。器は部品が持ち、文章の体裁はページ側が持つ、という分担になります。

タイトルを省略すると、部品に書いた既定のテキストが出ます。<slot> の中身がそのまま代替値になる仕組みです。

``` ```css /* slot に差し込まれた中身は「外側の DOM のまま」なので、 外側の CSS がそのまま効く。Shadow の内側とは扱いが違う。 */ note-box p { margin: 0 0 .5rem; line-height: 1.85; } note-box p:last-child { margin-bottom: 0; } note-box a { color: var(--accent); } note-box code { background: rgba(27, 32, 48, .07); padding: .1em .35em; border-radius: 5px; font-size: .92em; } ``` ```js const tpl = document.createElement("template"); tpl.innerHTML = `
メモ
`; class NoteBox extends HTMLElement { connectedCallback() { if (this.shadowRoot) return; this.attachShadow({ mode: "open" }).append(tpl.content.cloneNode(true)); } } customElements.define("note-box", NoteBox); ``` **AIへの指示文** slot を使って中身を差し込めるメモ枠の部品を作ってください。 要件: - 名前付き slot(name="title")と既定の slot の2つを持たせる。 - slot の中に既定値を書き、省略されたときはそれが表示されるようにする。 - :host([tone="info"]) のように属性セレクタで配色を切り替える。 - slot に差し込まれた中身は外側の DOM のままなので外側の CSS が効く、 という違いをコメントで明記する。 - ::slotted() は差し込まれた要素の一番外側にしか当たらず、 子孫には届かないことも書く。 - 使用例では、外側の CSS で本文の余白やリンク色を指定して、 実際に効いていることが分かるようにする。 ### 属性の監視とプロパティ プレビュー: https://vibe.itlibra.com/sample/wc-attr/ observedAttributes に列挙した属性だけが変更通知されます。属性は文字列しか持てないので、真偽値は存在の有無で表し、数値はプロパティ側で受けるのが定石です。 ```html

```

```js
const tpl = document.createElement("template");
tpl.innerHTML = `
  
  
    
    
  
  
`; class ProgressRing extends HTMLElement { // 監視したい属性をここに列挙する。書き忘れると変更が届かない。 static observedAttributes = ["value", "label", "compact"]; connectedCallback() { if (!this.shadowRoot) { this.attachShadow({ mode: "open" }).append(tpl.content.cloneNode(true)); } this.render(); } // 属性が変わるたびに呼ばれる。connectedCallback より先に呼ばれることも // あるので、shadowRoot の有無を必ず確認してから触る。 attributeChangedCallback() { if (this.shadowRoot) this.render(); } // 属性は文字列。数値として扱う窓口をプロパティで用意しておくと、 // JS からは el.value = 60 と書けて読みやすくなる。 get value() { return Number(this.getAttribute("value") ?? 0); } set value(v) { this.setAttribute("value", String(v)); } // 真偽値は「属性があるかどうか」で表す。value="false" は // 文字列 "false" が入るだけで真になってしまうので使わない。 get compact() { return this.hasAttribute("compact"); } set compact(on) { this.toggleAttribute("compact", Boolean(on)); } render() { const pct = Math.max(0, Math.min(100, this.value)); const circumference = 2 * Math.PI * 42; const bar = this.shadowRoot.querySelector(".bar"); bar.style.strokeDasharray = String(circumference); bar.style.strokeDashoffset = String(circumference * (1 - pct / 100)); this.shadowRoot.querySelector(".txt").textContent = pct + "%"; this.shadowRoot.querySelector(".lbl").textContent = this.getAttribute("label") ?? ""; this.shadowRoot.querySelector("svg") .setAttribute("aria-label", (this.getAttribute("label") ?? "進捗") + " " + pct + "%"); } } customElements.define("progress-ring", ProgressRing); // --- 動作確認 --- const ring = document.querySelector("progress-ring"); const out = document.getElementById("out"); const log = () => (out.textContent = "属性 value: " + JSON.stringify(ring.getAttribute("value")) + "(文字列)\n" + "プロパティ .value: " + ring.value + "(数値)\n" + "属性 compact: " + (ring.hasAttribute("compact") ? "あり" : "なし") + "\n" + "プロパティ .compact: " + ring.compact); document.querySelectorAll("[data-set]").forEach((b) => b.addEventListener("click", () => { ring.value = Number(b.dataset.set); log(); }), ); document.getElementById("toggle-compact").addEventListener("click", () => { ring.compact = !ring.compact; log(); }); log(); ``` **AIへの指示文** 属性の変更に追従する円形の進捗表示コンポーネントを作ってください。 要件: - static observedAttributes に監視したい属性を列挙する。 書き忘れると変更が届かない点をコメントで書く。 - attributeChangedCallback は connectedCallback より先に呼ばれることが あるので、shadowRoot の有無を確認してから DOM を触る。 - 数値は get/set プロパティで受け、JS からは el.value = 60 と 書けるようにする。属性は文字列しか持てないことを説明する。 - 真偽値は属性の有無で表す(hasAttribute と toggleAttribute)。 value="false" は文字列として真になるので使わない、と明記する。 - 進捗は SVG の circle と stroke-dasharray / stroke-dashoffset で描く。 - svg に aria-label を付け、値が変わるたびに更新する。 - prefers-reduced-motion では transition を切る。 ## FAQ **いまさらWeb Componentsを学ぶ意味はありますか。** 長く置くものほど価値があります。ビルドが要らず依存も増えないので、5年後に触っても同じコードが動く可能性が高い。複数のサイトやフレームワークから使い回したい部品、CMSの本文に埋め込みたい表現、ビルド環境を持ち込めない案件では、いまでも最も確実な選択肢です。 **Shadow DOM は必ず使うべきですか。** いいえ。外側のデザインシステムに馴染ませたい部品なら、使わないほうが楽です。Shadow DOM の利点は「壊されない」ことですが、裏返すと「外から直せない」ことでもあります。サイト内で完結する部品なら使わず、配布する部品なら使う、という切り分けが実務的です。 **属性とプロパティはどう使い分けますか。** 属性はHTMLに書く初期値、プロパティはJavaScriptから読み書きする窓口です。属性は文字列しか持てないので、数値や配列はプロパティ側で変換します。真偽値は特に注意が必要で、disabled="false" と書いても属性が存在する以上は真になります。有無で表してください。 **定義される前に一瞬崩れて見えます。** スクリプトの読み込みが終わるまでカスタム要素はただの未知のタグなので、中身がそのまま表示されます。:not(:defined) セレクタで visibility: hidden にしておくか、中身に意味のある代替表現を書いておくのが対処です。後者のほうが、スクリプトが失敗したときにも強い作りになります。 --- # 画面幅ではなく親の幅で切り替える URL: https://vibe.itlibra.com/tech/container-queries/ 技術: コンテナクエリ (container-queries) 検索キーワード: コンテナクエリ 更新日: 2026-09-01 ライセンス: CC0 1.0 Universal(完全無料・著作権フリー・商用利用OK・クレジット不要) 料金: ¥0 ## 概要 コンテナクエリのサンプル集。container-typeの指定、@containerによる分岐、cqi単位でのタイポグラフィ、名前付きコンテナ、同じ部品を本文とサイドバーの両方に置く例を、動くプレビュー付きで収録します。 メディアクエリは画面の幅しか見られません。だから同じカード部品でも、本文の中に置くときとサイドバーに置くときで別のクラスを用意する羽目になります。コンテナクエリは見る対象を画面から親要素に変える仕組みです。親に container-type を付け、@container で分岐する。それだけで部品は自分の置かれた場所を見て形を決めるようになり、置き場所ごとの分岐がCSSの中に閉じます。2023年に主要ブラウザが出揃い、いまは前提にしてよい機能です。 ## 向いている場面 - 同じカードを本文とサイドバーの両方に置く画面 - 段組みが変わるダッシュボードの部品 - CMSで置き場所が決まらない埋め込み部品 - デザインシステムとして配る再利用部品 ## つまずきやすい点 - container-type を付けた要素は、その方向のサイズが子から決まらなくなる - @container が見るのは祖先だけ。自分自身は問い合わせられない - 名前を付けないと最も近い祖先コンテナが選ばれる - cqi は親コンテナ基準。ネストすると基準が変わる ## サンプルとコード ### 置き場所で形が変わるカード プレビュー: https://vibe.itlibra.com/sample/cq-card/ HTMLもクラスも同じカードを、幅の違う3か所に置いています。ブラウザの幅を変えなくても形が変わることが、メディアクエリとの違いそのものです。 ```html
CSS

置き場所の幅で形が決まる

このカードは画面幅を見ていません。親要素の幅だけを見て、縦積みと横並びを切り替えています。

``` ```css /* 置き場所をコンテナとして宣言する。inline-size は「横幅だけを 問い合わせ対象にする」指定で、縦は今までどおり中身で決まる。 */ .card-slot { container-type: inline-size; } /* 既定は縦積み。狭いほうを既定にしておくと、 対応していない環境でも壊れずに読める形が残る。 */ .card { display: grid; gap: .9rem; padding: 1rem; border: 1px solid var(--line); border-radius: 12px; background: #fff; } .card-thumb { aspect-ratio: 16 / 10; border-radius: 8px; background: linear-gradient(135deg, #dbeafe, #bfdbfe 60%, #93c5fd); } .card-title { margin: 0; font-size: 1rem; line-height: 1.6; } .card-text { margin: .35rem 0 0; font-size: .86rem; line-height: 1.85; color: var(--muted); } .card-tag { display: inline-block; margin-bottom: .4rem; font-size: .72rem; line-height: 1.8; color: var(--accent); } /* ここが本題。見ているのは画面幅ではなく、直近の祖先コンテナの幅。 同じカードでも、置かれた場所が 380px 以上なら横並びになる。 */ @container (min-width: 380px) { .card { grid-template-columns: 160px 1fr; align-items: center; } .card-title { font-size: 1.08rem; } } /* 段階を足すのもメディアクエリと同じ書き方でよい。 */ @container (min-width: 560px) { .card { grid-template-columns: 220px 1fr; padding: 1.2rem; } .card-text { font-size: .92rem; } } ``` **AIへの指示文** コンテナクエリで、置き場所の幅によって形が変わるカードを作ってください。 要件: - カードを受け取る要素に container-type: inline-size を指定する。 なぜ size ではなく inline-size なのかをコメントで書く (size を指定すると高さも中身から決まらなくなる)。 - 既定は縦積みにする。対応していない環境でも読める形を既定に 置く、という理由をコメントで書く。 - @container (min-width: 380px) で横並びに切り替え、 560px でさらに余白と文字を大きくする。 - 同じHTMLのカードを、幅の違う3つの入れ物に置いて並べる。 - メディアクエリは1つも使わない。 - 日本語の本文なので line-height は 1.85、letter-spacing は 0 にする。 ### cqi で文字を親幅に追随させる プレビュー: https://vibe.itlibra.com/sample/cq-units/ cqi は親コンテナの横幅の1%です。vw と違い画面ではなく親を見るので、同じ部品を狭い欄に置いても比率が保たれます。clamp で挟むのが実用上の必須条件です。 ```html
CSS Container Query Units

親の幅に、文字が追いつく

見出しも本文も余白も、画面幅ではなく親要素の幅を基準にしています。同じ部品を狭い欄に置いても、比率を保ったまま縮みます。

``` ```css .panel { /* container-type を付けた時点で、この要素は コンテナクエリ単位(cqw / cqi / cqh / cqb)の基準にもなる。 */ container-type: inline-size; border: 1px solid var(--line); border-radius: 12px; background: #fff; padding: clamp(1rem, 4cqi, 2rem); } /* cqi は「コンテナの inline 方向のサイズの1%」。 横書きの日本語では横幅の1%にあたる。 vw と違って、画面ではなく親の幅に追随する。 */ .panel-title { margin: 0; /* 下限・上限を clamp で必ず挟む。cqi 単体だと、 狭いときに読めない大きさまで縮み続ける。 */ font-size: clamp(1.05rem, 5cqi, 2rem); line-height: 1.45; } .panel-text { margin: .6rem 0 0; font-size: clamp(.84rem, 2.1cqi, 1rem); line-height: 1.85; color: var(--muted); /* 1行の文字数も親幅に合わせる。読みやすさは フォントサイズだけでなく行の長さで決まる。 */ max-width: 40em; } .panel-badge { display: inline-block; margin-bottom: .5rem; /* 余白も cqi にすると狭いときに消えてしまう。 ここも clamp で下限を確保する。 */ padding: clamp(.15rem, .6cqi, .3rem) clamp(.55rem, 2cqi, 1rem); border-radius: 999px; background: #eff6ff; color: var(--accent); font-size: clamp(.68rem, 1.6cqi, .82rem); line-height: 1.8; } ``` **AIへの指示文** コンテナクエリ単位を使って、親の幅に追随するパネルを作ってください。 要件: - パネルの外側に container-type: inline-size を付け、 cqi の基準にする。 - 見出し・本文・バッジの文字サイズを clamp(下限, Ncqi, 上限) で指定する。 clamp で挟まないと狭いときに読めない大きさまで縮む、という理由を コメントで書く。 - 余白にも cqi を使い、幅に比例して詰まるようにする。 - 本文には max-width を em で与え、1行の文字数も制御する。 - 親要素の幅をスライダーで変えられるようにして、 画面幅を変えずに変化することを確かめられるようにする。 ### 名前付きコンテナで祖先を選ぶ プレビュー: https://vibe.itlibra.com/sample/cq-named/ コンテナが入れ子になると、名前なしの@containerは最も近い祖先を見ます。段組みは内側、余白は外側、というように参照先を分けたいときに名前が要ります。 ```html

近い祖先と、遠い祖先

段組みは内側のコンテナ、余白は名前で指定した外側のコンテナを見て決めています。

``` ```css /* コンテナに名前を付ける。省略記法の container は `container: / ` の順で書く。 */ .page { container: page / inline-size; border: 1px solid var(--line); border-radius: 14px; background: #fff; padding: 1rem; } /* 内側にもう1つコンテナがある。名前を付けていない匿名コンテナでも、 「最も近い祖先」として選ばれてしまう点に注意。 */ .column { container: column / inline-size; border: 1px dashed var(--line); border-radius: 10px; padding: .8rem; } .card { display: grid; gap: .8rem; padding: .9rem; border: 1px solid var(--line); border-radius: 10px; background: #fbfcfe; } .card-thumb { aspect-ratio: 16 / 9; border-radius: 8px; background: linear-gradient(135deg, #dbeafe, #93c5fd); } .card-title { margin: 0; font-size: .98rem; line-height: 1.6; } .card-text { margin: .3rem 0 0; font-size: .84rem; line-height: 1.85; color: var(--muted); } /* 名前なしの @container は、最も近い祖先コンテナ(= .column)を見る。 中身の詰まり具合を決めるのはこちらが自然。 */ @container (min-width: 320px) { .card { grid-template-columns: 120px 1fr; align-items: center; } } /* 名前を書くと、近い祖先を飛び越して指定した祖先を見にいく。 「ページ全体が広いときだけ余白を増やす」のように、 役割の違う判断を混ぜずに書ける。 */ @container page (min-width: 620px) { .card { padding: 1.2rem; gap: 1.1rem; } .card-title { font-size: 1.08rem; } } ``` **AIへの指示文** 入れ子になったコンテナを名前で使い分ける例を作ってください。 要件: - 外側に container: page / inline-size、内側に container: column / inline-size を指定する。 省略記法が「名前 / 型」の順であることをコメントで書く。 - 名前なしの @container は最も近い祖先(内側)を見る。 これで段組みを切り替える。 - @container page (min-width: 620px) で外側を参照し、 こちらでは余白と見出しサイズだけを変える。 - 名前を付けないと途中の要素が意図せずコンテナになっていて 参照先がずれることがある、という注意をコメントで書く。 - 外側の幅をスライダーで変えられるようにする。 ### 本文とサイドバーに同じ部品を置く プレビュー: https://vibe.itlibra.com/sample/cq-sidebar/ コンテナクエリが本当に効くのはこの場面です。修飾クラスも別コンポーネントも作らず、部品自身が置き場所を見て情報量まで変えています。 ```html

本文欄

2026.09.01

サーバー移設のお知らせ

9月14日の午前2時から4時にかけて、一時的にアクセスできない時間があります。

詳細
``` ```css /* ページ側のレイアウト。ここは画面幅の話なので メディアクエリのままでよい。役割が違う。 */ .layout { display: grid; gap: 1.2rem; grid-template-columns: 1fr; } @media (min-width: 720px) { .layout { grid-template-columns: minmax(0, 1fr) 260px; } } /* 部品を受け取る場所は、どこもコンテナにしておく。 こうしておけば、あとで置き場所を入れ替えても 部品側のCSSを触らずに済む。 */ .region { container-type: inline-size; } /* --- 以下は「お知らせ」部品。置き場所を一切知らない --- */ .notice { display: grid; gap: .7rem; padding: .9rem; border: 1px solid var(--line); border-left: 3px solid var(--accent); border-radius: 10px; background: #fff; } .notice-date { font-size: .74rem; line-height: 1.8; color: var(--muted); font-variant-numeric: tabular-nums; } .notice-title { margin: 0; font-size: .96rem; line-height: 1.6; } .notice-text { margin: 0; font-size: .84rem; line-height: 1.85; color: var(--muted); } .notice-action { justify-self: start; padding: .45rem .9rem; border-radius: 8px; background: var(--accent); color: #fff; font-size: .82rem; line-height: 1.8; text-decoration: none; } /* 狭い場所では説明文を落として、日付と見出しだけにする。 情報量を減らす判断も、部品の中に書ける。 */ .notice-text { display: none; } @container (min-width: 300px) { .notice-text { display: block; } } @container (min-width: 460px) { .notice { grid-template-columns: 6rem 1fr auto; align-items: center; padding: 1.1rem; } .notice-title { grid-column: 2; } .notice-text { grid-column: 2; margin-top: -.4rem; } .notice-date { grid-row: 1 / span 2; } .notice-action { grid-column: 3; grid-row: 1 / span 2; } } ``` **AIへの指示文** 同じお知らせ部品を、本文欄とサイドバーの両方に置いてください。 要件: - ページの2段組み自体はメディアクエリで作る。画面の話は メディアクエリ、部品の話はコンテナクエリ、と役割で分ける 理由をコメントで書く。 - 部品を受け取る領域にはすべて container-type: inline-size を付ける。 あとで置き場所を入れ替えても部品側を触らずに済む、と書く。 - 部品は既定で説明文を display: none にし、 @container (min-width: 300px) で表示する。 狭いときに情報量を減らす判断も部品の中に書ける、と示す。 - @container (min-width: 460px) では日付・見出し・ボタンを grid で1行に並べる。 - 2か所に置くHTMLは完全に同一にし、修飾クラスを一切足さない。 ## FAQ **メディアクエリはもう使わなくていいのですか。** 役割が違うので両方使います。ページ全体の骨格(2段組みにするか1段にするか、ヘッダーを畳むか)は画面の話なのでメディアクエリのままが自然です。コンテナクエリは、置き場所が決まっていない部品の中身をどう畳むかを担当します。画面の話はメディアクエリ、部品の話はコンテナクエリ、と分けると迷いません。 **container-type は inline-size と size のどちらを指定すべきですか。** 基本は inline-size です。size を指定すると横だけでなく高さも中身から決まらなくなり、明示的に高さを与えていない要素は潰れます。高さを問い合わせたい場面は実務ではほとんど無いので、迷ったら inline-size にしてください。 **自分自身の幅で分岐できますか。** できません。@container が見るのは祖先のコンテナだけです。自分をコンテナにして自分に問い合わせると、分岐でサイズが変わり、そのサイズでまた分岐して、という循環になるため禁じられています。部品の外側に1枚ラッパーを置いて、そこをコンテナにしてください。 **cqi と vw は何が違いますか。** 基準が違います。vw は画面の幅の1%で、部品をどこへ置いても同じ値になります。cqi は直近のコンテナの inline 方向のサイズの1%なので、狭い欄に置けば小さくなります。ただし cqi は単体では下限も上限も無いので、必ず clamp で挟んでください。 **対応していないブラウザではどうなりますか。** @container のブロックが丸ごと無視され、その外に書いたスタイルだけが適用されます。だから既定を狭いレイアウト(縦積み)にしておけば、崩れずに読める形が残ります。主要ブラウザは2023年に出揃っているので、いまは既定側を整えておく程度の配慮で足ります。 --- # 遷移の動きをブラウザに任せる URL: https://vibe.itlibra.com/tech/view-transitions/ 技術: View Transitions (view-transitions) 検索キーワード: View Transitions API 更新日: 2026-09-01 ライセンス: CC0 1.0 Universal(完全無料・著作権フリー・商用利用OK・クレジット不要) 料金: ¥0 ## 概要 View Transitions APIのサンプル集。startViewTransitionによるクロスフェード、view-transition-nameで要素をつなぐ拡大、一覧の並べ替えと削除、擬似要素での独自の動きを、非対応環境への備えまで含めて収録します。 画面が切り替わるとき、前と後ろのどこがつながっているのかを利用者に伝えたい。従来これを実現するには、消える要素と現れる要素の座標を自分で測り、差分をアニメーションさせる必要がありました。View Transitions APIは、その一連の作業をブラウザに任せます。document.startViewTransition() の中でDOMを書き換えると、前後のスナップショットが自動で撮られ、間が補間される。つながりを示したい要素に view-transition-name を付ければ、位置もサイズも越えて移動していきます。 ## 向いている場面 - 一覧から詳細へ、同じ画像を拡大しながら遷移する画面 - 並べ替えや絞り込みで並びが変わる一覧 - タブやステップの切り替え - 追加・削除のあるリスト ## つまずきやすい点 - view-transition-name は同時に1つしか存在できない - 非対応ブラウザ向けに、遷移なしで動く道を必ず残す - prefers-reduced-motion では動きを止める - 遷移中はページ全体が操作できない。長い動きにしない ## サンプルとコード ### startViewTransition の最小構成 プレビュー: https://vibe.itlibra.com/sample/vt-basic/ アニメーションのCSSを1行も書かずに、タブの中身がクロスフェードします。DOMの書き換えを関数で包むだけ、というのがこのAPIの全体像です。 ```html
``` ```css /* 何も書かなくても既定でクロスフェードする。既定の名前 root は ページ全体のスナップショットに割り当てられている。 時間や曲線だけ変えたいときは、この2つを上書きすればよい。 */ ::view-transition-old(root) { animation-duration: 220ms; animation-timing-function: ease; } ::view-transition-new(root) { animation-duration: 220ms; animation-timing-function: ease; } /* 動きを減らす設定では、遷移そのものを一瞬で終わらせる。 切り替え自体は成立したまま、動きだけが消える。 */ @media (prefers-reduced-motion: reduce) { ::view-transition-old(root), ::view-transition-new(root) { animation-duration: 1ms; } } ``` ```js const CONTENT = [ { h: "概要", p: "画面の切り替わりを、前後のスナップショットからブラウザが補間します。座標を自分で測る必要はありません。" }, { h: "仕様", p: "startViewTransition に渡した関数の中でDOMを書き換えます。書き換えが終わってから遷移が始まります。" }, { h: "料金", p: "ブラウザに入っている機能なので、追加のライブラリも読み込みも必要ありません。" }, ]; const panel = document.getElementById("panel"); function render(i) { const c = CONTENT[i]; panel.innerHTML = ""; const h = document.createElement("h2"); h.textContent = c.h; const p = document.createElement("p"); p.textContent = c.p; panel.append(h, p); } function select(i) { for (const b of document.querySelectorAll(".tab")) { b.setAttribute("aria-selected", String(Number(b.dataset.tab) === i)); } render(i); } document.addEventListener("click", (e) => { const btn = e.target.closest(".tab"); if (!btn) return; const i = Number(btn.dataset.tab); // 対応していないブラウザでは startViewTransition が存在しない。 // その場合はそのまま書き換える。ここを省くと非対応環境で落ちる。 if (!document.startViewTransition) { select(i); return; } // 渡した関数の中でDOMを書き換える。関数が同期的に終わった時点の // 見た目が「後」のスナップショットになる。 const t = document.startViewTransition(() => select(i)); // 遷移が中断されると ready と finished は reject する。タブを // 連打すれば普通に起きるので、握りつぶさないとコンソールに // Uncaught (in promise) が積み上がる。 t.ready.catch(() => {}); t.finished.catch(() => {}); }); select(0); ``` **AIへの指示文** View Transitions API でタブの中身を切り替えてください。 要件: - タブを押したら panel の中身を差し替える。差し替え処理は document.startViewTransition() に渡す関数の中で行う。 - document.startViewTransition が存在しない場合は、 そのまま書き換えるだけの道を必ず用意する。 これを省くと非対応ブラウザで落ちる、とコメントで書く。 - ::view-transition-old(root) と ::view-transition-new(root) で 時間と曲線だけを指定する。root が既定の名前であることを書く。 - prefers-reduced-motion: reduce では animation-duration を 1ms にして、 切り替えは成立させたまま動きだけ消す。 - role="tablist" と aria-selected を正しく付ける。 - 日本語の本文なので line-height は 1.85、letter-spacing は 0 にする。 ### 一覧から詳細へ、画像をつなぐ プレビュー: https://vibe.itlibra.com/sample/vt-name/ 押したサムネイルだけが拡大しながら移動します。座標も差分も計算していません。同じ名前を付けた要素を、ブラウザが同じものと見なして補間します。 ```css /* つなげたい要素に同じ名前を付けると、前後で「同じもの」と見なされ、 位置とサイズの差がそのまま動きになる。フェードではなく移動になる。 名前はページ内で一意でなければならない。 */ .hero-shot { view-transition-name: hero-shot; } /* 名前を付けた要素は root のスナップショットから切り離される。 個別に時間や曲線を指定できる。 */ ::view-transition-group(hero-shot) { animation-duration: 320ms; animation-timing-function: cubic-bezier(.2, .7, .2, 1); } @media (prefers-reduced-motion: reduce) { ::view-transition-group(*), ::view-transition-old(*), ::view-transition-new(*) { animation-duration: 1ms; } } ``` ```js const ITEMS = [ { id: "a", name: "夜明けの海", cls: "sw-a", text: "水平線の色が数分で変わっていく時間帯を、青の階調だけで表しています。" }, { id: "b", name: "花の市", cls: "sw-b", text: "彩度の高い桃色を主役に置き、周囲の要素はすべて彩度を落としています。" }, { id: "c", name: "苔の庭", cls: "sw-c", text: "緑を明度差だけで組み立て、輪郭を出さずに奥行きを作っています。" }, ]; const view = document.getElementById("view"); let current = null; // null なら一覧 function renderList() { view.innerHTML = ""; const grid = document.createElement("div"); grid.className = "grid"; for (const it of ITEMS) { const btn = document.createElement("button"); btn.className = "item"; btn.dataset.id = it.id; const thumb = document.createElement("div"); // 「いま開こうとしているもの」だけに名前を付ける。 // 全部に付けると名前が重複して遷移が止まる。 thumb.className = "item-thumb " + it.cls + (it.id === current ? " hero-shot" : ""); const name = document.createElement("span"); name.className = "item-name"; name.textContent = it.name; btn.append(thumb, name); grid.append(btn); } view.append(grid); } function renderDetail(id) { const it = ITEMS.find((x) => x.id === id); view.innerHTML = ""; const box = document.createElement("div"); box.className = "detail"; const thumb = document.createElement("div"); thumb.className = "detail-thumb " + it.cls + " hero-shot"; const h = document.createElement("h2"); h.textContent = it.name; const p = document.createElement("p"); p.textContent = it.text; const back = document.createElement("button"); back.className = "back"; back.id = "back"; back.textContent = "← 一覧へ戻る"; box.append(thumb, h, p, back); view.append(box); } // 遷移の有無で処理を分けたくないので、書き換えだけを渡す形に寄せる。 function transition(update) { if (!document.startViewTransition) { update(); return; } const t = document.startViewTransition(update); // 遷移が中断されると ready と finished は reject する。前の遷移が // 終わる前に次を始めれば普通に起きるので、握りつぶさないと // コンソールに Uncaught (in promise) が積み上がる。 t.ready.catch(() => {}); t.finished.catch(() => {}); } document.addEventListener("click", (e) => { const item = e.target.closest(".item"); if (item) { current = item.dataset.id; // 名前を付けた状態の一覧を先に作ってから遷移すると、 // 「どこから来たか」がブラウザに伝わる。 renderList(); transition(() => renderDetail(current)); return; } if (e.target.closest("#back")) { transition(() => { renderList(); current = null; }); } }); renderList(); ``` **AIへの指示文** View Transitions で、一覧から詳細への遷移を作ってください。 要件: - サムネイル一覧と詳細画面を、同じ領域の描き換えで切り替える。 - 開こうとしている項目のサムネイルと、詳細画面の大きな画像に 同じ view-transition-name を付ける。位置とサイズの差が そのまま動きになる、という仕組みをコメントで書く。 - 名前はページ内で一意でなければならない。一覧の全項目に 付けると重複して遷移が止まる、という注意を必ず書く。 - 遷移の直前に「名前を付けた状態の一覧」を描いてから startViewTransition を呼ぶ。 - ::view-transition-group(名前) で時間と曲線を指定する。 - prefers-reduced-motion: reduce では動きを止める。 ### 並べ替えと削除をなめらかに プレビュー: https://vibe.itlibra.com/sample/vt-list/ 行ごとに違う名前を付けると、並べ替えで各行がいまいる位置から次の位置へ移動します。消える行と現れる行だけ、別の動きに差し替えています。 ```html
    ``` ```css .list { list-style: none; margin: 0; padding: 0; display: grid; gap: .5rem; } .row { display: grid; grid-template-columns: 2.2rem 1fr auto auto; align-items: center; gap: .7rem; padding: .7rem .9rem; border: 1px solid var(--line); border-radius: 10px; background: #fff; } .row-rank { font-variant-numeric: tabular-nums; font-size: .8rem; line-height: 1.8; color: var(--muted); } .row-name { font-size: .92rem; line-height: 1.8; } .row-score { font-variant-numeric: tabular-nums; font-size: .86rem; line-height: 1.8; color: var(--muted); } .row-del { border: 0; background: none; cursor: pointer; padding: .2rem .4rem; color: var(--muted); font-family: inherit; font-size: .82rem; line-height: 1.8; } .row-del:hover { color: var(--danger); } /* 行ごとに違う名前を付ける。名前が一致した行だけが 「移動した同じ行」として扱われ、並べ替えの軌跡が描かれる。 名前は JS で style.viewTransitionName に入れる(下のJS参照)。 */ /* 消える行と現れる行の動きだけ差し替える。行数が可変なので、 名前を1つずつ書く代わりに view-transition-class でまとめて当てる。 比較的新しい機能で、未対応のブラウザではこの2行が無視され、 既定のクロスフェードになる。動かなくなることはない。 */ ::view-transition-old(.row-anim) { animation: row-out 200ms ease both; } ::view-transition-new(.row-anim) { animation: row-in 200ms ease both; } @keyframes row-out { to { opacity: 0; transform: translateX(-12px); } } @keyframes row-in { from { opacity: 0; transform: translateX(12px); } } @media (prefers-reduced-motion: reduce) { ::view-transition-group(*), ::view-transition-old(*), ::view-transition-new(*) { animation-duration: 1ms; } } ``` ```js const SOURCE = [ { id: 1, name: "青木", score: 82 }, { id: 2, name: "石田", score: 95 }, { id: 3, name: "上原", score: 71 }, { id: 4, name: "遠藤", score: 88 }, { id: 5, name: "大石", score: 64 }, ]; let rows = [...SOURCE]; const list = document.getElementById("list"); function render() { list.innerHTML = ""; rows.forEach((r, i) => { const li = document.createElement("li"); li.className = "row"; // 行ごとに一意な名前を付ける。前後で同じ名前の行が // 「移動した同じ行」として補間される。 li.style.viewTransitionName = "row-" + r.id; // 現れる/消える行の動きをまとめて指定するためのクラス。 // 未対応のブラウザでは無視されるだけで、並べ替え自体は動く。 li.style.viewTransitionClass = "row-anim"; const rank = document.createElement("span"); rank.className = "row-rank"; rank.textContent = String(i + 1).padStart(2, "0"); const name = document.createElement("span"); name.className = "row-name"; name.textContent = r.name; const score = document.createElement("span"); score.className = "row-score"; score.textContent = r.score + "点"; const del = document.createElement("button"); del.className = "row-del"; del.dataset.del = String(r.id); del.setAttribute("aria-label", r.name + "を削除"); del.textContent = "削除"; li.append(rank, name, score, del); list.append(li); }); } function update(fn) { if (!document.startViewTransition) { fn(); render(); return; } const t = document.startViewTransition(() => { fn(); render(); }); // 遷移が中断されると ready と finished は reject する。並べ替えを // 連打すれば普通に起きるので、握りつぶさないとコンソールに // Uncaught (in promise) が積み上がる。 t.ready.catch(() => {}); t.finished.catch(() => {}); } document.addEventListener("click", (e) => { const del = e.target.closest("[data-del]"); if (del) { const id = Number(del.dataset.del); update(() => { rows = rows.filter((r) => r.id !== id); }); return; } const btn = e.target.closest("[data-sort]"); if (!btn) return; const kind = btn.dataset.sort; update(() => { if (kind === "score") rows = [...rows].sort((a, b) => b.score - a.score); else if (kind === "name") rows = [...rows].sort((a, b) => a.name.localeCompare(b.name, "ja")); else if (kind === "shuffle") rows = [...rows].reverse(); else rows = [...SOURCE]; }); }); render(); ``` **AIへの指示文** 一覧の並べ替えと削除に View Transitions を使ってください。 要件: - 行ごとに一意な view-transition-name を JavaScript で設定する (style.viewTransitionName に "row-" + id を入れる)。 - 得点順・名前順・逆順・元に戻す、の並べ替えと、行の削除を用意する。 - 状態を変えてから再描画するまでを1つの関数にまとめ、それを startViewTransition に渡す。非対応時は同じ関数をそのまま呼ぶ。 - 消える行と現れる行だけ、左右にずらしながらフェードする動きにする。 行数が可変なので view-transition-class でまとめて指定し、 未対応ブラウザでは既定のクロスフェードに戻るだけだと書く。 - 行数が多いとスナップショットが増えて重くなる、という注意を書く。 - prefers-reduced-motion: reduce では動きを止める。 ### 擬似要素で動きを差し替える プレビュー: https://vibe.itlibra.com/sample/vt-custom/ 進むと左へ、戻ると右へ流します。方向をルート要素のdata属性で渡し、遷移中のCSSを切り替えるのが定石です。後始末まで含めて書いてあります。 ```html
    ``` ```css /* 差し替えたい範囲だけに名前を付ける。ページ全体(root)を 動かすとヘッダーまで滑ってしまい、目が疲れる。 */ .stage-inner { view-transition-name: stage; } /* 前の中身と次の中身は、遷移中この2つの擬似要素として重なっている。 既定ではどちらも opacity のアニメーションが当たっているので、 上書きすれば好きな動きにできる。 */ ::view-transition-old(stage) { animation: slide-out 240ms cubic-bezier(.4, 0, 1, 1) both; } ::view-transition-new(stage) { animation: slide-in 240ms cubic-bezier(0, 0, .2, 1) both; } /* 戻るときは向きを反転させたい。方向は data 属性で外から与える。 :root に付けた属性で遷移中のCSSを切り替えるのが定石。 */ :root[data-dir="back"] ::view-transition-old(stage) { animation: slide-out-back 240ms cubic-bezier(.4, 0, 1, 1) both; } :root[data-dir="back"] ::view-transition-new(stage) { animation: slide-in-back 240ms cubic-bezier(0, 0, .2, 1) both; } @keyframes slide-out { to { opacity: 0; transform: translateX(-24px); } } @keyframes slide-in { from { opacity: 0; transform: translateX(24px); } } @keyframes slide-out-back { to { opacity: 0; transform: translateX(24px); } } @keyframes slide-in-back { from { opacity: 0; transform: translateX(-24px); } } @media (prefers-reduced-motion: reduce) { ::view-transition-old(stage), ::view-transition-new(stage) { animation-duration: 1ms; } } ``` ```js const STEPS = [ { h: "1. 送り先を選ぶ", p: "登録済みの住所から選ぶか、新しい住所を入力します。ここで選んだ内容は次の画面でも表示されます。" }, { h: "2. 支払い方法", p: "クレジットカード、コンビニ払い、代金引換から選べます。あとから変更もできます。" }, { h: "3. 内容の確認", p: "送り先と支払い方法、金額を確認します。確定するまで注文は成立しません。" }, ]; const stage = document.getElementById("stage"); const prev = document.getElementById("prev"); const next = document.getElementById("next"); const dots = document.querySelectorAll(".dot"); let i = 0; function render() { stage.innerHTML = ""; const h = document.createElement("h2"); h.textContent = STEPS[i].h; const p = document.createElement("p"); p.textContent = STEPS[i].p; stage.append(h, p); dots.forEach((d, n) => d.classList.toggle("on", n <= i)); prev.disabled = i === 0; next.disabled = i === STEPS.length - 1; } function go(delta) { const target = i + delta; if (target < 0 || target >= STEPS.length) return; // 向きをCSS側へ渡す。属性は遷移が終わったら必ず外す。 // 付けっぱなしにすると次の遷移の向きが狂う。 document.documentElement.dataset.dir = delta < 0 ? "back" : "forward"; if (!document.startViewTransition) { i = target; render(); delete document.documentElement.dataset.dir; return; } const t = document.startViewTransition(() => { i = target; render(); }); // 後始末は成功時も中断時も必ず走らせる。then の第2引数を使うのは、 // finally だと reject がそのまま素通りして // Uncaught (in promise) になるため。 const cleanup = () => { delete document.documentElement.dataset.dir; }; t.finished.then(cleanup, cleanup); t.ready.catch(() => {}); } next.addEventListener("click", () => go(1)); prev.addEventListener("click", () => go(-1)); render(); ``` **AIへの指示文** 3ステップの入力画面を、進む・戻るで向きが変わる遷移にしてください。 要件: - 動かす範囲は中身だけに絞る。ページ全体を動かすとヘッダーまで 滑って目が疲れる、という理由をコメントで書く。 - ::view-transition-old と ::view-transition-new に keyframes を当てて、既定のクロスフェードを置き換える。 - 進む・戻るの向きは :root の data-dir 属性で切り替える。 遷移中のCSSを外から制御する定石だと書く。 - startViewTransition の戻り値の finished を使って、 終わったら data-dir を必ず外す。付けっぱなしにすると 次の遷移の向きが狂う、と書く。 - 遷移中はページ全体が操作を受け付けないので、 250ミリ秒を超える動きにしない、という注意を書く。 - prefers-reduced-motion: reduce では動きを止める。 ## FAQ **対応していないブラウザではどうなりますか。** document.startViewTransition が undefined になります。その場合はDOMの書き換えだけを実行する道を用意しておけば、アニメーションが付かないだけで機能は普通に動きます。この分岐を書き忘れると、非対応ブラウザで例外が出て切り替えそのものが止まります。必ず入れてください。 **view-transition-name はページ内で重複してもよいですか。** いけません。同じ名前の要素が同時に2つ存在すると、その遷移は中断されて何も起きません。一覧の全項目に同じ名前を付けてしまう間違いが最も多いので、「いま開こうとしている1つ」にだけ付けて、それ以外からは外してください。 **ページ全体ではなく一部だけを動かせますか。** できます。動かしたい範囲に view-transition-name を付けると、その要素は root のスナップショットから切り離され、個別にアニメーションできます。ヘッダーやサイドバーまで一緒に滑ると目が疲れるので、実務では範囲を絞るほうが結果がよくなります。 **ページ遷移そのものには使えますか。** 使えます。同一オリジンのページ間なら、CSSに @view-transition { navigation: auto } と書くだけで、通常のリンク遷移にも適用されます。ここで紹介しているのは1ページ内でDOMを書き換える使い方ですが、view-transition-name の考え方はどちらでも同じです。 **アニメーションの時間はどのくらいが適切ですか。** 200から300ミリ秒に収めてください。遷移中はページ全体が操作を受け付けないため、長い動きはそのまま操作できない時間になります。「きれいに見える長さ」ではなく「待たされない長さ」で決めるのが実用的です。 **prefers-reduced-motion にはどう対応しますか。** 擬似要素の animation-duration を 1ms にするのが簡単です。遷移そのものは成立したまま、動きだけが消えます。処理を分岐させる必要がないので、JavaScript側は何も変えずに済みます。 --- # 決まった型に流し込んで速く出す URL: https://vibe.itlibra.com/tech/bootstrap/ 技術: Bootstrap (bootstrap) 検索キーワード: Bootstrap 更新日: 2026-09-01 ライセンス: CC0 1.0 Universal(完全無料・著作権フリー・商用利用OK・クレジット不要) 料金: ¥0 ## 概要 Bootstrap 5のサンプル集。12分割グリッドとブレークポイント、カードとユーティリティ、標準検証を使ったフォーム、JavaScriptが要るナビバーとモーダルを、CDNに頼らず本体を持ち込んだ状態で収録します。 Bootstrapは「もう決まっている」ことが価値です。12分割のグリッド、6段階のブレークポイント、ボタンやカードの見た目、余白の刻み方。全部あらかじめ決まっていて、クラス名を書けばその通りになります。だから議論も設計も要らず、動くものが速く出ます。裏返せば、その決定を覆したい場面ではずっと戦うことになる。作るものが管理画面や社内ツールのように「整っていれば十分」なら、いまでも最短距離です。 ## 向いている場面 - 管理画面や社内ツールを短時間で形にする - デザイナーがいない案件の見た目を底上げする - 既存のBootstrap製サイトに機能を足す - プロトタイプを1日で組んで議論の土台にする ## つまずきやすい点 - CSSが232KB。使うのが数クラスでも全部読み込まれる - 見た目が既定のままだと「Bootstrapのサイト」だと一目で分かる - 上書きは詳細度との戦いになりやすい - ドロップダウンやモーダルはJavaScript本体が要る - 見出しの行間が1.2。欧文前提の値で、和文だと詰まりすぎる ## サンプルとコード ### 12分割グリッドとブレークポイント プレビュー: https://vibe.itlibra.com/sample/bs-grid/ container / row / col の3階層と、6段階のブレークポイント。すべて min-width で効き、指定しない画面幅は直前の指定を引き継ぎます。だからクラスは小さい画面から順に書きます。 ```html
    col-12 / md-6 / lg-4
    col-12 / md-6 / lg-4
    col-12 / md-12 / lg-4
    col
    col
    col
    col-auto
    col(残り全部)
    col-md-6 offset-md-3
    HTMLでは1番目 / 表示は2番目
    HTMLでは2番目 / 表示は1番目
    ``` **AIへの指示文** Bootstrap 5 のグリッドの使い方が一目で分かるページを作ってください。 要件: - container / row / col-* の3階層を崩さない。row の直下は必ず col-* にする、という制約をコメントで書く。 - col-12 / col-md-6 / col-lg-4 のように、小さい画面から順に指定する。 すべて min-width で効くので大きいほうへ引き継がれる、と書く。 - 数字なしの col(残りを均等に分ける)、col-auto(中身の幅だけ)、 offset-md-3(左に空きを作る)、order-md-*(表示順だけ入れ替える) の4パターンを並べる。 - order は HTML の順序を変えずに見た目だけ入れ替えられるので、 読み上げ順を保ったまま右に置ける、という利点をコメントで書く。 - 列同士の隙間は row 側に g-3 を付ける。 - Bootstrap は CDN ではなく相対パスで読み込む。 - 日本語の本文なので line-height は 1.85、letter-spacing は 0 にする。 ### カードとユーティリティクラス プレビュー: https://vibe.itlibra.com/sample/bs-card/ h-100 で高さを揃え、d-flex flex-column と mt-auto でボタンを下端に寄せる。最頻出の組み合わせです。あわせて、和文向けに行間を直すCSSも載せました。 ```html
    新着 無料

    クラス名だけで形が決まる

    カードの枠線・角丸・内側の余白は、あらかじめ決められています。設計せずに済むぶん速く出せます。

    詳しく見る
    残りわずか

    中身の量が違っても下端が揃う

    このカードは説明文が短いですが、h-100 と mt-auto のおかげでボタンの位置は隣と揃います。

    詳しく見る
    対応済み CC0

    色の名前は6種類に決まっている

    primary / secondary / success / danger / warning / info。この語彙で色を指定するので、配色を毎回決め直す必要がありません。

    詳しく見る
    直近の更新
    • サンプルを4件追加 09/01
    • フォームの検証表示を修正 08/31
    • ナビバーの折りたたみを調整 08/28
    ``` ```css /* Bootstrap の既定は欧文前提で、見出しの line-height が 1.2 しかない。 和文をこの行間で組むと、2行目以降が前の行に食い込んで読みにくい。 5.3 は主要な値を CSS変数で持っているので、クラスを上書きせず 変数を差し替えるだけで直る。詳細度の戦いにならない。 */ :root { --bs-body-font-family: "Inter", "Helvetica Neue", Arial, "Hiragino Kaku Gothic ProN", "Hiragino Sans", "Yu Gothic Medium", "Yu Gothic", "Meiryo", sans-serif; --bs-body-line-height: 1.8; /* 和文では字間を詰めない。0 が既定だが、意図を残すために明示する。 */ letter-spacing: 0; } /* 見出しの行間には変数が用意されていないので直接指定する。 Bootstrap は h1〜h6 だけでなく .h1〜.h6 のユーティリティクラスにも 同じ 1.2 を当てている。要素セレクタ(詳細度 0,0,1)だけでは クラス(0,1,0)に負けるので、両方を並べて書く必要がある。 詳細度が同じなら、あとに書いたこちらが勝つ。 */ h1, h2, h3, h4, h5, h6, .h1, .h2, .h3, .h4, .h5, .h6 { line-height: 1.5; } ``` **AIへの指示文** Bootstrap 5 でカードを3枚並べたページを作ってください。 要件: - row-cols-1 row-cols-md-2 row-cols-lg-3 で「1行に何列並べるか」を指定する。 col-md-4 と同じ結果だが列数のほうが意図が読みやすい、と書く。 - カードには h-100 を付けて高さを揃える。card-body に d-flex flex-column、ボタンに mt-auto を付けて下端に寄せる。 説明文の長さが違っても下端が揃うことをコメントで書く。 - badge は text-bg-primary / text-bg-danger / text-bg-success を使い分ける。 色の語彙が6種類に決まっているので配色を毎回決めなくてよい、と書く。 - 余白は m/p + 方向 + 0〜5 の刻みだけで指定し、独自の値を書かない。 - list-group-flush を使った更新履歴のリストも1つ置く。 - Bootstrap の既定は見出しの line-height が 1.2 で和文だと詰まりすぎる。 本文側は CSS変数(--bs-body-font-family / --bs-body-line-height)で直す。 - 見出しには変数が無いので直接指定する。ただし Bootstrap は h1〜h6 だけでなく .h1〜.h6 のクラスにも同じ値を当てているため、 要素セレクタだけでは詳細度で負ける。両方を並べて書くこと。 この理由をコメントで書く。 - Bootstrap は CDN ではなく相対パスで読み込む。 ### 標準検証を使ったフォーム プレビュー: https://vibe.itlibra.com/sample/bs-form/ 判定はブラウザの標準検証、見せ方だけがBootstrapです。was-validated を付けるかどうかで表示が切り替わるので、自前の正規表現は1つも書きません。 ```html
    お名前を入力してください。
    確認のご連絡にのみ使います。
    メールアドレスの形式で入力してください。
    プランを選択してください。
    内容を入力してください。
    同意が必要です。
    送信しました。折り返しご連絡します。
    ``` ```js // Bootstrap の検証表示は「was-validated を付けるかどうか」だけで切り替わる。 // 判定は checkValidity()、つまりブラウザ標準。自前の正規表現は書かない。 const form = document.querySelector(".needs-validation"); const done = document.getElementById("done"); form.addEventListener("submit", (e) => { e.preventDefault(); // was-validated を付けた時点で、:valid / :invalid に応じた // 緑・赤の表示と invalid-feedback の出し分けが始まる。 form.classList.add("was-validated"); if (!form.checkValidity()) { // 最初の不正な入力へフォーカスを移す。これが無いと、 // 長いフォームでどこが悪いのか探すことになる。 form.querySelector(":invalid")?.focus(); done.classList.add("d-none"); return; } done.classList.remove("d-none"); }); // やり直すときは was-validated を外す。付けっぱなしにすると、 // 入力し直している最中ずっと赤いままになる。 form.addEventListener("reset", () => { form.classList.remove("was-validated"); done.classList.add("d-none"); }); ``` **AIへの指示文** Bootstrap 5 の検証つきお問い合わせフォームを作ってください。 要件: - form に novalidate を付けてブラウザ既定の吹き出しを止め、 表示は Bootstrap に任せる。判定は checkValidity()、つまり標準検証を そのまま使う。自前の正規表現は書かない、とコメントで書く。 - submit 時に was-validated を付ける。これを付けた時点で :valid / :invalid の色分けと invalid-feedback の出し分けが始まる、と書く。 - 不正な入力があれば最初の :invalid にフォーカスを移す。 これが無いと長いフォームでどこが悪いか探すことになる、と書く。 - reset 時には was-validated を外す。付けっぱなしだと入力し直している 最中ずっと赤いままになる、と書く。 - すべての入力に label を for と id で結び、autocomplete を指定する。 - 送信成功のメッセージは role="status" で伝える。 - Bootstrap は CDN ではなく相対パスで読み込む。 ### ナビバー・ドロップダウン・モーダル プレビュー: https://vibe.itlibra.com/sample/bs-navbar/ 折りたたみもドロップダウンもモーダルも、書くのは data-bs-* 属性だけです。ここだけはCSSに加えてJavaScript本体(Popper同梱のbundle)が要ります。 ```html ``` **AIへの指示文** Bootstrap 5 のナビバーを作ってください。 要件: - navbar-expand-md で、md 以上は横並び、それ未満はハンバーガーにする。 この1クラスで折りたたみの境界が決まる、とコメントで書く。 - 開閉ボタンは data-bs-toggle="collapse" と data-bs-target で宣言する。 JavaScript は本体を読み込むだけで自分では書かない、と書く。 - aria-controls / aria-expanded / aria-label を必ず付ける。 省くとキーボードと読み上げで何が起きたか伝わらない、と書く。 - 現在地のリンクには active だけでなく aria-current="page" を付ける。 active は見た目だけなので、これが無いと現在地が伝わらない、と書く。 - ドロップダウンと、data-bs-toggle="modal" で開くモーダルを1つずつ置く。 - ドロップダウンは Popper が要るので bootstrap.js ではなく bootstrap.bundle.js を読む、という理由をコメントで書く。 - Bootstrap は CDN ではなく相対パスで読み込む。 ## FAQ **いまから新規で作るならBootstrapとTailwindのどちらですか。** 「見た目を決めたいか、決めたくないか」で選んでください。Bootstrapは見た目まで決まっているので、決めたくない場合に速い。管理画面や社内ツール、デザイナーのいない案件が典型です。Tailwindは見た目を決めないので、独自のデザインがある場合に向きます。BootstrapでデザインをゼロからやるとCSSの上書きと詳細度の戦いになり、かえって遅くなります。 **CSSが232KBもあるのは重くないですか。** 重いです。ただし gzip で約30KB、しかもページをまたいでキャッシュが効くので、多くの案件では許容できます。気になる場合は Sass から必要なコンポーネントだけを import してビルドすると小さくできます。それでも「使う数クラスのために全部読む」構造は変わらないので、ここが本当に問題になる規模なら別の選択肢を検討してください。 **bootstrap.js と bootstrap.bundle.js の違いは何ですか。** bundle のほうは Popper を同梱しています。ドロップダウン、ツールチップ、ポップオーバーは位置の計算に Popper を使うため、bootstrap.js だけを読むと動きません。これらを使わないと確実に言えないなら bundle を選んでおくのが安全です。逆に折りたたみとモーダルしか使わないなら bootstrap.js で足ります。 **見た目が「Bootstrapのサイト」だと分かってしまいます。** 既定の青、既定の角丸、既定のフォントをそのまま使うとそうなります。CSS変数を上書きするのが最も軽い対処です。Bootstrap 5.3 は色や角丸を CSS変数で持っているので、--bs-primary-bg-subtle などを差し替えるだけで印象が変わります。クラスを直接上書きすると詳細度の戦いになるので、変数から手を付けてください。 **サンプルがCDNではなく相対パスで読み込んでいるのはなぜですか。** このサイトのサンプルは、1枚のHTMLとして単独で完結し、ダウンロードして file:// で開いても動くことを条件にしているためです。CDNを参照すると、オフラインでは動かず、将来CDNが消えたときにサンプルが壊れます。実務でCDNを使うのは問題ありませんが、その場合も integrity 属性を付けて配信内容が差し替わっていないことを確認してください。 **Bootstrap 4 から 5 へ上げるとき、何が変わりますか。** 大きいのは jQuery 依存が無くなったこと、方向を表すクラスが left/right から start/end になったこと、そしてデータ属性が data-toggle から data-bs-toggle に変わったことです。この3つだけでも既存コードはほぼ全面的に触ることになります。段階的な移行は難しいので、ページ単位で切り替えるほうが現実的です。 --- # ライブラリなしで横に流して見せる URL: https://vibe.itlibra.com/tech/carousel/ 技術: スライダーとカルーセル (carousel) 検索キーワード: カルーセル 更新日: 2026-09-01 ライセンス: CC0 1.0 Universal(完全無料・著作権フリー・商用利用OK・クレジット不要) 料金: ¥0 ## 概要 カルーセルのサンプル集。scroll-snapによる横送り、IntersectionObserverでの現在位置の連動、サムネイル連動のギャラリー、自動送りと4つの停止条件を、ライブラリを1つも使わずに収録します。 カルーセルは長いあいだライブラリの領分でした。位置の計算、慣性のあるスワイプ、リサイズへの追随。どれも自分で書くのは面倒だったからです。いまはブラウザ側が済ませてくれます。scroll-snap を指定すれば、スワイプの慣性も端末ごとの操作感もそのまま使えて、位置は勝手に吸い付く。残る仕事は、いまどこを見ているかを伝えることと、動きを止められるようにすることだけです。 ## 向いている場面 - 写真や実績を横に流して見せる一覧 - スマートフォンで縦に長くしたくない並び - 商品画像とサムネイルの連動 - お客様の声を数件ずつ見せる区画 ## つまずきやすい点 - 自動送りは読んでいる途中で流れる。止める手段が要る - スクロールできることが見た目で伝わらないと気づかれない - キーボードとスクリーンリーダーでの移動手段を忘れやすい - 重要な情報を2枚目以降に置くと、ほとんど読まれない - 1画面に複数枚入るレールは、末尾の要素に到達できなくなる ## サンプルとコード ### scroll-snap だけの横送り プレビュー: https://vibe.itlibra.com/sample/car-snap/ JavaScriptは1行もありません。スワイプの慣性も端末ごとの操作感も、ブラウザのスクロールをそのまま使います。書き忘れやすい指定に注釈を付けました。 ```html
    • スナップは指定するだけ

      位置の計算も慣性の再現も要りません。ブラウザのスクロールをそのまま使います。

    • 縮まないようにする

      flex: 0 0 auto を忘れると全部が1画面に詰め込まれ、スクロールが生まれません。

    • スクロールバーは消さない

      消すと動かせることが伝わりません。細くして目立たせないだけにします。

    • 端の余白は2つ書く

      padding だけだと最後の1枚が端まで行けません。scroll-padding も同じ値にします。

    • 1枚ずつ送る

      scroll-snap-stop: always を付けると、勢いよく振っても飛ばされません。

    ``` ```css .rail { display: flex; gap: 1rem; overflow-x: auto; /* これが本体。x 方向にスナップし、mandatory なので 指を離すと必ずどれかの位置に吸い付く。proximity にすると 「近ければ吸い付く」になり、中途半端な位置で止まれる。 */ scroll-snap-type: x mandatory; /* 端の余白。padding だけだと最後の1枚が端まで行けないので、 scroll-padding で「スナップ位置の基準」もずらす。 */ padding-inline: .25rem; scroll-padding-inline: .25rem; /* iOS で慣性スクロールの途中に別の要素へ引っかからないようにする。 */ overscroll-behavior-x: contain; /* スクロールバーは残す。消すと「動かせること」が伝わらなくなる。 細くして目立たせないだけにとどめる。 */ scrollbar-width: thin; padding-bottom: .6rem; } .slide { /* flex の縮みを止める。これを書かないと全部が1画面に詰め込まれ、 そもそもスクロールが生まれない。最頻出の書き忘れ。 */ flex: 0 0 auto; width: min(72%, 320px); /* 各要素のどこを基準に吸い付くか。start なら左端が揃う。 center にすると中央寄せのカルーセルになる。 */ scroll-snap-align: start; /* スクロール中にこの要素を飛び越して次へ行かせない。 1枚ずつ確実に送りたいときに効く。 */ scroll-snap-stop: always; border-radius: 14px; border: 1px solid var(--line); background: #fff; overflow: hidden; } .slide-shot { aspect-ratio: 16 / 10; } .slide-body { padding: .8rem .9rem 1rem; } .slide-title { margin: 0; font-size: .98rem; line-height: 1.6; } .slide-text { margin: .3rem 0 0; font-size: .84rem; line-height: 1.85; color: var(--muted); } /* 1画面に複数枚入るレールでは、最後の要素の「左端揃え」の位置が スクロールできる最大値を超える。mandatory は必ずどれかの スナップ位置に吸い付くので、そのままだと末尾の数枚に 到達できなくなる。最後だけ右端揃えにして、 スクロールの終端を有効なスナップ位置にする。 */ .slide:last-child { scroll-snap-align: end; } /* 動きを減らす設定では、スナップの滑らかな移動を止める。 スナップ自体は残るので、機能は失われない。 */ @media (prefers-reduced-motion: reduce) { .rail { scroll-behavior: auto; } } ``` **AIへの指示文** CSSのscroll-snapだけで横送りのカルーセルを作ってください。 JavaScriptは使いません。 要件: - 外側に scroll-snap-type: x mandatory、各要素に scroll-snap-align: start。 mandatory と proximity の違いをコメントで書く。 - 各要素に flex: 0 0 auto を指定する。これを書かないと全部が1画面に 詰め込まれてスクロールが生まれない、という最頻出の書き忘れを書く。 - scroll-snap-stop: always で1枚ずつ確実に送る。 - 最後の要素だけ scroll-snap-align: end にする。1画面に複数枚入る レールでは末尾の要素の「左端揃え」の位置がスクロール最大値を 超えるため、mandatory だと末尾に到達できなくなる、と理由を書く。 - 端の余白は padding だけでなく scroll-padding にも同じ値を入れる。 padding だけだと最後の1枚が端まで行けない理由を書く。 - overscroll-behavior-x: contain を指定する。 - スクロールバーは消さず細くするだけにする。消すと動かせることが 伝わらなくなる、という理由をコメントで書く。 - 横並びの一覧なので ul と li で組む。 - prefers-reduced-motion では scroll-behavior を auto にする。 - 日本語の本文なので line-height は 1.85、letter-spacing は 0 にする。 ### 現在位置を IntersectionObserver で追う プレビュー: https://vibe.itlibra.com/sample/car-indicator/ スワイプ・ボタン・ドットのどれで動かしても表示が一致します。位置は scrollLeft の割り算ではなく「実際に見えているか」で判定するので、余白や丸め誤差でずれません。 ```html ``` ```css .carousel { position: relative; } .rail { display: flex; gap: 1rem; overflow-x: auto; scroll-snap-type: x mandatory; scroll-behavior: smooth; list-style: none; margin: 0; padding: 0 0 .6rem; scrollbar-width: thin; } .slide { /* ドットで現在地を示すなら、1枚ずつ見せる。1画面に複数枚入る レールでは「いま何枚目か」が定義できず、さらに末尾の要素の 左端揃えの位置がスクロール最大値を超えて到達できなくなる。 */ flex: 0 0 100%; scroll-snap-align: start; scroll-snap-stop: always; border: 1px solid var(--line); border-radius: 14px; background: #fff; overflow: hidden; } .slide-shot { aspect-ratio: 16 / 9; } .slide-body { padding: .8rem .9rem 1rem; } .slide-title { margin: 0; font-size: .98rem; line-height: 1.6; } .bar { display: flex; align-items: center; gap: .6rem; margin-top: .8rem; } .nav-btn { width: 2.1rem; height: 2.1rem; flex: none; border: 1px solid var(--line); border-radius: 999px; background: #fff; color: var(--muted); cursor: pointer; font-family: inherit; font-size: .9rem; line-height: 1; } .nav-btn:hover:not(:disabled) { border-color: var(--accent); color: var(--accent); } /* 端に着いたら押せなくする。押しても何も起きないボタンは、 壊れているのか端なのかが利用者に区別できない。 */ .nav-btn:disabled { opacity: .35; cursor: default; } .dots { display: flex; gap: .4rem; margin-left: auto; } .dot { width: .55rem; height: .55rem; padding: 0; border: 0; border-radius: 999px; cursor: pointer; background: var(--line); transition: background .2s, transform .2s; } /* 現在地は aria-current で持つ。見た目のクラスを別に足すと、 状態が2か所に散って必ずずれる。 */ .dot[aria-current="true"] { background: var(--accent); transform: scale(1.25); } @media (prefers-reduced-motion: reduce) { .rail { scroll-behavior: auto; } .dot { transition: none; } } ``` ```js const rail = document.getElementById("rail"); const slides = [...rail.children]; const dots = document.getElementById("dots"); const prev = document.getElementById("prev"); const next = document.getElementById("next"); let current = 0; // ドットは枚数から作る。HTMLに手で並べると、枚数を変えたときに // 必ず片方だけ直し忘れる。 slides.forEach((_, i) => { const b = document.createElement("button"); b.className = "dot"; b.type = "button"; b.setAttribute("aria-label", `${i + 1}枚目へ`); b.addEventListener("click", () => go(i)); dots.append(b); }); function go(i) { // scrollIntoView に任せる。座標の計算をしないので、 // 余白やレイアウトが変わっても壊れない。 slides[i].scrollIntoView({ behavior: "smooth", block: "nearest", inline: "start" }); } // 現在地の更新はここだけ。スワイプ・ボタン・ドットのどれで動かしても、 // 最後は必ずスクロールが起きるので、この1か所に集約できる。 function render() { [...dots.children].forEach((d, i) => d.setAttribute("aria-current", String(i === current)), ); prev.disabled = current === 0; next.disabled = current === slides.length - 1; } // scrollLeft を幅で割る実装は、余白や端末ごとの丸めでずれる。 // 「実際に見えているか」はブラウザが知っているので、それを聞く。 // 見えている面積がいちばん大きいものを現在地とする。 // 「最初に交差した要素」を採ると、2枚が同時に見えている瞬間に // 右側の要素を拾ってしまい、表示が1つ先へずれる。 const ratios = new Map(slides.map((s) => [s, 0])); const io = new IntersectionObserver( (entries) => { for (const e of entries) ratios.set(e.target, e.intersectionRatio); let best = 0; for (let i = 1; i < slides.length; i++) { // 同率のときは先頭側を採るため、わずかに上回った場合だけ入れ替える。 if (ratios.get(slides[i]) > ratios.get(slides[best]) + 0.01) best = i; } if (best === current) return; current = best; render(); }, // レール自身を基準にする。段階的な閾値を渡すと、 // 少し動くたびに面積が更新されて追随が滑らかになる。 { root: rail, threshold: [0, 0.25, 0.5, 0.75, 1] }, ); slides.forEach((s) => io.observe(s)); prev.addEventListener("click", () => go(Math.max(0, current - 1))); next.addEventListener("click", () => go(Math.min(slides.length - 1, current + 1))); // 矢印キーでも動かせるようにする。マウスもタッチも使わない利用者が // カルーセルから抜け出せなくなるのを防ぐ。 rail.addEventListener("keydown", (e) => { if (e.key === "ArrowRight") { e.preventDefault(); next.click(); } if (e.key === "ArrowLeft") { e.preventDefault(); prev.click(); } }); render(); ``` **AIへの指示文** 前後ボタンとドットの付いたカルーセルを作ってください。 要件: - 各要素は flex: 0 0 100% にして1枚ずつ見せる。ドットで現在地を 示すなら「いま何枚目か」が定義できる必要がある、と理由を書く。 - 移動は scrollIntoView に任せ、座標の計算をしない。 - 現在位置は IntersectionObserver で判定する。scrollLeft を幅で割る 実装は余白や端末ごとの丸めでずれる、という理由をコメントで書く。 - 判定は「見えている面積がいちばん大きい要素」にする。 「最初に交差した要素」を採ると、2枚が同時に見えている瞬間に 右側を拾って表示が1つ先へずれる、と理由を書く。 threshold は [0, 0.25, 0.5, 0.75, 1] のように段階で渡す。 - 現在地は aria-current で持ち、見た目はその属性から CSS で描く。 状態を2か所に持つと必ずずれる、と書く。 - ドットは枚数から JavaScript で生成する。HTMLに手で並べると 枚数を変えたときに片方だけ直し忘れる、と書く。 - 端に着いたら前後ボタンを disabled にする。押しても何も起きない ボタンは、壊れているのか端なのか区別が付かない、と書く。 - レールに tabindex="0" と aria-label を付け、左右キーで動かせるようにする。 role="region" は当てない(ul の list 役割を上書きしてしまうため)。 - 外側は role="group" と aria-roledescription="カルーセル" で包む。 - ドットの入れ物を role="tablist" にはしない。tab と tabpanel の 対応が無いのに role だけ付けると読み上げが実態と食い違う、と書く。 ### サムネイル連動のギャラリー プレビュー: https://vibe.itlibra.com/sample/car-thumbs/ サムネイルから主役へ、主役からサムネイルへ、どちらの向きにも追随します。選択状態は色だけでなく枠線と明度の2つで示しています。 ```html

      ``` ```css .stage { display: flex; overflow-x: auto; scroll-snap-type: x mandatory; scroll-behavior: smooth; list-style: none; margin: 0; padding: 0; border-radius: 14px; border: 1px solid var(--line); background: #fff; /* 主役側のスクロールバーは隠す。サムネイルが位置を示すので、 ここに二重の手がかりは要らない。ただし操作は殺さない。 */ scrollbar-width: none; } .stage::-webkit-scrollbar { display: none; } .shot { flex: 0 0 100%; scroll-snap-align: center; scroll-snap-stop: always; aspect-ratio: 16 / 10; } .thumbs { display: flex; gap: .5rem; overflow-x: auto; list-style: none; margin: .7rem 0 0; padding: 0 0 .4rem; scrollbar-width: thin; } .thumb { flex: 0 0 auto; width: 68px; aspect-ratio: 16 / 10; padding: 0; border: 2px solid transparent; border-radius: 8px; cursor: pointer; overflow: hidden; /* 選択中でない側は少し落とす。色だけで示すと、 色覚特性によっては区別が付かない。枠線と明度の両方で示す。 */ opacity: .55; transition: opacity .2s, border-color .2s; } .thumb[aria-current="true"] { opacity: 1; border-color: var(--accent); } .thumb:hover { opacity: 1; } .thumb > span { display: block; width: 100%; height: 100%; } .caption { margin: .6rem 0 0; font-size: .86rem; line-height: 1.85; color: var(--muted); /* 切り替えで高さが変わると下の要素が跳ねる。2行ぶん確保しておく。 */ min-height: 3.2em; } @media (prefers-reduced-motion: reduce) { .stage { scroll-behavior: auto; } .thumb { transition: none; } } ``` ```js const stage = document.getElementById("stage"); const thumbs = document.getElementById("thumbs"); const caption = document.getElementById("caption"); const shots = [...stage.children]; let current = 0; // サムネイルは主役の枚数から作る。HTMLに二重で並べると、 // 写真を増やしたときに必ず片方だけ直し忘れる。 shots.forEach((shot, i) => { const li = document.createElement("li"); const b = document.createElement("button"); b.type = "button"; b.className = "thumb"; b.setAttribute("aria-label", `${i + 1}枚目:${shot.dataset.caption}`); const inner = document.createElement("span"); // 主役と同じ見た目を使い回す。クラス名を二重管理しない。 inner.className = shot.className.replace("shot", "").trim(); b.append(inner); b.addEventListener("click", () => { shot.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "center" }); }); li.append(b); thumbs.append(li); }); function render() { [...thumbs.querySelectorAll(".thumb")].forEach((t, i) => t.setAttribute("aria-current", String(i === current)), ); caption.textContent = shots[current].dataset.caption; // 選択中のサムネイルが画面外にあると、どれが選ばれているか分からない。 // 主役に追従してサムネイル側もスクロールさせる。 thumbs.querySelectorAll(".thumb")[current] ?.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "nearest" }); } // 見えている面積がいちばん大きいものを現在地とする。「最初に交差した // 要素」を採ると、切り替わりの途中で次の写真を先に拾ってしまう。 const ratios = new Map(shots.map((s) => [s, 0])); const io = new IntersectionObserver( (entries) => { for (const e of entries) ratios.set(e.target, e.intersectionRatio); let best = 0; for (let i = 1; i < shots.length; i++) { if (ratios.get(shots[i]) > ratios.get(shots[best]) + 0.01) best = i; } if (best === current) return; current = best; render(); }, { root: stage, threshold: [0, 0.25, 0.5, 0.75, 1] }, ); shots.forEach((s) => io.observe(s)); stage.addEventListener("keydown", (e) => { const d = e.key === "ArrowRight" ? 1 : e.key === "ArrowLeft" ? -1 : 0; if (!d) return; e.preventDefault(); const i = Math.min(shots.length - 1, Math.max(0, current + d)); shots[i].scrollIntoView({ behavior: "smooth", block: "nearest", inline: "center" }); }); render(); ``` **AIへの指示文** 商品写真のギャラリーを、サムネイル連動で作ってください。 要件: - 主役は scroll-snap の横スクロール、サムネイルは別の横スクロール。 - サムネイルは button にする。div に click を付けるとキーボードで 押せない、という理由をコメントで書く。 - サムネイルは主役の枚数から生成し、見た目のクラスも主役から 使い回す。HTMLに二重で並べると写真を増やしたとき片方だけ 直し忘れる、と書く。 - 選択状態は色だけで示さず、枠線と明度の2つで示す。 色覚特性によっては色だけでは区別が付かない、と書く。 - 主役をスワイプしたら、選択中のサムネイルも画面内へスクロールさせる。 - 説明文の差し替えは aria-live="polite" で伝える。 - 説明文の高さを min-height で確保し、切り替えで下が跳ねないようにする。 ### 自動送りと、4つの停止条件 プレビュー: https://vibe.itlibra.com/sample/car-auto/ ポインタ・フォーカス・タブの表示状態・本人の操作。停止理由を集合で持つことで、ポインタを外した瞬間にフォーカス中でも再開してしまう事故を防いでいます。 ```html
      • 制作会社

        止められない自動送りは、読ませない

        読んでいる途中で流れると、利用者は最初から読み直すか、諦めます。

      • 事業会社

        止める条件は4つある

        ポインタ、フォーカス、タブの表示状態、そして本人の操作。どれも「読んでいる」合図です。

      • 個人開発

        一度自分で動かしたら、もう戻さない

        操作した人には意思があります。そのあと勝手に動き出すのは邪魔にしかなりません。

      ``` ```css .rail { display: flex; overflow-x: auto; scroll-snap-type: x mandatory; scroll-behavior: smooth; list-style: none; margin: 0; padding: 0; border: 1px solid var(--line); border-radius: 14px; background: #fff; scrollbar-width: none; } .rail::-webkit-scrollbar { display: none; } .slide { flex: 0 0 100%; scroll-snap-align: center; scroll-snap-stop: always; padding: clamp(1.2rem, 4vw, 2rem); min-height: 8.5rem; } .slide-eyebrow { margin: 0 0 .3rem; font-size: .74rem; line-height: 1.8; color: var(--accent); } .slide-title { margin: 0; font-size: 1.05rem; line-height: 1.6; } .slide-text { margin: .4rem 0 0; font-size: .88rem; line-height: 1.85; color: var(--muted); } .bar { display: flex; align-items: center; gap: .6rem; margin-top: .7rem; } .toggle { padding: .35rem .9rem; border: 1px solid var(--line); border-radius: 999px; background: #fff; color: var(--muted); cursor: pointer; font-family: inherit; font-size: .82rem; line-height: 1.8; } .toggle:hover { border-color: var(--accent); color: var(--accent); } .dots { display: flex; gap: .4rem; margin-left: auto; } .dot { width: .5rem; height: .5rem; padding: 0; border: 0; border-radius: 999px; background: var(--line); cursor: pointer; } .dot[aria-current="true"] { background: var(--accent); } /* 進み具合の帯。止まっているときは animation-play-state で その場に留める。幅を0に戻すと「最初からやり直し」に見えてしまう。 */ .progress { height: 2px; background: var(--line); border-radius: 2px; overflow: hidden; } .progress > i { display: block; height: 100%; width: 100%; background: var(--accent); transform-origin: left; animation: fill var(--dwell) linear infinite; } .progress[data-paused="true"] > i { animation-play-state: paused; } @keyframes fill { from { transform: scaleX(0); } to { transform: scaleX(1); } } /* 動きを減らす設定では帯自体を出さない。自動送りもJS側で止める。 */ @media (prefers-reduced-motion: reduce) { .rail { scroll-behavior: auto; } .progress { display: none; } } ``` ```js const DWELL = 4000; const rail = document.getElementById("rail"); const slides = [...rail.children]; const dots = document.getElementById("dots"); const toggle = document.getElementById("toggle"); const progress = document.getElementById("progress"); let current = 0; let timer = null; // 「止まっている理由」を集合で持つ。真偽値1つで管理すると、 // ポインタを外した瞬間にフォーカス中でも再開してしまう。 const holds = new Set(); // 動きを減らす設定の人には、そもそも自動送りをしない。 const reduced = matchMedia("(prefers-reduced-motion: reduce)"); if (reduced.matches) holds.add("reduced-motion"); slides.forEach((_, i) => { const b = document.createElement("button"); b.type = "button"; b.className = "dot"; b.setAttribute("aria-label", `${i + 1}枚目へ`); b.addEventListener("click", () => { hold("user"); // 自分で選んだ人には、もう自動で動かさない go(i); }); dots.append(b); }); function go(i) { slides[i].scrollIntoView({ behavior: "smooth", block: "nearest", inline: "center" }); } function render() { [...dots.children].forEach((d, i) => d.setAttribute("aria-current", String(i === current)), ); const paused = holds.size > 0; progress.dataset.paused = String(paused); toggle.setAttribute("aria-pressed", String(holds.has("user"))); toggle.textContent = holds.has("user") ? "自動送りを再開する" : "自動送りを止める"; document.getElementById("state").textContent = paused ? `停止中(理由: ${[...holds].join(", ")})` : "自動送り中"; } function tick() { if (holds.size) return; go((current + 1) % slides.length); } function hold(reason) { holds.add(reason); clearInterval(timer); timer = null; render(); } function release(reason) { holds.delete(reason); if (holds.size === 0 && timer === null) timer = setInterval(tick, DWELL); render(); } // 1. ポインタが乗っている間 rail.addEventListener("pointerenter", () => hold("hover")); rail.addEventListener("pointerleave", () => release("hover")); // 2. 中の要素にフォーカスがある間(キーボード利用者が読んでいる) rail.addEventListener("focusin", () => hold("focus")); rail.addEventListener("focusout", () => release("focus")); // 3. タブが裏に回っている間。見えていないものを送っても意味が無く、 // 戻ってきたときに何枚も飛んでいるのは混乱のもとになる。 document.addEventListener("visibilitychange", () => document.hidden ? hold("hidden") : release("hidden"), ); // 4. 停止ボタン。押したら明示的な意思なので、こちらから再開はしない。 toggle.addEventListener("click", () => holds.has("user") ? release("user") : hold("user"), ); // 見えている面積がいちばん大きいものを現在地とする。 const ratios = new Map(slides.map((s) => [s, 0])); const io = new IntersectionObserver( (entries) => { for (const e of entries) ratios.set(e.target, e.intersectionRatio); let best = 0; for (let i = 1; i < slides.length; i++) { if (ratios.get(slides[i]) > ratios.get(slides[best]) + 0.01) best = i; } if (best === current) return; current = best; // 送るたびに帯を頭から流し直す。アニメーションを一度 none に // 戻して強制的に再計算させるのが、いちばん短く確実。 const bar = progress.firstElementChild; bar.style.animation = "none"; void bar.offsetWidth; bar.style.animation = ""; render(); }, { root: rail, threshold: [0, 0.25, 0.5, 0.75, 1] }, ); slides.forEach((s) => io.observe(s)); rail.addEventListener("keydown", (e) => { const d = e.key === "ArrowRight" ? 1 : e.key === "ArrowLeft" ? -1 : 0; if (!d) return; e.preventDefault(); hold("user"); go(Math.min(slides.length - 1, Math.max(0, current + d))); }); progress.style.setProperty("--dwell", DWELL + "ms"); if (!holds.size) timer = setInterval(tick, DWELL); render(); ``` **AIへの指示文** 自動送りのカルーセルを、止める条件まで含めて作ってください。 要件: - 停止理由を Set で持つ。真偽値1つで管理すると、ポインタを外した 瞬間にフォーカス中でも再開してしまう、という理由をコメントで書く。 - 止める条件は4つ。ポインタが乗っている間(pointerenter/leave)、 中にフォーカスがある間(focusin/focusout)、タブが裏に回っている間 (visibilitychange)、そして停止ボタン。 - 停止ボタンは必須。WCAG 2.2.2 が5秒以上続く自動的な動きに 停止手段を求めている、と書く。aria-pressed で状態を持つ。 - 利用者が自分でドットや矢印キーを操作したら、以降は自動で 動かさない。操作した人には意思がある、という理由を書く。 - prefers-reduced-motion では最初から自動送りをしない。 - 進み具合の帯は animation-play-state: paused で止める。 幅を0に戻すと「最初からやり直し」に見える、と書く。 - 自動送りはそもそも必要かを疑うべきで、載せたいものが多いという 作り手の都合であることが大半だ、という注意を本文に置く。 ## FAQ **カルーセルは使わないほうがいいと聞きました。** 「1枚目以外はほとんど見られない」という点は事実として受け止めてください。伝えたい情報が複数あるなら、縦に並べるほうが確実に読まれます。カルーセルが向くのは、写真のように「同じ種類のものを何点か見せる」場合です。逆に、性質の違う訴求を順番に流すのは向きません。 **なぜライブラリを使わないのですか。** いま自分で書く部分が少ないからです。かつてライブラリが引き受けていた仕事のうち、スワイプの慣性、位置の吸い付き、リサイズへの追随は scroll-snap がブラウザ側で済ませます。残るのは現在位置の表示と停止条件だけで、これは50行ほどです。数十KBの依存を足して、更新に追随し続ける価値があるかを比べてください。 **スクロールバーを消してもいいですか。** 主役の写真のように、別途サムネイルやドットで位置を示している場合だけにしてください。何の手がかりも無い状態でスクロールバーを消すと、横に動かせること自体に気づかれません。「見た目がすっきりする」は、操作できると分からないことの代償に見合いません。 **現在位置を scrollLeft から計算してはいけませんか。** 動きはしますが、余白・端末ごとの端数処理・ズーム時の丸めでずれます。IntersectionObserver なら「実際に見えているか」をブラウザが答えるので、計算も補正も要りません。コード量もこちらのほうが短くなります。 **自動送りはどのくらいの間隔が適切ですか。** 間隔を調整する前に、本当に自動で送る必要があるかを疑ってください。必要だと結論が出たなら、1枚を読み切れる時間(短い見出しでも4〜6秒)を確保し、そのうえで必ず停止手段を付けます。WCAG 2.2.2 は5秒以上続く自動的な動きに一時停止の手段を求めています。 **スワイプの実装は自分で書かなくてよいのですか。** 不要です。overflow-x: auto のスクロールに、端末ごとの慣性やゴムのような跳ね返りがすでに実装されています。touchstart と touchmove から自前で作ると、その端末らしい操作感を再現しきれず、かえって使いにくくなります。 --- # 狭い画面でも読める表を作る URL: https://vibe.itlibra.com/tech/table/ 技術: テーブルとデータ表示 (table) 検索キーワード: テーブル 更新日: 2026-09-01 ライセンス: CC0 1.0 Universal(完全無料・著作権フリー・商用利用OK・クレジット不要) 料金: ¥0 ## 概要 テーブルのサンプル集。横スクロールと見出し列の固定、コンテナクエリでのカード化、aria-sortを使った並べ替え、数字を比較できる組み方を、divに置き換えずtableのまま収録します。 表を div で組み直したくなるのは、たいてい狭い画面で崩れるからです。しかしそれをやると、行と列の対応が機械には読めなくなり、スクリーンリーダーでは意味の無い文字列の羅列になります。table 要素のまま解決する方法はあります。横スクロールと見出し列の固定、狭い画面でのカード化、並べ替えの状態を aria-sort で伝えること。どれも表の構造を保ったまま実現できます。 ## 向いている場面 - 料金プランの比較表 - 管理画面の一覧と並べ替え - 仕様や項目の比較 - 検索結果を表形式で見せる画面 ## つまずきやすい点 - divで組み直すと行と列の対応が機械に読めなくなる - 横スクロールできることが見た目で伝わらない - 数字が等幅でないと桁が揃わず比べられない - 並べ替えの状態を aria-sort で伝えていない - 漢字の氏名は localeCompare では五十音順にならない ## サンプルとコード ### 横スクロールと見出し列の固定 プレビュー: https://vibe.itlibra.com/sample/tbl-scroll/ 右にまだ続きがあることを示す影を、CSSだけで出しています。background-attachment の local と scroll の差を使う手法で、JavaScriptは1行もありません。 ```html
      2026年9月現在の月額料金(税込)
      プラン 月額 保存容量 同時接続 サポート SLA バックアップ 独自ドメイン 監査ログ
      フリー ¥0 1 GB 1 コミュニティ なし なし 不可 なし
      スタンダード ¥1,480 100 GB 5 メール(平日) 99.5% 日次・7日保持 1件 30日
      プロ ¥4,800 1 TB 25 メール(24時間) 99.9% 日次・30日保持 5件 1年
      エンタープライズ ¥19,800 無制限 100 専任担当 99.99% 時間単位・90日保持 無制限 無期限
      ``` ```css /* スクロールするのは table ではなく、外側の入れ物。 table 自体に overflow を掛けても効かない。 */ .table-scroll { overflow-x: auto; border: 1px solid var(--line); border-radius: 12px; background: #fff; /* 端に影を出して「まだ右がある」ことを見た目で伝える。 背景を2枚(白のグラデ=マスク/影)重ね、 background-attachment: local と scroll の差で スクロール位置に応じて影が出入りする。JavaScript は要らない。 */ background-image: linear-gradient(to right, #fff 30%, rgba(255, 255, 255, 0)), linear-gradient(to left, #fff 30%, rgba(255, 255, 255, 0)), linear-gradient(to right, rgba(20, 24, 36, .14), rgba(20, 24, 36, 0)), linear-gradient(to left, rgba(20, 24, 36, .14), rgba(20, 24, 36, 0)); background-position: left center, right center, left center, right center; background-repeat: no-repeat; background-size: 28px 100%, 28px 100%, 12px 100%, 12px 100%; background-attachment: local, local, scroll, scroll; } table { border-collapse: collapse; /* 折り返させない列があるので、中身なりに広がってよい。 width: 100% にすると狭い画面で潰れて読めなくなる。 */ min-width: 100%; font-size: .88rem; } caption { caption-side: top; text-align: left; padding: .8rem .9rem .2rem; font-size: .84rem; line-height: 1.85; color: var(--muted); } th, td { padding: .6rem .8rem; border-bottom: 1px solid var(--line); text-align: left; white-space: nowrap; line-height: 1.8; } thead th { font-size: .78rem; color: var(--muted); font-weight: 600; background: #fbfcfe; } tbody tr:last-child th, tbody tr:last-child td { border-bottom: 0; } /* 見出し列を左に貼り付ける。横に流したとき「どの行か」を 見失わないための処置で、比較表では必須に近い。 */ tbody th[scope="row"], thead th:first-child { position: sticky; left: 0; background: #fff; /* 固定した列と本体の境目。影が無いと重なりが分からない。 */ box-shadow: 1px 0 0 var(--line); } thead th:first-child { background: #fbfcfe; } /* 数字は等幅の数字書体にする。桁が揃わないと比較できない。 */ td.num { text-align: right; font-variant-numeric: tabular-nums; } ``` **AIへの指示文** 横スクロールする比較表を作ってください。 要件: - スクロールさせるのは table ではなく外側の入れ物にする。 table 自体に overflow を掛けても効かない、とコメントで書く。 - 端に影を出して「まだ右がある」ことを伝える。白のグラデ2枚と 影2枚を重ね、background-attachment の local と scroll の差で スクロール位置に応じて出入りさせる。JavaScript は使わない。 - 見出し列(th[scope="row"])を position: sticky で左に固定し、 box-shadow で本体との境目を出す。 - table には width: 100% ではなく min-width: 100% を指定する。 100% にすると狭い画面で潰れて読めなくなる、と書く。 - 列見出しに scope="col"、行見出しに scope="row" を必ず付ける。 これが無いとスクリーンリーダーがセルの対応を推測するしかない、と書く。 - caption で表の内容と時点を書く。 - 数値のセルは右寄せ+font-variant-numeric: tabular-nums にする。 - 外側に tabindex="0" と role="region"、aria-label を付ける。 - 日本語の本文なので line-height は 1.85、letter-spacing は 0 にする。 ### 狭い場所で行をカードに畳む プレビュー: https://vibe.itlibra.com/sample/tbl-cards/ 判断は画面幅ではなく置き場所の幅(コンテナクエリ)です。display を block に変えると表の内部構造が失われるので、role を書き足して意味だけ残しています。 ```html
      直近の注文
      注文番号 日付 金額 状態
      #10482 2026-08-28 ¥12,800 発送済み
      #10481 2026-08-27 ¥3,200 準備中
      #10480 2026-08-25 ¥48,600 発送済み
      #10479 2026-08-21 ¥980 キャンセル
      ``` ```css .table-host { container-type: inline-size; } table { border-collapse: collapse; width: 100%; font-size: .88rem; background: #fff; } caption { caption-side: top; text-align: left; padding: 0 0 .5rem; font-size: .84rem; line-height: 1.85; color: var(--muted); } th, td { padding: .6rem .8rem; border-bottom: 1px solid var(--line); text-align: left; line-height: 1.8; } thead th { font-size: .78rem; color: var(--muted); font-weight: 600; background: #fbfcfe; } td.num { text-align: right; font-variant-numeric: tabular-nums; } .badge { display: inline-block; padding: .05rem .5rem; border-radius: 999px; font-size: .74rem; line-height: 1.8; background: #eff6ff; color: var(--accent); } /* 置き場所が 520px を切ったらカードに畳む。 画面幅ではなく親の幅で判断するので、この表を サイドバーに置いても本文に置いても正しく振る舞う。 */ @container (max-width: 520px) { /* thead は隠すが display: none にはしない。 見出しの文字列は各セルの ::before で使い回すため、 HTML からは消さずに視覚的にだけ隠す。 */ thead { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; } /* table / tr / td の display を変えると、ブラウザ内部の表としての 扱いも変わる。行と列の対応を失わないよう、role を明示して 元の意味を保ったまま見た目だけを変える(HTML 側に role を書く)。 */ table, tbody, tr, th, td { display: block; } tr { border: 1px solid var(--line); border-radius: 12px; padding: .7rem .9rem; margin-bottom: .7rem; } tr:last-child { margin-bottom: 0; } th[scope="row"] { border-bottom: 1px solid var(--line); padding: 0 0 .5rem; margin-bottom: .5rem; font-size: .98rem; } td { display: flex; justify-content: space-between; gap: 1rem; border-bottom: 0; padding: .15rem 0; } /* 見出しは data 属性から出す。文字列を CSS に直接書くと、 HTML の見出しを変えたときに必ず食い違う。 */ td::before { content: attr(data-label); color: var(--muted); font-size: .8rem; flex: none; } td.num { text-align: right; } } ``` **AIへの指示文** 狭い場所ではカードに畳まれる表を作ってください。 要件: - 判断はメディアクエリではなくコンテナクエリで行う。同じ表を サイドバーに置いても本文に置いても正しく振る舞う、と書く。 - 520px を切ったら table / tbody / tr / th / td を display: block にする。 - display を変えると表としての内部構造が失われるので、HTML 側に role="table" から role="cell" まで書いて意味を保つ。これが この手法の必須条件だ、とコメントで書く。 - thead は display: none にせず、視覚的に隠す(position: absolute と clip-path)。見出しの文字列を各セルの表示に使い回すため。 - 畳んだときの項目名は td::before で data-label から出す。 文字列を CSS に直接書くと HTML と食い違う、と書く。 - 幅をスライダーで変えられるようにして、画面幅を変えずに 切り替わることを確かめられるようにする。 ### 並べ替えと aria-sort プレビュー: https://vibe.itlibra.com/sample/tbl-sort/ 矢印は aria-sort 属性から CSS で描いています。状態をクラスで二重に持たないので、表示と実際の並びがずれません。並べ替えたことは aria-live でも伝えます。 ```html
      担当者ごとの実績

      ``` ```css table { border-collapse: collapse; width: 100%; font-size: .88rem; background: #fff; border: 1px solid var(--line); border-radius: 12px; overflow: hidden; } caption { caption-side: top; text-align: left; padding: 0 0 .5rem; font-size: .84rem; line-height: 1.85; color: var(--muted); } th, td { padding: .6rem .8rem; border-bottom: 1px solid var(--line); text-align: left; line-height: 1.8; } thead th { background: #fbfcfe; font-size: .78rem; color: var(--muted); font-weight: 600; padding: 0; } tbody tr:last-child th, tbody tr:last-child td { border-bottom: 0; } td.num, th.num { text-align: right; font-variant-numeric: tabular-nums; } /* 見出しの中身は button。th 自体に click を付けると キーボードで押せず、押せることも伝わらない。 */ thead th button { display: flex; align-items: center; gap: .35rem; width: 100%; padding: .6rem .8rem; border: 0; background: none; color: inherit; font: inherit; cursor: pointer; text-align: inherit; } thead th.num button { justify-content: flex-end; } thead th button:hover { color: var(--accent); } /* 矢印は aria-sort から出す。状態をクラスで二重に持つと必ずずれる。 ::after なので読み上げには乗らない(読み上げには aria-sort が伝わる)。 */ thead th button::after { content: "↕"; opacity: .35; font-size: .8em; } thead th[aria-sort="ascending"] button::after { content: "↑"; opacity: 1; color: var(--accent); } thead th[aria-sort="descending"] button::after { content: "↓"; opacity: 1; color: var(--accent); } thead th[aria-sort] button { color: var(--accent); } ``` ```js // 並べ替えの元になるのは「表示用の文字列」ではなく元の値。 // 「¥1,000」を文字列比較すると 200 より小さくなる。 // 日本語の氏名は、漢字のままでは五十音順に並ばない。 // localeCompare は漢字を「読み」ではなく符号位置の照合順で比べるため、 // 青木・石田・上原…(あ→い→う)ではなく別の順序になる。 // 読みは漢字から機械的に求められないので、必ず別の項目として持つ。 // 実務でも「氏名カナ」を入力させるのはこのためで、後付けは効かない。 const DATA = [ { name: "青木", kana: "あおき", deals: 18, amount: 4820000, updated: "2026-08-28" }, { name: "石田", kana: "いしだ", deals: 7, amount: 12400000, updated: "2026-07-14" }, { name: "上原", kana: "うえはら", deals: 24, amount: 3150000, updated: "2026-08-30" }, { name: "遠藤", kana: "えんどう", deals: 11, amount: 980000, updated: "2026-06-02" }, { name: "大石", kana: "おおいし", deals: 3, amount: 22500000, updated: "2026-08-19" }, ]; const tbl = document.getElementById("tbl"); const rows = document.getElementById("rows"); const live = document.getElementById("live"); const yen = new Intl.NumberFormat("ja-JP", { style: "currency", currency: "JPY" }); let sortKey = null; let dir = "ascending"; function render() { const list = [...DATA]; if (sortKey) { const th = tbl.querySelector(`th[data-key="${sortKey}"]`); const type = th.dataset.type ?? "string"; const sign = dir === "ascending" ? 1 : -1; list.sort((a, b) => { // 並べ替えに使う値と、画面に出す値は別でよい。 // 氏名は表示が漢字、比較は読み(kana)で行う。 const key = th.dataset.sortBy ?? sortKey; const x = a[key], y = b[key]; // かな同士なら localeCompare で正しく五十音順になる。 // 濁点や長音の扱いも規則が決まっているので、自分で比較しない。 if (type === "string") return x.localeCompare(y, "ja") * sign; if (type === "date") return (new Date(x) - new Date(y)) * sign; return (x - y) * sign; }); } rows.innerHTML = ""; for (const r of list) { const tr = document.createElement("tr"); const th = document.createElement("th"); th.scope = "row"; th.textContent = r.name; const deals = document.createElement("td"); deals.className = "num"; deals.textContent = r.deals; const amount = document.createElement("td"); amount.className = "num"; amount.textContent = yen.format(r.amount); const updated = document.createElement("td"); // 機械が読む値は time の datetime に、人が読む形は本文に。 const t = document.createElement("time"); t.dateTime = r.updated; t.textContent = r.updated.replaceAll("-", "/"); updated.append(t); tr.append(th, deals, amount, updated); rows.append(tr); } } tbl.querySelectorAll("thead th[data-key]").forEach((th) => { th.querySelector("button").addEventListener("click", () => { const key = th.dataset.key; // 同じ列を押したら向きだけ反転。別の列なら昇順から始める。 dir = sortKey === key && dir === "ascending" ? "descending" : "ascending"; sortKey = key; // aria-sort は表の中で1つだけ。残したままにすると // 「2つの列で同時に並んでいる」という嘘になる。 tbl.querySelectorAll("thead th[data-key]").forEach((x) => x.setAttribute("aria-sort", x === th ? dir : "none"), ); render(); live.textContent = `${th.textContent.trim()}の${dir === "ascending" ? "昇順" : "降順"}で並べ替えました`; }); }); render(); ``` **AIへの指示文** 見出しを押すと並べ替わる表を作ってください。 要件: - 並べ替えの状態は aria-sort(ascending / descending / none)だけで持つ。 矢印はその属性から CSS の ::after で描く。状態をクラスで二重に 持つと必ずずれる、とコメントで書く。 - aria-sort が付くのは表の中で1列だけ。他は none に戻す。 残すと「2列で同時に並んでいる」という嘘になる、と書く。 - 見出しの中身は button にする。th 自体に click を付けると キーボードで押せず、押せることも伝わらない、と書く。 - 並べ替えの基準は表示用の文字列ではなく元の値にする。 「¥1,000」を文字列比較すると 200 より小さくなる、と書く。 - 型ごとに比較を分ける。かな文字列は localeCompare(x, "ja") に任せる。 - 日本語の氏名は、漢字のままでは五十音順にならない。localeCompare は 漢字を読みではなく符号位置の照合順で比べるため。データに読み(kana)を 別項目として持ち、表示は漢字・比較は読み、と分ける。読みは漢字から 機械的に求められないので後付けは効かない、という理由をコメントで書く。 - 日付は time 要素の datetime に機械可読な値を入れる。 - 並べ替えたことを aria-live="polite" で伝える。画面を見ていない人には 表が入れ替わったことが分からない、と書く。 ### 数字が比較できる組み方 プレビュー: https://vibe.itlibra.com/sample/tbl-figures/ 右寄せ・等幅数字・単位は見出しへ、の3点セット。同じデータを組まずに置いた表を下に並べたので、桁の読み取りやすさの差がそのまま見えます。 ```html
      四半期ごとの売上(2026年度)
      部門 Q1 (千円) Q2 (千円) 前期比
      法人営業 48,200 51,940 7.8%増加
      個人向け 9,140 8,305 9.1%減少
      保守サポート 122,600 122,600 0.0%増減なし
      その他 705 1,488 111.1%増加
      合計 180,645 184,333 2.0%増加
      ``` ```css table { border-collapse: collapse; width: 100%; font-size: .88rem; background: #fff; border: 1px solid var(--line); border-radius: 12px; overflow: hidden; } caption { caption-side: top; text-align: left; padding: 0 0 .5rem; font-size: .84rem; line-height: 1.85; color: var(--muted); } th, td { padding: .55rem .8rem; border-bottom: 1px solid var(--line); text-align: left; line-height: 1.8; } thead th { background: #fbfcfe; font-size: .78rem; color: var(--muted); font-weight: 600; } tbody tr:last-child th, tbody tr:last-child td { border-bottom: 0; } /* 数字の列は3点セットで組む。どれか1つでも欠けると比較しづらい。 1. 右寄せ … 桁の位置が縦に揃う 2. tabular-nums … 文字ごとの幅を等しくして桁をずらさない 3. 単位は見出しへ … セルの中に単位があると桁が右へずれる */ .num { text-align: right; font-variant-numeric: tabular-nums; } thead th .unit { font-weight: 400; opacity: .75; } /* 増減は色だけで示さない。色覚特性によっては区別が付かないので、 必ず記号も添える。記号は ::before で出し、 読み上げ用の語は視覚的に隠したテキストで別に持つ。 */ .delta { font-variant-numeric: tabular-nums; text-align: right; } .delta[data-dir="up"] { color: var(--up); } .delta[data-dir="down"] { color: var(--down); } .delta[data-dir="up"]::before { content: "▲ "; } .delta[data-dir="down"]::before { content: "▼ "; } .delta[data-dir="flat"]::before { content: "— "; color: var(--muted); } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; } /* 合計行は本文と区別する。tfoot は tbody の後ろに書いても ブラウザが最後に描くので、HTML 上の位置に依存しない。 */ tfoot th, tfoot td { border-top: 2px solid var(--line); background: #fbfcfe; font-weight: 600; } ``` **AIへの指示文** 数字を比較しやすい表を作ってください。 要件: - 数値の列は3点セットで組む。右寄せ、font-variant-numeric: tabular-nums、 単位はセルではなく見出しに書く。それぞれの理由をコメントで書く (セルに単位を入れると桁の位置が右へずれて縦に比較できない)。 - 増減は色だけで示さない。::before で ▲ ▼ — の記号を添え、 読み上げ用の「増加」「減少」は視覚的に隠したテキストで別に持つ。 赤と緑の区別が付きにくい人は珍しくない、と書く。 - 合計は tfoot に入れ、上罫線と背景で本文と区別する。 - 同じデータを「組んでいない状態」でもう1つ並べ、 桁の読み取りやすさの差が見えるようにする。 - caption で表の内容と時点を書く。 ## FAQ **表を div で組んではいけませんか。** 避けてください。行と列の対応が機械に読めなくなり、スクリーンリーダーでは意味の無い文字列の羅列になります。狭い画面で崩れるという理由で構造を捨てる必要はありません。横スクロール、見出し列の固定、カード化のいずれも table 要素のまま実現できます。 **カード化のために display を変えるのは問題ありませんか。** そのままだと問題があります。display を block にすると、ブラウザ内部での「表」としての扱いが失われ、scope や行と列の対応が効かなくなります。role="table" から role="row"、role="cell" まで書き足して意味を明示してください。この書き足しがこの手法の必須条件です。 **スクロールできることをどう伝えればよいですか。** 端に影を出すのが最も軽い方法です。白のグラデーションと影を重ね、background-attachment の local と scroll の違いを使えば、スクロール位置に応じて影が出入りします。JavaScript は要りません。あわせて外側に tabindex="0" を付け、キーボードでもスクロールできるようにしてください。 **font-variant-numeric: tabular-nums は必要ですか。** 数字を縦に並べて比較させるなら必要です。多くの書体では「1」が他の数字より細く、桁が縦に揃いません。tabular-nums を指定すると全ての数字の幅が等しくなり、桁の位置が揃います。右寄せだけでは揃わない、というのが見落とされがちな点です。 **並べ替えの状態はどう伝えればよいですか。** aria-sort を使ってください。値は ascending / descending / none の3つで、表の中で1列にだけ付けます。見た目の矢印はこの属性から CSS で描くと、状態が1か所に収まってずれません。さらに、並べ替えた直後に aria-live で「◯◯の昇順で並べ替えました」と伝えると、画面を見ていない人にも変化が届きます。 **日本語の氏名が五十音順に並びません。** 漢字を localeCompare で比べているからです。照合の基準になるのは読みではなく符号位置の順序なので、青木・石田・上原(あ→い→う)のようには並びません。読みは漢字から機械的に求められないため、データ側に「氏名カナ」を別項目として持ち、表示は漢字・比較は読み、と分けてください。実務のフォームでカナ入力を求めるのはこのためで、後から付け足すことはできません。 **セルを丸ごとリンクにしたいのですが。** td の中に a を置いてください。td 自体に click を付けると、キーボードで到達できず、新しいタブで開くこともできません。行全体を押せるようにしたい場合も、実体は行内のどこか1つのリンクにして、残りは装飾として扱うのが安全です。 --- # 欧文の既定値のまま組まない URL: https://vibe.itlibra.com/tech/typography/ 技術: 和文Web組版 (typography) 検索キーワード: 日本語 フォント 更新日: 2026-09-01 ライセンス: CC0 1.0 Universal(完全無料・著作権フリー・商用利用OK・クレジット不要) 料金: ¥0 ## 概要 日本語のWeb組版サンプル集。行間と字間の決め方、webフォントを載せずに済ませるフォントスタック、折り返しと禁則の指定、縦書きと縦中横を、悪い例と並べて比較できる形で収録します。 CSSの既定値は欧文を前提にしています。行間1.2、字間0、単語の切れ目での折り返し。この設定で日本語を組むと、行が詰まって読みにくく、句読点が行頭に来て、変な位置で改行されます。日本語には日本語の値があります。そして和文のwebフォントは1書体で数MBに達するため、載せるかどうかは設計判断になります。このサイト自身も和文はOS標準フォントで組み、その代わり行間と字間をビルドで機械検査しています。 ## 向いている場面 - 日本語の本文を長く読ませる記事やブログ - webフォントを載せずに見た目を整えたい案件 - 見出しの折り返し位置を制御したい画面 - 縦書きを使う和風・出版系のデザイン ## つまずきやすい点 - 既定の line-height 1.2 は欧文の値。和文には狭すぎる - 和文で letter-spacing をマイナスにすると読みにくくなる - 和文webフォントは1書体で数MB。安易に載せない - 英数字が混じると行の途中で不自然に折り返される ## サンプルとコード ### 行間と字間を決める プレビュー: https://vibe.itlibra.com/sample/typo-lineheight/ スライダーで値を動かすと、どこで読みにくくなるかが分かります。欧文の既定値・字間を詰めた例・和文の推奨値を同じ文章で並べました。 ```css /* 和文の本文はこの2つで決まる。 line-height: 1.7〜1.9 CSSの既定値 1.2 は欧文の値。ラテン文字は小文字の高さが低く、 行が詰まっても字面が重ならない。和文は正方形の枠いっぱいに 字面があるため、1.2 では上下の行が接触して読めなくなる。 letter-spacing: 0 和文のフォントは、字と字の間隔をすでに含んだ設計になっている。 ここをマイナスにすると設計を壊すだけで、読みやすくはならない。 見出しでも 0 から下げない。 */ .prose { line-height: 1.85; letter-spacing: 0; font-size: 1rem; } /* 1行の長さも読みやすさを決める。日本語は 35〜45 文字くらいが目安で、 em で指定すると文字サイズを変えても比率が保たれる。 */ .prose p { max-width: 40em; margin: 0 0 1em; } .prose p:last-child { margin-bottom: 0; } /* 見出しは本文より詰める。文字が大きいぶん、同じ比率だと 行間が開きすぎて塊に見えなくなる。それでも 1.3 は下回らない。 */ .prose h2 { font-size: 1.35rem; line-height: 1.5; letter-spacing: 0; margin: 0 0 .5em; } /* 短い標識(ボタン・タブ・ラベル)だけは、少し開けると締まる。 読ませる文ではないので、ここは例外にしてよい。 */ .tag { display: inline-block; font-size: .74rem; line-height: 1.8; letter-spacing: .12em; } ``` **AIへの指示文** 日本語の本文の行間と字間を比較できるページを作ってください。 要件: - line-height と letter-spacing をスライダーで変えられるようにし、 同じ文章に「欧文の既定値(1.2)」「字間を詰めた例(-0.05em)」 「和文の推奨値(1.85 / 0)」を並べて比較させる。 - 和文の本文は line-height 1.7〜1.9、letter-spacing 0 にする。 既定の1.2は欧文の値で、ラテン文字は小文字の高さが低いから行が 詰まっても重ならないが、和文は字面が枠いっぱいなので接触する、 という理由をコメントで書く。 - 和文フォントは字と字の間隔を含めて設計されているので letter-spacing をマイナスにしても読みやすくならない、と書く。 - 見出しは 1.5 程度まで詰めてよいが 1.3 は下回らない、と書く。 - 短い標識(ボタン・タブ・ラベル)だけは字間を開けてよい例外だ、と書く。 - 1行の長さは max-width を em で指定し、日本語は35〜45文字が目安と書く。 - 現在の値が推奨範囲を外れたら、その旨を画面に表示する。 ### webフォントを載せずに組む プレビュー: https://vibe.itlibra.com/sample/typo-fontstack/ webフォントは1つも読み込んでいません。指定ごとに「実際にどの書体が当たったか」をcanvasで測って表示しているので、自分の端末での結果が分かります。 ```html

      推奨のスタック — var(--font-sans)

      春はあけぼの。Web Typography 2026。

      OSのUIフォント — system-ui

      春はあけぼの。Web Typography 2026。

      指定なし(ブラウザ既定) — 初期値

      春はあけぼの。Web Typography 2026。

      Yu Gothic を Medium 抜きで指定 — "Yu Gothic", sans-serif(Windowsで細くなる)

      春はあけぼの。Web Typography 2026。

      ``` ```css /* 和文のwebフォントは1書体で 2〜5MB になる。1文字ずつ字形を 持つ必要があるためで、欧文の 20〜100KB とは桁が2つ違う。 サブセット分割で改善はするが、それでも重い。 「本文は OS 標準、必要ならロゴや見出しだけ webフォント」が 現実的な落としどころになる。 */ :root { /* 欧文と和文を1つのスタックに並べる。ブラウザは1文字ずつ 前から順に探すので、欧文書体を先に書けば英数字はそちらが当たり、 かなと漢字は後ろの和文書体が当たる。1行で混植ができる。 */ --font-sans: /* 1. 欧文(英数字だけをここで拾わせる) */ "Helvetica Neue", Arial, /* 2. macOS / iOS */ "Hiragino Kaku Gothic ProN", "Hiragino Sans", /* 3. Windows。Yu Gothic は素の指定だと細すぎるので Medium を先に置く。ここを外すと Windows だけ薄く見える。 */ "Yu Gothic Medium", "Yu Gothic", "Meiryo", /* 4. Android / Linux */ "Noto Sans JP", sans-serif; /* 等幅は数字とコードのため。和文は等幅にしない (和文の等幅は全角幅になり、英数字と揃わない)。 */ --font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; } body { font-family: var(--font-sans); } /* system-ui は「OSのUIフォント」を指す。ブラウザのUIと同じ見え方に なるので、アプリ的な画面では選択肢になる。ただし何が当たるかを こちらで決められないため、読み物の本文には向かない。 */ .ui-font { font-family: system-ui, sans-serif; } ``` ```js // 指定したスタックのうち、実際にどの書体が使われたかを調べる。 // getComputedStyle は「指定した文字列」を返すだけなので、描画して測る。 // // 和文でここが難しくなる理由: // 欧文なら「幅が変われば別の書体」で判定できる。しかし和文の字は // どれも同じ全角幅なので、書体が変わっても width は 1px も動かない。 // そこで width だけでなく、実際に墨が乗る範囲(actualBoundingBox) // まで含めた署名で比べる。書体が違えば字の高さや張り出しが変わる。 const GENERIC = ["monospace", "serif", "sans-serif"]; // 欧文と和文を別々に測る。同じスタックでも当たる書体が違うからで、 // それを見せるのがこのサンプルの目的。 const SAMPLES = { 欧文: "Web Typography 2026", 和文: "春はあけぼの年鬱" }; function signature(font, text, cv) { cv.font = font; const m = cv.measureText(text); return [ m.width, m.actualBoundingBoxAscent, m.actualBoundingBoxDescent, m.actualBoundingBoxLeft, m.actualBoundingBoxRight, ] .map((v) => Math.round((v || 0) * 100) / 100) .join("/"); } // 「その書体 → 総称ファミリ」の署名が、総称ファミリ単体と違えば、 // その書体が実在して使われている。総称ファミリ3種のどれか1つでも // 違えば有りとする(1種類だけだと偶然の一致で取りこぼす)。 function fontCovers(name, size, cv, sample) { return GENERIC.some( (g) => signature(`${size} "${name}", ${g}`, sample, cv) !== signature(`${size} ${g}`, sample, cv), ); } function actualFont(el, sample) { const cs = getComputedStyle(el); const size = cs.fontSize; const cv = document.createElement("canvas").getContext("2d"); const stack = cs.fontFamily .split(",") .map((s) => s.trim().replace(/^["']|["']$/g, "")); for (const name of stack) { // 総称ファミリ(sans-serif / system-ui など)は書体名ではなく // 「ブラウザに選ばせる」という指示。ここに到達したら、 // それ以降は実際の書体名を特定できない。 if (GENERIC.includes(name) || name.startsWith("ui-") || name === "system-ui") { return `${name}(ブラウザ任せ)`; } if (fontCovers(name, size, cv, sample)) return name; } // この方法の限界。当たっている書体が、総称ファミリの既定と // まったく同じ書体だった場合、測っても差が出ないので区別できない。 return "判別できず(総称ファミリと同じ字形)"; } for (const el of document.querySelectorAll("[data-probe]")) { const p = document.createElement("p"); p.className = "actual"; p.textContent = Object.entries(SAMPLES) .map(([label, sample]) => `${label}: ${actualFont(el, sample)}`) .join(" / "); el.after(p); } ``` **AIへの指示文** 和文のフォントスタックを比較できるページを作ってください。 要件: - 欧文書体を先、和文書体を後ろに並べた1本のスタックを定義する。 ブラウザは1文字ずつ前から探すので、英数字は欧文書体、かなと漢字は 和文書体が当たり、1行で混植ができる、という仕組みをコメントで書く。 - Windows 向けには "Yu Gothic Medium" を "Yu Gothic" より先に置く。 素の Yu Gothic は細すぎてWindowsだけ薄く見える、と理由を書く。 - macOS は Hiragino、Android/Linux は Noto Sans JP を含める。 - system-ui は OS の UI フォントを指し、何が当たるかを制御できないので 読み物の本文には向かない、と書く。 - 推奨スタック / system-ui / 指定なし / Yu Gothic を Medium 抜きで指定、 の4パターンを並べる。 - 各パターンで「実際に使われた書体名」を JavaScript で判定して表示する。 getComputedStyle は指定した文字列を返すだけなので、canvas の measureText で1書体ずつ描画幅を測って比較する、と方法をコメントで書く。 - 和文webフォントは1ウェイト2〜5MB、欧文は20〜100KBという容量差を 表で示す。判断は「まずOS標準で組み、足りない理由を言えるときだけ webフォントを検討する」順序だと書く。 ### 折り返しと禁則 プレビュー: https://vibe.itlibra.com/sample/typo-wrap/ 幅を変えると折り返し位置の違いが見えます。word-break や text-wrap の指定を変えた3つを並べたので、どれが和欧混植で使えないかがそのまま分かります。 ```html

      推奨 — line-break: strict / overflow-wrap: break-word / text-wrap

      見出しは各行の長さを均す

      仕様書は 2026年9月1日 に更新しました。詳細は https://example.com/docs/typography を参照してください。処理は、およそ 120ms で完了します。

      word-break: break-all — 和文の行末は揃うが、英単語が途中で切れる

      見出しは各行の長さを均す

      仕様書は 2026年9月1日 に更新しました。詳細は https://example.com/docs/typography を参照してください。処理は、およそ 120ms で完了します。

      指定なし — 小書きのかなが行頭に来ることがある

      見出しは各行の長さを均す

      しゃっくりがちょうど止まったところで、きゅうに雨が降ってきました。仕様書は 2026年9月1日 に更新しています。

      ``` ```css /* 和文は単語の切れ目が無いので、既定でどこでも折り返す。 そのままだと「行頭に句読点が来る」「英単語が途中で切れる」 といった読みにくさが出る。指定すべきは次の4つ。 */ /* 1. 禁則処理の強さ。 normal は小書きのかな(っゃゅょ)や長音を行頭に許すが、 strict にすると許さなくなる。読み物では strict のほうが整う。 句読点や閉じ括弧が行頭に来ないのは、どちらでも既定で守られる。 */ .prose { line-break: strict; } /* 2. 長い英数字やURLのはみ出し対策。 anywhere ではなく break-word にする。anywhere は はみ出しの計算方法まで変わり、隣の要素の幅に影響する。 */ .prose { overflow-wrap: break-word; } /* 3. 英単語を途中で切らない。 word-break: break-all は和文の行末は揃うが、英単語が 無意味な位置で切れる。和欧混植では使わない。 */ .prose { word-break: normal; } /* 4. 行の分け方。 見出しは balance で各行の長さを均す(最大4行程度まで)。 本文は pretty で、最終行が1文字だけ残る状態を避ける。 */ .prose h2 { text-wrap: balance; } .prose p { text-wrap: pretty; } /* 切りたくない語のまとまりは、その範囲だけ nowrap にする。 製品名・数値と単位・日付などが対象。 */ .nowrap { white-space: nowrap; } ``` **AIへの指示文** 日本語の折り返しと禁則を比較できるページを作ってください。 要件: - 表示幅をスライダーで変えられるようにする。 - line-break: strict を指定する。句読点や閉じ括弧が行頭に来ないことは 指定しなくても守られ、strict が変えるのは小書きのかな(っゃゅょ)と 長音を行頭に許すかどうかだ、と正確にコメントで書く。 - overflow-wrap: break-word を指定する。anywhere は はみ出しの計算方法まで変わって隣の要素の幅に影響する、と書く。 - word-break: break-all は使わない。和文の行末は揃うが英単語が 無意味な位置で切れる、という理由を書き、悪い例として並べて見せる。 - 見出しに text-wrap: balance、本文に text-wrap: pretty を当てる。 - 切りたくない語(製品名・日付・数値と単位・URL)は span で囲んで white-space: nowrap にする。 - 日本語の本文なので line-height は 1.85、letter-spacing は 0 にする。 ### 縦書きと縦中横 プレビュー: https://vibe.itlibra.com/sample/typo-vertical/ writing-mode の1行で始まりますが、その先で height の意味も line-height の向きもスクロール方向も入れ替わります。ルビと圏点、縦中横まで入れました。 ```html

      縦書き(writing-mode: vertical-rl)

      はるはあけぼの。やうやう白くなりゆく山ぎは、すこしあかりて、むらさきだちたる雲のほそくたなびきたる。

      この文章は圏点で強調しています。日本語の強調は、太字や下線ではなく点を打つのが古くからの方法です。

      この記事は26日に公開しました。行間は横書きより20%ほど広く取ってあります。2桁の数字は縦中横で1マスに収めますが、4桁の年号のような長い数字には使いません。潰れて読めなくなるからです。

      同じ指定を横書きで(比較用)

      はるはあけぼの。この文章は圏点で強調しています。ルビも圏点も、書き方は縦横で変わりません。

      ``` ```css .tategaki { /* 縦書きの本体。右の行から左へ流れる。 */ writing-mode: vertical-rl; /* 英数字の向き。既定(mixed)は英数字が横倒しになる。 upright にすると1文字ずつ立つが、単語が読みにくくなるので 本文では mixed のままにして、必要な箇所だけ縦中横にする。 */ text-orientation: mixed; /* 縦書きでは line-height が「行と行の左右の間隔」になる。 横書きより広めに取ると読みやすい。 */ line-height: 2; /* 高さを決めないと1行に全部入ってしまう。縦書きでは height が「1行の長さ」にあたる。 */ height: 17em; padding: 1.2rem 1.4rem; border: 1px solid var(--line); border-radius: 12px; background: #fff; font-family: var(--serif); font-size: 1rem; /* はみ出したら横方向にスクロールする。縦書きでは 行が増える方向が「左」なので、これは横スクロールになる。 */ overflow-x: auto; overscroll-behavior-x: contain; } .tategaki p { margin: 0 0 1em; } .tategaki p:last-child { margin-bottom: 0; } /* 縦中横。2桁までの数字を1マスに収めて横向きに組む。 「26日」「20%」のような箇所に使う。3桁以上や4桁の年号に 当てると潰れて読めなくなるので、対象は2桁までにとどめる。 */ .tcy { text-combine-upright: all; } /* 圏点(傍点)。日本語の強調は、太字や下線ではなく点を打つのが 伝統的な方法で、縦書きとの相性がよい。 */ .kenten { text-emphasis: filled dot; text-emphasis-position: over right; } /* ルビは ruby / rt。縦書きでも横書きでも同じ書き方で通る。 */ ruby { ruby-position: over; } rt { font-size: .5em; line-height: 1; } ``` **AIへの指示文** 縦書きの本文を作ってください。 要件: - writing-mode: vertical-rl で縦書きにする。 - text-orientation は mixed のままにする。upright は1文字ずつ立つが 単語が読みにくくなるので、必要な箇所だけ縦中横にする、と書く。 - 縦書きでは height が「1行の長さ」、line-height が「行同士の左右の 間隔」になる。横書きの感覚のまま値を決めると必ずずれる、と コメントで書く。height を指定しないと1行に全部入ってしまう点も書く。 - 2桁の数字に text-combine-upright: all(縦中横)を当てる。 3桁以上に当てると潰れて読めないので2桁までにとどめる、と書く。 - 強調は太字や下線ではなく圏点(text-emphasis: filled dot)を使う。 日本語の伝統的な強調方法だと書く。 - ルビは ruby と rt で組む。書き方は縦横で変わらない。 - はみ出しは overflow-x: auto で受ける。縦書きでは行が増える方向が 左なので横スクロールになる、と書く。 - 比較用に、同じ指定を横書きにしたものを下に並べる。 ## FAQ **和文の line-height はいくつが適切ですか。** 本文は 1.7〜1.9 の範囲にしてください。CSSの既定値 1.2 は欧文の値です。ラテン文字は小文字の高さが低いため行が詰まっても字面が重なりませんが、日本語は正方形の枠いっぱいに字面があるので、1.2 では上下の行が接触して読めなくなります。見出しは文字が大きいぶん詰めてよく、1.3〜1.5 が目安です。 **和文で letter-spacing を詰めてはいけませんか。** 読ませる文では詰めないでください。和文のフォントは字と字の間隔をすでに含んだ設計になっているので、削っても締まって見えるだけで読みにくくなります。欧文で使われる「少し詰めて締める」手法は和文には移植できません。例外は、ボタンやタブのような短い標識で、ここは 0.1〜0.5em ほど開けると締まって見えます。 **和文のwebフォントは使わないほうがよいですか。** 使ってはいけないわけではありませんが、容量が桁違いだという前提で判断してください。欧文が1ウェイト20〜100KBなのに対し、和文は2〜5MBです。漢字だけで数千字ぶんの字形を持つためで、サブセット分割しても欧文より重いままです。現実的な落としどころは「本文はOS標準フォント、ロゴや見出しだけwebフォント」です。 **Windowsだけ文字が細く見えます。** フォントスタックに "Yu Gothic Medium" が入っていない可能性が高いです。素の "Yu Gothic" を指定すると Regular が当たり、Windows の描画では細く薄く見えます。"Yu Gothic Medium" を "Yu Gothic" より先に書いてください。1行足すだけで解決します。 **word-break と overflow-wrap はどちらを使いますか。** 和欧混植では overflow-wrap: break-word です。word-break: break-all は和文の行末がきれいに揃う一方、英単語やURLが意味のない位置で切れます。overflow-wrap: break-word なら、はみ出す長い文字列だけを折り返し、通常の英単語は保たれます。anywhere は避けてください。はみ出しの幅計算まで変わり、隣の要素の幅に影響します。 **line-break: strict は何を変えるのですか。** 小書きのかな(っ・ゃ・ゅ・ょ)と長音記号を行頭に許すかどうかです。句読点や閉じ括弧が行頭に来ないことは、指定しなくてもブラウザが守ります。読み物では strict にすると行頭が整いますが、1行が極端に短い場合は行末の空きが目立つので、幅とあわせて判断してください。 **縦書きは実務で使えますか。** 主要ブラウザで実装は揃っているので、技術的には使えます。ただし横書きの感覚がほぼ通用しません。height が1行の長さになり、line-height が行同士の左右の間隔になり、はみ出しは横スクロールになります。既存の横書き用CSSを流用せず、縦書きに切り替えてから値を決め直してください。