CSS helpers

Custom Elements with Shadow DOM, sometimes, are too hermetic, style-wise. These helpers make one or many sheets adoptable, with no FOUC.

Adoptable stylesheets offer a modular way to share CSS between your HTML Custom Elements and/or the light DOM.
However, due to their relative recency, and missing browser features, there is still some gaps to fill.

Gracile aims to align with available web standard discussions, which is possible thanks to it’s underlying bundler: Vite.
The build step offers us a way to experiment proof of concepts for their viability in the field.

Caution

Experimental. This is not well tested, nor it is customizable enough.

Actually, this package provides two helpers:

1. Adoptable global styles

For adopting every light-dom stylesheets (those referenced via a <link>) in a given shadow root.

Goal is to ease loading your own ubiquitous styles, but also for vendored CSS frameworks (Bootstrap, Tailwind…).

Note that, by design, Vite is bundling <link>

This helper is an interpretation of the “Open stylable” idea.

2. Declarative shadow DOM style sharing

Allows cherry-picking specific stylesheets for a given shadow root, all while in an SSR’ed context.

Inspired by the first implementation of this explainer.

This method doesn’t use externally loaded stylesheets (via <link>), but a special kind of <script type="css-module"> with inline CSS. That allows the Declarative Shadow DOM to be rendered as fast as possible without any flash of un-styled content.

This is a good step toward reducing duplicated inline <style> that you will typically see with Lit SSR, and which is kind of the go-to method for a nice CEs DSD rendition, until something better emerges.

Installation

npm i @gracile-labs/css-helpers

Usage

See the Minimal Bootstrap/Tailwind template for a full working example.

After installing the @gracile-labs/css-helpers, you will first have to setup the document where you want them to take effect:

📄 ./src/document.ts

import { html } from '@gracile/gracile/server-html';
import { AdoptableStylesProvider } from '@gracile-labs/css-helpers/declarative-adoptable';
import { GlobalStylesProvider } from '@gracile-labs/css-helpers/global-css-provider';

 Loading with `.css?inline` is the typical way to construct stylesheets with Vite.
import adopted1 from './features/adopted-1.css?inline';
import adopted2 from './features/adopted-2.css?inline';

export const document = () => html`
  <!doctype html>
  <html lang="en">
    <head>
      <!-- ... -->

       1. Adoptable global styles 
       '<adopt-global-styles>' will pick up all CSS '<link>'s for this page that'll finish in your bundle. 
      <link
        rel="stylesheet"
        href=${new URL('./document.css', import.meta.url).pathname}
      />
      <link rel="stylesheet" href="..." />
      <link rel="stylesheet" href="..." />
       Enabling '<adopt-global-styles>' in Custom Elements shadow hosts. 
      ${GlobalStylesProvider()}


       Specifiers are unique. They are referenced where you need to cherry pick them. 
      <script type="css-module" specifier="/adopted-1.css">
        ${adopted1}
      </script>
      <script type="css-module" specifier="/adopted-2.css">
        ${adopted2}
      </script>
       Enabling '<adopted-style sheets="/x.css, /y.css">' in CEs shadow hosts. 
      ${AdoptableStylesProvider()}

      <!-- ... -->
    </head>

    <body>
      <route-template-outlet></route-template-outlet>
    </body>
  </html>
`;

GlobalStylesProvider or AdoptableStylesProvider, under the hood, will inject critically loaded, tiny JavaScript snippets, so everything is ready when needed on the first browser render pass of your Custom Elements.


Then, you can consume global or single sheets for any given custom element (vanilla, Lit, etc.):

📄 ./src/features/my-greetings-adopted.ts

import { css, html, LitElement } from 'lit';
import { customElement, property } from 'lit/decorators.js';

@customElement('my-greetings-adopted')
export class MyGreetingsAdopted extends LitElement {
  @property() name = 'World';

  render() {
    return html`
       These are the sheets specifiers you defined before, in the document. 
      <adopted-style sheets="/adopted-1.css, /adopted-2.css"></adopted-style>

      <h1>Hello</h1>
      <button>${this.name}</button>
    `;
  }
}

📄 ./src/features/my-greetings-global-styles.ts

import { css, html, LitElement } from 'lit';
import { customElement, property } from 'lit/decorators.js';

@customElement('my-greetings-global-styles')
export class MyGreetingsGlobalStyles extends LitElement {
  @property() name = 'World';

  render() {
    return html`
      <adopt-global-styles></adopt-global-styles>

      <h1>Hello</h1>
      <button>${this.name}</button>
    `;
  }
}

The <adopted-style sheets="/foo.css, /bar.css"></adopted-style> or <adopt-global-styles></adopt-global-styles> elements must be defined before any other HTML content in the host root.
They can also be used together, to have global and specific shared style altogether when needed.

Note that at one point, browsers could implement a leaner way to indicate that we want to adopt some sheets, for specific custom elements, in a declarative way.