Skip to main content
css 4 mins read

CSS Nesting: Viết CSS Gọn Gàng Như Sass Nhưng Không Cần Build

Khám phá CSS Nesting — tính năng bản địa cho phép lồng selector như Sass, giúp code sạch hơn, bảo trì dễ dàng hơn mà không cần preprocessors.

#css #nesting #modern-css #web-development

Minh họa CSS Nesting

Giới thiệu

Bạn có nhớ lần đầu tiên bạn dùng Sass và thấy dòng code này?

.card {
  padding: 1.5rem;
  border-radius: 0.75rem;

  &__title {
    font-size: 1.25rem;
    font-weight: 600;
    margin-bottom: 0.5rem;
  }

  &__body {
    color: #6b7280;
    line-height: 1.6;
  }

  &:hover {
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
  }
}

Thật tuyệt vời, phải không? CSS Nesting — nay đã là tính năng bản địa trong CSS — mang lại trải nghiệm tương tự mà không cần bất kỳ bước build nào.

Trong bài viết này, chúng ta sẽ khám phá mọi thứ bạn cần biết về CSS Nesting: cú pháp, trường hợp sử dụng, best practices và cách migrate từ Sass.

CSS Nesting là gì?

CSS Nesting cho phép bạn viết selector bên trong selector khác, thay vì phải lặp lại tên class ở mỗi cấp độ. Trình duyệt sẽ tự động xử lý việc nối selector giống như preprocessors từng làm.

Trước đây (không có nesting)

.card {
  padding: 1.5rem;
  border-radius: 0.75rem;
}

.card-title {
  font-size: 1.25rem;
  font-weight: 600;
  margin-bottom: 0.5rem;
}

.card-body {
  color: #6b7280;
  line-height: 1.6;
}

.card:hover {
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}

Bây giờ (với CSS Nesting)

.card {
  padding: 1.5rem;
  border-radius: 0.75rem;

  .card-title {
    font-size: 1.25rem;
    font-weight: 600;
    margin-bottom: 0.5rem;
  }

  .card-body {
    color: #6b7280;
    line-height: 1.6;
  }

  &:hover {
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
  }
}

Nhìn sự khác biệt chưa? Code gọn hơn, cấu trúc rõ ràng hơn, và quan trọng nhất — không cần cài thêm tool nào cả.

Cú pháp cơ bản

Nối selector đơn giản

Khi bạn viết một selector bên trong một selector khác, trình duyệt sẽ nối chúng lại bằng dấu cách (descendant combinator):

.navbar {
  display: flex;
  align-items: center;
  padding: 1rem 2rem;

  .nav-link {
    color: white;
    text-decoration: none;
    padding: 0.5rem 1rem;
    border-radius: 0.375rem;
    transition: background-color 0.2s;

    &:hover {
      background-color: rgba(255, 255, 255, 0.1);
    }
  }
}

Code trên sẽ được trình duyệt dịch thành:

.navbar {
  display: flex;
  align-items: center;
  padding: 1rem 2rem;
}

.navbar .nav-link {
  color: white;
  text-decoration: none;
  padding: 0.5rem 1rem;
  border-radius: 0.375rem;
  transition: background-color 0.2s;
}

.navbar .nav-link:hover {
  background-color: rgba(255, 255, 255, 0.1);
}

Sử dụng & để tham chiếu phần tử cha

Ký hiệu & đại diện cho selector của phần tử cha. Đây là tính năng quen thuộc từ Sass và giờ đã có sẵn trong CSS native:

.button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0.75rem 1.5rem;
  border-radius: 0.5rem;
  font-weight: 600;
  cursor: pointer;
  transition: all 0.2s;

  /* Variants */
  &--primary {
    background-color: #3b82f6;
    color: white;

    &:hover {
      background-color: #2563eb;
    }

    &:active {
      transform: scale(0.98);
    }
  }

  &--secondary {
    background-color: transparent;
    color: #3b82f6;
    border: 2px solid #3b82f6;

    &:hover {
      background-color: #eff6ff;
    }
  }

  /* Sizes */
  &--sm {
    padding: 0.5rem 1rem;
    font-size: 0.875rem;
  }

  &--lg {
    padding: 1rem 2rem;
    font-size: 1.125rem;
  }
}

