Working with styles
CSS is an indispensable piece of tech. for making your website pleasurable, and accessible. Working with stylesheets should be made easy, too.
Hopefully, Gracile is leveraging Vite features in this area: pre or post-processors, bundling, etc.
Styles, overall, are still a contentious topic in the front-end development
space, with many different (and often incompatible ways) to consume, scope and
distribute them. All that with SSR compatibility in mind.
Hopefully, things will get better with CSS @scope, βOpen-Stylablesβ or
standard CSS modules, just to name a few possible paths of resolution.
In the meantime, we will see how to achieve that with web platform standards in mind with what we have at hand.
Be sure to take a read at Working with assets before taking the plunge here.
Client or Server import in JS module
While Vite-based frameworks often allow importing CSS from a route or client code like this:
π /src/routes/demo.ts
import { defineRouteimport defineRoute } from '@gracile/gracile/route';
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.
} from 'lit';
We are inside a server module, imported CSS here is ignored!
import './my-styles-foo.css';
export default defineRouteimport defineRoute ({
documentdocument: (context: any) => any : (contextcontext: any ) => documentvar document: Documentwindow.document returns a reference to the document contained in the window.
(contextcontext: any ),
templatetemplate: () => TemplateResult<1> : () => 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.
`
<h1>Wow</h1>
<!-- -->
`,
});
π /src/routes/demo.client.ts
We are inside a client module, imported CSS here will work but is not recommended!
import './my-styles-bar.css';
This is not standard!
Behind the scenes, a lot of stuff has to happen at the bundler level to achieve this kind of UX. Sadly, that can produce unexpected CSS ordering, which is a widely known issue, even with Webpack.
While this adds a bit of comfort, you should just import CSS in CSS (with
the @import standard at-rule), and have a total, predictable control.
Ultimately, establish your CSS entry points at the document or page
template level, in the HTML itself.
You do that using the good old and
reliable <link> tag.
Pre/post-processors
Gracile supports every Viteβs features regarding CSS.
Adopted Stylesheets
CSSStyleSheet, βStandard CSS Modulesβ and βAdopted Stylesheetsβ are
new standards working together allowing to import CSS like JavaScript modules,
meaning theyβre dynamic, take part of the module graphβ¦
Itβs particularly useful for building Custom Elements with a Shadow Root, from
which you can adopt them.
Itβs not reserved to Custom Elements, it also works for the global document
object (meaning global Light-DOM styling), and any kind of detached <template>
Shadow Root, too.
We will use the ?inline import query parameter for our examples.
Caution
?inline etc. are non-standard, and as soon as Vite support Import
Attributes (with { type: 'css' }) this stop-gap will be superseeded.
Adopt from a Custom Element
π /src/vanilla-element.ts
import stylesTextconst stylesText: string from './vanilla-element.css?inline';
or SCSS, Less, etc.
const stylesconst styles: CSSStyleSheet = await new CSSStyleSheetvar CSSStyleSheet: new (options?: CSSStyleSheetInit) => CSSStyleSheetThe CSSStyleSheet interface represents a single CSS stylesheet, and lets you inspect and modify the list of rules contained in the stylesheet. It inherits properties and methods from its parent, StyleSheet.
().replaceCSSStyleSheet.replace(text: string): Promise<CSSStyleSheet>The replace() method of the CSSStyleSheet interface asynchronously replaces the content of the stylesheet with the content passed into it. The method returns a promise that resolves with the CSSStyleSheet object.
('stylesText');
export class MyVanillaCounterclass MyVanillaCounter extends HTMLElementvar HTMLElement: {
new (): HTMLElement;
prototype: HTMLElement;
}
The HTMLElement interface represents any HTML element. Some elements directly implement this interface, while others implement it via an interface that inherits it.
{
async connectedCallbackMyVanillaCounter.connectedCallback(): Promise<void> () {
const shadowconst shadow: ShadowRoot = this.attachShadowElement.attachShadow(init: ShadowRootInit): ShadowRootThe Element.attachShadow() method attaches a shadow DOM tree to the specified element and returns a reference to its ShadowRoot.
({ modeShadowRootInit.mode: ShadowRootMode : 'open' });
shadowconst shadow: ShadowRoot .adoptedStyleSheetsDocumentOrShadowRoot.adoptedStyleSheets: CSSStyleSheet[] = [stylesconst styles: CSSStyleSheet ];
shadowconst shadow: ShadowRoot .innerHTMLShadowRoot.innerHTML: stringThe innerHTML property of the ShadowRoot interface gets or sets the HTML markup to the DOM tree inside the ShadowRoot.
= `<h1>Wow</h1>`;
}
}
Adopt from a Lit Element
Important
This will NOT work with SSR.
CSSStyleSheet doesnβt exist in Node.
π /src/my-lit-element.ts
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.
, 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.
, 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.
, unsafeCSSconst unsafeCSS: (value: unknown) => CSSResultWrap a value for interpolation in a
{@linkcode
css
}
tagged template literal.
This is unsafe because untrusted CSS text can be used to phone home
or exfiltrate data to an attacker controlled site. Take care to only use
this with trusted input.
} from 'lit';
import { customElementconst customElement: (tagName: string) => CustomElementDecoratorClass decorator factory that defines the decorated class as a custom element.
} from 'lit/decorators.js';
import stylesconst styles: CSSStyleSheet
import styles
const styles: CSSStyleSheet
from './my-lit-element.css?inline';
const stylesconst styles: CSSStyleSheet
import styles
const styles: CSSStyleSheet
= await new CSSStyleSheetvar CSSStyleSheet: new (options?: CSSStyleSheetInit) => CSSStyleSheetThe CSSStyleSheet interface represents a single CSS stylesheet, and lets you inspect and modify the list of rules contained in the stylesheet. It inherits properties and methods from its parent, StyleSheet.
().replaceCSSStyleSheet.replace(text: string): Promise<CSSStyleSheet>The replace() method of the CSSStyleSheet interface asynchronously replaces the content of the stylesheet with the content passed into it. The method returns a promise that resolves with the CSSStyleSheet object.
('stylesText');
or SCSS, Less, etc.
@customElementfunction customElement(tagName: string): CustomElementDecoratorClass decorator factory that defines the decorated class as a custom element.
('my-lit-element')
export class MyLitElementclass MyLitElement 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.
{
renderMyLitElement.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.
`<h1>Wow</h1>`;
}
static stylesMyLitElement.styles: CSSStyleSheet[]Array of styles to apply to the element. The styles should be defined
using the
{@linkcode
css
}
tag function, via constructible stylesheets, or
imported from native CSS module scripts.
Note on Content Security Policy:
Element styles are implemented with <style> tags when the browser doesn't
support adopted StyleSheets. To use such <style> tags with the style-src
CSP directive, the style-src value must either include 'unsafe-inline' or
nonce-<base64-value> with <base64-value> replaced be a server-generated
nonce.
To provide a nonce to use on generated <style> elements, set
window.litNonce to a server-generated nonce in your page's HTML, before
loading application code:
<script>
// Generated and unique per request:
window.litNonce = 'a1b2c3d4';
</script>
= [stylesconst styles: CSSStyleSheet
import styles
const styles: CSSStyleSheet
];
}
For SSR + Client usage, we still have to use Litβs own CSSResult like
this:
π /src/my-lit-element-ssr.ts
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.
, 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.
, 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';
import stylesconst styles: string from './my-lit-element-ssr.css?inline';
@customElementfunction customElement(tagName: string): CustomElementDecoratorClass decorator factory that defines the decorated class as a custom element.
('my-lit-element-ssr')
export class MyLitElementSsrclass MyLitElementSsr 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.
{
renderMyLitElementSsr.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.
`<h1>Wow</h1>`;
}
static stylesMyLitElementSsr.styles: any[]Array of styles to apply to the element. The styles should be defined
using the
{@linkcode
css
}
tag function, via constructible stylesheets, or
imported from native CSS module scripts.
Note on Content Security Policy:
Element styles are implemented with <style> tags when the browser doesn't
support adopted StyleSheets. To use such <style> tags with the style-src
CSP directive, the style-src value must either include 'unsafe-inline' or
nonce-<base64-value> with <base64-value> replaced be a server-generated
nonce.
To provide a nonce to use on generated <style> elements, set
window.litNonce to a server-generated nonce in your page's HTML, before
loading application code:
<script>
// Generated and unique per request:
window.litNonce = 'a1b2c3d4';
</script>
= [unsafeCSS(stylesconst styles: string )];
}
Of course, you can just use, for example, a my-styles.css.ts (or
my-styles.styles.ts) exporting a Lit css, instead of using pure CSS +
?inline + unsafeCSS. E.g:
π /src/my-lit-element.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.
} from 'lit';
export const stylesconst styles: CSSResult = 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.
`
:host {
display: block;
color: var(--color-red-600, tomato);
}
`;
π /src/my-lit-element.ts
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.
, 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.
, 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';
import { stylesconst styles: CSSResult } from './my-lit-element.styles.js';
@customElementfunction customElement(tagName: string): CustomElementDecoratorClass decorator factory that defines the decorated class as a custom element.
('my-lit-element')
export class MyLitElementclass MyLitElement 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.
{
renderMyLitElement.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.
`<h1>Wow</h1>`;
}
static stylesMyLitElement.styles: CSSResult[]Array of styles to apply to the element. The styles should be defined
using the
{@linkcode
css
}
tag function, via constructible stylesheets, or
imported from native CSS module scripts.
Note on Content Security Policy:
Element styles are implemented with <style> tags when the browser doesn't
support adopted StyleSheets. To use such <style> tags with the style-src
CSP directive, the style-src value must either include 'unsafe-inline' or
nonce-<base64-value> with <base64-value> replaced be a server-generated
nonce.
To provide a nonce to use on generated <style> elements, set
window.litNonce to a server-generated nonce in your page's HTML, before
loading application code:
<script>
// Generated and unique per request:
window.litNonce = 'a1b2c3d4';
</script>
= [stylesconst styles: CSSResult ];
}
But please note that, in nearly all cases, tooling works better within their
original environment (editor hints, linters, processorsβ¦), in this case, a
.css file.
Composite files make tooling exponentially buggy.
Adopt in the global document
We will use replaceSync this time, to show off.
replace needs an await (not always possible at the top level), but has the
benefit of returning the instance (chainable).
π /src/document.client.ts
import globalAdoptedStylesTextconst globalAdoptedStylesText: string from './global-adopted.css?inline';
const globalAdoptedStylesconst globalAdoptedStyles: CSSStyleSheet = new CSSStyleSheetvar CSSStyleSheet: new (options?: CSSStyleSheetInit) => CSSStyleSheetThe CSSStyleSheet interface represents a single CSS stylesheet, and lets you inspect and modify the list of rules contained in the stylesheet. It inherits properties and methods from its parent, StyleSheet.
();
globalAdoptedStylesconst globalAdoptedStyles: CSSStyleSheet .replaceSyncCSSStyleSheet.replaceSync(text: string): voidThe replaceSync() method of the CSSStyleSheet interface synchronously replaces the content of the stylesheet with the content passed into it.
(globalAdoptedStylesTextconst globalAdoptedStylesText: string );
documentvar document: Documentwindow.document returns a reference to the document contained in the window.
.adoptedStylesheets = [globalAdoptedStylesconst globalAdoptedStyles: CSSStyleSheet ];
Adopt in a Template HTML element
This is kind of a fringe usage, but, hey, itβs possible!
π /src/features/template-demo.ts
import templateStylesTextconst templateStylesText: string from './template-demo.css?inline';
const templateStylesconst templateStyles: CSSStyleSheet = new CSSStyleSheetvar CSSStyleSheet: new (options?: CSSStyleSheetInit) => CSSStyleSheetThe CSSStyleSheet interface represents a single CSS stylesheet, and lets you inspect and modify the list of rules contained in the stylesheet. It inherits properties and methods from its parent, StyleSheet.
();
templateStylesconst templateStyles: CSSStyleSheet .replaceSyncCSSStyleSheet.replaceSync(text: string): voidThe replaceSync() method of the CSSStyleSheet interface synchronously replaces the content of the stylesheet with the content passed into it.
(templateStylesTextconst templateStylesText: string );
const templateconst template: HTMLTemplateElement | null = documentvar document: Documentwindow.document returns a reference to the document contained in the window.
.querySelectorParentNode.querySelector<HTMLTemplateElement>(selectors: string): HTMLTemplateElement | null (+4 overloads)Returns the first element that is a descendant of node that matches selectors.
<HTMLTemplateElement>(
'template#my-template',
);
templateconst template: HTMLTemplateElement | null .shadowRootElement.shadowRoot: ShadowRoot | nullThe Element.shadowRoot read-only property represents the shadow root hosted by the element.
.adoptedStylesheets = [templateStylesconst templateStyles: CSSStyleSheet ];