ข้ามไปเนื้อหาหลัก

Category: reference

CSS Subgrid — Nested Grids ที่ Align กับ Parent

CSS Subgrid ช่วยให้ nested elements align กับ grid ของ parent ได้ — แก้ปัญหา misaligned columns ใน card grids

· อ่านประมาณ 3 นาที

สารบัญ

ปัญหาที่ Subgrid แก้

ก่อน Subgrid เวลามี grid ของ cards ที่มีเนื้อหาความยาวต่างกัน elements ข้างในจะไม่ align กัน:

/* ❌ ปัญหา: title และ body ของแต่ละ card ไม่ align ข้าม column */
.grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 1rem;
}

.card {
  display: flex;
  flex-direction: column;
  /* title ของ card A สูง 2 บรรทัด, card B สูง 1 บรรทัด → body ไม่ตรงกัน */
}

Subgrid คืออะไร

/* ✓ Subgrid: card ใช้ rows จาก parent grid */
.grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  grid-template-rows: auto auto 1fr auto;  /* title | meta | body | link */
  gap: 1.5rem 1rem;
}

.card {
  display: grid;
  grid-row: span 4;      /* card ใช้ 4 rows */
  grid-template-rows: subgrid;  /* inherit rows จาก parent */
}

/* ทุก card จะ align กันอัตโนมัติ แม้ content ความยาวต่างกัน */
.card-title   { grid-row: 1; }  /* row แรกของ parent */
.card-meta    { grid-row: 2; }
.card-body    { grid-row: 3; }
.card-link    { grid-row: 4; align-self: end; }

ตัวอย่างสมบูรณ์: Card Grid

<div class="card-grid">
  <article class="card">
    <h2 class="card-title">Short Title</h2>
    <p class="card-meta">2 min read</p>
    <p class="card-body">This card has a very long description that spans multiple lines and pushes the link down further than expected.</p>
    <a class="card-link" href="#">Read more</a>
  </article>
  <article class="card">
    <h2 class="card-title">A Much Longer Card Title That Wraps to Two Lines</h2>
    <p class="card-meta">5 min read</p>
    <p class="card-body">Short description.</p>
    <a class="card-link" href="#">Read more</a>
  </article>
  <article class="card">
    <h2 class="card-title">Another Card</h2>
    <p class="card-meta">3 min read</p>
    <p class="card-body">Medium length description that takes up some space.</p>
    <a class="card-link" href="#">Read more</a>
  </article>
</div>
.card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  grid-template-rows: auto;  /* rows สร้างอัตโนมัติ */
  align-items: start;
  gap: 1.5rem;
}

.card {
  display: grid;
  grid-row: span 4;
  grid-template-rows: subgrid;  /* ✓ key: subgrid */
  padding: 1.5rem;
  border: 1px solid #e2e8f0;
  border-radius: 12px;
}

.card-title { font-size: 1.1rem; font-weight: 700; }
.card-meta  { font-size: 0.85rem; color: #94a3b8; }
.card-body  { color: #334155; line-height: 1.6; }
.card-link  {
  align-self: end;  /* ✓ ดันลงล่างสุด */
  color: #2563eb;
  font-weight: 600;
  text-decoration: none;
}

Subgrid บน Columns ด้วย

/* ใช้ subgrid กับ columns ก็ได้ */
.grid {
  display: grid;
  grid-template-columns: 1fr 2fr 1fr;
}

.item {
  grid-column: 1 / -1;  /* span ทั้ง row */
  display: grid;
  grid-template-columns: subgrid;  /* ✓ ใช้ columns จาก parent */
}

.item .label   { grid-column: 1; }  /* column แรก */
.item .content { grid-column: 2; }  /* column กลาง */
.item .action  { grid-column: 3; }  /* column สุดท้าย */

Full-bleed Elements ใน Article Layout

/* Layout ที่ content อยู่กลาง แต่บาง elements ยื่นออก */
.article {
  display: grid;
  grid-template-columns:
    1fr
    min(65ch, 100%)  /* content width */
    1fr;
  gap: 0;
}

.article > * {
  grid-column: 2;  /* ทุก element อยู่ column กลาง */
}

.full-bleed {
  grid-column: 1 / -1;  /* ยืดเต็มความกว้าง */
}

/* ถ้า .article มี children ที่เป็น wrapper เอง → ใช้ subgrid */
.article .prose {
  grid-column: 1 / -1;
  display: grid;
  grid-template-columns: subgrid;  /* ใช้ columns เดียวกับ parent */
}

.article .prose > * { grid-column: 2; }
.article .prose > .full-bleed { grid-column: 1 / -1; }

Named Grid Lines กับ Subgrid

.parent {
  display: grid;
  grid-template-columns: [sidebar-start] 240px [sidebar-end content-start] 1fr [content-end];
}

.child {
  grid-column: sidebar-start / content-end;
  display: grid;
  grid-template-columns: subgrid;
  /* ✓ ได้ named lines จาก parent ด้วย! */
}

.child .nav   { grid-column: sidebar-start / sidebar-end; }
.child .main  { grid-column: content-start / content-end; }

Browser Support

รองรับ Chrome 117+, Firefox 71+, Safari 16+ — ใช้งานได้ทั่วไปแล้วใน 2024+

/* Progressive enhancement */
.card {
  display: flex;
  flex-direction: column;  /* fallback */
}

@supports (grid-template-rows: subgrid) {
  .card-grid {
    display: grid;
    grid-template-rows: auto;
  }
  .card {
    display: grid;
    grid-row: span 4;
    grid-template-rows: subgrid;
  }
}

เปรียบเทียบกับวิธีเก่า

/* ❌ วิธีเก่า: ใช้ min-height ตายตัว — แตกเมื่อ content ยาว */
.card-title { min-height: 3rem; }

/* ❌ วิธีเก่า: ใช้ JS วัด height — ช้า, janky */
const heights = [...cards].map(c => c.querySelector('.title').offsetHeight);
const max = Math.max(...heights);
cards.forEach(c => c.querySelector('.title').style.minHeight = `${max}px`);

/* ✓ Subgrid: ไม่ต้องทำอะไรเพิ่ม — CSS จัดการเอง */