Styles

How the stylesheets of a public module are compiled and delivered by Beyond Packages: CSS and SCSS with partials and a theme, Tailwind scanning only the sources a module declares, selecting another module's stylesheet with .css, the shared global stylesheet of a package, what a failed stylesheet does, and how a change reaches a page.

  • Availability: Experimental
  • Evidence: Recorded run
  • How-to guide

One stylesheet per public module

A public module compiled by the ts bundler has at most one stylesheet: every .css and .scss file in the module directory is compiled into it, in file order, with a source map. The code of the module does not import them. The stylesheet is delivered beside the code, at /styles/<subpath> of the module, and the page links it or a widget adopts it in its root. A module compiled in the esbuild packaging mode instead imports its stylesheet from its code, and gets the same delivery: see Your first UI.

CSS and SCSS, partials and a theme

An SCSS file is compiled with Sass. A relative @use or @import resolves against the importing file, so a partial in the module directory (_tokens.scss, which produces no stylesheet of its own) or a theme elsewhere in the package becomes a dependency of the module: editing it rebuilds the stylesheet of that module and of no other.

JSONmodule.json
{
	"platforms": ["web"],
	"entry": "index.ts"
}
Textstyles.scss
// The stylesheet of the module: a partial of the module and a theme elsewhere in the package
@use "./tokens";
@use "../theme/palette";

.card {
	border: 1px solid palette.$line;
	padding: tokens.$space;
}
Text_tokens.scss
// A partial: included by the stylesheets that use it, it produces no stylesheet of its own
$space: 16px;
TypeScriptindex.ts
// The stylesheet of the module is not imported by its code: every .css and .scss file of the module
// directory is compiled into the stylesheet of the module, which a page links or a widget adopts
export const card = (text: string): string => `<div class="card">${text}</div>`;

url() references are left as written: declare the file under assets in the manifest so that it is served with the module, and address it relative to the source.

A plain CSS file is passed through Sass as CSS, which resolves relative imports of Sass sources and reports syntax errors. A CSS @import "./file.css" of a .css file is left for the browser, as CSS itself does.

Tailwind, scanning only what you declare

A stylesheet that imports Tailwind (@import "tailwindcss") is compiled with Tailwind CSS 4. Tailwind scans nothing on its own: the module manifest declares the sources whose classes are candidates, relative to the module, and only those files are read.

JSONmodule.json
{
	"platforms": ["web", "node"],
	"entry": "index.ts",
	"widget": {
		"element": { "name": "hello-widget", "attrs": ["subject"] }
	},
	"tailwind": { "sources": ["view.tsx"] }
}
CSShello.css
/* The stylesheet of the widget: Tailwind utilities for the classes in view.tsx, the theme, and its own rules */
@import "tailwindcss";

@theme {
	--color-brand: rgb(30, 64, 175);
}

.greeting {
	font-size: 28px;
	margin: 0 0 8px;
}

.counter {
	background-color: rgb(30, 64, 175);
	color: rgb(255, 255, 255);
	border: 0;
	padding: 8px 12px;
}

Adding a class such as underline to a declared source emits its utility; removing the class removes the utility from the stylesheet. A theme (@theme) can be declared in the stylesheet itself or imported from a file of the package, which is then watched too. A Tailwind utility lives in a CSS layer, so an unlayered rule of another sheet for the same property wins over it, as the cascade defines.

The shared stylesheet of a package

A package publishes a stylesheet shared by all of its widgets by exporting it as a public module with no code:

JSONpackage.json
{ "exports": { "./global": "./global.css" } }
CSSglobal.css
/* The shared stylesheet of the package: every widget of the package adopts it in its own root */
:host {
	font-family: system-ui, sans-serif;
}

.card {
	border: 1px solid rgb(203, 213, 225);
	border-radius: 8px;
}

Its sources are the entry file and what it imports, compiled by the default bundler of the package. Every widget of the package adopts it first in its own shadow root, before its own sheet and the sheets of its dependencies; the registration of the widget says so (global: true). It is not injected into the document of the page.

Publishing ./global is the whole declaration: there is no other flag. Preparing a single widget of the package for delivery prepares the sheet too, a widget of another package does not adopt it, and a package that does not publish it produces no request for one.

What reaches a widget, and what reaches the page

  • A widget adopts, inside its shadow root: the shared sheet of its package, its own stylesheet, and the stylesheet of every public module it imports that is not itself a widget, transitively. A nested widget owns its own root: traversal stops at it.
  • The document links the stylesheets of the modules it reaches without crossing a widget, such as the entry module of a page. A stylesheet reached through a widget only is never linked by the document.
  • No rule of a widget reaches the page, and no rule of the page reaches a widget.

Select the stylesheet of another public module

An import selects the JavaScript of a public module. To use only the stylesheet of another public module, name it with .css:

TypeScript
import '@example/ui/theme.css';

This selects the stylesheet of the public module ./theme of @example/ui, or the literal subpath ./theme.css when that package publishes one, as npm packages that export CSS files do. It is a style relation, not an import of code: the compiled module does not import the code of ./theme, and the stylesheet is applied where the module is, adopted inside the root of the widget that imports it, or linked by the document for a module outside any widget. Nothing is applied because of the extension.

Diagnostic When
OUTPUT_NOT_FOUND A module that is only a stylesheet imported without .css (the message names the specifier to write), or .css of a module that produces no stylesheet
OUTPUT_AMBIGUOUS The specifier names two different public modules: a literal ./theme.css and a module ./theme with its own stylesheet
STYLE_BINDING_UNSUPPORTED A stylesheet asked for a value: import sheet from '…css', a named import, or with { type: 'css' }

A failed stylesheet keeps the page

A syntax error in a stylesheet fails the build of its module: the module is invalid, its stylesheet answers 422 BUILD_FAILED with a STYLE_ERROR that names the file and the position, and nothing else is published. A page that already loaded the module keeps its last good style; a widget keeps its last adopted sheet. Correcting the file publishes the stylesheet again and the page applies it.

A change reaches the page

When a build changes the stylesheet of a module, the service announces its new hash. The development runtime in the page requests the new sheet and replaces it: the document replaces its link, and every widget that adopted the sheet adopts the new one in its root, keeping its state. Two edits saved in quick succession end in the last one. The code of a module compiled in the esbuild packaging mode is not updated in place; its stylesheet is.

Next

Give a widget its own styles and a styled dependency: Author a Beyond widget.