A common UI pattern in modern web development involves displaying dynamic tabs, panels, or views based on a parent container's active state. If you find yourself asking, "Can CSS compare an attribute on a parent element to an attribute on a child element dynamically?", you are not alone.

Developers often dream of writing a selector like this:

/* NOTE: This does NOT work in CSS */
[data-active-slug] :not([data-slug=attr(data-active-slug)]) {
  display: none;
}

Unfortunately, CSS selectors do not support dynamic cross-element attribute comparisons, nor does the attr() function evaluate inside selector definitions. However, there are clean, scalable, and modern techniques to accomplish this effect without relying on brute-force selector lists.

Why Pure CSS Cannot Compare Dynamic Attributes

CSS selectors evaluate rules against the DOM tree statically based on the defined syntax. While CSS Variables (custom properties) allow dynamic inheritance of values, selector queries themselves cannot evaluate variables or functions like attr() to match element identifiers dynamically.

Unless you define explicit rules for known constants (such as [data-active="tab1"] [data-tab="tab1"]), pure CSS alone cannot inspect two arbitrary strings at render time and equate them.

Solution 1: The Modern CSS :target Pseudo-Class (No JS)

If you genuinely want a CSS-only solution and your architecture allows URL hash navigation (ideal for tabs, multi-step forms, or modals), the :target pseudo-class is the cleanest pure-CSS technique available.

By structuring child elements with unique IDs matching an anchor link, you can conditionally show or hide them with pure CSS:

<div class="tab-container">
  <nav>
    <a href="#abc-0">Tab 1</a>
    <a href="#def-42">Tab 2</a>
    <a href="#xyz-321">Tab 3</a>
  </nav>

  <div id="abc-0" class="panel">zarro</div>
  <div id="def-42" class="panel">fortwo</div>
  <div id="xyz-321" class="panel">contact</div>
</div>
/* Hide all panels by default */
.panel {
  display: none;
}

/* Show only the active targeted panel */
.panel:target {
  display: block;
}

Solution 2: Lightweight Dynamic <style> Tag Injection (Minimal JS)

If you cannot change the HTML markup or modify URL hashes, you need a sliver of JavaScript to bridge the gap. Instead of looping through all child elements to manually toggle classes or styles, inject a dynamic CSS rule into a dedicated <style> tag.

This keeps your DOM manipulation at an absolute minimum (O(1) complexity) while letting CSS do the heavy lifting of hiding and displaying:

<style id="slug-controller"></style>

<div id="container" data-active-slug="def-42">
  <div data-slug="abc-0">zarro</div>
  <div data-slug="def-42">fortwo</div>
  <div data-slug="xyz-321">contact</div>
</div>
function setActiveSlug(slug) {
  const container = document.getElementById('container');
  container.dataset.activeSlug = slug;

  // Update a single stylesheet rule to handle all children
  const styleSheet = document.getElementById('slug-controller');
  styleSheet.textContent = `
    [data-active-slug="${slug}"] > [data-slug]:not([data-slug="${slug}"]) {
      display: none;
    }
  `;
}

// Switch active item dynamically
setActiveSlug('def-42');

Solution 3: Automatic DOM-Level MutationObserver (Zero Configuration)

If changing data-active-slug is handled by third-party code or an external framework, you can observe changes using a generic MutationObserver. Whenever the attribute updates, synchronize an active class or hidden property without manual loops in your business logic:

const observer = new MutationObserver((mutations) => {
  mutations.forEach((mutation) => {
    if (mutation.attributeName === 'data-active-slug') {
      const parent = mutation.target;
      const activeSlug = parent.getAttribute('data-active-slug');
      
      // Query only the non-matching elements and hide them
      parent.querySelectorAll('[data-slug]').forEach(child => {
        child.hidden = child.getAttribute('data-slug') !== activeSlug;
      });
    }
  });
});

const container = document.querySelector('[data-active-slug]');
observer.observe(container, { attributes: true });

Summary: Which Approach Should You Use?

  • CSS :target (Pure CSS): Best when panel state can be represented by URL hash anchors.
  • Dynamic Style Rule (Minimal JS): Best when dealing with hundreds or thousands of child items, as updating a single CSS rule is significantly faster than querying and modifying multiple DOM nodes.
  • MutationObserver (Reactive JS): Best when integrating into decoupled applications or component libraries where you cannot control how or when the parent attribute gets updated.