Lồng nhiều cấp độ

Bạn có thể lồng sâu bao nhiêu tùy thích (nhưng đừng lạm dụng nhé!):

.sidebar {
  width: 280px;
  background-color: #1f2937;
  color: white;
  padding: 1.5rem;

  .sidebar-header {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    margin-bottom: 1.5rem;
    padding-bottom: 1rem;
    border-bottom: 1px solid #374151;

    .avatar {
      width: 40px;
      height: 40px;
      border-radius: 50%;
      object-fit: cover;
    }

    .user-info {
      flex: 1;

      .username {
        font-weight: 600;
        font-size: 0.95rem;
      }

      .email {
        font-size: 0.8rem;
        color: #9ca3af;
      }
    }
  }

  .sidebar-menu {
    list-style: none;
    padding: 0;
    margin: 0;

    .menu-item {
      padding: 0.75rem 1rem;
      border-radius: 0.5rem;
      color: #d1d5db;
      text-decoration: none;
      display: flex;
      align-items: center;
      gap: 0.75rem;
      transition: all 0.2s;

      &:hover {
        background-color: #374151;
        color: white;
      }

      &.active {
        background-color: #3b82f6;
        color: white;
      }
    }
  }
}

Các tình huống sử dụng thực tế

1. Component Card hoàn chỉnh

.card {
  background: white;
  border-radius: 1rem;
  overflow: hidden;
  border: 1px solid #e5e7eb;
  transition: box-shadow 0.3s, transform 0.3s;

  &:hover {
    box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
    transform: translateY(-4px);
  }

  .card-image {
    width: 100%;
    height: 200px;
    object-fit: cover;
  }

  .card-content {
    padding: 1.5rem;

    .card-title {
      font-size: 1.25rem;
      font-weight: 700;
      color: #111827;
      margin-bottom: 0.75rem;
    }

    .card-description {
      color: #6b7280;
      line-height: 1.6;
      margin-bottom: 1rem;
    }

    .card-footer {
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding-top: 1rem;
      border-top: 1px solid #e5e7eb;

      .card-author {
        display: flex;
        align-items: center;
        gap: 0.5rem;

        .author-avatar {
          width: 32px;
          height: 32px;
          border-radius: 50%;
        }

        .author-name {
          font-size: 0.875rem;
          font-weight: 500;
          color: #374151;
        }
      }

      .card-date {
        font-size: 0.8rem;
        color: #9ca3af;
      }
    }
  }
}

2. Form đẹp với validation

.form-group {
  margin-bottom: 1.5rem;

  label {
    display: block;
    font-weight: 600;
    color: #374151;
    margin-bottom: 0.5rem;
    font-size: 0.95rem;
  }

  input,
  textarea,
  select {
    width: 100%;
    padding: 0.75rem 1rem;
    border: 2px solid #e5e7eb;
    border-radius: 0.5rem;
    font-size: 1rem;
    transition: border-color 0.2s, box-shadow 0.2s;

    &:focus {
      outline: none;
      border-color: #3b82f6;
      box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
    }

    &::placeholder {
      color: #9ca3af;
    }
  }

  /* Validation states */
  &:has(input:invalid:not(:placeholder-shown)) {
    input {
      border-color: #ef4444;

      &:focus {
        box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.1);
      }
    }

    .error-message {
      display: block;
    }
  }

  &:has(input:valid:not(:placeholder-shown)) {
    input {
      border-color: #22c55e;
    }
  }

  .error-message {
    display: none;
    color: #ef4444;
    font-size: 0.85rem;
    margin-top: 0.375rem;
  }

  .helper-text {
    color: #6b7280;
    font-size: 0.85rem;
    margin-top: 0.375rem;
  }
}

3. Modal / Dialog

