position – CSS: カスケーディングスタイルシート | MDN#sticky
端の一列は簡単です。td 要素にposition: sticky
をつけ、固定したい方向にleft: 0
の様にするのみでよしなにしてくれます。次のデモがこれです。
/* 1列目を左端に固定 */ td:first-child { position: sticky;// 固定 left: 0; // 左端 }
上下の複数行はちょっと工夫が要りますが、シンプルではあります。上部はthead
で固定したい範囲を囲ってthead
にposition: sticky
をかけます。下部はtfoot
として同様です。次のデモがこれです。
thead { position: sticky; top: 0; } tfoot { position: sticky; bottom: 0; }
左右の複数列は上下複数行に比べてずいぶん手間です。画面を描画した時、画面が変わった時、それぞれで都度左から何pxに置くべきかを計算して、それを要素に与える必要があります。あんまりにもあんまりなのでその内 colgroup に position:stickey をかけたらそれだけで複数列固定になるやもしれません。
これを実装したデモとソースコードが次です。
ソースコードを展開
// App.tsx import "./styles.css"; import { AppTableCell } from "./AppTableCell"; export default function App() { // 表のデータを作成 const arr = Array(50) .fill(null) .map((v, i) => { return Array(50) .fill(null) .map((v, j) => `${i},${j}`); }); return ( <div className="App"> <table> <caption>左複数列を固定</caption> <tbody> {arr.map((vp, i) => ( <tr key={i}> {/* sticky にしたい列だけ特別なコンポーネントを呼出し */} {vp.map((vc, j) => j === 0 || j === 1 ? ( <AppTableCell key={`${i},${j}`} stickyIndex={j}> {vc} </AppTableCell> ) : ( <td key={`${i},${j}`}>{vc}</td> ) )} </tr> ))} </tbody> </table> </div> ); } // AppTableCell.tsx import React, { CSSProperties, useEffect, useRef } from "react"; import { atom, useAtom } from "jotai"; type AppTableCellProps = { stickyIndex?: number; // sticky として左からいくつ目か(いくつ目と書いたが 0 スタートのインデックス的採番) }; // 表中の各列の幅を管理する atom を用意。Cellコンポーネントの外でグローバルに管理できればなんでも良し const tableCellWidths = atom<number[]>([]); export const AppTableCell: React.FC<AppTableCellProps> = (props) => { const { stickyIndex } = props; const [tcw, setTcw] = useAtom(tableCellWidths); // マスを直に触るので ref を用意 // @see https://ja.reactjs.org/docs/refs-and-the-dom.html const cellRef = useRef<HTMLTableCellElement>(null); // style.left const [left, setLeft] = React.useState(""); // マスが変わった時に管理されている幅を更新 useEffect(() => { if (typeof stickyIndex !== "number" || !cellRef.current) { return; } // 更新ロジック const updateAction = () => { const tcwUpdater = (old: number[]) => { if (typeof stickyIndex !== "number" || !cellRef.current) { return old; } // 以前の幅と現在の幅が違うのであれば更新 if ( old[stickyIndex] !== cellRef.current.getBoundingClientRect().width ) { old[stickyIndex] = cellRef.current.getBoundingClientRect().width; } return old; }; setTcw(tcwUpdater); // 左から何 px 目で固定されるべきなのかをグローバルに管理している各列の幅から計算する setLeft( `${tcwUpdater(tcw) .filter((v, i) => i < stickyIndex) .reduce((carry, cur) => carry + cur, 0)}px` ); }; updateAction(); const observer = new ResizeObserver(updateAction); observer.observe(cellRef.current); return () => { observer.disconnect(); }; }, [cellRef, tcw, setTcw, stickyIndex]); const style: CSSProperties = {}; if (typeof stickyIndex === "number") { // 左から何 px 目で固定されるべきなのかをグローバルに管理している各列の幅から計算する // 計算内容は自分の左のマスまでで使われている幅の合計の算出 style.left = left; style.background = "rgb(200, 200, 200)"; } // 計算結果を含んだ style を受け渡し return ( <td ref={cellRef} style={style}> {props.children} </td> ); };
余談ですがこのコードは left の値を決定するコードにすぎず、固定対象の列の幅が固定されているのであれば、デモの様に都度計算する必要はなく、単にtd:first-child {left:0px;}
,td:nth-child(2) {left:1列目の幅px;}
,td:nth-child(3) {left:(1列目の幅+2列目の幅)px;}
,…と固定幅に合わせてべた書きするだけで同じものを作れます。