Bascik Scoping Compatibility

Legend

  • Supported and tested
  • Partially supported (see notes)
  • Intentionally unsupported (see notes)
  • Not yet supported

Component Template Structure

Bascik supports flexible HTML, CSS, and JavaScript structures inside .html component files.

CapabilityStatusNotes
Multiple top-level HTML elementsSupported naturally without requiring single wrapper elements or fragment tags. All root elements are inserted in order. Inherited usage attributes merge onto the first root HTML element.
Multiple <style> blocksExtracted and combined with any companion .css file before scoping and deduplication. Note: Using multiple <style> tags in a single component file is supported but not recommended for readability and maintainability.
Multiple <script> blocksClient <script> blocks are each wrapped in an independent IIFE. Recommended for clean, maintainable code when separating unrelated logic within a component. Build (data-bascik-build), server (data-bascik-server), and data scripts (e.g. type="application/ld+json") are processed according to their script type.

CSS Scoping

CSS scoping applies to .css files paired with a component's HTML file. Place the .css file in the same directory as the component and give it the same base name.

Selectors

PatternExampleStatusNotes
Class selector.foo {}Scoped with unique instance prefix
Descendant with class.foo .bar {}All class names in selector scoped
Multi-class.foo.bar {}Both class names scoped
Standalone element selectorp {}Converted to a generated class and injected on matching elements in component HTML, including indented selectors inside at-rules such as @media and inline <style> blocks.
Element pseudo-classp:hover {}Element converted to class; pseudo-class preserved: .bascik__...__el__p:hover {}
Element pseudo-elementp::before {}Element converted to class; pseudo-element preserved: .bascik__...__el__p::before {}
@keyframes name@keyframes spin {}Name scoped; animation: and animation-name: references updated to match
@media query@media (max-width: 600px) {}Media condition untouched; class names inside scoped normally
@supports@supports (display: grid) { .foo {} }Class names inside @supports blocks are scoped normally.
@layer@layer base { .foo {} }Layer name scoped in all forms: declaration blocks, single-name and comma-list ordering statements.
@container@container sidebar (min-width: …) {}Container names declared via container-name: or the container: shorthand are scoped; @container name (…) queries updated to match. Unnamed queries untouched.
CSS custom properties--brand: #d3ff8d / var(--brand)Declarations and all var() references in the same file scoped together. var(--prop, fallback) is fully supported, the fallback value is preserved and the property name is scoped.
Multiple animation: valuesanimation: a 1s, b 2sBoth keyframe name references are scoped when an animation: shorthand lists more than one animation.
Child / sibling combinators.a > .b, .a + .b, .a ~ .bAll class names on both sides of >, +, and ~ are scoped.
:is() / :where() / :has() with class args:is(.foo, .bar) {}Class names inside :is(), :where(), and :has() are scoped normally. Element names inside these functions are not converted (see below).
Inline <style> in component HTML<style>.foo {}</style>Full CSS scoping pipeline applied to inline <style> blocks. Extracted from component HTML into component CSS, deduplicated across component instances, and injected into page <head>.
CSS #id selector#btn {}Converted to a component-scoped class selector (.bascik__comp__id__btn {}) using a context-aware lookahead that correctly distinguishes selector position from hex color values. The generated class is injected onto the HTML element. Specificity drops from (0,1,0,0) to (0,0,1,0).
[id] / [id="…"] attribute selector[id] {}Stripped at compile time. Attribute-selector forms cannot be scoped without DOM wrapping.
Attribute selector[data-foo="bar"] {}Passed through untouched, not scoped. Will apply globally. Avoid in component CSS or use a class-based selector alongside it.
Compound / descendant element selectorsdiv p {}, .card p {}, .list > li {}.class element {} and .class > element {} (class followed by descendant/child element) are now scoped: the element name is converted to a class and injected onto matching HTML elements. Patterns with two bare element types (div p {}, p + p {}) still require a class anchor on the left side of the combinator.
Comma-separated element selector listh1, h2 {}All elements in a comma list are converted, both multi-line (each at column 0) and same-line (h1, h2 {}). A ) stop in the lookahead prevents false positives inside :is(), :where(), :has().
Cross-boundary root element selectorshtml[data-theme="dark"] .foo {}html, body, and head are excluded from element-to-class conversion so cross-boundary selectors compile with the root element name intact. html[data-theme="light"] .component-class {} becomes html[data-theme="light"] .bascik__comp__class {} and correctly matches the component element when the document root carries a theme or state attribute.
:is() / :where() / :has() with element names:is(p, h2) {}Element names inside these functions are not converted. Class equivalents work fine: :is(.foo, .bar) {}.
CSS nesting, class selectors& .child {}Class selectors inside nesting are scoped normally.
CSS nesting, element selectors& p {}, & > h2 {}Element selectors directly after & (with optional single combinator >, +, ~) are converted. Complex patterns (& .parent p {}, &p {}) are not converted.
@scope (native)@scope (.foo) { .bar {} }Class names in both the @scope (.selector) argument and the optional to (.selector) clause are scoped normally (handled by the global class-scoping pass). Class names inside the @scope block are also scoped. Element names in @scope arguments and indented element selectors inside the block follow the same rules as other at-rules.
:nth-child(An+B of .selector):nth-child(2n+1 of .item) {}Class names in the of <selector> argument are scoped by the global class-scoping pass (the same (?<=\.) regex that handles :is(), :where(), and :has() class arguments). Works for :nth-child and :nth-last-child.

