Skip to content
fandhe-frontendcore v0.4.3
GitHub

pre-styled-ui motion feature ガイド

本ドキュメントはイシュー #2416 を契機に作成しました。fandhe-frontend-pre-styled-ui の Cargo feature motion(既定 off)を利用者の目的別に説明します。機械可読な一次情報は crates/pre-styled-ui/Cargo.toml の [features] 直前コメント・crates/pre-styled-ui/src/lib.rs クレート doc「Cargo feature motion」節であり、本書はそれを読みやすく再構成したものです。

1. 対象読者・要約

  • fandhe-frontend-pre-styled-ui を直接依存に持ち、アニメーション拡張(spring 近似イージング等、親トラッキング #2379 配下)を使いたい利用者
  • motion feature を有効化しなければ、crate サイズ・ビルド時間・Theme::to_css(crates/pre-styled-ui/src/theme.rs)の処理量・CSS 出力サイズのいずれも変わりません(docs/design/motion-reference-adoption-policy.md §7「ゼロコスト方針」)。この保証は crates/pre-styled-ui/tests/motion_zero_cost.rs の 4 テストが CI で fail-closed に固定しています。

2. 有効化手順

TOML
[dependencies]
fandhe-frontend-pre-styled-ui = { version = "0.192", features = ["motion"] }

有効化すると fandhe-animation(外部依存ゼロ・forbid(unsafe_code)、プラットフォーム非依存のアニメーション演算基幹)が依存グラフに加わります。

有効化で使えるようになる公開 API:

  • 共通 @keyframes ライブラリ(イシュー #2382、crates/pre-styled-ui/src/motion.rs):
    • fandhe_frontend_pre_styled_ui::motion::KEYFRAMES_CSS: 共通 @keyframes ライブラリの CSS 全文(フェード・ズーム・4 方向スライド・バウンス・シェイクの 10 種 + prefers-reduced-motion: reduce 再定義ブロック)。StyleSheet へ取り込む場合は sheet.push_css(motion::KEYFRAMES_CSS) を使う。
    • fandhe_frontend_pre_styled_ui::motion::FADE_IN_KEYFRAMES_NAME 等(10 個): 各 @keyframes の名前定数。decl("animation-name", motion::FADE_IN_KEYFRAMES_NAME) のように参照する。
    • Theme::to_css_with_keyframes(): Theme::to_css() の出力へ KEYFRAMES_CSS を追記して返す opt-in メソッド(Theme::to_css 本体は無変更のまま)。
  • stagger CSS ユーティリティ(イシュー #2384、crates/pre-styled-ui/src/recipe.rs): recipe::STAGGER_INDEX_VAR・recipe::stagger_delay_declaration(step)・recipe::stagger_index_style(index)・recipe::SlotRecipe::stagger_delay(slot, step)。要素ごとの「起点からの距離」を --fandhe-motion-stagger-index custom property として SSR/アプリコード側が ("style", &stagger_index_style(i)) で書き出し、recipe 側は stagger_delay(slot, MotionDuration::Fast) の 1 行で animation-delay: calc(var(--fandhe-motion-stagger-index, 0) * var(--fandhe-motion-duration-fast)) を登録できます。fandhe_animation:: timeline::Stagger::new(each).delay(index, total)(from: First)と意味論が一致することを recipe::stagger_parity_tests が固定しています。
  • scroll-driven reveal(イシュー #2385、crates/pre-styled-ui/src/recipe.rs): recipe::SlotRecipe::scroll_reveal(slot)。animation-timeline: view() 対応ブラウザではスクロールインに応じてフェード+わずかな上方向スライドが発火し、非対応ブラウザでは @supports ブロックごと無視されるため常に可視のまま安全に劣化します(JS 不要)。
  • scroll-linked parallax / sticky progress(イシュー #2534、crates/pre-styled-ui/src/recipe.rs): recipe::SlotRecipe::parallax(slot, recipe::ParallaxSpeed)・recipe::SlotRecipe::sticky_progress(slot)。view() timeline の cover/contain 相当区間で translate/opacity・scale を線形補間します。非対応ブラウザ向けには @supports not (animation-timeline: view()) ブロックで --fandhe-motion-scroll-progress(fandhe_frontend_animation::scroll_driver::SCROLL_PROGRESS_PROPERTY)を読む calc() フォールバックを出力しますが、このフォールバックが実際に進捗値を得るには対象要素へ data-fandhe-scroll-progress 属性を付与し fandhe-frontend-wasm-full の scroll-driver feature(既定 on)を配線する必要があります(属性値 "cover"/"contain" でネイティブと同じ進捗範囲を選べます。属性なしでは静止したまま安全に劣化します)。
  • view-transition-name ヘルパ(イシュー #2515、crates/pre-styled-ui/src/recipe.rs): recipe::view_transition_name_declaration(name)。共有要素遷移(motion.dev AnimateView 相当)の固定名を割り当てる静的なケース向けで、name はコンパイル時に確定する &'static str のみ受け付けます。keyed list の行等、値ごとに一意な動的名前が必要な場合は fandhe_frontend_wasm_full::view_transition_name::set_view_transition_name(wasm-full 側、view-transition-name feature 既定 on)を使ってください。
  • border-beam 装飾オプション(イシュー #2531、crates/pre-styled-ui/src/border_beam.rs): 単体コンポーネントではなく、card / pricing 等の任意要素へ外側からラップして付与する opt-in 装飾です。
    • border_beam::BORDER_BEAM_CLASS("fd-border-beam"): 装飾したい要素を el("div", vec![("class", BORDER_BEAM_CLASS)], vec![inner]) のように素の <div> でラップするための class 名。
    • border_beam::BORDER_BEAM_CSS: 周回する光の CSS 全文。sheet.push_css(border_beam::BORDER_BEAM_CSS) で個別に取り込めます。
    • Theme::to_css_with_border_beam(): Theme::to_css() の出力へ BORDER_BEAM_CSS を追記して返す opt-in メソッド。
    • 4 トークン(--fandhe-border-beam-width/-color/-spread/ -duration)で太さ・色・光弧の長さ・周期を上書きできます。
    • @property によるカスタムプロパティ型登録は使いません(<angle> 等 < を含むリテラルは本クレートの CSS 不変条件に抵触するため)。代わりに transform: rotate() で光源レイヤーを回転させる技法を採ります。光源レイヤーはラッパーの長辺基準(2 * max(幅, 高さ))の正方形にサイズされ、縦長・横長いずれのラッパーでも回転角度に関わらず外周全体を覆います。ラッパーの overflow: hidden が子要素の box-shadow・フォーカスリングの outline も一緒に切り抜く既知の制約があります(詳細は border_beam モジュール doc「技術選定」節参照)。
    • prefers-reduced-motion: reduce 下では回転を止め、静的な border へフォールバックします。
  • text アニメーション部品(イシュー #2532、crates/pre-styled-ui/src/text_reveal.rs): split-text reveal(chars/words、SSR + CSS のみで完結)と typewriter/scramble(マークアップのみ。実際の文字送り・乱数置換は wasm-full 側 text-animation feature 既定 on が担う)を提供します。
    • text_reveal::chars(content)/text_reveal::words(content): 分割前の全文を visually-hidden な .fd-text-reveal__sr で保持しつつ、aria-hidden="true" の分割済みレイヤー(.fd-text-reveal__unit)を @keyframes fd-text-reveal-in で --fandhe-motion-stagger-index(recipe::stagger_index_style)に応じて順に現します。
    • text_reveal::typewriter(content, duration_ms)/ text_reveal::scramble(content, duration_ms): data-fandhe-typewriter/ data-fandhe-scramble を付与し、.fd-text-reveal__display へ初期表示として全文を書きます(JS 不在・reduced-motion のいずれでも全文が見える)。duration_ms に Some(ms) を渡すと属性値へ上書きします。
    • text_reveal::TEXT_REVEAL_CSS/Theme::to_css_with_text_reveal(): border_beam と同型の pure append。公開トークンは --fandhe-text-reveal-step(既定 40ms)1 件のみです。
    • prefers-reduced-motion: reduce 下では .fd-text-reveal__unit の animation を none へ縮退します(typewriter/scramble は元々全文表示のため追加の CSS は不要)。
  • field / input の Motion+ 由来フォームアニメーション(イシュー #2545、crates/pre-styled-ui/src/forms_motion.rs): field/input 自体は変更せず、既存 anatomy へ参照するだけの opt-in 追加 CSS 4 種です。
    • forms_motion::SHAKE_CSS: input[data-invalid] へ単発の水平シェイクを適用します(headless 側が既に出力する data-invalid を利用、新規配線不要)。
    • forms_motion::UNDERLINE_GROW_CSS: input::InputVariant::Flushed 限定で、:focus-visible 時に下線(background-size)を 0% → 100% へ伸長します。
    • forms_motion::FLOATING_LABEL_CLASS("fd-field-floating-label")/ forms_motion::FLOATING_LABEL_CSS: field::root 全体ではなく input/label の 2 要素だけを el("div", vec![("class", FLOATING_LABEL_CLASS)], vec![input, label]) でラップし、その wrapper を field::root の children の 1 要素として渡して opt-in します(helper-text/error-text は wrapper の外・root 直下の兄弟のまま)。wrapper 自身が位置決め基準(position: relative)になるため、helper-text/error-text の有無・行数でラベルの縦位置がずれません。呼び出し側の契約: (a) wrapper の children は input → label の順(一般兄弟結合子 ~ は後続要素にのみ効くため)、(b) <input> へ placeholder=" "(半角スペース 1 文字)を指定すること。
    • forms_motion::error_text_presence_css(): error-text slot へ recipe::SlotRecipe::presence_transition(既存 #2497 の共通 preset)を適用し、検証結果表示の出現・消失をフェード遷移で表現します(「送信中→完了」の本来の意味であるスピナー→チェックマーク変化は field/input の headless anatomy に対応する data-* を持たないため実装対象外です)。
    • forms_motion::forms_motion_css(): 上記 4 種を決定的な順序で連結した CSS 全文。Theme::to_css_with_forms_motion(): Theme::to_css() の出力へ forms_motion_css() を追記して返す opt-in メソッド。
    • shake(リテラル animation)は個別の @media (prefers-reduced-motion: reduce) ブロックで animation: none へ縮退します。下線伸長・error-text presence は var(--fandhe-motion-duration-*) 経由の transition のみで構成され、Theme::to_css が既に duration トークンを reduced motion 下で 0ms へ上書きするため個別ブロックは不要です。
  • カスタムカーソル(イシュー #2542、crates/pre-styled-ui/src/cursor.rs): ポインタに spring で追従するカスタムカーソル(Motion+ Cursor 相当)です。
    • cursor::cursor(attrs): カーソル要素本体(<div>)を組み立てる。data-fandhe-cursor(値なし存在属性)・aria-hidden="true" を持つ。hover 対象は利用者が任意の要素へ data-fandhe-cursor-target(バリアント名)・data-fandhe-cursor-target-label・data-fandhe-cursor-target-magnetic を静的に付与する。
    • cursor::CURSOR_CSS: カーソル要素の固定位置スタイル・hover バリアント(ring)・ラベル表示・@media (prefers-reduced-motion: reduce), (pointer: coarse), (hover: none) フェイルセーフの CSS 全文。Theme::to_css_with_cursor(): Theme::to_css() の出力へ追記する opt-in メソッド。
    • 追従の spring 演算・rAF 駆動・hover 対象の data-* 写し配線は fandhe-frontend-wasm-full の cursor feature(既定 on)が担う(本クレートは wasm-full に依存しない)。
  • list の並べ替え・追加削除遷移(イシュー #2544、crates/pre-styled-ui/src/list_motion.rs): list 自体は変更せず、行の追加(enter)・削除(exit)用の @keyframes CSS のみを持つ opt-in モジュールです(Motion AnimatePresence〔popLayout〕相当)。並べ替え(Move)は既存の fandhe-frontend-wasm-full layout-animation feature(FLIP、イシュー #2518)がそのまま担うため、本モジュールに追加実装はありません。
    • list_motion::PRESENCE_AUTO_ATTR("data-fandhe-presence-auto"): fandhe-frontend-wasm-full::list_presence::PRESENCE_AUTO_ATTR と同一リテラルで、両クレート間の契約はこの文字列一致のみです(Cargo 依存は発生しません)。list::root へこの属性を付けると enter CSS・fandhe-frontend-wasm-full の presence/stagger feature(いずれも既定 on)の配線対象になります。
    • list_motion::enter_css(): [PRESENCE_AUTO_ATTR] を持つ list::root 直下の list::item へ、stagger 遅延付きの enter アニメーションを適用します(recipe::stagger_delay_declaration を再利用、stagger feature 無効時は既定 --fandhe-motion-stagger- index: 0 のため遅延なしで動きます)。
    • list_motion::exit_css(): fandhe-frontend-wasm-full:: list_presence::play_exit_after が挿入するゴースト(data-state= "exiting")へ exit アニメーションを適用します(animation-delay: 0s で stagger を打ち消し、退場は一斉に行います)。
    • list_motion::list_motion_css()/Theme::to_css_with_list_motion(): 上記 2 種を決定的な順序で連結した CSS 全文・Theme::to_css() の出力へ追記して返す opt-in メソッド。
    • transform プロパティには一切触れません(fandhe-frontend-wasm- full の FLIP が transform を毎フレーム inline !important で書き込むため、transition: transform/@keyframes で transform に触れると追従が鈍ります)。recipe::SlotRecipe:: presence_transition([hidden] 状態遷移前提)は list 行には適用しません(list 行は hidden を経由せず、stagger との結合・FLIP 非干渉の要件にも合わないため)。
    • 両 @keyframes は --fandhe-motion-duration-* トークン経由の animation-duration/animation-delay のみを使うため、Theme:: to_css が一括生成する reduced-motion ブロックの対象に自然に含まれます(個別の @media ブロックは不要)。
    • 既知の挙動(意図的): SSR 初回描画時、行に既に PRESENCE_AUTO_ATTR が付いていると enter アニメーションが初回表示でも 1 回再生されます(CSS animation は「属性が変化した瞬間」を検知できないため、forms_motion::SHAKE_CSS と同じ既知のトレードオフです)。
  • spring 近似 easing プリセット(イシュー #2381): theme::Theme:: push_spring_easing() を呼ぶと、motion.dev spring() 既定値(stiffness=100/damping=10/mass=1)を from=0.0/to=1.0/ initial_velocity=0.0 で解いた軌道を CSS linear() タイミング関数へ事前サンプリングした motion-easing-spring/motion-duration-spring トークンを追加します。JS フレームループなしで spring の減衰振動(オーバーシュート付き)を近似できます。
Rust
use fandhe_frontend_pre_styled_ui::theme::Theme;

let mut theme = Theme::default();
theme.push_spring_easing()?; // easing-spring / duration-spring を追加
let css = theme.to_css();
// :root に `--fandhe-motion-easing-spring: linear(...)`・
// `--fandhe-motion-duration-spring: 1473ms` が並ぶ。
# Ok::<(), fandhe_frontend_pre_styled_ui::theme::ThemeError>(())

duration- 接頭辞のため prefers-reduced-motion: reduce 下では自動的に 0ms へ上書きされます(既存の motions スケール経由、追加の分岐は不要)。共通 @keyframes プリセット(イシュー #2382)と組み合わせる場合は、push_spring_easing() を呼んでから to_css_with_keyframes()(#2382 側 API)を呼び出してください(合成専用のメソッドは設けていません。両者は独立した opt-in の組み合わせです)。値は fandhe-animation の同一パラメータでの再計算結果と crates/pre-styled-ui/tests/motion_spring_css.rs がパリティ検証しています。

  • named view transition CSS プリセット(イシュー #2516・#2537、crates/pre-styled-ui/src/view_transition.rs): document. startViewTransition() によるページ遷移の見た目を選べるようにする、Motion+ Curtains 相当のプリセット集です(fade/slide/wipe/iris/doors/ shutter/blinds/strips/pixels/mask-wipe/mask-radial の 11 種)。
    • view_transition::VIEW_TRANSITION_PRESET_ATTR("data-fandhe-view-transition"): プリセット選択の属性名。fandhe-frontend-wasm-full::view_transition_preset:: VIEW_TRANSITION_PRESET_ATTR と同一リテラルで、両クレート間の契約はこの文字列一致のみです(Cargo 依存は発生しません)。
    • view_transition::VIEW_TRANSITION_PRESETS_CSS: :root[data-fandhe- view-transition="<preset>"]::view-transition-old(root)/ ::view-transition-new(root) の 11 プリセット CSS 全文。blinds/strips/pixels/mask-wipe/mask-radial は複数 mask-image 層・グラデーションによる形状遷移で、unprefixed mask プロパティ(Chrome 120+ / Safari 15.4+)のみに依存します(-webkit-mask-* の複製はしていません)。
    • Theme::to_css_with_view_transition_presets(): Theme::to_css() の出力へ VIEW_TRANSITION_PRESETS_CSS を追記して返す opt-in メソッド。
    • プリセットの属性設定自体(document.documentElement への set/ remove)は fandhe-frontend-wasm-full::Runtime:: apply_with_view_transition_named(feature view-transition-preset)が担い、本クレートは CSS のみを持ちます。
    • 各 animation-duration は --fandhe-motion-duration-slow トークン経由のため、prefers-reduced-motion: reduce 下では自動的に 0ms へ上書きされます。加えて @media (prefers-reduced-motion: reduce) ブロックで全プリセットの old/new(root) へ animation: revert; を明示的に再宣言し、UA 既定のクロスフェードへ戻します。mask 系プリセットの静的 mask-* 宣言は animation: revert; だけでは戻らないため、同ブロックで mask-image: none; も併せて再宣言します。
  • toast の stack 表示(イシュー #2543、crates/pre-styled-ui/src/ toast_motion.rs): 既存 toast 部品への opt-in 追加装飾です。toast_motion::STACK_ATTR("data-fandhe-toast-stack")を toast::group の attrs へ渡すと積層表示(後ろの通知ほど縮小・オフセット)になり、:hover/:focus-within で展開します。動的な追加・削除・並べ替えを行う場合は toast_motion::stack_group_keyed が stagger 書き戻し(stagger)+ layout FLIP(layout-animation)を自動配線します。Theme::to_css_with_toast_motion(): Theme::to_css() の出力へ toast_motion::TOAST_STACK_CSS を追記して返す opt-in メソッド。

3. 無効時ゼロコスト保証の内容

指標保証内容対応する契約テスト
crate サイズ既定 feature の依存グラフに fandhe-animation が現れないmotion_off_excludes_fandhe_animation_from_dependency_graph
ビルド時間上記の帰結(依存グラフに現れない依存はビルド対象にならない)同上
Theme::to_css の処理量走査ループへ motion feature 由来の実行時分岐(cfg!(feature = "motion") 等)を追加しないto_css_body_has_no_feature_cfg_or_motion_branch
CSS 出力既定テーマの to_css() 全文がバイト一致(--features motion 実行でも同一)default_theme_css_matches_pre_motion_golden

陽性対照(feature 名・dep: 配線の破損を検知)として motion_on_includes_fandhe_animation_in_dependency_graph も併走します。

CI では .github/workflows/ci.yml の clippy ジョブが --no-default-features/--features motion の両構成で cargo check・cargo clippy --all-targets -- -D warnings を実行し、test ジョブが --features motion 構成で上記契約テストを実行します。

4. gating の範囲

motion feature が除外するのは「C 群のみに分類され実装対象と定められた拡張出力」(spring linear() プリセット等、docs/design/motion-reference-adoption-policy.md §7)です。

  • presence(#2383)は feature 配下に置きません: 同文書 §7 が「既定出力に無条件で含む」と明示しています。
  • 共通 @keyframes(#2382)は feature 配下に実装済みです(motion::KEYFRAMES_CSS、§2 参照)。stagger(#2384)も feature 配下に実装済みです(§2 参照)。scroll-driven(#2385)は同文書 §4 各行の採用方針に従い、追加時に判断します。
  • カスタムカーソル(#2542)も feature 配下に実装済みです(cursor モジュール、§2 参照。C 群のみに分類され実装対象と定められた拡張出力)。

5. 消費者別の指定方針

  • docs-site(fandhe-frontend-docs-site): イシュー #2524 で motion を有効化しました(crates/docs-site/Cargo.toml の fandhe-frontend-pre-styled-ui 依存へ features = ["motion"] を指定)。Themes Demo の presence(dialog/drawer/popover/tooltip/hover-card/menu の closed インスタンス)・scroll-driven reveal・stagger の実演がこれを消費します。イシュー #2549 の Blocks bento-staggered は、crate:: showcase が SlotRecipe の builder(scroll_reveal/stagger_delay)経由で間接的に使うのに対し、motion::KEYFRAMES_CSS(SLIDE_FROM_BOTTOM_ KEYFRAMES_NAME)を直接 push_css する最初の消費者です。あわせて structure.toml の directories.pre-styled-ui.depends_on へ "animation" を、directories.animation.allowed_dependents へ "pre-styled-ui" を対称に追加済みです(optional 依存が既定 feature 側で解決されるようになったため、fw structure の depends_on 完全一致検証の対象に入っています)。
  • dist-server 配布物: fandhe-frontend-dist-server の配布 WASM は crates/dist-server/src/wasm_dist_features.rs の WASM_DIST_FEATURES(wasm-full 側の feature 集合)に従い、pre-styled-ui の CSS 生成(Theme::to_css)自体には関与しません。motion feature の on/off は配布物のビルド設定に影響しません。
  • examples: examples/headless-pre-styled-ui 等は crates.io バージョン依存のため既定 off のままです。

6. 公開順序の注意

fandhe-animation は本 issue(#2416)時点で crates.io 未公開です(docs/ci/version-bump-publish-order-gap.md §11)。cargo publish は optional 依存であっても registry 上の解決を要求するため、fandhe-frontend-pre-styled-ui 0.188.0 以降を公開する場合は fandhe-animation の初回公開(同文書 §11 C 手順)を先に完了し、sparse index への反映を確認してから実行してください(.github/workflows/release.yml の該当コメント参照)。

7. 検証方法

Shell
cargo check  -p fandhe-frontend-pre-styled-ui --no-default-features --all-targets --locked
cargo clippy -p fandhe-frontend-pre-styled-ui --no-default-features --all-targets --locked -- -D warnings
cargo check  -p fandhe-frontend-pre-styled-ui --features motion --all-targets --locked
cargo clippy -p fandhe-frontend-pre-styled-ui --features motion --all-targets --locked -- -D warnings
cargo test   -p fandhe-frontend-pre-styled-ui --test motion_zero_cost --locked
cargo test   -p fandhe-frontend-pre-styled-ui --features motion --test motion_zero_cost --locked
cargo test   -p fandhe-frontend-pre-styled-ui --features motion --test motion_stagger_css --locked
cargo test   -p fandhe-frontend-pre-styled-ui --features motion --test motion_spring_css --locked
cargo test   -p fandhe-frontend-pre-styled-ui --features motion --test motion_scroll_reveal_css --locked
cargo test   -p fandhe-frontend-pre-styled-ui --features motion --test motion_border_beam_css --locked
cargo test   -p fandhe-frontend-pre-styled-ui --features motion --test motion_parallax_css --test motion_sticky_progress_css --locked
cargo test   -p fandhe-frontend-pre-styled-ui --features motion --test motion_forms_css --locked
cargo test   -p fandhe-frontend-pre-styled-ui --features motion --test motion_view_transition_css --locked
cargo test   -p fandhe-frontend-pre-styled-ui --features motion --test motion_text_reveal_css --locked
cargo tree   -p fandhe-frontend-pre-styled-ui -e normal --prefix none --locked | grep -c fandhe-animation   # 0