コンテンツにスキップ

並べ替えリスト

サーバが順序を所有するリスト — カンバン列、優先順リスト、ピン留め ダッシュボード — には、「誰が決めるか」に忠実なクライアントサイド 並べ替えが必要です。installSortable ビヘイビアは DOM ノードを動かして 確定した順序を報告するだけで、ネットワークは htmx が所有します。 各アイテムが自分の hidden input を持つため、アイテムを動かせば input も 動き、新しい順序は追加の管理コードゼロでリクエスト 1 本に シリアライズされます — すべてマークアップから、インライン JS なしで。

別名: ドラッグ&ドロップ並べ替え。

下のプレビューはそのままライブです — 並べ替えはクライアントサイド なので、ハンドルをドラッグする(またはフォーカスして Space、続けて矢印キー)とリストが並び替わります。結果の 永続化は htmx 属性ひとそろい(下記)で完了します。

  • リリースノートを公開
  • オープン PR をレビュー
  • ロードマップを更新

動作:

  1. data-hc-sortable がコンテナをマークし、その要素子が並べ替え対象の アイテムになります。
  2. すべての data-hc-sortable-handle はインストール時(および htmx で スワップインされたとき)に準備されます: touch-action: nonearia-pressed="false"、そして のような記号だけのハンドルには 既定の aria-label(i18n キー sortable.handle)。
  3. ポインタドラッグは 4px のしきい値を超えてから始まるので、普通の クリックは素通りします。アイテムはポインタ位置に追従してライブに 並び替わり、行/列レイアウトはジオメトリから自動判定されます。
  4. 確定した並べ替えは共有の role="status" ライブリージョンで アナウンスされ、バブリングする hc:sortchange イベントを ディスパッチします — 順序が実際に変わったときだけ。

htmx をイベントに向けるだけです。シリアライズは hidden input が 担います:

<ul class="hc-stack" data-hc-sortable
data-hx-post="/items/order"
data-hx-trigger="hc:sortchange"
data-hx-include="this"
data-hx-swap="none">
</ul>

リクエストボディは新しい DOM 順で id を列挙します — order[]=b&order[]=a&order[]=c<li> を動かせばその hidden input も 動くからです。応答は 204 No Content(確認用に HX-Trigger トーストを 添えても)か、順序が計算済みラベルに影響する場合は再レンダリングした コンテナを返してスワップします。リストを <form> で包んで送信ボタンを 置けば、htmx なしでも同じように動きます。

hidden input の代わりにイベントを読む構成も可能です:

list.addEventListener('hc:sortchange', ({ detail }) => {
// detail = { item, from, to, order: ['b', 'a', 'c'] }
});

order は各アイテムの data-hc-sortable-id(なければ id、それも なければ null)を列挙します。

ハンドルは本物の <button> — それ自体がキーボードインターフェース です:

キー状態動作
Space / Enterアイテムをつかむ(data-grabbed="true"aria-pressed="true"、アナウンス)
/ つかみ中アイテムを上/前へ移動(アナウンス)
/ つかみ中アイテムを下/後ろへ移動(アナウンス)
Space / Enterつかみ中ドロップ — 確定して hc:sortchange を発火
Escapeつかみ中/ドラッグ中キャンセルして元の位置に復元

ハンドルのフォーカスが外れると現在位置で確定します。つかむ・移動・ ドロップ・キャンセルはすべて視覚的に隠された role="status" リージョンでアナウンスされます — sortable.* キーは setMessages() で翻訳 できます(ja は locales/ja に同梱)。

ドラッグは何もしなくても「ドラッグの手応え」があります: 移動中の アイテムはポインタに追従してシャドウで浮き上がり(ハンドルは grab / grabbing カーソル)、押し退けられた兄弟は新しいスロットへ スライドし、ドロップで定位置に収まります。モーションは モーションスケール (--hc-motion-duration-fast)に乗り、prefers-reduced-motion では すべてスキップされます。DOM の順序が唯一の真実で、アニメーション層は それを決して変えません。

状態は属性にあるので、既定のリフトは素のセレクタで上書きできます — トグルするクラスはありません:

/* 例: 組み込みのリフトに加えて移動中のアイテムを減光する */
[data-dragging='true'],
[data-grabbed='true'] {
opacity: 0.6;
}
属性対象意味
data-dragging="true"アイテムポインタドラッグが進行中
data-grabbed="true"アイテムキーボードのつかみがアクティブ
aria-pressedハンドル支援技術へつかみ状態を伝える

プログレッシブエンハンスメント

Section titled “プログレッシブエンハンスメント”

ビヘイビアなしではリストはサーバ順で描画され、ハンドルは何もしない ボタンになります — 内容は読める・完全なまま。並べ替えは エンハンスメントであり、依存ではありません。JS なしの並べ替え フォールバックが必要なら、サーバが処理するアイテムごとの 「上へ/下へ」送信ボタンを追加してください — このレシピとそのまま 共存します。

  • 視覚上の順序は常に DOM の順序そのもの — スクリーンリーダーの 読み上げ順が見た目とずれることはありません。
  • ハンドルはフォーカス可能な要素であること。機械契約(checks.json)は ハンドルが <button> でない場合に警告します。
  • touch-action: none はハンドルだけに適用 — リストの他の場所からは ページが普通にスクロールします。