Skip to content

Evaluate container style queries #128

@R-Bower

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions