GODRICH
#cssアニメーション#ボタン#ローディング#トランジション#スクロール#ダッシュボード#実用ui#デザイントレンド

ブックマーク ボタン html 実装 9パターン

ブックマーク ボタン htmlでは、保存。たった一言に九つの表情がある — 03番は押した瞬間にスプリング値が弾み、06番は保存数の値が一桁ずつ積み重なってバッジを押し上げ、09番はドラッグ距離の値が届くとリストにスッと引き寄せられる。

自動再生 · タイルを押すとその項目へ · すべて1つのzip

値ひとつがブックマーク ボタン html の保存の感触をどう変えるか — 9パターンの並び順

以下の9つはどれも「保存した/していない」という同じ2状態にたどり着きますが、そこに至る計算はそれぞれ違う値ひとつが握っています。並び順はおおよそ装飾寄りから機構寄りへ進みます — 01はポインターが乗っている間だけ反応し、02と03は一度のタップを形かスケールに変え、04と05はそのタップにもう一段(折れる・満ちる)物理的な読み取りを重ね、06と07は保存をもう一箇所の変化に伝え、08と09は一度のクリックではなく長押しやドラッグという長い操作に駆動値を渡します。お気に入り ボタン デザインとして紹介される実装の多くは前半の3〜4種で止まりますが、リストや件数、フォルダが絡む本物の保存機能はむしろ後半5種に近づきます。

ブックマーク ボタン html が実際に置かれる画面

01と02は密度優先の画面向きです — 30枚の記事カードが並ぶ一覧の中で、ホバーの傾きや輪郭から塗りへのモーフィングは20px前後でも十分読み取れ、行の他の要素が保存に合わせて動く必要もありません。03・04・06はもう少し大きな舞台を求めます — コメント欄・記事ヘッダー・カウンターバッジのように、一度の保存が画面上で何度も起きない場所でこそ、小さなお祝いの動きが意味を持ちます。

07・08・09は単発のタップ以外の何かに駆動される3つです。トーストは画面下にまだ何も占有していない場所を必要とし、コレクションのポップオーバーはボタンの下にパネルが開くための余白を必要とし、ドラッグして並べ替えるしおり ボタンは実際に落とし込める2つ目のリストを必要とします。3つとも密なグリッドに置くと読み取りにくく、カードではなくページのために作られています。

01ホバーで軽く傾いて光るブックマークアイコン

アイコンは本物のポインターが乗っている間だけ動きます。applyVector()がボタン中心からのカーソルのずれをひとつの角度に変換し、その角度がtransformによる傾きとconic-gradientの光の位置を同時に決めます。保存状態そのものには触れず、記事一覧カードの保存アイコンをクリック前からカーソルを追いかけているように見せる層です。

ホバーで軽く傾いて光るブックマークアイコン
新しいタブで開く

01 · ホバーで軽く傾いて光るブックマークアイコン — 計測 累積面積 17.316% · 動いたフレーム 21/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

TILT_MAX_DEGGLOW_RADIUS_PXapplyVector
  // 値ひとつ: ポインターベクトル(dx,dy) → 角度。その角度が傾き(rotateX/rotateY)と
  // 光沢のsweep位置(conic-gradient角度)を同時に決める。
  function applyVector(dx, dy) {
    var dist = Math.hypot(dx, dy);
    var strength = clamp(1 - dist / GLOW_RADIUS_PX, 0, 1);
    var angleDeg = Math.atan2(dy, dx) * 180 / Math.PI;
    var tiltX = clamp(-dy / GLOW_RADIUS_PX, -1, 1) * TILT_MAX_DEG * strength;
    var tiltY = clamp(dx / GLOW_RADIUS_PX, -1, 1) * TILT_MAX_DEG * strength;
    btn.style.transform = "perspective(600px) rotateX(" + tiltX.toFixed(2) + "deg) rotateY(" + tiltY.toFixed(2) + "deg)";
    glow.style.setProperty("--glow-deg", angleDeg.toFixed(1) + "deg");
    glow.style.setProperty("--glow-strength", strength.toFixed(3));
  }

