Modern CSS features: container queries and :has() selector

Alex Chang Feb 2026
1 tab
/* Container Queries */
.card-container {
  container-type: inline-size; /* Creates query container */
  container-name: card;
}

.card {
  padding: 1rem;
  border: 1px solid #ddd;
}

/* Query container width */
@container (min-width: 400px) {
  .card {
    display: grid;
    grid-template-columns: 150px 1fr;
    gap: 1rem;
  }
}

@container (min-width: 600px) {
  .card {
    grid-template-columns: 200px 1fr;
    padding: 2rem;
  }

  .card-title {
    font-size: 1.5rem;
  }
}

/* Named container queries */
.sidebar {
  container-name: sidebar;
  container-type: inline-size;
}

@container sidebar (min-width: 300px) {
  .widget {
    flex-direction: row;
  }
}

/* Container query units */
.responsive-text {
  font-size: clamp(1rem, 5cqw, 2rem); /* 5% of container width */
}

.container-relative {
  padding: 2cqi; /* Container inline size */
  margin: 1cqb; /* Container block size */
}

/* Multiple container conditions */
@container (min-width: 400px) and (max-width: 700px) {
  .card {
    background: lightblue;
  }
}

/* :has() selector - parent selection */
.card:has(img) {
  display: grid;
  grid-template-columns: 200px 1fr;
}

.card:has(> .card-image) {
  /* Direct child only */
  border-color: blue;
}

/* Form validation styling */
form:has(input:invalid) {
  border: 2px solid red;
}

form:has(input:valid) {
  border: 2px solid green;
}

.form-group:has(input:focus) {
  background-color: #f0f8ff;
}

/* Sibling selection with :has() */
h2:has(+ p) {
  /* h2 followed by p */
  margin-bottom: 0.5rem;
}

h2:not(:has(+ p)) {
  /* h2 NOT followed by p */
  margin-bottom: 2rem;
}

/* Multiple descendants */
article:has(h2, h3) {
  padding: 2rem;
}

article:has(img):has(video) {
  /* Contains both img and video */
  display: grid;
}

/* Interactive state propagation */
.button-group:has(button:hover) {
  background-color: #f5f5f5;
}

.menu:has(.menu-item:focus) {
  box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}

/* Checkbox hack replacement */
label:has(input[type="checkbox"]:checked) {
  font-weight: bold;
  color: blue;
}

.accordion:has(input:checked) .content {
  max-height: 500px;
}

/* Empty state detection */
.list:has(li) .empty-state {
  display: none;
}

.list:not(:has(li)) .empty-state {
  display: block;
}

/* Cascade Layers */
@layer reset, base, components, utilities;

@layer reset {
  * {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
  }
}

@layer base {
  body {
    font-family: system-ui, sans-serif;
    line-height: 1.5;
  }

  h1, h2, h3 {
    line-height: 1.2;
  }
}

@layer components {
  .button {
    padding: 0.5rem 1rem;
    border: none;
    border-radius: 4px;
    cursor: pointer;
  }

  .card {
    padding: 1rem;
    border: 1px solid #ddd;
    border-radius: 8px;
  }
}

@layer utilities {
  .text-center { text-align: center; }
  .flex { display: flex; }
  .hidden { display: none; }
}

/* Unlayered styles have highest priority */
.important-override {
  color: red !important;
}

/* @supports feature queries */
@supports (container-type: inline-size) {
  .card-container {
    container-type: inline-size;
  }
}

@supports not (container-type: inline-size) {
  /* Fallback for browsers without container queries */
  @media (min-width: 640px) {
    .card {
      display: grid;
      grid-template-columns: 200px 1fr;
    }
  }
}

@supports (selector(:has(*))) {
  .card:has(img) {
    grid-template-columns: 200px 1fr;
  }
}

@supports not (selector(:has(*))) {
  .card.has-image {
    grid-template-columns: 200px 1fr;
  }
}

/* Combining modern features */
@container (min-width: 500px) {
  .product-card:has(.badge) {
    position: relative;
    padding-top: 2rem;
  }

  .product-card:has(.badge) .badge {
    position: absolute;
    top: 1rem;
    right: 1rem;
  }
}

/* Color functions */
:root {
  --primary: oklch(60% 0.2 250); /* Modern color space */
}

.color-mix {
  background: color-mix(in srgb, blue 50%, red);
}

.relative-color {
  --base: #3b82f6;
  --lighter: oklch(from var(--base) calc(l + 0.2) c h);
  --darker: oklch(from var(--base) calc(l - 0.2) c h);
}

/* :is() and :where() with :has() */
:is(article, section):has(h2) {
  margin-bottom: 2rem;
}

:where(.card, .panel):has(img) {
  /* Zero specificity */
  display: grid;
}

/* Nesting (experimental) */
.card {
  padding: 1rem;

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

  & h2 {
    margin: 0;
  }

  @container (min-width: 400px) {
    & {
      display: grid;
      grid-template-columns: 200px 1fr;
    }
  }
}

/* Subgrid */
.grid-parent {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 1rem;
}

.grid-child {
  display: grid;
  grid-template-columns: subgrid; /* Inherit parent columns */
  grid-column: span 3;
}

/* Scroll-driven animations */
@supports (animation-timeline: scroll()) {
  .fade-in-on-scroll {
    animation: fade-in linear;
    animation-timeline: view();
    animation-range: entry 0% cover 30%;
  }

  @keyframes fade-in {
    from { opacity: 0; transform: translateY(20px); }
    to { opacity: 1; transform: translateY(0); }
  }
}
1 file · css Explain with highlit

Container queries enable responsive components based on container size using @container at-rule. I set container-type: inline-size to create query containers. The @container (min-width: 400px) queries container width instead of viewport. Named containers with container-name allow targeting specific parents. The :has() relational pseudo-class selects parents based on descendants. Using .card:has(img) selects cards containing images. The :has() enables sibling selectors like h2:has(+ p). Container query units (cqw, cqh, cqi, cqb) size relative to containers. The @layer rule manages cascade layers for better specificity control. Modern CSS reduces JavaScript dependency for responsive design. Feature detection with @supports ensures graceful degradation.