Using Skylab SDK with shadow DOM
Load skylab-sdk.css once from your app's outer
<head> and Skylab's design tokens, theming, and fonts cascade
into every shadow root your app (or its dependencies) creates — including
any Skylab components mounted inside a shadow tree.
Skylab's global class and element rules — buttons, form controls,
tables, the grid, typography, and the utility classes — are a different
matter. Style rules do not cross a shadow boundary, so they only apply where the
stylesheet itself is present. If you are authoring a custom element that renders
into its own shadow root and you want those rules inside it, adopt the shared
stylesheet with
globalThis.SkylabSdk.adoptStylesheet.
Recommended setup
Add the standard Skylab <link> and
<script> tags to the outer document. This is the same setup
described in our Best practices
and is what App Archetype provides out of the box.
With that in place, components mounted in shadow trees pick up the same T1 palette, T2 semantic tokens, T3 component tokens, and the active theme from the document-level cascade, with no per-shadow-root setup. Skylab's global class and element rules still need to be adopted into each shadow root — see below.
Why tokens and theming need no per-shadow-root setup
Three browser features combine to make this work:
-
CSS custom properties inherit through shadow boundaries. Skylab sets all of
its tokens on
:root(i.e.<html>), so every descendant — in the light DOM or inside any shadow root — sees the inherited values. -
Theme switching is driven by a single attribute on
<html>. The<s-theme-provider>element togglesdata-sk-theme="dark"on the document root, which flips:root[data-sk-theme='dark']inskylab-sdk.cssand re-binds every T2 token. Those new values inherit straight through every shadow root with no extra plumbing. -
@font-facedeclarations are scoped to a Document, not to individual style scopes. The fonts shipped inskylab-sdk.cssare visible to text rendered anywhere in the page, including inside shadow trees.
Adopting Skylab's stylesheet into a shadow root
skylab-stylesheet.js from the
recommended setup above builds
one CSSStyleSheet per page load and exposes it as
globalThis.SkylabSdk. It is an ES module, so load it with
type="module".
Module scripts are deferred and run in document order alongside
skylab-sdk.esm.js, so the bootstrap still runs first with no
defer attribute of its own. Do not add async: it drops
that ordering guarantee, and a constructor that runs before the bootstrap loses
Skylab's global rules for the life of that element.
data-skylab-css-href is optional. When it is absent the module
derives the stylesheet URL from import.meta.url, since the two are
published under the same versioned prefix. Set it explicitly if you serve the
stylesheet from somewhere else — another host, or another Skylab version's
CSS.
A custom element then adopts the shared sheet from its constructor, immediately
after attachShadow():
Use .call(this).
adoptStylesheet reads this.shadowRoot at the moment it
is invoked, and nothing stores the function, so .call does the job
in one step. Inside the constructor, this already is the
instance being constructed.
What the API guarantees
-
One stylesheet object for the whole page. Every shadow root
adopts the same
CSSStyleSheetinstance, so the CSS is parsed once no matter how many elements adopt it. -
Calling early is safe. Constructors usually run before the
stylesheet has finished downloading.
adoptStylesheetrecords the shadow root and adopts into it as soon as the sheet is ready, so you do not have to think about timing. If you want to await it explicitly,globalThis.SkylabSdk.readyresolves with the sheet. -
A failed stylesheet degrades quietly. If the stylesheet cannot
be fetched, or its URL cannot be resolved at all — a
blob:ordata:module URL gives nothing to derive it from and carries no tag to readdata-skylab-css-hrefoff —globalThis.SkylabSdk.readyrejects and the failure is logged once.globalThis.SkylabSdkitself is established either way. The rejection is one of the error types published onglobalThis.SkylabSdk.errors—CssUrlError, carrying themoduleUrlit could not resolve, orCssFetchError, carrying thehrefandstatusit got back — and both extendStylesheetError, so you can branch on the cause withinstanceofrather than matching on a message.adoptStylesheetstays safe to call — it becomes a no-op rather than throwing, and it stops retaining shadow roots, so a broken stylesheet URL costs you Skylab's global rules but never a leak. Your elements still render, and tokens and theming still reach them from the outer<link>. - Adoption is idempotent. Calling it more than once on the same shadow root adopts the sheet once, and stylesheets the shadow root already adopted are preserved.
-
Your own styles still win. Adopted stylesheets are ordered
before a shadow tree's own
<style>elements, so component-scoped CSS continues to override Skylab's global rules. -
First script to load wins. If a page ends up loading two
versions of Skylab, the first bootstrap script to run establishes
globalThis.SkylabSdkand later ones are no-ops. That keeps a single shared sheet, but it does mean the older stylesheet can be the one adopted. Worth knowing when debugging a page that mixes versions.
Loading Skylab CSS only inside a shadow root is not supported
Adopting the stylesheet into a shadow root is additive. It does not
replace the <link> in the outer <head>,
and Skylab does not work with the stylesheet present only inside a shadow root.
Skylab's semantic-token defaults and the data-sk-theme theming
trigger are scoped to :root, and inside a shadow tree
:root matches nothing — the shadow root is a
DocumentFragment, not a Document. Components inside
the shadow root would be missing T2 semantic tokens entirely, and the light/dark
switch would have no effect. @font-face and
@property rules are document-scoped for the same reason: the
browser ignores them inside a shadow root, so it is the outer
<link> that registers Skylab's fonts and the
--sk-theme property.
This scoping is deliberate, and it is what makes adoption safe: because the T2
tokens are declared on :root only, an adopted copy of the
stylesheet does not re-declare them on the shadow host, so the values set on
<html> — including dark-mode values — keep
inheriting through untouched.
If your app cannot include skylab-sdk.css in the outer
<head>, please reach out on the
#skylab
Slack channel so we can discuss the integration constraints.