Skip to content
NativeCSS
Docs contents

<ncss-container>

js/ncss-container.js encapsulates ncss content in a real Shadow DOM tree, so it can run inside a page built on a FOREIGN framework/CSS system (Bootstrap, UIkit, Tailwind, a CMS theme, ...) without styles leaking in either direction.

Setup

<script src="js/ncss-container.js" defer></script>
<ncss-container src="dist/ncss.css">
  <div class="ncss-card">
    <div class="ncss-card-body">Isolated content</div>
  </div>
</ncss-container>
AttributePurpose
srcRequired (or data-ncss-src on the loader <script> as a page-wide default) - path to ncss.css, a completely normal <link>, no ncss-specific rule
themeOptional, an additional stylesheet loaded AFTER src - for a container with a different brand color/radii/typeface than the rest of the page

Two technical prerequisites

1. Token isolation

ncss's tokens are deliberately declared in colors.css/tokens.css/theme.css/webawesome-bridge.css as :root, :host { ... } (rather than just :root) - :root always matches the actual page's root element, never a shadow host; :host, on the other hand, only matches a shadow tree's host node and has no effect outside of one. The same file, unmodified, therefore produces a fully independent copy of every token the moment it's loaded inside a shadow root instead of the page's <head> - no second, duplicated token file, no build step. Confirmed with a real test: a .ncss-btn--primary inside the container shows the correct brand color even though the host page itself never loaded ncss at all.

2. Content isolation

The element's own content is not wired up via <slot> - instead, on connect, it's moved into the shadow root once via plain JS (a real appendChild re-parenting). A <slot> would only display the child elements inside the shadow root - they'd remain part of the light DOM and would therefore still be reachable by the outer page's global stylesheets (a framework reset, or !important rules). Confirmed with a real test: a <slot>-based button picked up an outer !important rule unchanged, while a button embedded via the move approach was completely unaffected.

The trade-off is accepted deliberately: no automatic re-sync on later dynamic DOM changes to the original markup - not a practical downside for a content block that's embedded once and stays put.

Live demo: UIkit 3 + isolated container

Tested in both directions: a full page built on UIkit 3 with an <ncss-container> running the stacked-cards component in the middle. UIkit's button/h1-h6 reset doesn't reach the container, and ncss's own reset (* { margin: 0 } etc.) doesn't leak out to affect UIkit content before/after the container. Scroll-driven effects (position:sticky, animation-timeline: view()) also work normally across the shadow boundary - pure layout/rendering mechanics, unaffected by DOM/style encapsulation.

Demo: demo/uikit-integration.html.

Opt-in scripts work inside the container too

code-block.js, events.js, downloads.js, scroll-stack-fallback.js, hide-on-scroll-fallback.js, and wa-close-on-scroll.js no longer look for their elements via a plain document.querySelectorAll(...) (which doesn't cross Shadow DOM boundaries by default) - instead they use deepQueryAll(), which recurses into every open shadow root (including any <ncss-container>) and behaves exactly like querySelectorAll when there's no Shadow DOM on the page at all. code-block.js additionally calls Prism.highlightElement() per block found, instead of the document-global Prism.highlightAll().

Setup is always the same, with or without an <ncss-container> - just load the script on the page as usual; it finds its elements regardless of whether they sit inside a shadow root. One exception: wa-close-on-scroll.js runs on every scroll event, so it deliberately searches only inside <ncss-container> elements rather than doing a full DOM walk (for performance), and is now also throttled via requestAnimationFrame.