Other CSS Features

FeatureStatusNotes
CSS deduplicationWhen a component is used multiple times on a page, its CSS is injected only once.
minify.identifiersIn production builds, verbose names like bascik__site-nav__a1b2c3__logo are hashed to short strings (e.g. ba1b2c3d) for name compression.
minify.cssWhitespace in the compiled <style> block is collapsed.
CommentsStripped before processing.
SVG elements in component HTMLclass attributes on SVG elements (<svg>, <circle>, <path>, <rect>, etc.) are scoped with the same pipeline as HTML elements. JS classList and querySelector calls targeting SVG children are rewritten.
@font-facePassed through untouched, the font-family name is not scoped. Both the declaration and all usage sites remain unmodified, so the font resolves correctly within the page. Declare @font-face in a shared global stylesheet rather than a component .css file to avoid duplicate declarations when a component is used multiple times.
@importNot followed by the scoping pipeline. The imported CSS file is not processed or scoped. Include CSS directly in the component file instead.
@property@property --name { } declaration names are scoped. Any matching --name: element declarations and var(--name) references in the same component file are scoped to match.
@starting-styleClass names and element selectors inside @starting-style blocks are scoped by the same passes that handle other at-rules. Both standalone @starting-style { .foo { } } and nested .foo { @starting-style { } } forms are handled.
@counter-style@counter-style name { } declaration names are scoped. References in list-style, list-style-type, counter(counter, name), and counters(counter, sep, name) in the same component file are updated to match.
view-transition-nameview-transition-name: name values are scoped to the component. Matching ::view-transition-old(name), ::view-transition-new(name), ::view-transition-group(name), and ::view-transition-image-pair(name) pseudo-element references in the same file are updated to match. The keywords none and auto are not scoped.
anchor-name / @position-tryanchor-name: --name declarations are scoped per component. Matching position-anchor: --name references and @position-try --name { } at-rules in the same CSS file are updated to match. Only anchors declared in the component's own CSS are scoped, external anchor references are left untouched.

JavaScript Scoping

Bascik rewrites DOM selector references inside component <script> tags to match scoped attribute values. All rewrites happen at build time with no runtime is added.

IIFE Isolation

PatternStatusNotes
<script> (no type)Wrapped in an IIFE to prevent variable leakage between components.
<script type="text/javascript">Wrapped in an IIFE.
<script type="module">Not wrapped in an IIFE (modules are already isolated by spec). DOM selector references still rewritten.
<script type="application/json"> (and any non-JS type)Left completely untouched.
<script data-bascik-build>Executed at transpile time as a Node.js ESM module. The script's stdout is injected in place of the tag. Runs in both dev and build modes. Use console.log() / process.stdout.write() to output HTML. Top-level import and await are supported.
Literal component tags inside <script>, <style>, or <textarea>Treated as text, never resolved into components. Safe to mention tags like <my-card> in JSON-LD strings, inline scripts, or code examples.
HTML comments containing component tagsHTML comments (<!-- <my-card> -->) are stripped during HTML minification, so commented custom tags are never expanded into components.

DOM Selector Rewriting