測定値: 累積面積17.3%、平均強度24.3、動いたフレーム21/23。

02輪郭線から塗りつぶしに変わるブックマークボタン

タップ前のリボンは輪郭線だけで描かれています。setSaved()がひとつの真偽値を切り替え、render(p)がその進行率を読んで塗りつぶし用rectの高さを下から育てるので、アイコンが差し替わるのではなく自分自身が塗りで満ちていきます。商品カードのウィッシュリストボタンが欲しい、買い手が目で追える状態変化そのものです。

輪郭から塗りつぶしに変わるブックマークボタン
新しいタブで開く

02 · 輪郭から塗りつぶしに変わるブックマークボタン — 計測 累積面積 16.04% · 動いたフレーム 7/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

CYCLE_SrendersetSaved
  function render(p) {
    // 値ひとつ: 塗りつぶしの進行率(p, 0~1) → SVGのrectのy・height(塗りつぶし比率になる)
    var y = 24 * (1 - p);
    fillRect.setAttribute("y", y.toFixed(3));
    fillRect.setAttribute("height", (24 - y).toFixed(3));
    panel.style.setProperty("--fill-p", p.toFixed(3));
  }

  function setSaved(v) {
    saved = v;
    target = v ? 1 : 0;
    btn.setAttribute("aria-pressed", String(v));
    status.textContent = v ? "保存済み" : "未保存";
  }

測定値: 累積面積16.0%、平均強度37.6、動いたフレーム7/23。

03スプリングで弾んで大きくなるブックマークボタン

タップした瞬間、アイコンは最終サイズへ素直に近づくのではありません。integrate()が毎フレーム本物のバネ方程式を計算するので、スケールは一度1を追い越し、自分の減衰で引き戻されます。この行き過ぎこそ、コメントの「いいね」隣にある保存ボタンを設定トグルではなく反応らしく見せる部分です。

スプリングで弾んで大きくなるブックマークボタン
新しいタブで開く

03 · スプリングで弾んで大きくなるブックマークボタン — 計測 累積面積 9.194% · 動いたフレーム 12/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

BOOKMARK_SPRING_OVERSHOOT_PERCENTSPRING_STIFFNESSintegrate
  // スプリング方程式(剛性SPRING_STIFFNESS・減衰SPRING_DAMPING)を毎フレーム積分する。
  // 押した瞬間に速度を一度蹴り込む(kick)と、スプリングが目標値(1)を超えてから戻る。
  function integrate(dt) {
    var accel = SPRING_STIFFNESS * (1 - pos) - SPRING_DAMPING * vel;
    vel += accel * dt;
    pos += vel * dt;
    if (pos > maxScale) { pos = maxScale; vel *= -0.35; }
    if (pos < 1 && vel < 0 && Math.abs(pos - 1) < 0.001 && Math.abs(vel) < 0.01) { pos = 1; vel = 0; }
  }

測定値: 累積面積9.2%、平均強度28.0、動いたフレーム12/23。

04リボンが折れて落ちるブックマークボタン

保存するタップと同じ入力が折りたたみも起こします。render(p)がサインカーブに沿ったひとつの進行値を読み、rotateXの折れ角度・小さなtranslateYの落下・そして落ちるほど濃くなる影のぼかしと不透明度に変換します。記事詳細ページ上部の保存ボタンのように、折れと影がきちんと読み取れる余白がある場所向けです。

リボンが折れて落ちるブックマークボタン
新しいタブで開く

04 · リボンが折れて落ちるブックマークボタン — 計測 累積面積 10.631% · 動いたフレーム 17/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

