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 { htmlfunction html(strings: TemplateStringsArray, ...values: unknown[]): ServerRenderedTemplateA lit-html template that can only be rendered on the server, and cannot be
hydrated.
These templates can be used for rendering full documents, including the
doctype, and rendering into elements that Lit normally cannot, like
<title>, <textarea>, <template>, and non-executing <script> tags
like <script type="text/json">. They are also slightly more efficient than
normal Lit templates, because the generated HTML doesn't need to include
markers for updating.
Server-only html templates can be composed, and combined, and they support
almost all features that normal Lit templates do, with the exception of
features that don't have a pure HTML representation, like event handlers or
property bindings.
Server-only html templates can only be rendered on the server, they will
throw an Error if created in the browser. However if you render a normal Lit
template inside a server-only template, then it can be hydrated and updated.
Likewise, if you place a custom element inside a server-only template, it can
be hydrated and update like normal.
A server-only template can't be rendered inside a normal Lit template.
} from '@gracile/gracile/server-html';
import { AdoptableStylesProviderimport AdoptableStylesProvider } from '@gracile-labs/css-helpers/declarative-adoptable';
import { GlobalStylesProviderimport GlobalStylesProvider } from '@gracile-labs/css-helpers/global-css-provider';
Loading with `.css?inline` is the typical way to construct stylesheets with Vite.
import adopted1const adopted1: string from './features/adopted-1.css?inline';
import adopted2const adopted2: string from './features/adopted-2.css?inline';
export const documentconst document: () => ServerRenderedTemplate = () => htmlfunction html(strings: TemplateStringsArray, ...values: unknown[]): ServerRenderedTemplateA lit-html template that can only be rendered on the server, and cannot be
hydrated.
These templates can be used for rendering full documents, including the
doctype, and rendering into elements that Lit normally cannot, like
<title>, <textarea>, <template>, and non-executing <script> tags
like <script type="text/json">. They are also slightly more efficient than
normal Lit templates, because the generated HTML doesn't need to include
markers for updating.
Server-only html templates can be composed, and combined, and they support
almost all features that normal Lit templates do, with the exception of
features that don't have a pure HTML representation, like event handlers or
property bindings.
Server-only html templates can only be rendered on the server, they will
throw an Error if created in the browser. However if you render a normal Lit
template inside a server-only template, then it can be hydrated and updated.
Likewise, if you place a custom element inside a server-only template, it can
be hydrated and update like normal.
A server-only template can't be rendered inside a normal Lit template.
`
<!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 URLvar URL: new (url: string | URL, base?: string | URL) => URLThe URL interface represents an object providing static methods used for creating object URLs.
URL class is a global reference for import { URL } from 'url'
https://nodejs.org/api/url.html#the-whatwg-url-api
('./document.css', import.meta.urlImportMeta.url: stringThe absolute file: URL of the module.
).pathnameURL.pathname: string }
/>
<link rel="stylesheet" href="..." />
<link rel="stylesheet" href="..." />
Enabling '<adopt-global-styles>' in Custom Elements shadow hosts.
${GlobalStylesProviderimport GlobalStylesProvider ()}
2. Declarative shadow DOM style sharing
Specifiers are unique. They are referenced where you need to cherry pick them.
<script type="css-module" specifier="/adopted-1.css">
${adopted1const adopted1: string }
</script>
<script type="css-module" specifier="/adopted-2.css">
${adopted2const adopted2: string }
</script>
Enabling '<adopted-style sheets="/x.css, /y.css">' in CEs shadow hosts.
${AdoptableStylesProviderimport 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 { cssconst css: (strings: TemplateStringsArray, ...values: (CSSResultGroup | number)[]) => CSSResultA template literal tag which can be used with LitElement's
{@linkcode
LitElement.styles
}
property to set element styles.
For security reasons, only literal string values and number may be used in
embedded expressions. To incorporate non-literal values
{@linkcode
unsafeCSS
}
may be used inside an expression.
, htmlconst html: (strings: TemplateStringsArray, ...values: unknown[]) => TemplateResult<1>Interprets a template literal as an HTML template that can efficiently
render to and update a container.
const header = (title: string) => html`<h1>${title}</h1>`;
The html tag returns a description of the DOM to render as a value. It is
lazy, meaning no work is done until the template is rendered. When rendering,
if a template comes from the same expression as a previously rendered result,
it's efficiently updated instead of replaced.
, LitElementclass LitElementBase element class that manages element properties and attributes, and
renders a lit-html template.
To define a component, subclass LitElement and implement a
render method to provide the component's template. Define properties
using the
{@linkcode
LitElement.properties
properties
}
property or the
{@linkcode
property
}
decorator.
} from 'lit';
import { customElementconst customElement: (tagName: string) => CustomElementDecoratorClass decorator factory that defines the decorated class as a custom element.
, propertyfunction property(options?: PropertyDeclaration): PropertyDecoratorA class field or accessor decorator which creates a reactive property that
reflects a corresponding attribute value. When a decorated property is set
the element will update and render. A
{@linkcode
PropertyDeclaration
}
may
optionally be supplied to configure property features.
This decorator should only be used for public fields. As public fields,
properties should be considered as primarily settable by element users,
either via attribute or the property itself.
Generally, properties that are changed by the element should be private or
protected fields and should use the
{@linkcode
state
}
decorator.
However, sometimes element code does need to set a public property. This
should typically only be done in response to user interaction, and an event
should be fired informing the user; for example, a checkbox sets its
checked property when clicked and fires a changed event. Mutating public
properties should typically not be done for non-primitive (object or array)
properties. In other cases when an element needs to manage state, a private
property decorated via the
{@linkcode
state
}
decorator should be used. When
needed, state properties can be initialized via public properties to
facilitate complex interactions.
class MyElement {
} from 'lit/decorators.js';
@customElementfunction customElement(tagName: string): CustomElementDecoratorClass decorator factory that defines the decorated class as a custom element.
('my-greetings-adopted')
export class MyGreetingsAdoptedclass MyGreetingsAdopted extends LitElementclass LitElementBase element class that manages element properties and attributes, and
renders a lit-html template.
To define a component, subclass LitElement and implement a
render method to provide the component's template. Define properties
using the
{@linkcode
LitElement.properties
properties
}
property or the
{@linkcode
property
}
decorator.
{
@propertyfunction property(options?: PropertyDeclaration<unknown, unknown> | undefined): PropertyDecoratorA class field or accessor decorator which creates a reactive property that
reflects a corresponding attribute value. When a decorated property is set
the element will update and render. A
{@linkcode
PropertyDeclaration
}
may
optionally be supplied to configure property features.
This decorator should only be used for public fields. As public fields,
properties should be considered as primarily settable by element users,
either via attribute or the property itself.
Generally, properties that are changed by the element should be private or
protected fields and should use the
{@linkcode
state
}
decorator.
However, sometimes element code does need to set a public property. This
should typically only be done in response to user interaction, and an event
should be fired informing the user; for example, a checkbox sets its
checked property when clicked and fires a changed event. Mutating public
properties should typically not be done for non-primitive (object or array)
properties. In other cases when an element needs to manage state, a private
property decorated via the
{@linkcode
state
}
decorator should be used. When
needed, state properties can be initialized via public properties to
facilitate complex interactions.
class MyElement {
() nameMyGreetingsAdopted.name: string = 'World';
renderMyGreetingsAdopted.render(): TemplateResult<1>Invoked on each update to perform rendering tasks. This method may return
any value renderable by lit-html's ChildPart - typically a
TemplateResult. Setting properties inside this method will not trigger
the element to update.
() {
return htmlconst html: (strings: TemplateStringsArray, ...values: unknown[]) => TemplateResult<1>Interprets a template literal as an HTML template that can efficiently
render to and update a container.
const header = (title: string) => html`<h1>${title}</h1>`;
The html tag returns a description of the DOM to render as a value. It is
lazy, meaning no work is done until the template is rendered. When rendering,
if a template comes from the same expression as a previously rendered result,
it's efficiently updated instead of replaced.
`
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.nameMyGreetingsAdopted.name: string }</button>
`;
}
}
📄 ./src/features/my-greetings-global-styles.ts
import { cssconst css: (strings: TemplateStringsArray, ...values: (CSSResultGroup | number)[]) => CSSResultA template literal tag which can be used with LitElement's
{@linkcode
LitElement.styles
}
property to set element styles.
For security reasons, only literal string values and number may be used in
embedded expressions. To incorporate non-literal values
{@linkcode
unsafeCSS
}
may be used inside an expression.
, htmlconst html: (strings: TemplateStringsArray, ...values: unknown[]) => TemplateResult<1>Interprets a template literal as an HTML template that can efficiently
render to and update a container.
const header = (title: string) => html`<h1>${title}</h1>`;
The html tag returns a description of the DOM to render as a value. It is
lazy, meaning no work is done until the template is rendered. When rendering,
if a template comes from the same expression as a previously rendered result,
it's efficiently updated instead of replaced.
, LitElementclass LitElementBase element class that manages element properties and attributes, and
renders a lit-html template.
To define a component, subclass LitElement and implement a
render method to provide the component's template. Define properties
using the
{@linkcode
LitElement.properties
properties
}
property or the
{@linkcode
property
}
decorator.
} from 'lit';
import { customElementconst customElement: (tagName: string) => CustomElementDecoratorClass decorator factory that defines the decorated class as a custom element.
, propertyfunction property(options?: PropertyDeclaration): PropertyDecoratorA class field or accessor decorator which creates a reactive property that
reflects a corresponding attribute value. When a decorated property is set
the element will update and render. A
{@linkcode
PropertyDeclaration
}
may
optionally be supplied to configure property features.
This decorator should only be used for public fields. As public fields,
properties should be considered as primarily settable by element users,
either via attribute or the property itself.
Generally, properties that are changed by the element should be private or
protected fields and should use the
{@linkcode
state
}
decorator.
However, sometimes element code does need to set a public property. This
should typically only be done in response to user interaction, and an event
should be fired informing the user; for example, a checkbox sets its
checked property when clicked and fires a changed event. Mutating public
properties should typically not be done for non-primitive (object or array)
properties. In other cases when an element needs to manage state, a private
property decorated via the
{@linkcode
state
}
decorator should be used. When
needed, state properties can be initialized via public properties to
facilitate complex interactions.
class MyElement {
} from 'lit/decorators.js';
@customElementfunction customElement(tagName: string): CustomElementDecoratorClass decorator factory that defines the decorated class as a custom element.
('my-greetings-global-styles')
export class MyGreetingsGlobalStylesclass MyGreetingsGlobalStyles extends LitElementclass LitElementBase element class that manages element properties and attributes, and
renders a lit-html template.
To define a component, subclass LitElement and implement a
render method to provide the component's template. Define properties
using the
{@linkcode
LitElement.properties
properties
}
property or the
{@linkcode
property
}
decorator.
{
@propertyfunction property(options?: PropertyDeclaration<unknown, unknown> | undefined): PropertyDecoratorA class field or accessor decorator which creates a reactive property that
reflects a corresponding attribute value. When a decorated property is set
the element will update and render. A
{@linkcode
PropertyDeclaration
}
may
optionally be supplied to configure property features.
This decorator should only be used for public fields. As public fields,
properties should be considered as primarily settable by element users,
either via attribute or the property itself.
Generally, properties that are changed by the element should be private or
protected fields and should use the
{@linkcode
state
}
decorator.
However, sometimes element code does need to set a public property. This
should typically only be done in response to user interaction, and an event
should be fired informing the user; for example, a checkbox sets its
checked property when clicked and fires a changed event. Mutating public
properties should typically not be done for non-primitive (object or array)
properties. In other cases when an element needs to manage state, a private
property decorated via the
{@linkcode
state
}
decorator should be used. When
needed, state properties can be initialized via public properties to
facilitate complex interactions.
class MyElement {
() nameMyGreetingsGlobalStyles.name: string = 'World';
renderMyGreetingsGlobalStyles.render(): TemplateResult<1>Invoked on each update to perform rendering tasks. This method may return
any value renderable by lit-html's ChildPart - typically a
TemplateResult. Setting properties inside this method will not trigger
the element to update.
() {
return htmlconst html: (strings: TemplateStringsArray, ...values: unknown[]) => TemplateResult<1>Interprets a template literal as an HTML template that can efficiently
render to and update a container.
const header = (title: string) => html`<h1>${title}</h1>`;
The html tag returns a description of the DOM to render as a value. It is
lazy, meaning no work is done until the template is rendered. When rendering,
if a template comes from the same expression as a previously rendered result,
it's efficiently updated instead of replaced.
`
<adopt-global-styles></adopt-global-styles>
<h1>Hello</h1>
<button>${this.nameMyGreetingsGlobalStyles.name: string }</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.