CSS helpers
Helpers for Custom Elements with Shadow DOM and SSR (Declarative Shadow DOM).
These address two common friction points: duplicated inline <style> blocks
in DSD output, and sharing light-DOM stylesheets inside shadow roots.
Both helpers define a lightweight Custom Element that runs synchronously during HTML parse. No FOUC.
[!CAUTION] Experimental. API surface may change.
[!TIP] You can use these helpers with any Vite+Lit SSR setup! They are totally decoupled from the Gracile framework.
1. DSD Style Deduplication
When Lit SSR renders multiple instances of the same component, each gets an
identical <style> block inside its <template shadowrootmode>. This helper
keeps the styles only for the first occurrence and replaces subsequent ones
with an <adopt-shared-style> reference that adopts a cached CSSStyleSheet.
Vite plugin (recommended)
// vite.config.ts
import { gracile } from '@gracile/gracile/plugin';
import { dsdStyleDedup } from '@gracile-labs/css-helpers/shared/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [gracile(), dsdStyleDedup()],
});
This automatically:
- Registers the
DedupLitElementRendererinto Gracile’s SSR pipeline. - Injects the
<adopt-shared-style>CE definition<script>into every page’s<head>.
SSR template snippet (manual)
If you prefer explicit control, you can inline the CE definition yourself and register the renderer manually. Here, an example with Gracile:
// Document template
import { SharedStyleProvider } from '@gracile-labs/css-helpers/shared/shared-style-provider';
import { html } from '@gracile/gracile/server-html';
export const document = () => html`
<!doctype html>
<html lang="en">
<head>
${SharedStyleProvider()}
</head>
<body>
<route-template-outlet></route-template-outlet>
</body>
</html>
`;
// Gracile config — register renderer via user config
import { DedupLitElementRenderer } from '@gracile-labs/css-helpers/shared/dedup-renderer';
gracile({
litSsr: {
renderInfo: { elementRenderers: [DedupLitElementRenderer] },
},
});
How it works
| Occurrence | SSR output | Browser behavior |
|---|---|---|
First <my-button> | <style id="__lit-s-my-button">…</style> + <adopt-shared-style> | CE finds sibling <style>, creates & caches a CSSStyleSheet |
Second+ <my-button> | <adopt-shared-style style-id="__lit-s-my-button"> (no <style>) | CE pushes the cached sheet to adoptedStyleSheets |
The <adopt-shared-style> element removes itself after running.
2. Global Styles Adopter
Adopts every light-DOM stylesheet (those referenced via <link> in the
document) into a given shadow root. Useful for sharing your own ubiquitous
styles or vendored CSS frameworks (Bootstrap, Tailwind…).
Shares goals with the “Open Stylable” proposal and draws from Eisenberg’s pattern.
Vite plugin (recommended)
// vite.config.ts
import { gracile } from '@gracile/gracile/plugin';
import { globalStylesAdopter } from '@gracile-labs/css-helpers/global/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [gracile(), globalStylesAdopter()],
});
SSR template snippet (manual)
Here, an example with Gracile:
import { GlobalStylesProvider } from '@gracile-labs/css-helpers/global/provider';
import { html } from '@gracile/gracile/server-html';
export const document = () => html`
<!doctype html>
<html lang="en">
<head>
<link rel="stylesheet" href="..." />
${GlobalStylesProvider()}
</head>
<body>
<route-template-outlet></route-template-outlet>
</body>
</html>
`;
Usage in components
Place <adopt-global-styles> before any other content in your shadow root:
import { 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.
} from 'lit/decorators.js';
@customElementfunction customElement(tagName: string): CustomElementDecoratorClass decorator factory that defines the decorated class as a custom element.
('my-greetings')
export class MyGreetingsclass MyGreetings 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.
{
renderMyGreetings.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>
<div class="my-global-class">Hello!</div>
`;
}
}
The <adopt-global-styles> element removes itself after adopting.
Installation
npm i @gracile-labs/css-helpers