Motion 9 rules

transition の duration は 0.15s か 0.2s の二値。easing は書かない(既定の ease)

色・背景色の変化は 0.2s、アイコンや戻るボタンなど小さな chrome は 0.15s。0.18s や 0.1s は例外扱い。

  • color 0.2s
  • background-color 0.2s
  • transform 0.15s ease
blur-up の解除は例外なく filter 250ms ease-in-out

5 アプリと共有パッケージで同一の値。プレースホルダは幅 20px、ぼかしは blur(36px)。

  • transition: filter 250ms ease-in-out
  • filter: blur(36px)
  • width: 20
:active は押し込みのスケール。要素が大きいほど縮小率は小さい

カード 0.99、タイルやチップ 0.98、ボタン 0.95、アイコンボタン 0.9。

  • scale(0.99)
  • scale(0.98)
  • scale(0.95)
  • scale(0.9)
hover は彩度ではなく明度で示す。--c-sub から --c-text へ

opacity を下げる hover は使わない。既定色が --c-text のブランドリンク(共有ヘッダーのタイトル)は逆向きに --c-sub へ、画像(memo のアバター)は outline を --c-border から --c-sub へ。

  • color: var(--c-sub)
  • &:hover { color: var(--c-text) }
リスト行・カードの hover 背景は --c-surface の 50% 透過で固定

8 箇所で完全一致。ベタ塗りの --c-surface はメニュー項目など「明確に選択する」要素だけに使う。

  • background-color: oklch(from var(--c-surface) l c h / 50%)
popover / dialog の開閉は入り口と出口の両端を必ず対で宣言する

@starting-style と :not([open]) / :not(:popover-open) を組にし、display と overlay を allow-discrete で 0.2s。移動量は popover 4px、dialog 8px。

  • @starting-style
  • transition-behavior: allow-discrete
  • translate: 0 4px
  • translate: 0 8px
ループするアニメーションはスピナーの回転だけ

1s linear infinite。装飾目的の無限アニメーションは持たない。

  • animation: spin 1s linear infinite
スクロール駆動アニメーションは端フェードのマスクにだけ使う

時間ベースの演出には使わない。横スクロールするテーブルとカテゴリナビの 2 箇所。

prefers-reduced-motion では transition を潰し、animation-duration は触らない

共有 base.css で一括。唯一の @keyframes はスクロール駆動のマスクなので animation は残す。装飾アニメは局所で opt-in する(art の view transition)。

  • transition-duration: 0.01ms !important
  • scroll-behavior: auto !important
chrome(ナビ・フッター・メタ)のリンクは既定で下線なし。hover で下線か色変化
  • a { color: inherit; text-decoration: none }
本文リンクは色付き + text-underline-offset。下線の太さは 1px

prose は --c-link、トップページは本文色のまま下線色だけ --c-sub の 50% に落とす。

  • text-decoration-thickness: 1px
  • text-underline-offset: 4px
外部リンクは target=_blank + rel=noreferrer

noreferrer は noopener を含意するので、rel はこの一語で足りる。

  • target="_blank" rel="noreferrer"
visited 色を変えるのは prose 本文だけ

--c-link-visited の利用箇所は prose.css のみ。それ以外は visited でも色を据え置く。

  • --c-link-visited
外部リンクは末尾に move-up-right アイコンをインラインで添える

0.625〜0.75rem、vertical-align -0.05em。

  • width: 0.625rem
  • vertical-align: -0.05em

Typography 8 rules

見出しは共有 base で palt + text-wrap: balance。本文側の palt は .palt で明示付与
  • font-feature-settings: "palt"
  • text-wrap: balance
  • .palt
文字サイズは小さい側に寄せる。本文は --fs-sm、--fs-base 以上は滅多に使わない

--fs-xs 約 60 箇所、--fs-sm 約 43 箇所、--fs-2xs 約 24 箇所に対し、--fs-base 以上は 5 箇所。

  • --fs-sm
  • --fs-xs
  • --fs-2xs
数字が並ぶ箇所は tabular-nums を付ける

