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 配下)を使いたい利用者motionfeature を有効化しなければ、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. 有効化手順
[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-indexcustom 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-driverfeature(既定 on)を配線する必要があります(属性値"cover"/"contain"でネイティブと同じ進捗範囲を選べます。属性なしでは静止したまま安全に劣化します)。 view-transition-nameヘルパ(イシュー #2515、crates/pre-styled-ui/src/recipe.rs):recipe::view_transition_name_declaration(name)。共有要素遷移(motion.devAnimateView相当)の固定名を割り当てる静的なケース向けで、nameはコンパイル時に確定する&'static strのみ受け付けます。keyed list の行等、値ごとに一意な動的名前が必要な場合はfandhe_frontend_wasm_full::view_transition_name::set_view_transition_name(wasm-full 側、view-transition-namefeature 既定 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-animationfeature 既定 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-textslot へ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のcursorfeature(既定 on)が担う(本クレートはwasm-fullに依存しない)。
- list の並べ替え・追加削除遷移(イシュー #2544、
crates/pre-styled-ui/src/list_motion.rs):list自体は変更せず、行の追加(enter)・削除(exit)用の@keyframesCSS のみを持つ opt-in モジュールです(MotionAnimatePresence〔popLayout〕相当)。並べ替え(Move)は既存のfandhe-frontend-wasm-fulllayout-animationfeature(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/staggerfeature(いずれも既定 on)の配線対象になります。list_motion::enter_css():[PRESENCE_AUTO_ATTR]を持つlist::root直下のlist::itemへ、stagger 遅延付きの enter アニメーションを適用します(recipe::stagger_delay_declarationを再利用、staggerfeature 無効時は既定--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 回再生されます(CSSanimationは「属性が変化した瞬間」を検知できないため、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で解いた軌道を CSSlinear()タイミング関数へ事前サンプリングしたmotion-easing-spring/motion-duration-springトークンを追加します。JS フレームループなしで spring の減衰振動(オーバーシュート付き)を近似できます。
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層・グラデーションによる形状遷移で、unprefixedmaskプロパティ(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(featureview-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 の Blocksbento-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)自体には関与しません。motionfeature の 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. 検証方法
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