[Next.js] stickyで上部・左列を固定したらボーダーが消える?

に公開

テーブルの上部ヘッダーと左列を固定してスクロールできるよう実装したところ、スクロール中に borderが消えたり重なって見える問題が発生しました。
結論から申し上げると、実際の border は使わず 擬似要素で線を「描く方法で解決しました。
border-collapse はそのまま維持し、Tailwind のユーティリティとして再利用できる形にしました。

💭 なぜこの現象が起きるのか

  • position: sticky によりセルが 別レイヤー に持ち上がります。
  • 隣接セルの border が 重なり順(z-index)競合 により消えたり 二重線 に見えます。
  • 拡大表示や高DPIでは サブピクセル の影響で 1px のにじみが目立ちます。
    → 要点は「実ボーダーを描かない」ことです。擬似要素で線を描くとレイヤー競合を回避できます。

❌ Before: sticky + border

<div className="w-full h-[380px] overflow-y-auto border rounded-lg">
  <table className="table-auto border-collapse w-full text-sm">
    <thead>
      <tr className="text-left text-white">
        <th className="sticky top-0 left-0 z-20 bg-blue-400 border border-black px-3 py-2">順序</th>
        <th className="sticky top-0 left-0 z-20 bg-blue-400 border border-black px-3 py-2">メール</th>
        <th className="sticky top-0 left-0 z-20 bg-blue-400 border border-black px-3 py-2">ユーザー名</th>
        {/* ... */}
      </tr>
    </thead>
    <tbody>
      <tr key={r.id} className="odd:bg-white even:bg-gray-100 text-black">
        <td className="sticky left-0 z-10 bg-blue-200 border border-black px-3 py-2">1</td>
        <td className="border border-black px-3 py-2">user01@example.com</td>
        {/* ... */}
      </tr>
    </tbody>
  </table>
</div>

現象:スクロール時に 境界線が消える、ヘッダー×左列の交点が 二重線 になる。

✅ After: 擬似要素で線を描く Tailwind ユーティリティ

  addUtilities({
        ".sticky-cell": {
          position: "sticky",
          border: "none",
          backgroundClip: "padding-box",
          isolation: "isolate",
        },

        ".sticky-lines-y": { position: "sticky" },
        ".sticky-lines-y::before": {
          content: "''",
          position: "absolute",
          left: "0",
          right: "0",
          top: "0",
          bottom: "-1px", // 下端1px拡張(ヘアライン対策)
          backgroundImage:
            "linear-gradient(#2B0000, #2B0000), linear-gradient(#2B0000, #2B0000)",
          backgroundRepeat: "no-repeat",
          backgroundPosition: "top left, bottom left",
          backgroundSize: "100% 1px, 100% 1px",
          pointerEvents: "none",
          zIndex: "5",
        },

        ".sticky-lines-x": { position: "sticky" },
        ".sticky-lines-x::after": {
          content: "''",
          position: "absolute",
          top: "0",
          bottom: "0",
          left: "0",
          right: "-1px", // 右端1px拡張(ヘアライン対策)
          backgroundImage:
            "linear-gradient(#2B0000, #2B0000), linear-gradient(#2B0000, #2B0000)",
          backgroundRepeat: "no-repeat",
          backgroundPosition: "left top, right top",
          backgroundSize: "1px 100%, 1px 100%",
          pointerEvents: "none",
          zIndex: "5",
        },

        ".sticky-nudge-r::after": {
          backgroundPosition: "left top, right -1px top",
        },
      });

➡️ 適用するとこうなります

  • 縦スクロールでヘッダーの上下線が切れないこと
  • 左列の右境界線が常に安定していること
  • 交点(先頭セル)で線が太くなったり消えたりしないこと

が確認できます🙌

原理説明(background-position と background-size)

擬似要素に複数の背景を重ねて「線」を描きます。
linear-gradient(#2B0000, #2B0000) は実質単色の塗りつぶしで、これを細長い矩形にして端に貼り付けると1pxの罫線になります。

  1. 上下ライン(.sticky-lines-y::before)
background-image:
  linear-gradient(#2B0000, #2B0000), /* 上線 */
  linear-gradient(#2B0000, #2B0000); /* 下線 */

background-position:
  top left,    /* 上線は上端に貼る */
  bottom left; /* 下線は下端に貼る */

background-size:
  100% 1px,    /* 横100% × 縦1px → 横罫線 */
  100% 1px;

background-size: 100% 1px で「横いっぱい・高さ1px」の帯を作ります。

top/bottom left でそれぞれの帯を上端/下端に吸着させます。

設定例の bottom: -1px は、ズームや高DPIで発生しがちなヘアラインの隙間を隠すために擬似要素の矩形を1px広げる工夫です。

  1. 左右ライン(.sticky-lines-x::after)
background-image:
  linear-gradient(#2B0000, #2B0000), /* 左線 */
  linear-gradient(#2B0000, #2B0000); /* 右線 */

background-position:
  left top,   /* 左線は左端に貼る */
  right top;  /* 右線は右端に貼る */

background-size:
  1px 100%,   /* 横1px × 縦100% → 縦罫線 */
  1px 100%;

background-size: 1px 100% で「幅1px・高さ100%」の帯を作り、縦罫線にします。
設定例の right: -1px は擬似要素の矩形を右側に1px広げ、境界での消え・にじみを避けます。

右線がズレて見える場合は .sticky-nudge-r::after で background-position: right -1px top; とし、内側へ1px引き込みます。

💡まとめ

本記事では、stickyテーブルで罫線が消える/二重になる問題を取り上げ、borderを使わず擬似要素の背景で線を描く実装で安定化する方法を示しました。要点は次のとおりです。

  • 問題の性質を整理position: sticky によるレイヤー分離で border の重なり順が不安定化。高DPIやズームでサブピクセル誤差も増幅。

  • アプローチの転換:実 border を捨て、::before/::afterlinear-gradient を敷き、background-positionbackground-size1pxの帯を端に貼る。

  • 実装の要点

    • 横線=100% 1pxtop|bottom left に配置。
    • 縦線=1px 100%left|right top に配置。
    • 交点は z-index を段階化(先頭ヘッダー>他ヘッダー>本文)。
    • ヘアライン対策として right:-1px/bottom:-1px や微調整クラスで補正。
  • 適用ガイド:stickyセルには border を付けない。不透明背景を指定し、ユーティリティ化して再利用性を確保。border-collapse は維持可能。

小さなユーティリティにまとめると、他のテーブルへも簡単に展開できます。
同じ状況に遭遇されたら、まず border ではなく擬似要素を試してみてください🙇‍♀️

BLT SDC Tech Blog

Discussion