日付・件数・ランク・年号・ページ番号。全アプリに分布。日付の書式は YYYY.MM.DD。

  • font-variant-numeric: tabular-nums
  • YYYY.MM.DD
日本語の改行は BudouX。CSS 側は .budoux の 2 プロパティだけ

<Budoux> でラップするか、me のように rehype で本文へ自動適用する。

  • word-break: keep-all
  • overflow-wrap: anywhere
text-wrap: pretty は使わない

長い CJK 本文で Chrome のレンダラが落ちるため、prose では明示的に回避している。

letter-spacing は原則 0。例外は mono キャプションの 0.02em
  • letter-spacing: 0.02em
行送りは UI が --lh-sm、読み物が --lh-base、prose 本文だけ 2
  • --lh-sm: 1.5
  • --lh-base: 1.75
  • line-height: 2

Spacing & shell 7 rules

body グリッドの行名は [top] auto [main-start] 1fr [main-end] auto [bottom]

共有ヘッダー・フッターはこの行名に自分を配置する。

  • grid-template-rows: [top] auto [main-start] 1fr [main-end] auto [bottom]
本文カラムは 640px 未満で padding-inline 1rem、640px 以上で 0

42rem をフルに使う。

  • padding-inline: 1rem
  • @media (width >= 640px) { padding-inline: 0 }
ブレークポイントは 640 / 768 / 1280px の 3 段。range syntax で書く
  • (width >= 640px)
  • (width >= 768px)
  • (width >= 1280px)
右サイドバー(目次)は 1280px 以上でのみ fixed で出す
  • position: fixed
  • top: 13rem
  • right: 2rem
縦リズムは --space-1 / 2 / 3 を最小単位に、セクション間は --space-8〜12

margin / padding / gap でトークンと一致する値は var(--space-*) で書く。0.125rem や 2.5rem のようにトークンのない値だけ生 rem。

  • --space-1: 0.25rem
  • --space-3: 0.75rem
  • --space-8: 2rem
  • --space-12: 3rem
幅の上限は用途別に 3 種: 読み物 42rem、画像 1440px、ドキュメント 84rem
  • --content-width: 42rem
  • min(100% - 5rem, 1440px)
  • --site-width: 84rem
コンテナクエリは局所レイアウトにだけ使う

メディアクエリの代替ではない。alert ブロックと design のステージ列。

Images 8 rules

blur-up はプレースホルダを背景に敷き、.image-loaded で blur(36px) を解く
  • getImage({ width: 20, height: 11, format: "avif" })
  • filter: blur(36px)
解除ハンドラは onload と onerror の両方をインライン属性で付ける

共有ヘルパ blurLoadHandlers を使う。

  • blurLoadHandlers(".blur-load")
配信形式は avif(写真は webp 併記)、quality 80、widths + 明示 sizes
  • format="avif"
  • quality={80}
  • widths={[...]}
  • sizes="..."
遅延読み込みは loading=lazy と decoding=async を対で。優先画像は eager + fetchpriority=high
  • loading="lazy" decoding="async"
  • loading="eager" fetchpriority="high"
CLS 回避のため aspect-ratio を CSS 変数で流し込む
  • aspect-ratio: var(--aspect-ratio)
装飾画像と、キャプションが本文を持つ画像は alt を空にする

二重読み上げを避ける。me の図版はキャプションと alt が同文のときだけ空にする(figureAlt)。

  • alt=""
アバターは円形 + 内側 1px の outline
  • border-radius: 50%
  • outline: 1px solid var(--c-border)
  • outline-offset: -0.5px

Interaction & focus 10 rules

フォーカスリングは 1 種。クリップされる要素だけ負の offset で内側に描く

full-bleed な行やカードなど 7 箇所で同じ意図。

  • outline-offset: calc(-1 * var(--ring-width))
スクロールバーはページ 8px、内側スクローラ 3px + --c-sub。Firefox は @supports で分岐
  • ::-webkit-scrollbar { width: 3px }
  • @supports (-moz-appearance: none)
モーダルとメニューは JS を書かず popover / dialog + invoker commands で開く
  • command="show-modal"
  • commandfor
  • popovertarget
  • closedby="any"