.modal-overlay {
  position: fixed;
  inset: 0;
  background-color: rgba(0, 0, 0, 0.5);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 50;
  opacity: 0;
  visibility: hidden;
  transition: all 0.3s;

  &.is-open {
    opacity: 1;
    visibility: visible;
  }

  .modal-content {
    background: white;
    border-radius: 1rem;
    max-width: 500px;
    width: 90%;
    max-height: 85vh;
    overflow-y: auto;
    transform: scale(0.9) translateY(20px);
    transition: transform 0.3s;

    &.is-open {
      transform: scale(1) translateY(0);
    }

    .modal-header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding: 1.5rem;
      border-bottom: 1px solid #e5e7eb;

      .modal-title {
        font-size: 1.25rem;
        font-weight: 700;
        color: #111827;
      }

      .modal-close {
        background: none;
        border: none;
        font-size: 1.5rem;
        color: #6b7280;
        cursor: pointer;
        padding: 0.25rem;
        border-radius: 0.375rem;
        transition: all 0.2s;

        &:hover {
          background-color: #f3f4f6;
          color: #111827;
        }
      }
    }

    .modal-body {
      padding: 1.5rem;
      color: #374151;
      line-height: 1.6;
    }

    .modal-footer {
      display: flex;
      justify-content: flex-end;
      gap: 0.75rem;
      padding: 1rem 1.5rem;
      border-top: 1px solid #e5e7eb;
    }
  }
}

4. Table responsive

.table-container {
  overflow-x: auto;
  border-radius: 0.75rem;
  border: 1px solid #e5e7eb;

  table {
    width: 100%;
    border-collapse: collapse;

    thead {
      background-color: #f9fafb;

      th {
        padding: 0.875rem 1.25rem;
        text-align: left;
        font-weight: 600;
        color: #374151;
        font-size: 0.875rem;
        text-transform: uppercase;
        letter-spacing: 0.05em;
        border-bottom: 2px solid #e5e7eb;
      }
    }

    tbody {
      tr {
        border-bottom: 1px solid #f3f4f6;
        transition: background-color 0.15s;

        &:hover {
          background-color: #f9fafb;
        }

        &:last-child {
          border-bottom: none;
        }

        td {
          padding: 1rem 1.25rem;
          color: #374151;
          font-size: 0.95rem;
        }
      }
    }

    /* Status badges inside table cells */
    .status-badge {
      display: inline-flex;
      align-items: center;
      padding: 0.25rem 0.75rem;
      border-radius: 9999px;
      font-size: 0.8rem;
      font-weight: 600;

      &--active {
        background-color: #dcfce7;
        color: #166534;
      }

      &--pending {
        background-color: #fef3c7;
        color: #92400e;
      }

      &--inactive {
        background-color: #fee2e2;
        color: #991b1b;
      }
    }
  }
}

So sánh CSS Nesting vs Sass

Tính năngSassCSS NativeGhi chú
Nối selector&&Giống hệt nhau
Biến CSS$variable--variableKhác cú pháp
Hàm mixin@mixin, @include❌ Không cóCSS chưa hỗ trợ
Phép toán$width + 10calc()Khác cú pháp
Import file@import@import / @layerCSS có @layer
Điều kiện@if, @for❌ Không cóChỉ CSS thuần
Tốc độCần compile✅ Không cần buildCSS nhanh hơn

Khi nào nên giữ Sass?

Giữ Sass nếu bạn cần:

  • Mixin phức tạp (tạo ra nhiều CSS cùng lúc)
  • Phép toán nâng cao (không chỉ calc())
  • Điều kiện logic (@if/@else)
  • Lặp lại (@for/@each)

Dùng CSS Nesting khi bạn chỉ cần:

  • Tổ chức code theo component
  • Nối selector con/cha
  • Viết pseudo-class/pseudo-element

Hỗ trợ trình duyệt

CSS Nesting đã được hỗ trợ rộng rãi kể từ năm 2023–2024:

  • ✅ Chrome 117+
  • ✅ Firefox 119+
  • ✅ Safari 16.4+
  • ✅ Edge 117+

Hỗ trợ hiện tại vượt quá 92% người dùng web toàn cầu. Bạn có thể yên tâm sử dụng trong production.

💡 Mẹo: Nếu cần hỗ trợ trình duyệt cũ hơn, hãy dùng PostCSS với plugin postcss-nesting. Nó transpile CSS nesting sang CSS truyền thống.

Best Practices

1. Giới hạn độ sâu nesting

Đừng lồng quá 3–4 cấp. Code sẽ khó đọc và selector sẽ quá cụ thể:

/* ❌ Quá sâu — khó đọc, specificity cao */
.page .main .content .article .body .paragraph .text {
  font-size: 1rem;
}

