@verajs/styles
v0.1.2
Published
Adopts a component's `static styles` for VeraJS: constructed stylesheets into shadow roots, and `@scope`-wrapped hoisting for light DOM.
Maintainers
Readme
@verajs/styles
static styles for VeraJS components (772 B gzip): constructed stylesheets into shadow
roots, and @scope-wrapped hoisting for light DOM.
import { wire } from '@verajs/core';
import { renderer } from '@verajs/renderer';
import { styles } from '@verajs/styles';
wire([renderer, styles]);Once, at your app entry, alongside the renderer. Every component init() adopts its static styles
from that point on.
styles is the module; adoptStyles is the function it registers. Wiring that function directly
— wire({ on: 'init', fn: adoptStyles, priority: 50 }) — is the same registration written out, and
is what to write when you want a priority other than the default 50.
wire comes from @verajs/core, not from @verajs/inserts. A production .min.js inlines
the registry into every bundle, so registering through your own copy would write to a map core never
reads — working in development and silently doing nothing in production. Taking core's own wire
removes the question. Forget the wiring and core says so, once, in development.
Shadow DOM — constructed sheets go to shadowRoot.adoptedStyleSheets; plain strings become a
<style data-vm-sheet="styles"> in the shadow root. Both are naturally scoped and safe to re-init.
Light DOM — styles are hoisted to the document once per component class, wrapped in
@scope (tag-name) { … } so they apply only inside that component's subtree: scoping without a
shadow root, done by the platform. Hoisting also survives renders, since a <style> inside the
element would be wiped by the first render pass.
:host works in light DOM too — you write one stylesheet. Inside that @scope block the
scoping root is the element, so :host is translated to :scope and :host(.a) to :scope.a
when a component has no shadow root. Nothing to remember and nothing to write differently: the same
sheet styles the element in both modes, which matters most for a component you installed rather
than wrote, since it will use :host and cannot know how you render it. Only SELECTORS are
translated — a :host in a value (content: ":host", url(/x/:host.png)) is left exactly as
written, as is an escaped identifier like .md\:host.
::slotted() is the exception, and cannot be otherwise. In light DOM the nodes a user slots in
are ordinary descendants, so there is no selector that means "assigned to this slot" without
marking them — which would put framework attributes in your own markup. It is also the fair one to
lose: slotted content is the user's DOM, and page CSS already reaches it there. Development says so
if a light component's sheet uses it.
:host-context() is not translated either — Firefox and WebKit never shipped it.
On an engine with no @scope — Safari before 17.4, Firefox before 128 — the block is hoisted
unscoped rather than dropped, because a dropped block leaves the component unstyled while an
unscoped one still styles it. Every rule then applies page-wide on that engine and only inside the
tag everywhere else, so development says so once, by name. Attach a shadow root to scope them on
every engine, or write selectors that carry the tag.
Dynamic styles
A sheet is adopted once and never re-read. adoptStyles runs on the init insert — once per
element for shadow DOM, and once per component class ever for light DOM. Reassigning
MyComponent.styles afterwards changes nothing.
The sheet is also shared by every instance, because static styles is a static member:
a.shadowRoot.adoptedStyleSheets[0] === b.shadowRoot.adoptedStyleSheets[0] // trueThat is what makes constructed sheets cheap — one object, adopted by every instance, parsed once — and it is why the sheet is the wrong place to put anything that varies. Mutating it to restyle one component restyles all of them.
Custom properties are the seam, and they work with no help from this package. var() resolves
against the element's inherited custom properties at computed-style time, not when the sheet was
adopted, so it re-resolves the moment one changes — and custom properties inherit through the
shadow boundary:
import { init, createStore, render, css, html, wire } from '@verajs/core';
import { renderer } from '@verajs/renderer';
import { styles } from '@verajs/styles';
wire([renderer, styles]);
customElements.define(
'x-tinted',
class extends HTMLElement {
static styles = css`p { color: var(--accent, blue); }`;
connectedCallback() {
init(this, { mode: 'open' });
const state = createStore({ accent: 'blue' });
render(
() => html`
<div style="--accent: ${state.accent}">
<p>tinted</p>
<button @click=${() => (state.accent = 'red')}>Redden</button>
</div>
`
);
}
}
);
document.body.append(document.createElement('x-tinted'));Clicking the button writes state.accent, which re-renders the binding; the adopted sheet
re-resolves var(--accent) against the new value. The sheet itself was never touched.
Setting the property on the host works too, from anywhere — el.style.setProperty('--accent', 'red')
— as does inheriting it from an ancestor, and both apply equally to the light-DOM @scope path.
So static styles is deliberately not reactive: it carries the structure, custom properties carry
what changes. Verified against a real browser in tests/browser/styles-dynamic.test.js.
applyStyles(styles, element) is exported for manual use — the adoption step alone, for an element
whose lifecycle this package's init insert never sees:
import { styles, adoptStyles, applyStyles } from '@verajs/styles';
import { wire } from '@verajs/core';
wire([styles]); // the module — registers adoptStyles on 'init'
wire({ on: 'init', fn: adoptStyles, priority: 50 }); // the same registration, written out
applyStyles(MyPanel.styles, detachedPanel); // adopt into one element by handThis lived in @verajs/core until 0.2.0. It moved because most apps do not use static styles and
every app was paying for it. If a component declares static styles with this package absent, core
warns once in development.
For AI assistants — and anyone who wants the whole API on one page
The repository root's llms.txt is the complete, hand-maintained API
reference for every package, written to be pasted into a model's context window: full export
tables, the buildless CDN and JSX recipes, semantics that differ from other frameworks, and the
mistakes that come up most. Its recipes are executed by the test suite, so they stay honest.
