old-docs-site icon indicating copy to clipboard operation
old-docs-site copied to clipboard

Overview of styling possibilities

Open mercmobily opened this issue 8 years ago • 13 comments

I kept on getting confused by the styling options in Polymer. Also, I am employing a great web designer with no Polymer experience, and I had a bit of a hard time explaining everything.

So, I wrote this short guide. It sums up nicely the whole "CSS" thing so that anybody can hopefully understand. I feel it's important to have one file that:

  • Explains clearly the best practices when defining styles and custom properties in Polymer
  • Have everything in one spot

It would be awesome if you could:

  1. Check that it's all factually correct (I am especially worried about having used the right technical terms throughout)
  2. Give me the OK to turn this into a blog post (I will flesh things up)
  3. Include it in the official documentation "somehow".

I realise that the documentation covers all of this, but I think the documentation can be made stronger in terms of best practices etc.

I could just publish this in Free Software Magazine, but I think it's important enough to be in the main documentation...

Overview

In Polymer (and web components in general), CSS encapsulation is enforced by the following principle:

  • Global styles won't apply to tags in the shadow DOM of a component
  • The only things that can "leak" from a global <style> are CSS custom properties and CSS mixins that match the :root selector
  • It's still possible to create a stylesheet with selectors (like you would normally); however, if you want to be able to use those stylesheets both from a custom element (affecting its shadow DOM) and from a main document, they will need to be 1) encapsulated within a style-only component, 2) imported 3) included explicitly by the style tag of the component or document.

Here is a verbose explanation of all this.

Defining global custom properties with :root: my-props.html

Create a file with just the <style> tag (with the the custom-style attribute). Any property or mixin defined within a :root selector will be set in the CSS global scope, and will therefore be available to modules and main documents.

my-props.html

<link rel="import" href="../polymer/polymer.html">
<style is="custom-style">

  // The custom property `--red` is defined in the CSS global scope
  :root {
    --red: {
      color: red;
    };
    --main-background: #FFFFFF;
  }

  // The following selector will have NO effect in shadow DOM of modules
  // even if `my-props.html` is loaded in file containing the module.
  .blue {
    color: blue;
  }

</style>

Include properties in the main document

Here as soon as my-props is loaded, the custom CSS properties are added to the CSS global scope (in this case, the --red mixin and the --main-background property). So, they can be used at will immediately. Also, since there is no shadow dom involved (this is just a plain HTML file), the .blue style will also work.

file.html

<link rel="import" href="bower_components/my-props/my-props.html">

<style is="custom-style">
  .red {
    @apply(--red);
    background-color: var(--main-background);
  }
</style>


<body>
  <p class="red">I am red, and have FFFFFF as background color</p>
  <p class="blue">I am blue</p>
</body>

Import props in an element

Here too, as soon as my-mixinx.html is loaded, the custom CSS properties are available (in this case, --red). However, other selectors (like .blue) will fail as they are not allowed to pierce through the shadow DOM. So, within a module, only CSS custom properties and mixins matching :root can actually be used within that module.

my-element.html

<link rel="import" href="bower_components/my-props/my-props.html">
<link rel="import" href="../polymer/polymer.html">

<dom-module is="my-element">
  <style>
    .red {
      @apply(--red);
      background-color: var(--main-background);
    }
    ...
  </style>

  <p class="red">I am red, and have FFFFFF as background color</p>
  <p class="blue">I am NOT blue</p>

  <script>
    Polymer({
      is: 'my-element'
    });
  </script>

</dom-module>

Defining CSS selectors and use them in files and modules: my-classes.html

If you want to define styles and make them available both within an element or a main HTML document, you will need to encapsulated them and then include them in the element or the main HTML document.

Create a module where the template only contains a <style> -- no actual HTML nor Polymer() call:

my-classes.html

<link rel="import" href="../polymer/polymer.html">
<dom-module id="my-classes">
  <template>
    <style>
      .red {
        color: red;
      }
    </style>
  </template>
</dom-module>

Include classes in the main document

Just importing such a file will not affect CSS styles in the document -- it will just define a template within a <dom-module> element. In order to actually enable those styles, they need to be imported in a <style> tag.

file.html