/* ✅ Giới hạn hợp lý */
.article {
  .article-body {
    .article-text {
      font-size: 1rem;
    }
  }
}

2. Ưu tiên flat structure khi có thể

Nếu component nhỏ, không cần nesting:

/* ❌ Over-engineering cho component nhỏ */
.icon {
  width: 24px;
  height: 24px;

  &--small {
    width: 16px;
    height: 16px;
  }

  &--large {
    width: 32px;
    height: 32px;
  }
}

/* ✅ Đơn giản hơn */
.icon {
  width: 24px;
  height: 24px;
}

.icon--small {
  width: 16px;
  height: 16px;
}

.icon--large {
  width: 32px;
  height: 32px;
}

3. Kết hợp với CSS Custom Properties

.theme-card {
  --card-bg: white;
  --card-text: #111827;
  --card-accent: #3b82f6;

  background: var(--card-bg);
  color: var(--card-text);
  border-left: 4px solid var(--card-accent);

  .theme-card-title {
    color: var(--card-accent);
  }

  .theme-card-action {
    background: var(--card-accent);
    color: white;
  }
}

/* Override cho dark mode */
[data-theme="dark"] .theme-card {
  --card-bg: #1f2937;
  --card-text: #f9fafb;
  --card-accent: #60a5fa;
}

4. Dùng @media@container bên trong selector

Bạn có đặt media queries bên trong selector thay vì ở ngoài:

.responsive-grid {
  display: grid;
  gap: 1.5rem;
  grid-template-columns: repeat(3, 1fr);

  @media (max-width: 768px) {
    grid-template-columns: 1fr;
  }

  .grid-item {
    padding: 1.5rem;
    border-radius: 0.75rem;
    background: white;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);

    @media (max-width: 768px) {
      padding: 1rem;
    }
  }
}

5. Kết hợp với :is():where()

Để kiểm soát specificity:

.component {
  /* :where() — specificity = 0 */
  &:where(.variant-a, .variant-b, .variant-c) {
    padding: 1rem;
  }

  /* :is() — specificity cao nhất */
  &:is(.highlighted, [data-active]) {
    background-color: #eff6ff;
    border-color: #3b82f6;
  }
}

Ví dụ tổng hợp: Dashboard Layout

Hãy xem một ví dụ thực tế kết hợp nhiều kỹ thuật:

<div class="dashboard">
  <aside class="dashboard-sidebar">
    <div class="sidebar-brand">
      <img src="/logo.svg" alt="Logo" class="brand-logo" />
      <span class="brand-name">MyApp</span>
    </div>
    <nav class="sidebar-nav">
      <a href="/" class="nav-link active">
        <svg class="nav-icon">...</svg>
        <span class="nav-label">Dashboard</span>
      </a>
      <a href="/users" class="nav-link">
        <svg class="nav-icon">...</svg>
        <span class="nav-label">Users</span>
      </a>
    </nav>
  </aside>
  <main class="dashboard-main">
    <header class="main-header">
      <h1 class="page-title">Tổng quan</h1>
      <button class="btn btn-primary">+ Thêm mới</button>
    </header>
    <div class="stats-grid">
      <div class="stat-card">
        <h3 class="stat-label">Người dùng</h3>
        <p class="stat-value">12,450</p>
        <span class="stat-change positive">↑ 12%</span>
      </div>
    </div>
  </main>