MethodExampleAttribute ScopedStatusNotes
document.getElementByIdgetElementById("my-id")id
document.querySelector with #idquerySelector("#my-id")id
document.querySelectorAll with #idquerySelectorAll("#my-id")id
document.getElementsByClassNamegetElementsByClassName("my-cls")class
document.querySelector with .classquerySelector(".my-cls")classSingle-token class selector only.
document.querySelectorAll with .classquerySelectorAll(".my-cls")classSingle-token class selector only.
document.getElementsByNamegetElementsByName("my-name")name
element.closest with #idel.closest("#my-id")id
element.closest with .classel.closest(".my-cls")classSingle-token class selector only.
element.matches with #idel.matches("#my-id")id
element.matches with .classel.matches(".my-cls")classSingle-token class selector only. Works for event delegation: e.target.matches(".my-cls").
element.classList.addel.classList.add("my-cls")classSingle and multi-argument forms: classList.add("a", "b") rewrites all class names.
element.classList.removeel.classList.remove("my-cls")classSingle and multi-argument forms.
element.classList.toggleel.classList.toggle("my-cls")classThe optional boolean second argument is passed through unchanged.
element.classList.containsel.classList.contains("my-cls")class
element.classList.replaceel.classList.replace("old", "new")classBoth old and new class name arguments are rewritten.
Compound querySelector / querySelectorAllquerySelector(".foo .bar")class / idSpace and combinator tokens (>, +, ~) rewritten. Adjacent .foo.bar rewrites leading token.
element.className setterel.className = "my-cls"classSingle and multi-class string assignments rewritten (= and +=). Reading className is unchanged.
element.setAttribute("class", …)el.setAttribute("class", "my-cls")classString literal values are rewritten.
element.setAttribute("id", …)el.setAttribute("id", "my-id")idString literal values are rewritten.
element.setAttribute("name", …)el.setAttribute("name", "my-name")nameString literal values for known name attributes are rewritten.
innerHTML / insertAdjacentHTML stringsel.innerHTML = '<div class="box">'classKnown class names in static HTML string literals are rewritten.
element.removeAttributeel.removeAttribute("class")Attribute names (not values) passed with no rewriting needed.
element.hasAttributeel.hasAttribute("id")Same as removeAttribute: attribute name, not value.
element.toggleAttributeel.toggleAttribute("hidden")Boolean attribute name only with no value to rewrite.
element.style.setProperty for CSS varsel.style.setProperty("--accent", v)Runtime CSS custom property names are not rewritten. Use scoped property name explicitly.
Template literal in className / selectorsel.className = `box ${state}`Template literals with expressions are not rewritten. Use classList.add/remove.
element.id setterel.id = "my-id"idNot rewritten. Use getElementById to retrieve and operate on the reference.
querySelector attribute selectorquerySelector("[id='my-id']")idUse getElementById instead.

Notes on Gaps

The unsupported JS patterns above all involve dynamic attribute manipulation where static analysis cannot safely identify which component's attribute is being referenced from a string literal.

JS-only class discovery

Class names that only appear in JavaScript (never in a class="…" HTML attribute) are automatically discovered and added to the scope map before the JS rewrite runs. This covers all class-referencing patterns: classList.* arguments, .className tokens in querySelector / querySelectorAll / closest / matches selector strings, el.className = "…" assignments, and el.setAttribute("class", "…") values. CSS-only classes (only in the .css file, never in HTML or JS) are scoped in CSS only, which is fine since nothing in JS needs to reference them.

The exception is innerHTML / insertAdjacentHTML string scanning, which only recognizes classes that appear in the HTML template.

The recommended pattern is to query scoped elements by a single id or single-class selector first, store the reference, then use the reference for all further DOM operations:

html
<!-- source - works correctly -->
<div id="panel" class="card"></div>
<script>
  const panel = document.getElementById("panel"); // ← rewritten by Bascik
  panel.style.display = "none"; // ← operate on the reference
  panel.dataset.state = "closed"; // ← data attributes for state
</script>

Class queries are document-wide (not per-instance)

Class names are scoped to the component type, not to individual instances. This means querySelectorAll(".my-class") inside a component script, which Bascik rewrites to querySelectorAll(".bascik__comp__my-class"): will find matching elements across all instances of that component on the page, not just the current instance.

To operate only on the current instance's elements, query by id (which includes a per-instance hash) and traverse from the returned element:

javascript
// In component - gets only THIS instance's panel:
const panel = document.getElementById("panel"); // rewritten with instance hash

FormData with scoped name attributes

When a component uses <input name="username">, Bascik scopes the name attribute to a per-instance value like bascik__comp__a1b2c3__username. As a result, new FormData(form) entries use the scoped name as the key. If your server-side code expects the unscoped field name, you will need to adapt it, or extract values using formData.get with the scoped name, or via form.elements iteration.