<head>

  <link rel="import" href="bower_components/my-classes/my-classes.html">

  <style is="custom-style" include="my-classes"></style>
</head>
...
<body>
  <p class="red">This is red</p>
</body>

This will have the effect of actually applying the styles defined in my-classes.html.

TODO: check that the style will strictly only apply to that document, or if standard CSS rules will apply.

Import classes in an element

You can import my-classes.html, and then include it in the element's style tag, in order to have the extra classes available within the element's shadow DOM:

my-element.html

<link rel="import" href="bower_components/my-classes/my-classes.html">
<link rel="import" href="../polymer/polymer.html">

<dom-module is="my-element">
  <style include="my-classes">  
    ...
  </style>

  <p class="red">This is red</p>

  <script>
    Polymer({
      is: 'my-element'
    });
  </script>

</dom-module>

mercmobily avatar Nov 17 '16 01:11 mercmobily

Deleted my prior comments because I didn't realize your original intent was to create a theme file.

OK let's back way up.

First, a custom property != a mixin. Custom properties let you set 1 css property. A mixin lets you set a whole bunch of css properties. Custom properties are natively support by all browsers except for IE/Edge. Mixins are not natively supported by any browser.

Here are the correct patterns for defining and using custom properties and mixins in Custom Elements:

Defining a custom property:

html {
  --background-color: red;
}

Using a custom property in an element:

:host {
  background: var(--background-color, blue); /* blue is the fallback value */
}

Defining a mixin:

html {
  --background-mixin: {
    background-image: url('...');
    background-color: pink;
    background-position: 0% 0%;
  }
}

Using a mixin in an element:

:host {
  background-image: url('...');
  background-color: green;
  background-position: 50% 50%;  /* The above 3 properties are fallback values */
  @apply(--background-mixin);
}

The way your element exposes styling hooks is exactly as I have outlined above. For example, you could say that the above custom property element exposes a custom property of --background-color. The consumer of your custom element can choose to set this value to theme your element.

If you're building a custom element it probably should not define a value for a custom property/mixin, unless you are only using that property internally. Instead it should only consume properties/mixins. This is because you don't want the act of including a custom element to suddenly change the theme of your entire site. That would violate the idea of css scoping/encapsulation.

/* Inside a Custom Element's shadow root... */
:root {
  --background-color: red;  /* probably bad */
}
:host {
  background: var(--background-color, red); /* Good. We only consume properties */
}

Also I would suggest avoiding the use of :root and instead use html when you are trying to define document level custom properties. I know the docs say to use :root and technically I think that's fine to do at the document level, but Polymer also used to allow you to use :root inside of the Shadow DOM and that was actually a bug. Avoiding :root all together might just be a good habit to get into. cc @arthurevans to correct me if I'm totes wrong about stuff

robdodson avatar Nov 18 '16 20:11 robdodson

@robdodson wait a sec, my man intent wasn't about creating a theme, as much as explaining how styling works in general. Then, this info is usable to create themes, consume them, etc.

I know mixins and properties are different. The confusion in the document came from the fact that I see mixins as properties are strictly inter-related -- they are just used differently. I mean, writing:

:root {
  --my-property: red;
}

Or:

:root {
   --my-mixin: {
     color: red;
     background-color: blue;
     ...
   }
}

In my little head is still "assigning" something to the "variables" --my-property and --my-mixin. Sure, one is a property and the other one is a mixin. But... I updated my post, adding both a mixin and a property. This actually makes it more complete.

Thank you!

mercmobily avatar Nov 19 '16 05:11 mercmobily