</div>
.dashboard {
  display: grid;
  grid-template-columns: 260px 1fr;
  min-height: 100vh;

  @media (max-width: 768px) {
    grid-template-columns: 1fr;
  }

  .dashboard-sidebar {
    background-color: #111827;
    color: white;
    padding: 1.5rem;

    .sidebar-brand {
      display: flex;
      align-items: center;
      gap: 0.75rem;
      margin-bottom: 2rem;
      padding-bottom: 1.5rem;
      border-bottom: 1px solid #374151;

      .brand-logo {
        width: 36px;
        height: 36px;
        border-radius: 0.5rem;
      }

      .brand-name {
        font-size: 1.25rem;
        font-weight: 700;
      }
    }

    .sidebar-nav {
      display: flex;
      flex-direction: column;
      gap: 0.25rem;

      .nav-link {
        display: flex;
        align-items: center;
        gap: 0.75rem;
        padding: 0.75rem 1rem;
        border-radius: 0.5rem;
        color: #d1d5db;
        text-decoration: none;
        transition: all 0.2s;

        .nav-icon {
          width: 20px;
          height: 20px;
          flex-shrink: 0;
        }

        &:hover {
          background-color: #374151;
          color: white;
        }

        &.active {
          background-color: #3b82f6;
          color: white;
        }
      }
    }
  }

  .dashboard-main {
    background-color: #f9fafb;
    padding: 2rem;

    .main-header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      margin-bottom: 2rem;

      .page-title {
        font-size: 1.75rem;
        font-weight: 700;
        color: #111827;
      }

      .btn {
        padding: 0.75rem 1.5rem;
        border-radius: 0.5rem;
        font-weight: 600;
        cursor: pointer;
        transition: all 0.2s;

        &.btn-primary {
          background-color: #3b82f6;
          color: white;
          border: none;

          &:hover {
            background-color: #2563eb;
          }
        }
      }
    }

    .stats-grid {
      display: grid;
      grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
      gap: 1.5rem;

      .stat-card {
        background: white;
        padding: 1.5rem;
        border-radius: 0.75rem;
        border: 1px solid #e5e7eb;

        .stat-label {
          font-size: 0.875rem;
          color: #6b7280;
          font-weight: 500;
          margin-bottom: 0.5rem;
        }

        .stat-value {
          font-size: 2rem;
          font-weight: 700;
          color: #111827;
          margin-bottom: 0.5rem;
        }

        .stat-change {
          font-size: 0.875rem;
          font-weight: 600;

          &.positive {
            color: #16a34a;
          }

          &.negative {
            color: #dc2626;
          }
        }
      }
    }
  }
}

Migrate từ Sass sang CSS Nesting

Nếu bạn đang dùng Sass và muốn chuyển sang CSS Nesting native, đây là các bước:

Bước 1: Thay thế & — không đổi

Cú pháp & hoạt động giống hệt nhau:

/* Sass cũ */
.btn {
  padding: 0.75rem 1.5rem;
  &--primary { background: blue; }
}

/* CSS Nesting mới */
.btn {
  padding: 0.75rem 1.5rem;
  &--primary { background: blue; }
}

Bước 2: Thay biến Sass bằng CSS Custom Properties

/* Sass cũ */
$primary-color: #3b82f6;
$border-radius: 0.5rem;

.card {
  background: $primary-color;
  border-radius: $border-radius;
}
/* CSS Nesting mới */
:root {
  --primary-color: #3b82f6;
  --border-radius: 0.5rem;
}

.card {
  background: var(--primary-color);
  border-radius: var(--border-radius);
}

Bước 3: Thay calc() cho phép toán

/* Sass cũ */
.container {
  width: $content-width - $sidebar-width;
}
/* CSS Nesting mới */
.container {
  width: calc(var(--content-width) - var(--sidebar-width));
}

Bước 4: Loại bỏ mixin không cần thiết

Nếu mixin chỉ tạo ra CSS tĩnh (không có điều kiện), bạn có thể thay bằng CSS Custom Properties hoặc copy-paste đơn giản.

Kết luận

CSS Nesting là một bước tiến lớn cho CSS. Nó mang lại:

  • Code gọn gàng hơn — cấu trúc rõ ràng theo component
  • Không cần build step — trình duyệt hiểu trực tiếp
  • Hỗ trợ rộng rãi — 92%+ người dùng
  • Tương thích ngược — code CSS cũ vẫn hoạt động bình thường
  • Performance tốt hơn — không có thời gian compile

Những điểm chính cần nhớ:

  • ✅ Dùng & để tham chiếu phần tử cha
  • ✅ Giới hạn độ sâu nesting ở 3–4 cấp
  • ✅ Kết hợp với CSS Custom Properties cho flexibility
  • ✅ Đặt @media@container bên trong selector
  • ✅ Cân nhắc giữ Sass nếu cần mixin/phép toán phức tạp

Hãy thử CSS Nesting trong dự án tiếp theo của bạn. Bạn sẽ ngạc nhiên trước sự đơn giản và sức mạnh của nó!


Bài viết được đăng bởi Devs2.org. Nếu bạn thấy bài viết hữu ích, hãy chia sẻ với đồng nghiệp của bạn!

Recently Used Tools