アンカーポジショニングには必ず @supports not のフォールバックを添える
  • @supports not (anchor-name: --x)
  • @position-try
テキスト入力は 16px 未満にしない

iOS がフォーカス時にズームするのを防ぐ。

  • font-size: max(16px, var(--fs-base))
内側スクローラには overscroll-behavior: contain を付ける
  • overscroll-behavior: contain
カード全体クリックは data-href + 親のイベント委譲。選択中テキストと内部リンクは除外
  • data-clickable
  • data-href
ボタンは共有 .btn と 3 つの variant。個別実装は chrome の丸アイコンボタンだけ
  • height: 2.25rem
  • --radius-md
  • --fs-sm
  • --fw-medium
  • 1.5rem の丸ボタン
無効状態は opacity + pointer-events: none
  • opacity: 0.5
  • opacity: 0.25
  • pointer-events: none

Icons 4 rules

アイコンは Lucide 由来の線画。属性は定型
  • viewBox="0 0 24 24"
  • fill="none"
  • stroke="currentColor"
  • stroke-width="2"
  • stroke-linecap="round"
ブランドアイコンだけ fill=currentColor + 独自 viewBox

Feedback & loading 5 rules

検索の状態表示は p[role=status]。空のときも sr-only で残し、ライブリージョンを外さない
検索結果の文言は 2 アプリで同一
  • 「{query}」に一致する〜はありません
  • {total} 件中 {shown} 件を表示
無限スクロールは IntersectionObserver + rootMargin 200px。スピナーは最低 500ms 出す

終端と失敗はテキストで告知する。

  • rootMargin: "200px"
  • 500ms
ローディング枠は grid 中央寄せ、padding 2rem、--fs-xs
空状態は 1 行の日本語テキストだけ。イラストやアイコンは置かない
meta color-scheme を CSS より先に置く

ダーク環境での白フラッシュ防止。同じコメントが 5 アプリにある。lang は文書の言語に合わせる(art / lgtm は en)。

  • <meta name="color-scheme" content="light dark">
favicon は 4 点セット
  • favicon.ico sizes="32x32"
  • icon.svg
  • apple-touch-icon.png
  • manifest.webmanifest
Web フォントは読まない。本文は system-ui、Inter は OG 画像専用
  • font-family: system-ui, sans-serif
OG カードは 1200×630、左下寄せ、padding 80px、fontSize 70px
description は 200 字に切る。既定値をどのアプリも上書きしない
  • truncateDescription
Umami は websiteId と domains を対で渡す。イベント名は kebab-case、計測対象はナビと検索だけ
  • data-umami-event
  • data-umami-event-label
JSON-LD は script[is:inline][type=application/ld+json] に set:html で流す

Dark mode 4 rules

ダークの境界線は色ではなく不透明度で作る。ライトはグレーの実色
  • oklch(var(--uchu-yang-raw) / 10%)
影とグローはダークでだけ効かせ、ライトでは透明に落とす
外部由来の画像(favicon)はダークでもライト地を敷く
box-shadow は原則 --shadow-sm だけ。多層の落ち影はモーダルとポップオーバーの 3 箇所に限る
  • 0 14px 28px -6px
  • 0 2px 4px -1px

Other patterns 5 rules

リセットは kiso.css を @layer reset で読む。import 4 行の順序は全アプリ共通
アプリ固有の色は @layer tokens 内で --c-* を追加する

コンポーネントから --uchu-* を直接使わない。

  • --c-star
  • --c-faint
  • --c-bg-gray
  • --c-codeblock-bg
アンカーの飛び先には scroll-margin を置く
  • scroll-margin: 3rem
  • scroll-margin-top: 1.25rem
サーフェスに載せる小アイコンは 2〜2.5rem 角、--radius-md、--c-surface、--shadow-sm
行数を切るときは text-overflow ではなく -webkit-line-clamp
  • -webkit-line-clamp: 2
  • -webkit-line-clamp: 1

全 72 項目。根拠のリンクは GitHub の main ブランチを指す。