Skip to content

Evaluate container style queries #128

Description

@R-Bower

Container Style Queries: API Simplification Analysis

Current Approach: Data Attributes

The current API pattern requires data-size (and similar props) on every child element:

// checkbox.api.ts
export function createQdsCheckboxApi(props, normalize) {
  const size = props.size || "md"
  return {
    getControlBindings() {
      return normalize.element({
        className: checkboxClasses.control,
        "data-size": size,  // repeated
      })
    },
    getIndicatorBindings() {
      return normalize.element({
        className: checkboxClasses.indicator,
        "data-size": size,  // repeated
      })
    },
    getLabelBindings() {
      return normalize.element({
        className: checkboxClasses.label,
        "data-size": size,  // repeated
      })
    },
  }
}

CSS selects each element individually:

.qui-checkbox__control {
  --control-size: var(--sizing-70);
  &[data-size="md"] { --control-size: var(--sizing-80); }
  &[data-size="lg"] { --control-size: var(--sizing-90); }
}

.qui-checkbox__label {
  font: var(--font-static-body-xs-default);
  &[data-size="md"] { font: var(--font-static-body-sm-default); }
  &[data-size="lg"] { font: var(--font-static-body-lg-default); }
}

Proposed Approach: Container Style Queries

Set a CSS custom property on the root once, children query it:

// checkbox.api.ts - simplified
export function createQdsCheckboxApi(props, normalize) {
  const size = props.size || "md"
  return {
    getRootBindings() {
      return normalize.label({
        className: checkboxClasses.root,
        style: { "--qui-size": size },  // single declaration
      })
    },
    getControlBindings() {
      return normalize.element({
        className: checkboxClasses.control,
        // no data-size needed
      })
    },
    // ... other bindings become static
  }
}

CSS uses container style queries:

.qui-checkbox__control {
  --control-size: var(--sizing-70);

  @container style(--qui-size: md) {
    --control-size: var(--sizing-80);
  }
  @container style(--qui-size: lg) {
    --control-size: var(--sizing-90);
  }
}

.qui-checkbox__label {
  font: var(--font-static-body-xs-default);

  @container style(--qui-size: md) {
    font: var(--font-static-body-sm-default);
  }
  @container style(--qui-size: lg) {
    font: var(--font-static-body-lg-default);
  }
}

Tradeoff Comparison

Aspect Data Attributes (Current) Container Style Queries
API complexity Bindings recreated on prop change Static bindings, root sets --size once
DOM verbosity data-size on every child element Single custom property on root
DevTools debugging Easy - visible in Elements panel Harder - requires Computed Styles panel
Browser support Universal Chrome 111+, Safari 18+, Firefox 128+
CSS complexity Simple attribute selectors @container style() nesting
Performance More DOM attributes, more binding calls Fewer DOM mutations
Specificity Attribute selectors ([data-size]) Container queries (no specificity impact)

Browser Support Details

Container style queries for custom properties:

  • Chrome/Edge: 111+ (March 2023)
  • Safari: 18+ (September 2024)
  • Firefox: tbd

Note: Style queries for regular CSS properties (not custom properties) are not yet supported in any browser.

Considerations

Advantages

  1. Single source of truth - Size declared once on root
  2. Simpler API - Child bindings become constant/memoizable
  3. Reduced DOM churn - Fewer attributes to update on prop changes
  4. No specificity wars - Container queries don't add selector specificity

Disadvantages

  1. Browser support - Not available in older browsers
  2. Debugging friction - Cannot inspect active size from DOM tree alone
  3. CSS verbosity - More @container blocks vs inline attribute selectors
  4. Learning curve - Less familiar pattern for contributors

Technical Requirements

  • May need @property registration for reliable value matching:
    @property --qui-size {
      syntax: "<custom-ident>";
      inherits: true;
      initial-value: sm;
    }

Recommendation

The decision hinges on:

  1. Target browser support - If supporting older browsers, this is a non-starter without a fallback strategy
  2. Developer experience priority - Data attributes win for debuggability
  3. Performance priority - Container queries win for reduced DOM mutations

A hybrid approach is possible: use container style queries internally while still exposing data-size on the root for debugging visibility.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions