Search code examples
javascriptcssshadow-domcss-houdini

@property not recognized in shadow DOM


Now that @property recently got shipped with firefox 128, I thought it would be a good idea to try it out.

It works just fine when using regular HTML and CSS

@property --a1 {
  syntax: '<angle>';
  inherits: false;
  initial-value: 10deg;
}

.hi1 {
    border: 2px solid black;
    width: fit-content;
    margin: 0 auto;
}

.box1 {
    --a1:10deg; /*  needed for firefox to have a valid output */
    cursor:pointer;
    width:250px;
    height:200px;
    margin:15px;
    display:inline-block;
    transition:--a1 0.5s;
    background:linear-gradient(var(--a1),red ,blue);
}
.box1:hover {
    --a1:180deg;
}

body {
  text-align:center;
}
<div class="hi1">
    <h1>In regular DOM</h1>
    <div class="box1"></div>
    <div class="box1" style="background:conic-gradient(from var(--a1), red var(--a1),blue)"></div>
    <div class="box1" style="background:linear-gradient(calc(180deg - var(--a1)),red  ,blue,red )"></div>
</div>

But if I use it in a shadow DOM, it suddenly gets completely ignored.

const css = `@property --a2 {
  syntax: '<angle>';
  inherits: false;
  initial-value: 10deg;
}

.hi2 {
    border: 2px solid black;
    width: fit-content;
    margin: 0 auto;
}

.box2 {
    --a2:10deg; /*  needed for firefox to have a valid output */
    cursor:pointer;
    width:250px;
    height:200px;
    margin:15px;
    display:inline-block;
    transition:--a2 0.5s;
    background:linear-gradient(var(--a2),red ,blue);
}
.box2:hover {
    --a2:180deg;
}`;

class MyComponent extends HTMLElement {
    constructor() {
        super();

        const root = this.attachShadow({ mode: 'closed' });
        root.innerHTML = `
<style>${css}</style>
<div class="hi2">
    <h1>In shadow DOM</h1>
    <div class="box2"></div>
    <div class="box2" style="background:conic-gradient(from var(--a2), red var(--a2),blue)"></div>
    <div class="box2" style="background:linear-gradient(calc(180deg - var(--a2)),red  ,blue,red )"></div>
</div>`;
    }
    
    connectedCallback() {
        
    }
  }

  customElements.define('my-component', MyComponent);
<my-component>
</my-component>

Any idea why or how I might convince it to work?

Here's a codepen illustrating the issue https://codepen.io/true-cc/pen/eYwYJaM


Solution

  • It seems that this should work, though since no browsers support it, I guess it's a specs bug. The specs have this paragraph on this:

    Unlike many concepts in CSS (see CSS Scoping 1 § 3.5 Name-Defining Constructs and Inheritance), property registrations are not scoped to a tree scope. All registrations, whether they appear in the outermost document or within a shadow tree, interact in a single global registration map for the Document. Why can’t registrations be scoped?

    When custom properties are exposed as part of a Shadow DOM-using component’s public API, this global registration behavior works as intended. If the outer page is using a custom property of the same name for different purposes, that is already a conflict that needs to be resolved, and the registration behavior does not make it worse.

    If a custom property is intended for private internal usage for a component, however, it is recommended that the property be given a likely-unique name, to minimize the possibility of a clash with any other context. This can be done, for example, by including the project name, or some short random string of text, in the name of the property.

    What this says is that since @property is just another way of calling the global CSS.registerProperty() method, if you do define such a rule inside a shadow DOM, it will still affect the light DOM too, because there is only one registry.

    However, no browser supports the rule inside the shadow DOM, they'll all ignore it entirely.

    So, instead, you can declare that rule globally, or use CSS.registerProperty(), even if browsers were supporting definition if shadow DOM it would not be scoped to said shadow DOM anyway.

    Using a global rule:

    const css = `
    .hi2 {
        border: 2px solid black;
        width: fit-content;
        margin: 0 auto;
    }
    
    .box2 {
        --a2:10deg; /*  needed for firefox to have a valid output */
        cursor:pointer;
        width:250px;
        height:200px;
        margin:15px;
        display:inline-block;
        transition:--a2 0.5s;
        background:linear-gradient(var(--a2),red ,blue);
    }
    .box2:hover {
        --a2:180deg;
    }`;
    
    class MyComponent extends HTMLElement {
        constructor() {
            super();
    
            const root = this.attachShadow({ mode: 'closed' });
            root.innerHTML = `
    <style>${css}</style>
    <div class="hi2">
        <h1>In shadow DOM</h1>
        <div class="box2"></div>
        <div class="box2" style="background:conic-gradient(from var(--a2), red var(--a2),blue)"></div>
        <div class="box2" style="background:linear-gradient(calc(180deg - var(--a2)),red  ,blue,red )"></div>
    </div>`;
        }
        
        connectedCallback() {
            
        }
      }
    
      customElements.define('my-component', MyComponent);
    @property --a2 {
      syntax: '<angle>';
      inherits: false;
      initial-value: 10deg;
    }
    <my-component>
    </my-component>

    Using CSS.registerProperty():

    const css = `
    .hi2 {
        border: 2px solid black;
        width: fit-content;
        margin: 0 auto;
    }
    
    .box2 {
        --a2:10deg; /*  needed for firefox to have a valid output */
        cursor:pointer;
        width:250px;
        height:200px;
        margin:15px;
        display:inline-block;
        transition:--a2 0.5s;
        background:linear-gradient(var(--a2),red ,blue);
    }
    .box2:hover {
        --a2:180deg;
    }`;
    
    class MyComponent extends HTMLElement {
        constructor() {
            super();
    
            const root = this.attachShadow({ mode: 'closed' });
            root.innerHTML = `
    <style>${css}</style>
    <div class="hi2">
        <h1>In shadow DOM</h1>
        <div class="box2"></div>
        <div class="box2" style="background:conic-gradient(from var(--a2), red var(--a2),blue)"></div>
        <div class="box2" style="background:linear-gradient(calc(180deg - var(--a2)),red  ,blue,red )"></div>
    </div>`;
            CSS.registerProperty({
              name: "--a2",
              syntax: "<angle>",
              inherits: false,
              initialValue: "10deg",
            });
        }
        
        connectedCallback() {
            
        }
      }
    
      customElements.define('my-component', MyComponent);
    @property --a2 {
      syntax: '<angle>';
      inherits: false;
      initial-value: 10deg;
    }
    <my-component>
    </my-component>