FOLD_MAX_DEGFOLD_DROP_MAX_PXtriggerFold
  // 値ひとつ: 折りたたみ進行度p(0~1、サインカーブ1山) → 折れ角度(rotateX) + 落下距離
  // + 影のぼかし・不透明度(高さが下がるほど影が濃くなる)。
  function render(p) {
    scene.style.setProperty("--tone", p.toFixed(3));
    var deg = p * FOLD_MAX_DEG;
    var drop = p * FOLD_DROP_MAX_PX;
    wrap.style.transform = "perspective(300px) rotateX(" + deg.toFixed(2) + "deg) translateY(" + drop.toFixed(2) + "px)";
    var blur = 3 + p * 10;
    var spread = 6 - p * 4;
    var alpha = 0.22 + p * 0.28;
    shadow.style.boxShadow = "0 " + (2 + p * 6).toFixed(1) + "px " + blur.toFixed(1) + "px " + spread.toFixed(1) + "px rgba(23,20,26," + alpha.toFixed(2) + ")";
    shadow.style.transform = "scaleX(" + (1 - p * 0.18).toFixed(3) + ")";
  }

測定値: 累積面積10.6%、平均強度64.4、動いたフレーム17/23。

05液体が満ちていくブックマークボタン

waveD()は毎フレーム、水位と時間位相というたった2つの値からSVGパスを一から組み立て直します。塗りは静止した図形ではなく、1秒に60回描き直されるサインカーブそのものです。動画プレーヤーの保存ボタンのように、画面自体が常に描き直されている場所だからこそ許される計算量です。

液体が満ちていくブックマークボタン
新しいタブで開く

05 · 液体が満ちていくブックマークボタン — 計測 累積面積 10.059% · 動いたフレーム 11/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

WAVE_AMPLITUDEWAVE_FREQUENCYwaveD
  // 値ひとつ: 水位(level, 0~1) + 時間位相(phase) → 毎フレーム再計算するサイン波のpath。
  function waveD(lv, phase) {
    var baseY = 24 * (1 - lv);
    var d = "M0," + (baseY + WAVE_AMPLITUDE * Math.sin(phase)).toFixed(2) + " ";
    for (var x = 0; x <= 24; x += 2) {
      var y = baseY + WAVE_AMPLITUDE * Math.sin(x * WAVE_FREQUENCY * 0.26 + phase);
      d += "L" + x + "," + y.toFixed(2) + " ";
    }
    d += "L24,24 L0,24 Z";
    return d;
  }

測定値: 累積面積10.1%、平均強度68.8、動いたフレーム11/23。

06保存数が一桁ずつ増えるバッジ

保存はアイコンを切り替えるだけでなく、2桁のカウンターバッジも回します。beginRoll()はひとつの間隔値を読んで各桁がいつ回り始めるかを決め、一の位が先に、十の位はその一拍後に動き出します。この段差こそ、記事の保存数を示すカウンターバッジで数字がいきなり切り替わるのではなく「増えた」と読める理由です。

保存数が一桁ずつ増えるバッジ
新しいタブで開く

06 · 保存数が一桁ずつ増えるバッジ — 計測 累積面積 12.285% · 動いたフレーム 12/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

BOOKMARK_COUNT_STAGGER_MSDIGIT_ROLL_MSbeginRoll
  // 値ひとつ: 桁インデックス(右から) × BOOKMARK_COUNT_STAGGER_MS = その桁が回り始めるまでの遅延時間。
  var columns = [
    { delay: 1 * BOOKMARK_COUNT_STAGGER_MS, start: null, from: "0", to: "0" },
    { delay: 0 * BOOKMARK_COUNT_STAGGER_MS, start: null, from: "9", to: "9" }
  ];

  function pad2(n) { return (n < 10 ? "0" : "") + n; }

  function beginRoll(now) {
    var next = saved ? count + 1 : count - 1;
    var fromStr = pad2(count);
    var toStr = pad2(next);
    count = next;
    for (var i = 0; i < 2; i++) {
      columns[i].from = fromStr[i];
      columns[i].to = toStr[i];
      columns[i].start = now + columns[i].delay;
    }
    status.textContent = saved ? "保存済み" : "未保存";
    row.classList.add("cb__row--pulse");
    window.setTimeout(function () { row.classList.remove("cb__row--pulse"); }, 320);
  }

測定値: 累積面積12.3%、平均強度69.5、動いたフレーム12/23。

07保存を知らせるトーストのスライド

ここではアイコン自体はほとんど動きません。render(now)はshowToast()が呼ばれてから経過したひとつの時間値を読み、それを進入・保持・退出という3区間のどこにいるか、その区間内でどこまで進んだかに変換します。モバイルアプリ画面の保存完了通知のように、アイコンの状態変化が小さいぶん、実際に保存を伝えるのはトーストの役目になる場面向けです。

保存を知らせるトーストのスライド
新しいタブで開く

07 · 保存を知らせるトーストのスライド — 計測 累積面積 10.493% · 動いたフレーム 11/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

TOAST_ENTER_MSTOAST_HOLD_MSshowToast
  function showToast(now, msg) {
    toastText.textContent = msg;
    toastStart = now;
  }

  // 値ひとつ: トーストが出てからの経過時間(rAF dtの累積) → enter/hold/exitのどの区間か、
  // その区間内の進行度。
  function render(now) {
    if (toastStart === null) {
      toast.style.transform = "translateY(120%)";
      toast.style.opacity = "0";
      return;
    }
    var elapsed = now - toastStart;
    var enterMs = reduce ? 1 : TOAST_ENTER_MS;
    var exitMs = reduce ? 1 : TOAST_EXIT_MS;
    var enterEnd = enterMs;
    var holdEnd = enterEnd + TOAST_HOLD_MS;
    var exitEnd = holdEnd + exitMs;

測定値: 累積面積10.5%、平均強度109.0、動いたフレーム11/23。

08保存先コレクションを選ぶポップオーバー

引き金はタップではなく長押しです。保持時間がLONGPRESS_MSを超えると、renderPanel()が項目インデックスひとつと間隔値の掛け算を読み、コレクション名の一覧を一つずつ順に開いていき、選ぶとパネルは折りたたまれて閉じます。レシピサイトのフォルダ別保存ボタンのように、「保存した」だけでは足りず、どのリストに入れたかまで伝える必要がある場面向けです。

保存先コレクションを選ぶポップオーバー
新しいタブで開く

08 · 保存先コレクションを選ぶポップオーバー — 計測 累積面積 31.662% · 動いたフレーム 6/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

LONGPRESS_MSPOPOVER_STAGGER_MSselectCollection
  // 値ひとつ: リスト項目インデックス × POPOVER_STAGGER_MS = その項目が開き始めるまでの遅延。
  function renderPanel(now) {
    if (openStart === null && !open) {
      items.forEach(function (el) { el.style.opacity = "0"; el.style.transform = "translateY(6px)"; });
      return;
    }
    var elapsed = openStart === null ? POPOVER_ITEM_MS * 3 : now - openStart;
    items.forEach(function (el, i) {
      var delay = i * POPOVER_STAGGER_MS;
      var raw = (elapsed - delay) / POPOVER_ITEM_MS;
      var p = Math.max(0, Math.min(1, reduce ? 1 : raw));
      var eased = 1 - Math.pow(1 - p, 3);
      el.style.opacity = eased.toFixed(3);
      el.style.transform = "translateY(" + ((1 - eased) * 6).toFixed(2) + "px)";
    });
  }

測定値: 累積面積31.7%、平均強度79.1、動いたフレーム6/23。

09ドラッグして読書リストに落とすブックマーク

release()が気にする入力はドラッグひとつだけです。指を離した位置とドロップゾーンの距離をひとつのスナップ距離定数と比べ、その比較だけでチップがリストに吸い込まれるか元の位置へ跳ね返るかが決まります。ブックマーク管理画面の読書リスト整理のように、並び替え自体がドラッグでこそ意味を持つ場面向けです。

ドラッグして読書リストに落とすブックマーク
新しいタブで開く

09 · ドラッグして読書リストに落とすブックマーク — 計測 累積面積 14.492% · 動いたフレーム 19/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

BOOKMARK_DRAG_SNAP_PXDRAG_INERTIA_DECAYrelease
  // 値ひとつ: 離した位置とドロップゾーンの距離 vs BOOKMARK_DRAG_SNAP_PX →
  // リストにスナップするか元の位置に戻るか。
  function release() {
    var dx = chipX - dropX, dy = chipY - dropY;
    var dist = Math.sqrt(dx * dx + dy * dy);
    if (dist <= BOOKMARK_DRAG_SNAP_PX) {
      targetX = dropX; targetY = dropY;
      setSaved(true);
    } else {
      targetX = 0; targetY = 0;
      setSaved(false);
    }
  }

測定値: 累積面積14.5%、平均強度43.1、動いたフレーム19/23。

どこで壊れるか — 動作軽減設定・誤タップ・二重クリック

9つのボタンはどれも、動きがあってもなくてもクリックやpointerupのハンドラを毎回呼ぶので、モーションを省いてもスクリーンリーダーへの通知とsavedの実際の値がずれることはありません。ここまでは9つとも同じです。05番のprefers-reduced-motion: reduceの扱いも今は隙がありません — reduceがtrueになった瞬間に水位levelは目標値へ即座に固定され、波を作るphaseの更新も同時に止まるので、動作を減らす設定を入れると水面の波紋も完全に静止します。この形を再利用するときは、水位と位相の両方をreduce分岐でまとめて止める書き方をそのまま持っていく価値があります。

08番はキーボード操作の穴もすでに塞がれています。ボタンにフォーカスした状態でEnterかSpaceを押すとclickイベントのe.detail === 0分岐がshortSave()を呼んで保存を切り替え、ArrowDownまたはShift+Enterはパネルを開いて最初の項目にフォーカスを移し、開いた後はArrowUp/Downで項目間を移動してEnterで選び、Escapeでパネルを閉じてボタンへフォーカスを戻します。aria-expandedとaria-haspopup、そしてパネルのrole="menu"もあわせて付いているので、ポインターを使わない読者でも保存もコレクション選択もキーボードだけで完了できます。1〜7番に共通する小さなリスクは二重クリックで、どのハンドラも連打を防いでいないため、素早い2回タップはsavedを2回切り替えて見た目は元通りになり、遅い回線や重いフレームでは「押したのに何も起きなかった」ように映ります。

値が保存の動きを変える場所 — スプリングオーバーシュート比率・カウント間隔・ドラッグスナップ距離

項目 累積面積 平均強度 動いたフレーム
01 ホバーの傾きと光 17.3% 24.3 21/23
02 輪郭から塗りつぶしへの変形 16.0% 37.6 7/23
03 スプリングポップ拡大 9.2% 28.0 12/23
04 リボンの折れと落下 10.6% 64.4 17/23
05 液体の波で満ちる 10.1% 68.8 11/23
06 桁が積み重なるカウントバッジ 12.3% 69.5 12/23
07 トースト確認スライド 10.5% 109.0 11/23
08 コレクション選択ポップオーバー 31.7% 79.1 6/23
09 ドラッグで読書リストに落とす 14.5% 43.1 19/23

08番の累積面積(31.7%)が次点のほぼ2倍なのは、9つの中で唯一、アイコンをその場で描き直すのではなくレイアウトに新しい要素(パネルの各行)を足し、しかもその行それぞれが自分の遅延で描き直されるからです。動いたフレーム数がもっとも少ないのは08番(6/23)で、パネルの各行がその遅延の間だけ描き直されるぶん平均強度79.1が集中しており、02番(7/23)もsetSaved()が連続ループではなく本物のクリックでしか発火しない点は同じです。9つの違いの大部分は3つの定数が握っています — BOOKMARK_SPRING_OVERSHOOT_PERCENT(03番)はスプリングが1をどこまで超えられるかを、BOOKMARK_COUNT_STAGGER_MS(06番)は2つの桁が回り始める間隔を、BOOKMARK_DRAG_SNAP_PX(09番)は落とした位置がどれだけ近ければ保存扱いになるかを決めていて、上の9つの関数はどれもこの3種類の数値のどれかを、違う速さで読んでいるにすぎません。

3つのバリエーションとキーボード操作に残るアクセシビリティの穴

バリエーション 値の変化 変わること
スプリングオーバーシュート比率値を調整する BOOKMARK_SPRING_OVERSHOOT_PERCENT 12 → 38 値を小さくすると弾む度合いが減り落ち着いて大きくなります。値を大きくするとスプリングが大きく飛び出してから戻り、誇張されたポップになります
保存数バッジの桁上がり間隔値を調整する BOOKMARK_COUNT_STAGGER_MS 40 → 160 値を短くすると数字がほぼ同時に切り替わります。値を長くすると一桁ずつはっきり上がっていくのが見えます
ドラッグを離す瞬間のスナップ距離値を調整する BOOKMARK_DRAG_SNAP_PX 24 → 96 値を小さくすると目標のすぐ近くまで寄らないと吸着しません。値を大きくすると遠くからでもスッと吸着します

3つのバリエーションはどれも、09番のキーボード動作を変えません。BOOKMARK_DRAG_SNAP_PXが24でも96でも、チップは変わらずネイティブbutton要素なので、フォーカスしてEnter・Spaceを押せば同じclickハンドラが呼ばれ、ドラッグで吸着させたときと同じsaved・スナップ位置に切り替わります。9つすべてで変わらず保たれているのは、すべてのボタンがaria-pressedを持ち、すべての状態テキストがaria-live="polite"の領域に置かれていることです。09番でキーボードに再現できないのはドラッグという動きそのものだけで、保存・配置という結果はEnter・Spaceだけで届きます。w36j826fを開くと、09番のindex.htmlでこのclickハンドラが実際どう書かれているか確認できます。

しおり ボタンとブックマーク ボタンは何が違うのか?

多くのプロダクトで同じクリックを指す2つの言葉ですが、お気に入り ボタン デザインはたいてい並び順もフォルダもないフラットな集合を意味するのに対し、しおり ボタンは「その場所に印を挟む」という古いメタファーを引きずっていて、だからこそ上の06番・08番・09番は件数・コレクション・位置という「お気に入り」だけでは普通は要らない要素を足しています。ショッピングサイトのハートも読書アプリのリボンも、裏側ではまったく同じaria-pressedトグルを共有していながら、片方は「気に入った」を、もう片方は「この場所に戻ってくる」を意味するだけで読み手の受け取り方が変わります。同じ「毎フレーム値をひとつ読む」という考え方でまとめた編としては、9 CSS Heart Animationsが「気に入った/いない」の半分をより詳しく扱っていて、9 CSS Toast Notificationsは07番の確認バンドに一番近い編で、Drag to Reorder UIは09番が一度しか触れないドロップ位置の計算をさらに掘り下げています。

ブックマーク ボタン html を組み込む前によくある質問

モーションを切ると保存そのものも止まりますか?

9つとも止まりません。savedを切り替えてaria-pressedを更新するクリックやポインターのハンドラは、prefers-reduced-motionの値に関わらず同じように動くからです。05番はむしろ動作を減らす設定にいちばん忠実で、水位が目標値へ即座に切り替わるだけでなく波を作るphaseの更新も止まるため、保存は機能したまま見た目は最終的な塗りだけで静止します。

9つのうち、今すぐ本物の保存ボタンとして使えそうなのはどれですか?

1番から9番まで、実際の保存トグルはEnter・Space・クリック・タップのいずれかに結び付いていてキーボード経路も欠けていないので、見た目だけ差し替えて本番のボタンに落とし込めます。09番だけはドラッグという動きそのものをキーボードで再現できませんが、チップはネイティブbuttonなのでEnter・Spaceが同じclickハンドラを呼び、ドラッグで吸着させたのと同じ保存・位置に切り替わります。

ブックマーク ボタン と は違う、リスト画面向けの実装として9つをまとめて使い回せますか?

01番から06番は要素の参照と定数を自分の中に閉じて持っているので、ページ側の前提には触れません。一覧の行ごとに初期化関数を1回ずつ呼べば、ページ全体で1回呼ぶのと同じ感覚で並べられます。08番と09番は一覧画面ではより注意が必要です。1つのボタン向けに調整したポップオーバーやドラッグの判定距離が、30個並んだ状態では隣同士とぶつかりやすくなります。

解凍パスワードを入力してください

パスワードはこの記事の本文の中にあります。読み進めると出てきます。