Home
A thin, full-stack, web framework.
Standards oriented
Built with a platform-minded philosophy. Every time a standard can be leveraged
for a task, it should be.
It also means fewer vendor-specific idioms to churn on and a more portable
codebase overall.
Stop re-implementing the wheel, and embrace future-proof APIs, you’ll thank
yourself later!
Annotated example
Tip
You can hover/tap source code tokens, like in your local editor, to get more insights.
📄 /src/document.ts
import { htmlfunction html(strings: TemplateStringsArray, ...values: unknown[]): ServerRenderedTemplate
A 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 '@lit-labs/ssr';
import { helpersconst helpers: {
fullHydration: import("@lit-labs/ssr").ServerRenderedTemplate;
polyfills: {
declarativeShadowDom: import("@lit-labs/ssr").ServerRenderedTemplate;
requestIdleCallback: import("@lit-labs/ssr").ServerRenderedTemplate;
};
pageAssets: import("@lit-labs/ssr").ServerRenderedTemplate;
}
} from '@gracile/server/document';
export const defaultDocumentconst defaultDocument: (options: {
url: URL;
title?: string;
}) => ServerRenderedTemplate
= (optionsoptions: {
url: URL;
title?: string | undefined;
}
: { urlurl: URL
: URL; titletitle?: string | undefined
?: string }) => htmlfunction html(strings: TemplateStringsArray, ...values: unknown[]): ServerRenderedTemplate
A 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"
class=${`page${optionsoptions: {
url: URL;
title?: string | undefined;
}
.urlurl: URL
.pathnameURL.pathname: string
.replaceString.replace(searchValue: string | RegExp, replaceValue: string): string (+3 overloads)
Replaces text in a string, using a regular expression or search string.
('/', '-') || 'home'}`}
>
<head>
Helpers
${helpersconst helpers: {
fullHydration: import("@lit-labs/ssr").ServerRenderedTemplate;
polyfills: {
declarativeShadowDom: import("@lit-labs/ssr").ServerRenderedTemplate;
requestIdleCallback: import("@lit-labs/ssr").ServerRenderedTemplate;
};
pageAssets: import("@lit-labs/ssr").ServerRenderedTemplate;
}
.fullHydrationfullHydration: ServerRenderedTemplate
}
Global assets
<link rel="stylesheet" href="/src/styles/global.scss" />
<script type="module" src="/src/document.client.ts"></script>
SEO
<title>${optionsoptions: {
url: URL;
title?: string | undefined;
}
.titletitle?: string | undefined
?? 'My Website'}</title>
<!-- ... -->
</head>
<body data-pagefind-body>
Current route's page injection
<page-outlet></page-outlet>
</body>
</html>
`;
📄 /src/routes/index.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.
} from 'lit';
import { defineRoutefunction defineRoute<AllHandlerData extends Response, GetHandlerData extends object | Response, PutHandlerData extends Response, PostHandlerData extends object | Response, PatchHandlerData extends Response, DeleteHandlerData extends Response, StaticPathOptions extends {
params: Record<string, string | undefined>;
props?: any;
} | undefined, PageContext = {
url: URL;
params: StaticPathOptions extends {
params: any;
} ? StaticPathOptions['params'] : Record<string, string | undefined>;
props: StaticPathOptions extends {
params: any;
props: any;
} ? StaticPathOptions['props'] : StaticPathOptions extends object ? never : {
GET: GetHandlerData extends Response ? never : GetHandlerData | undefined;
POST: PostHandlerData extends Response ? never : PostHandlerData | undefined;
};
request: Request;
response: ResponseInit;
}>(options: {
...;
}): {
...;
}
Defines a route.
} from '@gracile/server/route';
import { dbconst db: {
query<T>(str: string): T[];
}
Dummy DB handler…
, sqlfunction sql(str: string): string
Dummy SQL template literal…
, Achievementtype Achievement = {
name: string;
date: Date;
}
} from '../lib/db.js';
import { defaultDocumentconst defaultDocument: (options: {
url: URL;
title?: string;
}) => ServerRenderedTemplate
} from '../document.js';
import * as homeReadmeimport homeReadme
from '../content/README.md' with { type: 'markdown-lit' };
import type { MyElementclass MyElement
} from '../components/my-element.ts';
import '../components/my-element.js';
export default defineRoutedefineRoute<Response, object | Response, Response, Response | {
success: boolean;
message: string;
}, Response, Response, {
params: Record<...>;
props?: any;
} | undefined, {
...;
}>(options: {
...;
}): {
...;
}
Defines a route.
({
handlerhandler?: Handler<Response> | {
GET?: Handler<object | Response> | undefined;
PUT?: Handler<Response> | undefined;
POST?: Handler<...> | undefined;
PATCH?: Handler<...> | undefined;
DELETE?: Handler<...> | undefined;
} | undefined
: {
POSTPOST?: Handler<Response | {
success: boolean;
message: string;
}> | undefined
: async (contextcontext: {
url: URL;
request: Request;
response: ResponseInit;
}
) => {
const formDataconst formData: FormData
= await contextcontext: {
url: URL;
request: Request;
response: ResponseInit;
}
.requestrequest: Request
.formDataBody.formData(): Promise<FormData>
();
const nameconst name: string | undefined
= formDataconst formData: FormData
.getFormData.get(name: string): FormDataEntryValue | null
('achievement')?.toStringfunction toString(): string
Returns a string representation of a string.
();
if (nameconst name: string | undefined
) {
await dbconst db: {
query<T>(str: string): T[];
}
Dummy DB handler…
.queryquery<unknown>(str: string): unknown[]
(sqlfunction sql(str: string): string
Dummy SQL template literal…
`INSERT INTO achievements (name); VALUES (${nameconst name: string
})`);
return Responsevar Response: {
new (body?: BodyInit | null | undefined, init?: ResponseInit | undefined): Response;
prototype: Response;
error(): Response;
json(data: any, init?: ResponseInit | undefined): Response;
redirect(url: string | URL, status?: number | undefined): Response;
}
This Fetch API interface represents the response to a request.
.redirectfunction redirect(url: string | URL, status?: number | undefined): Response
(contextcontext: {
url: URL;
request: Request;
response: ResponseInit;
}
.urlurl: URL
, 303);
}
contextcontext: {
url: URL;
request: Request;
response: ResponseInit;
}
.responseresponse: ResponseInit
.statusResponseInit.status?: number | undefined
= 400;
contextcontext: {
url: URL;
request: Request;
response: ResponseInit;
}
.responseresponse: ResponseInit
.statusTextResponseInit.statusText?: string | undefined
= 'Wrong input!';
return { successsuccess: boolean
: false, messagemessage: string
: contextcontext: {
url: URL;
request: Request;
response: ResponseInit;
}
.responseresponse: ResponseInit
.statusTextResponseInit.statusText?: string
};
},
},
documentdocument?: ((context: {
url: URL;
params: Record<string, string | undefined>;
props: {
GET: object | undefined;
POST: {
success: boolean;
message: string;
} | undefined;
};
request: Request;
response: ResponseInit;
}) => MaybePromise<...>) | undefined
: (contextcontext: {
url: URL;
params: Record<string, string | undefined>;
props: {
GET: object | undefined;
POST: {
success: boolean;
message: string;
} | undefined;
};
request: Request;
response: ResponseInit;
}
) =>
defaultDocumentfunction defaultDocument(options: {
url: URL;
title?: string | undefined;
}): ServerRenderedTemplate
({ urlurl: URL
: contextcontext: {
url: URL;
params: Record<string, string | undefined>;
props: {
GET: object | undefined;
POST: {
success: boolean;
message: string;
} | undefined;
};
request: Request;
response: ResponseInit;
}
.urlurl: URL
, titletitle?: string | undefined
: homeReadmeimport homeReadme
.title }),
pagepage?: ((context: {
url: URL;
params: Record<string, string | undefined>;
props: {
GET: object | undefined;
POST: {
success: boolean;
message: string;
} | undefined;
};
request: Request;
response: ResponseInit;
}) => MaybePromise<...>) | undefined
: async (contextcontext: {
url: URL;
params: Record<string, string | undefined>;
props: {
GET: object | undefined;
POST: {
success: boolean;
message: string;
} | undefined;
};
request: Request;
response: ResponseInit;
}
) => {
const initialDataconst initialData: {
foo: string;
}
= { foofoo?: string | undefined
: 'bar' } satisfies MyElementclass MyElement
['initialData'];
const achievementsconst achievements: Achievement[]
= await dbconst db: {
query<T>(str: string): T[];
}
Dummy DB handler…
.queryquery<Achievement>(str: string): Achievement[]
<Achievementtype Achievement = {
name: string;
date: Date;
}
>(
sqlfunction sql(str: string): string
Dummy SQL template literal…
`SELECT * FROM achievements`,
);
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.
`
You can use inline (deferred) modules or in-path scripts…
<script type="module">
await new Promise((r) => setTimeout(() => r(console.log('Hi!')), 1500));
</script>
<h1>${homeReadmeimport homeReadme
.title}</h1>
<main>
<article>${homeReadmeimport homeReadme
.content}</article>
</main>
<aside>
<form method="post">
<input type="text" name="achievement" />
<button>Add achievement</button>
<span>${contextcontext: {
url: URL;
params: Record<string, string | undefined>;
props: {
GET: object | undefined;
POST: {
success: boolean;
message: string;
} | undefined;
};
request: Request;
response: ResponseInit;
}
.propsprops: {
GET: object | undefined;
POST: {
success: boolean;
message: string;
} | undefined;
}
.POSTtype POST: {
success: boolean;
message: string;
} | undefined
?.messagemessage: string | undefined
}</span>
</form>
<my-element initialData=${JSONvar JSON: JSON
An intrinsic object that provides functions to convert JavaScript values to and from the JavaScript Object Notation (JSON) format.
.stringifyJSON.stringify(value: any, replacer?: ((this: any, key: string, value: any) => any) | undefined, space?: string | number | undefined): string (+1 overload)
Converts a JavaScript value to a JavaScript Object Notation (JSON) string.
(initialDataconst initialData: {
foo: string;
}
)}></my-element>
<my-client-only-element></my-client-only-element>
<footer>
${achievementsconst achievements: Achievement[]
.mapArray<Achievement>.map<TemplateResult<1>>(callbackfn: (value: Achievement, index: number, array: Achievement[]) => TemplateResult<1>, thisArg?: any): TemplateResult<...>[]
Calls a defined callback function on each element of an array, and returns an array that contains the results.
(
(achievementachievement: Achievement
) =>
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.
`<section class=${`achievement-${achievementachievement: Achievement
.namename: string
}`}>
<h1>${achievementachievement: Achievement
.namename: string
}</h1>
<p>${achievementachievement: Achievement
.datedate: Date
}</p>
</section>`,
)}
</footer>
</aside>
<footer>
<small>
You are visiting ${contextcontext: {
url: URL;
params: Record<string, string | undefined>;
props: {
GET: object | undefined;
POST: {
success: boolean;
message: string;
} | undefined;
};
request: Request;
response: ResponseInit;
}
.urlurl: URL
.hrefURL.href: string
},
<!-- -->
with method ${contextcontext: {
url: URL;
params: Record<string, string | undefined>;
props: {
GET: object | undefined;
POST: {
success: boolean;
message: string;
} | undefined;
};
request: Request;
response: ResponseInit;
}
.requestrequest: Request
.methodRequest.method: string
Returns request's HTTP method, which is "GET" by default.
}.
</small>
</footer>
`;
},
});
📄 /src/routes/index.client.ts
Importing your components in this page's client bundle entrypoint will make the server markup alive.
requestIdleCallbackfunction requestIdleCallback(callback: IdleRequestCallback, options?: IdleRequestOptions | undefined): number
(() => import('../components/my-element.js'));
// ...
Don't import on server-side, if you want a client-only element.
import '../components/my-client-only-element.js';
consolevar console: Console
The console
module provides a simple debugging console that is similar to the
JavaScript console mechanism provided by web browsers.
The module exports two specific components:
- A
Console
class with methods such as console.log()
, console.error()
andconsole.warn()
that can be used to write to any Node.js stream.
- A global
console
instance configured to write to process.stdout
and process.stderr
. The global console
can be used without callingrequire('console')
.
Warning: The global console object's methods are neither consistently
synchronous like the browser APIs they resemble, nor are they consistently
asynchronous like all other Node.js streams. See the note on process I/O
for
more information.
Example using the global console
:
console.log('hello world');
// Prints: hello world, to stdout
console.log('hello %s', 'world');
// Prints: hello world, to stdout
console.error(new Error('Whoops, something bad happened'));
// Prints error message and stack trace to stderr:
// Error: Whoops, something bad happened
// at [eval]:5:15
// at Script.runInThisContext (node:vm:132:18)
// at Object.runInThisContext (node:vm:309:38)
// at node:internal/process/execution:77:19
// at [eval]-wrapper:6:22
// at evalScript (node:internal/process/execution:76:60)
// at node:internal/main/eval_string:23:3
const name = 'Will Robinson';
console.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to stderr
Example using the Console
class:
const out = getStreamSomehow();
const err = getStreamSomehow();
const myConsole = new console.Console(out, err);
myConsole.log('hello world');
// Prints: hello world, to out
myConsole.log('hello %s', 'world');
// Prints: hello world, to out
myConsole.error(new Error('Whoops, something bad happened'));
// Prints: [Error: Whoops, something bad happened], to err
const name = 'Will Robinson';
myConsole.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to err
.logConsole.log(message?: any, ...optionalParams: any[]): void (+1 overload)
Prints to stdout
with newline. Multiple arguments can be passed, with the
first used as the primary message and all additional used as substitution
values similar to printf(3)
(the arguments are all passed to util.format()
).
const count = 5;
console.log('count: %d', count);
// Prints: count: 5, to stdout
console.log('count:', count);
// Prints: count: 5, to stdout
See util.format()
for more information.
('Welcome', navigatorvar navigator: Navigator
.userAgentNavigatorID.userAgent: string
);
// ...
📄 /src/components/my-element.ts
import { 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.
, 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.
, 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';
import { customElementconst customElement: (tagName: string) => CustomElementDecorator
Class decorator factory that defines the decorated class as a custom element.
, propertyfunction property(options?: PropertyDeclaration): PropertyDecorator
A 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';
import { styleMapconst styleMap: (styleInfo: Readonly<StyleInfo>) => import("../directive.js").DirectiveResult<typeof StyleMapDirective>
A directive that applies CSS properties to an element.
styleMap
can only be used in the style
attribute and must be the only
expression in the attribute. It takes the property names in the
{@link
StyleInfo
styleInfo
}
object and adds the properties to the inline
style of the element.
Property names with dashes (-
) are assumed to be valid CSS
property names and set on the element's style object using setProperty()
.
Names without dashes are assumed to be camelCased JavaScript property names
and set on the element's style object using property assignment, allowing the
style object to translate JavaScript-style names to CSS property names.
For example styleMap({backgroundColor: 'red', 'border-top': '5px', '--size': '0'})
sets the background-color
, border-top
and --size
properties.
} from 'lit/directives/style-map.js';
@customElementfunction customElement(tagName: string): CustomElementDecorator
Class decorator factory that defines the decorated class as a custom element.
('my-element')
export class MyElementclass MyElement
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.
{
static readonly GREETINGMyElement.GREETING: "Hello"
= 'Hello';
@propertyfunction property(options?: PropertyDeclaration<unknown, unknown> | undefined): PropertyDecorator
A 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 {
({ typePropertyDeclaration<unknown, unknown>.type?: unknown
Indicates the type of the property. This is used only as a hint for the
converter
to determine how to convert the attribute
to/from a property.
: Objectvar Object: ObjectConstructor
Provides functionality common to all JavaScript objects.
}) initialDataMyElement.initialData: {
foo?: string | undefined;
}
: { foofoo?: string | undefined
?: string } = {};
@propertyfunction property(options?: PropertyDeclaration<unknown, unknown> | undefined): PropertyDecorator
A 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 {
({ typePropertyDeclaration<unknown, unknown>.type?: unknown
Indicates the type of the property. This is used only as a hint for the
converter
to determine how to convert the attribute
to/from a property.
: Numbervar Number: NumberConstructor
An object that represents a number of any kind. All JavaScript numbers are 64-bit floating-point numbers.
}) bgTintMyElement.bgTint: number
= 0.5;
renderMyElement.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.
`
<div
@click=${() => (this.bgTintMyElement.bgTint: number
= Mathvar Math: Math
An intrinsic object that provides basic mathematics functionality and constants.
.randomMath.random(): number
Returns a pseudorandom number between 0 and 1.
())}
style=${styleMapfunction styleMap(styleInfo: Readonly<StyleInfo>): DirectiveResult<typeof StyleMapDirective>
A directive that applies CSS properties to an element.
styleMap
can only be used in the style
attribute and must be the only
expression in the attribute. It takes the property names in the
{@link
StyleInfo
styleInfo
}
object and adds the properties to the inline
style of the element.
Property names with dashes (-
) are assumed to be valid CSS
property names and set on the element's style object using setProperty()
.
Names without dashes are assumed to be camelCased JavaScript property names
and set on the element's style object using property assignment, allowing the
style object to translate JavaScript-style names to CSS property names.
For example styleMap({backgroundColor: 'red', 'border-top': '5px', '--size': '0'})
sets the background-color
, border-top
and --size
properties.
({ '--bg-tint': this.bgTintMyElement.bgTint: number
})}
>
${this.initialDataMyElement.initialData: {
foo?: string | undefined;
}
.foofoo?: string | undefined
} - ${MyElementclass MyElement
.GREETINGMyElement.GREETING: "Hello"
}
</div>
`;
}
static stylesMyElement.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>
= [
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;
margin: 1rem;
}
div {
background: hsl(calc(var(--bg-tint, 0) * 360), 50%, 50%);
}
`,
];
}
FAQ
Why?
There wasn’t much (if any?) platform-oriented, full-stack meta-framework
yet.
Rocket, Eleventy and Astro: all have great approaches on how to server render
Web Components (they all use Lit-SSR under the hood). However, Rocket and
Eleventy are content-oriented (static output) and Astro has a JSX
processor/template compiler that you might not want to minimize layers.
JSX looks like HTML, but that also means you will still be tied to the React
world (TS JSX namespace, IDE language tools…).
While that appears antagonistic toward the decentralized ethos of web platform enthusiasts, there is room for consolidation and reference implementations, especially on the server side.
The base goal is to make the plunge easier for beginners but also to bring comfortable bootstraps for accustomed developers.
Is it tied to any vendor?
Build-less, vendor-free, etc. are all reasonable goals to aim for.
Fewer tools generally mean fewer maintenance issues.
However, we didn’t reach those yet, that’s why we still need to use some tooling
to fill the gaps.
Also, it is perfectly reasonable to use TypeScript or CSS preprocessors, because
for many, the gains largely outweigh the cost, and source-maps are here to help.
In fact, this website is made with TS and SCSS. You don’t have to throw away
your favorite language superset to enjoy the benefits of being closer to the web
platform!
Gracile relies heavily on Vite/Rollup’s ecosystems but also on Lit’s SSR
library, which, like all Lit contributions, are forward-thinking intents for the
web.
Your server-side templates are just portable HTML, with JS-based composability,
safety and DX goodies underneath, meaning nearly zero lock-in.
Think of those as “blending” tools.
They don’t force you into unholy contortions.
Do I need a specific server runtime?
Gracile’s “server” mode build is just outputting a handler.
From there, you’re free to embed it in any standard-friendly HTTP server or use
a Request
/Response
adapter.
As for static builds, you’ll get conformed assets that can be deployed anywhere.
When developing, any JavaScript engine that supports Vite can be used.
Efforts are made to keep Gracile as runtime agnostic as possible, with the leading runtime, Node, as the standard baseline and WinterCG proposals as a compass. We’re slowly getting there, but fragmentation remains. That being said, you’re still free to engage in side-ways, if that’s your will!
How does it compare to XYZ?
You’ll find Gracile is inspired by Fresh, Astro, Elder.js, Nuxt, Rocket, Remix…
you name it!
At one point, everyone is copying everyone when an idea is valuable.
However, there are those things that make it a bit different:
It’s not centered around one or a few UI libraries, nor is it tied to
domain-specific languages or deep bundler-tied abstractions.
Every time a standard is going mainstream, that should be the occasion to prune
user-land stopgaps (the node_modules
black-hole, browser pony-fills…).
Also, expanding the scope of a framework outside core features has a cost: more
dependencies, maintenance and opinions.
”Scope creep” is what makes you ultimately dependent on a framework.
Do-everything frameworks are cool (think Rails, Laravel…), but that’s not the
goal of Gracile. It’s up to you to choose your data-sourcing strategy, UI stack,
HTTP server…
Do I need to use Web Components / Shadow DOM?
Not at all!
This whole website is mostly made with composed function calls returning
streamed, serialized light DOM, then rendered ahead of time.
Progressive enhancements can be achieved with good old, per-route or site-wide
inline JavaScript or modules.
It’s also possible to bring Alpine, HTMX, HTMZ, etc. if that’s your jam!
When you need more interactivity and composition superpowers, or if you are
already an avid Lit user you can jump straight into Lit flavored Web Components.
They are the only ones that are server renderable and client hydratable. That’s
because of their intrinsic template serialization capabilities.
Lit’s renderer is stitching template strings on the client and server, and guess
what, that’s already a perfect base for server rendering!
Strings concats are the inevitable approach used by every SSR engine (Astro,
SolidStart, Next.js…).
Conceivably, at some point, we’ll get “Vanilla” HTML Custom Elements to be SSR’ed canonically.
What is the current state of this project?
It’s experimental notably the server mode.
Lately, things are changing very fast in the web components or
platform-related spaces.
Declarative Shadow DOM has just rooted in every major browser, Constructible
StyleSheets and CSS Modules are following the same path… and that’s a few
“glaring” stuff. Lit SSR itself is an experimental tech., even though it has
already been implemented successfully in
well-known frameworks (Astro, Next, Nuxt, Rocket, Eleventy…).
This project is an attempt to normalize using the web platform amongst developer
who are more familiar with “branded” UI libraries and their respective
server/client meta-frameworks.
We are getting there, but what we often hear is Web Components and other modern
native APIs are “hidden gems” that need to be polished up, put in
context, and from there, really start to excel.
The big picture is better painted with a cohesive experience. It’s an invitation
to dig deeper in web knowledge, which can be overwhelming otherwise.
Hopefully, big names will start to invest in this niche and make it grow, as
they did with WC design systems (Spectrum, Carbon, Material, FAST…).
More competition will make this ecosystem flourish even more.