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 { defineRoutefunction defineRoute<GetHandlerData extends R.HandlerDataHtml = undefined, PostHandlerData extends R.HandlerDataHtml = undefined, CatchAllHandlerData extends R.HandlerDataHtml = undefined, StaticPathOptions extends R.StaticPathOptionsGeneric | undefined = undefined, RouteContext extends R.RouteContextGeneric = {
...;
}>(options: {
handler?: StaticPathOptions extends object ? never : R.Handler<CatchAllHandlerData> | {
GET?: R.Handler<GetHandlerData>;
POST?: R.Handler<PostHandlerData>;
QUERY?: R.Handler<Response>;
PUT?: R.Handler<Response>;
PATCH?: R.Handler<Response>;
DELETE?: R.Handler<Response>;
HEAD?: R.Handler<Response>;
OPTIONS?: R.Handler<Response>;
} | undefined;
staticPaths?: () => R.MaybePromise<StaticPathOptions[]> | undefined;
prerender?: boolean | undefined;
document?: R.DocumentTemplate<RouteContext> | undefined;
template?: R.BodyTemplate<RouteContext> | undefined;
}): (RouteModule: typeof R.RouteModule) => R.RouteModule
Defines a file-based route for Gracile to consume.
} 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 defineRoutedefineRoute<undefined, undefined, undefined, undefined, {
url: URL;
props: undefined;
params: Parameters;
}>(options: {
handler?: Handler<undefined> | {
GET?: Handler<undefined> | undefined;
... 6 more ...;
OPTIONS?: Handler<...> | undefined;
} | undefined;
staticPaths?: (() => MaybePromise<...> | undefined) | undefined;
prerender?: boolean | undefined;
document?: DocumentTemplate<...> | undefined;
template?: BodyTemplate<...> | undefined;
}): (RouteModule: typeof RouteModule) => RouteModule
Defines a file-based route for Gracile to consume.
({
documentdocument?: DocumentTemplate<{
url: URL;
props: undefined;
params: Parameters;
}> | undefined
A function that returns a server only template.
Route context is provided at runtime during the build.
: (contextcontext: {
url: URL;
props: undefined;
params: Parameters;
}
) => documentvar document: Document
(contextcontext: {
url: URL;
props: undefined;
params: Parameters;
}
),
templatetemplate?: BodyTemplate<{
url: URL;
props: undefined;
params: Parameters;
}> | undefined
A function that returns a server only or a Lit client hydratable template.
Route context is provided at runtime during the build.
: () => 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) => CSSStyleSheet
A single CSS style sheet. It inherits properties and methods from its parent, StyleSheet.
().replaceCSSStyleSheet.replace(text: string): Promise<CSSStyleSheet>
('stylesText');
export class MyVanillaCounterclass MyVanillaCounter
extends HTMLElementvar HTMLElement: {
new (): HTMLElement;
prototype: HTMLElement;
}
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): ShadowRoot
Creates a shadow root for element and returns it.
({ modeShadowRootInit.mode: ShadowRootMode
: 'open' });
shadowconst shadow: ShadowRoot
.adoptedStyleSheetsDocumentOrShadowRoot.adoptedStyleSheets: CSSStyleSheet[]
= [stylesconst styles: CSSStyleSheet
];
shadowconst shadow: ShadowRoot
.innerHTMLInnerHTML.innerHTML: string
= `<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)[]) => CSSResult
A 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 LitElement
Base 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) => CSSResult
Wrap 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) => CustomElementDecorator
Class 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) => CSSStyleSheet
A single CSS style sheet. It inherits properties and methods from its parent, StyleSheet.
().replaceCSSStyleSheet.replace(text: string): Promise<CSSStyleSheet>
('stylesText');
or SCSS, Less, etc.
@customElementfunction customElement(tagName: string): CustomElementDecorator
Class decorator factory that defines the decorated class as a custom element.
('my-lit-element')
export class MyLitElementclass MyLitElement
extends LitElementclass LitElement
Base 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)[]) => CSSResult
A 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 LitElement
Base 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) => CustomElementDecorator
Class 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): CustomElementDecorator
Class decorator factory that defines the decorated class as a custom element.
('my-lit-element-ssr')
export class MyLitElementSsrclass MyLitElementSsr
extends LitElementclass LitElement
Base 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)[]) => CSSResult
A 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)[]) => CSSResult
A 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)[]) => CSSResult
A 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 LitElement
Base 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) => CustomElementDecorator
Class 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): CustomElementDecorator
Class decorator factory that defines the decorated class as a custom element.
('my-lit-element')
export class MyLitElementclass MyLitElement
extends LitElementclass LitElement
Base 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) => CSSStyleSheet
A single CSS style sheet. It inherits properties and methods from its parent, StyleSheet.
();
globalAdoptedStylesconst globalAdoptedStyles: CSSStyleSheet
.replaceSyncCSSStyleSheet.replaceSync(text: string): void
(globalAdoptedStylesTextconst globalAdoptedStylesText: string
);
documentvar document: Document
.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) => CSSStyleSheet
A single CSS style sheet. It inherits properties and methods from its parent, StyleSheet.
();
templateStylesconst templateStyles: CSSStyleSheet
.replaceSyncCSSStyleSheet.replaceSync(text: string): void
(templateStylesTextconst templateStylesText: string
);
const templateconst template: HTMLTemplateElement | null
= documentvar document: Document
.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 | null
Returns element's shadow root, if any, and if shadow root's mode is "open", and null otherwise.
.adoptedStylesheets = [templateStylesconst templateStyles: CSSStyleSheet
];