(Ah, let me know if I should change :root with HTML too -- I did't address that in your response, sorry!)

mercmobily avatar Nov 19 '16 07:11 mercmobily

Hey @mercmobily. Sorry for the late response. Let me go though your first section, with advance apologies for being nitpicky.

In Polymer (and web components in general), CSS encapsulation is enforced by the following principle:

Just to clarify, we're talking about shadow DOM here, not anything specific to Polymer. (Except for style modules, at the end, and some details about Polyfills.) I note this because in the past we've had confusion about what is custom elements vs. what is shadow DOM vs. what is Polymer.

Global styles won't apply to tags in the shadow DOM of a component

This depends on what you mean by "apply". Really, when we're talking about shadow DOM style encapsulation, what we're talking about is selector matching. A selector in the main document can't match an element inside a shadow tree. A selector inside a shadow tree can only match:

  • Another element in the shadow tree,
  • The element hosting the shadow tree (with :host),
  • An immediate light DOM child, distributed through a <slot> (with ::slotted()).

If you're using shady DOM, note that CSS styles in the main document can leak into shadow trees. The fix for this is to define document-level styles inside a custom-style element.

The only things that can "leak" from a global

I would avoid the use "leak" here, which implies a failure. The shady DOM polyfill does allow styles to leak, as described above. This is undesirable behavior.

The thing that lets custom properties and mixins work is style inheritance. Any inheritable property can be inherited down through shadow roots. Inheritance isn't leakage: it's the way the specs are designed. Inheritable properties include foreground color (color property) and font-related properties, among others. Custom properties are also inheritable, which is what makes them useful for styling custom elements that have shadow DOM.

So, given a tree like this:

<html>
  <body>
    <div>
      <x-foo>
        #shadow-root
          <x-bar>

If I have a document-level rule like:

html { color: red; }

That inherits down the tree, unless another rule resets it. For example:

div { color: blue; } 

OK, now div and all of its descendants are going to be blue. How about this?

x-bar { color: green; } 

Is x-bar green? Nope, because this selector (in the main document scope) doesn't match the x-bar in x-foo's shadow root.

So, in theory, I can write a rule like this:

div { --custom-property: 'some-value'; } 

And everything will be whizbang. (This will in fact work on Firefox and Chrome, for example.) Of course, we don't live in a world of theory, and there's a hitch. Not all browsers support custom properties, and the custom properties Polymer uses has limitations. The shim only works if the selector matches html, :root, or a Polymer custom element. (I'm told this is an optimization to avoid doing a gazillion extra calculations.) That div ruleset above? That won't work on Edge, for example, which doesn't have native custom properties.

So that leaves us with two basic ways to set custom properties:

/* set a global default value for a custom property */
html { --custom-property: 'some-value'; } 
/* set a different custom property value for any `x-foo` in the main document scope */
x-foo { --custom-property: 'another-value'; }

So, this end-point is pretty close to what you said, with a couple of important differences:

  • Properties inherit, not leak, and it's not restricted to custom properties.
  • A document-level stylesheet can define custom properties in a ruleset that matches a top-level selector (html) or that matches a Polymer element (x-foo).
  • Theoretically, you could define custom properties on any element in the tree, but to work with the polyfill, you should stick with the two cases above.

It's still possible to create a stylesheet with selectors (like you would normally); however, if you want to be able to use those stylesheets both from a custom element (affecting its shadow DOM) and from a main document, they will need to be 1) encapsulated within a style-only component, 2) imported 3) included explicitly by the style tag of the component or document. Here is a verbose explanation of all this.

Just to note that this mechanism (we call them "style modules") is completely Polymer-specific, unlike the other things discussed here.

arthurevans avatar Nov 29 '16 00:11 arthurevans

Thank you for the amazing feedback! You are not nitpicking at all -- you are actually showing that I still have a long way to go before I "get" Polymer properly. Apologies for this.

I am going to fix the (now obvious) inaccuracies and send you a ping. If you think it's worth republishing, let me know!

(Publish it in your own name, or as the Polymer team -- I am really not after fame and exposure, I am too Zen for that!)

Thanks again for the feedback!

mercmobily avatar Nov 29 '16 06:11 mercmobily

@arthurevans Wow this is invaluable info for anybody out there. I am adapting and incorporating everything you wrote -- it's just so clearly written and explained... Definitely a must publishing it now!

mercmobily avatar Dec 06 '16 07:12 mercmobily

Overview

In Shadow DOM (and therefore in Polymer, which leverages it), CSS encapsulation is enforced by the following principle:

  • Styles are encapsulated by limiting scope of selectors.

  • The only way to affect the styling of an element inside the shadow tree of a contained element is by using CSS custom properties and CSS mixins that match the :root selector

  • It's still possible to create a stylesheet with selectors (like you would normally); however, if you want to be able to use those stylesheets both from a custom element (affecting its shadow DOM) and from a main document, they will need to be 1) encapsulated within a style-only component, 2) imported 3) included explicitly by the style tag of the component or document.

Here is a verbose explanation of all this.

Scope of selectors

A selector can't match an element inside the shadow tree of an element.

A selector can only match:

  1. Another element in the same shadow tree or in the same same root document as the selector
  2. The element hosting the shadow tree (with :host),
  3. An immediate light DOM child, distributed through a <slot> (with ::slotted()).

If using Shady DOM, remember to use <custom-style> rather than <style> to prevent leakage (since shadow-dom is simulated and leakage is actually possible).

CSS Custom properties and mixins

Custom properties can be set easily using a selector:

..
<html>
  <head>
    <custom-style>

      x-element { 
        --custom-color: 'red';
      }
    </custom-style>

An element can then use the custom property:

<dom-module is="x-element">
  <template>
    p {
      color: var(--custom-color)
    }
  </template>
  ...
</dom-module>

Style inheritance is a really important feature that make custom CSS properties and mixins work. Inheritable properties include foreground color (color property) and font-related properties, among others. Custom properties are also inheritable, which is what makes them useful for styling custom elements that have shadow DOM.

For example:

<custom-style>
...
</custom-style>

<html>
  <body>
    <div>
      <x-foo> <!-- This is a custom element -->
        #shadow-root
           <x-bar>

Imagine having the following selector in custom-style that inherits down the tree:

html { color: red; }

You can specify a rule that resets it, so that div and all of its descendants are going to be blue:

div { color: blue; } 

However, due to the selector incapsulation discussed earlier, it's not possible to style x-bar from this document's root. So, this won't work:

x-bar { color: green; } 

However, it is possible to set a custom property:

x-bar { --custom-color: green; } 

With the knowledge that x-bar will use --custom-color in its styling.

Shim's limitations

Polymer's shim to implement custom CSS properties and mixins has limitations. In terms of inheritance, you can only assign a custom property to html or to a custom element:

/* set a global default value for a custom property */
html { --custom-color: 'blue'; } 
/* set a different custom property value for any `x-foo` in the main document scope */
x-foo { --custom-color: 'red'; }

Defining styles and global custom properties in html: my-props.html

Create a file with just a <custom-style> element. Any property or mixin defined within a html selector will be set in the CSS global scope, and will therefore be available to modules and main documents.

my-props.html

<link rel="import" href="../polymer/polymer.html">
<custom-style>

  // The custom property `--red` is defined in the CSS global scope
  html {
    --red: {
      color: red;
    };
    --main-background: #FFFFFF;
  }

  // The following selector will NOT affect the shadow DOM of modules
  // even if `my-props.html` is loaded in file containing the module.
  .blue {
    color: blue;
  }

</custom-style>

Include properties in the main document

Here as soon as my-props is loaded, the custom CSS properties are added to the CSS global scope (in this case, the --red mixin and the --main-background property). So, they can be used immediately. Also, since there is no shadow dom involved (this is just a plain HTML file), the .blue style will also work.

file.html

<link rel="import" href="bower_components/my-props/my-props.html">

<custom-style">
  .red {
    @apply(--red);
    background-color: var(--main-background);
  }
</custom-style>


<body>
  <p class="red">I am red, and have FFFFFF as background color</p>
  <p class="blue">I am blue</p>
</body>

Import properties in an element

Here too, as soon as my-mixinx.html is loaded, the custom CSS properties are available (in this case, --red). However, other selectors (like .blue) will fail as they are not allowed to pierce through the shadow DOM. So, within a module, only CSS custom properties and mixins matching html can actually be used within that module.

my-element.html

<link rel="import" href="bower_components/my-props/my-props.html">
<link rel="import" href="../polymer/polymer.html">

<dom-module id="my-element">
  <style>
    .red {
      @apply(--red);
      background-color: var(--main-background);
    }
    ...
  </style>

  <p class="red">I am red, and have FFFFFF as background color</p>
  <p class="blue">I am NOT blue</p>

  <script>
    Polymer({
      is: 'my-element'
    });
  </script>

</dom-module>

QUESTION: WOULD IT EVEN EVER MAKE SENSE TO DEFINE CUSTOM PROPERTIES WITHIN A STYLE MODULE? IF SO, WHEN?

Defining styles and use them in files and modules: my-classes.html

If you want to define styles and make them available both within an element or a main HTML document, you will need to encapsulated them and then include them in the element or the main HTML document.

This is a mechanism that is characteristic of Polymer, rather than web components in general.

Create a module where the template only contains a <style> -- no actual HTML nor Polymer() call:

my-classes.html

<link rel="import" href="../polymer/polymer.html">
<dom-module id="my-classes">
  <template>
    <style>
      .red {
        color: red;
      }
    </style>
  </template>
</dom-module>

Include classes in the main document

Just importing such a file will not affect CSS styles in the document -- it will just define a template within a <dom-module> element. In order to actually enable those styles, they need to be imported in a <custom-style> tag.

QUESTION: IF my-classes DEFINED CUSTOM PROPERTIES/MIXINS, WOULD THEY BE ASSIGNED TO THE MATCHING ELEMENTS WITHIN THE FILE?

file.html

<head>

  <link rel="import" href="bower_components/my-classes/my-classes.html">

  <custom-style include="my-classes"></custom-style>
</head>
...
<body>
  <p class="red">This is red</p>
</body>

This will have the effect of actually applying the styles defined in my-classes.html.

TODO: check that the style will strictly only apply to that document, or if standard CSS rules will apply.

QUESTION: IF my-classes DEFINED CUSTOM PROPERTIES/MIXINS, WOULD THEY BE ASSIGNED TO THE MATCHING ELEMENTS WITHIN THE SHADOW DOM?

Import classes in an element

You can import my-classes.html, and then include it in the element's style tag, in order to have the extra classes available within the element's shadow DOM:

my-element.html

<link rel="import" href="bower_components/my-classes/my-classes.html">
<link rel="import" href="../polymer/polymer.html">

<dom-module is="my-element">
  <style include="my-classes">  
    ...
  </style>

  <p class="red">This is red</p>

  <script>
    Polymer({
      is: 'my-element'
    });
  </script>

</dom-module>

mercmobily avatar Dec 06 '16 08:12 mercmobily

Rather than changing the existing post (and see the comments lose their meanings), I attached the new guide here.

@arthurevans Is it now formally correct? Can you let me know about those questions?

I am really excited, as this is the first document I see that addresses neatly all of the use cases and best practices in terms of styling...

One note: I am writing this for Polymer 2. This is why I didn't address :root and am using custom-style etc.

mercmobily avatar Dec 06 '16 08:12 mercmobily

Style docs are now completed and can be found under "Shadow DOM and Styling" https://www.polymer-project.org/2.0/docs/devguide/shadow-dom

ghost avatar May 30 '17 22:05 ghost

This guide helped solve my problem of exposing a style module to both global scope and shadowdom. Thanks!

brettpostin avatar Jul 07 '17 09:07 brettpostin

@katejeffreys @arthurevans As much as I think that the style docs are great and explain things, I still feel that the guide I wrote faces the issue from a different angle. I am saying this from experience -- I gave two graphic designers the official docs, and came back looking like walking question marks. I gave them my guide (although it's not perfect), and they started to work.

I can turn it into a semi-popular article and get some adwords clicks, but I would much rather see it somewhere in the official site, as I think it deserves it.

I might well be wrong... please let me know either way.

mercmobily avatar Jul 10 '17 09:07 mercmobily

@mercmobily I will review.

ghost avatar Jul 17 '17 21:07 ghost

I want to chime in on this thread. I've been banging my head against the wall of trying to implement a way to have shared styles across all of the elements in my Polymer 2.0 app. (This includes shared css classes, shared css variables and shared css mixins.)

I was following the Polymer 2.0 docs and, try as I might, I just couldn't wrap my head around things.

The guide from @mercmobily totally did the trick. There are definitely some issues with it, but the approach is way easier to understand for my use case. (Thanks @mercmobily... you're a life saver!)

Anyway, for what it's worth, I think incorporating a more theming oriented approach would help others trying to style an entire app.

hankish avatar Jul 26 '17 02:07 hankish