Web Standards & Scoping Compatibility

This page documents Bascik's support matrix against authoritative W3C Recommendations, WHATWG Living Standards, IETF RFCs, and ECMA-262 specifications, cross-referenced with MDN Web Docs documentation and Baseline browser compatibility tiers.

Legend

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

Web Standards Authority Matrix

Bascik operates as a zero-runtime build-time compiler and HTTP delivery server. All transformations, protocols, and APIs are designed to comply with official web specifications:

Domain / SubsystemAuthoritative StandardMDN / Baseline StatusSummary
HTML Elements & CompositionWHATWG HTML §3 / §4Baseline: Widely AvailableCustom element naming, valid data-* attributes, slot fallback, raw-text masking
HTML MinificationCSS Text 3 / WHATWG HTMLBaseline: Widely AvailableSafe phrasing content space preservation (INLINE_TAGS) and block collapsing
CSS Selectors & NestingW3C Selectors 4 / CSS NestingBaseline: Widely AvailableClass scoping, 2023 relaxed nesting, pseudo-classes, combinators
Modern CSS At-RulesW3C CSS Module Level 3 / 5Baseline: Newly / Widely Available@layer, @container, @keyframes, @property, @counter-style, @starting-style, @position-try
JavaScript DOM ScopingWHATWG DOM §4 / WebIDLBaseline: Widely AvailableCompile-time query rewriting for standard DOM element methods
Script Execution IsolationECMA-262 / WHATWG HTML §7.1Baseline: Widely AvailableIIFE encapsulation for classic scripts, native module isolation for type="module"
HTTP/1.1 & HTTP/2 ProtocolsIETF RFC 9112 / RFC 9113Baseline: Widely AvailableALPN negotiation, HTTP/2 multiplexing, pseudo-headers, stream lifecycle
HTTP Semantics & CachingIETF RFC 9110 / RFC 6797Baseline: Widely AvailableStrong/weak ETags, conditional 304 responses, Vary, Brotli, HSTS header
MIME TypesIETF RFC 9239 / IANABaseline: Widely AvailableCurrent standard text/javascript; charset=utf-8 media types
Development Static AssetsNode.js filesystemN/AOptional relative symlinks for unchanged page assets; builds always emit regular files
Live Reload & EventsWHATWG EventSourceBaseline: Widely AvailableServer-Sent Events (SSE) /bascik-live-reload endpoint
Sitemaps & RobotsSitemaps 0.9 / IETF RFC 9309Standard ProtocolsCanonical XML sitemap and robots exclusion directives
Content Security PolicyW3C CSP Level 3Baseline: Widely AvailableSHA-256 hash manifests (.bascik/dist/csp-hashes.json) for inline scripts and styles; per-request nonces intentionally unsupported for static caching

Component Template Structure & HTML Standards

Bascik supports flexible HTML, CSS, and JavaScript structures inside .html component files without requiring runtime frameworks.

CapabilityStandard / SpecStatusNotes
Hyphenated custom element namesWHATWG HTML §4.13.1.2Component tags with hyphens (e.g. <my-button>, <site-nav>) follow the WHATWG custom element standard and prevent collisions with native tags.
Unique component filenamesWHATWG HTML §4.13.1.2Component names are derived strictly from filenames. Subfolders are supported for organization, but duplicate filenames producing the same tag name error at build time.
Single-word component filenamesWHATWG HTML §4.13.1.2Single-word component names (e.g. card.html -> <card></card>) compile for backward compatibility, but Bascik's CLI compiler and VS Code extension issue warnings recommending a hyphenated name (e.g. my-card.html).
Native element shadowing guardWHATWG HTML §4Bascik keeps the set of native element names, every element of WHATWG HTML (obsolete ones such as center and marquee included, since the parser still knows them) plus svg and math, and issues a build-time warning if a component filename shadows one (e.g. header.html, dialog.html or svg.html).
Exact component tag matchingWHATWG HTML §13.2.5A component name matches only the complete tag name. A card component never claims a longer hyphenated tag such as <card-header>.
Custom data-* attributesWHATWG HTML §3.2.6.6Internal directives (data-bascik-prop-*, data-bascik-attr-*, data-bascik-preserve, data-bascik-slot, data-bascik-build, data-bascik-build-once, data-bascik-routes, data-bascik-server, data-bascik-stream) strictly conform to XML NCName lower-case naming syntax. Directives match only as whole attribute names, so an unrelated attribute such as data-bascik-server-foo or data-bascik-build-x is left alone and the tag is treated as an ordinary client script.
URL-safe dynamic route parametersWHATWG URLDynamic route parameters reject illegal filename and URL characters (#, %, &, ', +, spaces, leading dots, Windows device names) to ensure predictable static serving.
Self-closing custom tagsWHATWG HTML §13.1.2In HTML source code, custom tags can use self-closing syntax with or without a space (<my-comp /> or <my-comp/>). Both forms balance correctly when nested inside a paired instance and expand at build time into standard HTML.
Multiple top-level HTML elementsWHATWG HTMLSupported naturally without requiring single wrapper elements or fragment tags. All root elements are inserted in document order. Inherited usage attributes merge onto the first content element after leading text, <link>, or <meta> nodes. Root opening tags are parsed quote-aware, so > inside an attribute value remains intact.
Multiple <style> blocksWHATWG HTML §4.2.6Extracted and combined with any companion .css file before scoping and deduplication. Note: Using multiple <style> tags in a single component file is supported, but using a single stylesheet pattern per component is recommended for maintainability.
Deterministic instance IDsWHATWG HTMLInstance IDs for scoped id attributes (and name, when scoping.attributes.name is on) are derived deterministically from (page path, component name, ordinal index), ensuring byte-identical builds across repeated runs.
Multiple <script> blocksWHATWG HTML §4.12Client <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, data-bascik-build-once), server (data-bascik-server), stream (data-bascik-stream), routes (data-bascik-routes), and data scripts (e.g. type="application/ld+json") are processed according to their script type.
Raw-text comments and nested scriptsWHATWG HTML §4.12Comments and comment-like text inside <pre>, <textarea>, scripts, and <style> are shielded before HTML comments are stripped. CSS CDO/CDC tokens (<!-- ... -->) inside a <style> element are preserved so authored styles survive HTML minification. Scripts nested inside containers remain in place.
Raw-text document closing tagsWHATWG HTML §13.2.5Literal </body> text inside <textarea> and </head> text inside <script> do not terminate document extraction or duplicate the remaining page during reassembly.
Global class passthroughWHATWG HTMLClass names in component HTML that are not declared in the component's stylesheet (such as global utility classes like skip-link, flex, hidden) pass through as unscoped global classes so global stylesheets continue to match them. A component with no stylesheet, an empty one, or one containing only comments declares no classes, so none of its classes are scoped.
Class attribute whitespaceWHATWG HTML §2.3.7Class tokens are normalized across spaces, tabs, and newlines. Leading, trailing, and repeated whitespace never creates an empty scoped token.
Quoted, unquoted, and whitespace-spaced attributesWHATWG HTML §13.1.2.3Double-quoted (class="card"), single-quoted (class='card'), whitespace-spaced (class = "card"), and unquoted (class=card, id=hero, for=email) attributes are supported across HTML attribute scoping, element/ID class injection, and ID reference rewriting. Component props, inherited attributes and class injection read an unquoted value to whitespace or >, as browsers do (data-bascik-prop-href=/a?b=c, class=a"b=c), except that a trailing / is read as a self-closing slash, so quote a value that ends in /.
Attribute reading in ID reference rewritingWHATWG HTML §13.2.5ID references and declared IDs are read the way the browser tokenizes attributes: a quoted value ends at its closing quote even with no space after it (for="x"class="y"), an attribute after a / separator counts (<label /for="x">), and id= or for= text inside another attribute's quoted value is never read. An unquoted value that holds a quote (for=x"y, title=it's) hides the whole tag from ID reference rewriting, so quote such values.
Slot fallback semanticsWHATWG DOM §4Default and named slots preserve their inner placeholder markup when no replacement content is passed from the parent template.
Prop value escapingWHATWG HTML §13.2.5Prop values are HTML-escaped on injection. Entity-encoded quotes round-trip as text; slots are the raw-markup path.
Prop-to-attribute bindingWHATWG HTML §3.2.6.6data-bascik-attr-{attribute}="{propName}" binds a supplied prop to plain and hyphenated attributes, then removes the directive. Directives match only as whole attribute names, never text or tokens inside another attribute value. Missing props add nothing; existing targets warn and are replaced. Bound id, name, and class values use normal scoping rules.
Default slot forwardingWHATWG HTML §4.12.4A valueless data-bascik-slot marker written between a nested component's usage tags receives the outer component's default slot content, or its own fallback when the outer tag is empty. Named wrappers stay with the nested component.
Nested prop boundaryWHATWG HTML §3.2.6.6Props are extracted only from a component's opening usage tag and whole data-bascik-prop-* attribute names, never matching text or tokens inside other attribute values, so declarations on nested components inside slot content never leak into the parent.
Tag-level preserveWHATWG HTML §13.1.2scoping.preserve keeps each configured tag's id, name, and class attributes, contents, and descendants unscoped through shared restorable shielding. Entries accept * wildcard patterns (for example vendor-*) with the same semantics as exact names.
External custom elementsWHATWG HTML §4.13components.external lists hyphenated tags owned by a custom element or library (exact names or * wildcards, case-insensitive). Matching tags are omitted from the Unresolved component tag build warning and the --check unmatched-tag findings. A real component of the same name still expands, and attribute scoping is unaffected.
Element-level preserveWHATWG HTML §3.2.6.6data-bascik-preserve applies to one subtree. A bare directive, or an empty value (data-bascik-preserve=""), preserves id, name, and class; a space-separated value preserves only listed attributes. The directive is removed from output.
Internal raw-text maskWHATWG HTML §13.1.2Internal scans use a hardcoded same-length discard mask for scripts, styles, textareas, and comments. It is not configurable and is distinct from author-facing preservation.
Inline phrasing whitespace preservationCSS Text Level 3Spaces between elements inline by default (INLINE_TAGS: span, a, code, ins, svg, video, etc., custom elements, SVG <tspan> and <textPath>) are kept as one space; block-level whitespace collapses. Only spaces, tabs, and line breaks collapse: U+00A0 and other Unicode spaces, attribute values, and <pre>, <textarea>, <script>, and <style> content are kept. CSS is not read, so white-space: pre on another element is not detected.
<meta> tag preservationWHATWG HTML §4.2.5Standard metadata attributes on <meta> tags (e.g. name="viewport", name="description") are shielded from attribute scoping.
Exclusive <details name> groupsWHATWG HTML §4.11.1name ships as written (scoping.attributes.name is off by default), so <details name> groups every <details> in the document with that name, as the browser defines it: one item per component instance makes one accordion. For a group per instance, bind the name to a prop (<details data-bascik-attr-name="group">). See Component Best Practices.
Declarative shadow DOMWHATWG HTML §4.12.3<template shadowrootmode="open"> in a component passes through as markup. Bascik does not scope inside a shadow root, which isolates its own styles.
Custom elements defined in scriptWHATWG HTML §4.13Real custom elements from a library or your own customElements.define work alongside Bascik components. List their tags in components.external so they are not reported as unresolved components. Their shadow roots are not scoped by Bascik.

ID References

When an id declaration is scoped, Bascik rewrites references that resolve to that declaration in the same component. Unresolved references remain byte-identical so components can still target literal page-level IDs.

ReferenceStatusNotes
for on <label>Rewritten as one ID so label activation continues to focus the scoped control.
form, list, popovertarget, commandforEach single-ID reference is rewritten when its target is declared locally.
aria-activedescendant, aria-errormessageSingle-ID ARIA references resolve locally.
aria-labelledby, aria-describedby, aria-controls, aria-owns, aria-flowto, aria-detailsSpace-separated tokens resolve independently; nonlocal tokens remain unchanged.
itemrefSpace-separated microdata item IDs resolve independently.
headers on <td> and <th>Space-separated header IDs resolve independently.
for on <output>Treated as a space-separated ID list, unlike the single-ID <label for>.
Fragment links on <a> and <area>Fragment-only values such as href="#section" resolve locally. Bare hashes and other-document URLs remain unchanged.
SVG href and xlink:href references (<use>, <textPath>, gradients, …)Fragment-only references to local SVG IDs are rewritten on <use>, <textPath>, <mpath>, <feImage>, <pattern>, <linearGradient>, <radialGradient>, <filter>, <animate>, <animateMotion>, <animateTransform> and <set>, and xlink:href on an SVG <a>. Undeclared IDs and URLs with a path (pic.svg#x) stay as written.
SVG presentation attributesfill, stroke, mask, clip-path, filter, marker-start, marker-mid, and marker-end rewrite local url(#id) fragments.
Inline style attributesLocal url(#id) fragments are rewritten. Component <style> blocks and stylesheets are covered separately by CSS scoping.
CSS url(#id) fragmentsLocal fragments in component stylesheets and <style> blocks are rewritten for properties including fill, stroke, clip-path, mask, filter, and marker properties. Components with resolvable fragments automatically emit per-instance CSS. New in 1.0 hardening: url-like text inside string literals (e.g. content: "url(#local)") is preserved byte-for-byte, and only genuine url(...) fragments are rewritten.
CSS cross-document fragmentsValues such as url(other.svg#icon) are deliberately untouched because the fragment belongs to another document. Real, remote, and data URLs also remain unchanged.
usemap on <img>Resolves against a <map name>, not an ID. Ships as written by default; with scoping.attributes.name on, it follows a scoped <map name> in the same component.
Cross-component ID referencesIDs are scoped per instance, so references cannot resolve safely across component boundaries at build time. They remain unchanged.
Preserved subtreesReferences and declarations inside scoping.preserve tags or data-bascik-preserve subtrees remain literal. A link inside a preserved subtree is never rewritten, even to a scoped target elsewhere in the component, so a link and its target must be both preserved or both scoped. A scoped link to a preserved target keeps working.

Server & API Features

CapabilityStatusNotes
WHATWG Request / Response handlersStandard handler signature (request: Request, context) => Response | Promise<Response> in src/api/*.ts.
Supported HTTP methodsGET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD.
Automatic Allow headers405 responses automatically list exported methods in Allow.
Derived HEADHEAD derives automatically from GET when unexported, returning headers with empty body.
Auto OPTIONSAuto-responds 204 with Allow header.
Multiple Set-Cookie headersPreserved via Headers.getSetCookie() without comma-flattening.
Streaming responsesWHATWG ReadableStream bodies stream directly to clients.
Dynamic API route parametersExtracted from [param] path segments into context.params. Each value is one raw request segment, decoded once; an encoded / or \ in it returns 400 instead of forging a segment.
Catch-all API routes ([...path].ts)Required catch-all: one or more remaining segments as a decoded string[]. Static beats [param] beats catch-all. Unsafe captures return 400; single-dot and hidden segments are blocked before matching with 404. API routes only; same behavior on --server, HTTP/2, and the Cloudflare adapter.
Client IP resolutionForwarded client IP provided via context.remoteIp, respecting http.trustProxy.
Request body streamingStandard WHATWG request.body stream with duplex: 'half' support.
Streaming body size limithttp.maxBodySize counts bytes on the fly (defaults to 1 MB) and answers 413 without buffering.
Handler execution timeouthttp.apiTimeout triggers 504 and cooperative AbortSignal for async work.
Error information protectionThrown handler errors return generic 500 without leaking stack traces or source paths.
Header injection protectionCR and LF in handler-supplied header values are rejected.
Directive scripts printed by a build scriptA data-bascik-build, data-bascik-build-once, data-bascik-server, data-bascik-stream, or data-bascik-routes script in build script output is removed with a warning, so HTML from a CMS or an API cannot run code at build or request time. Ordinary scripts and handlers in printed HTML are not removed; sanitize it.
Large dynamic route dataAny size the routes script prints. Over 32 KB, BASCIK_ROUTE is delivered through a temporary file, avoiding the OS environment limit (spawn E2BIG).
Middleware chains (_middleware.ts)Not supported by design. Handlers compose plain functions directly.
Built-in schema validationNot supported by design. Validate payloads using standard libraries (e.g. Zod, Valibot).
Automatic response compression on API routesDeliberately omitted to prevent BREACH attacks on sensitive dynamic data. Handlers set Content-Encoding manually if needed.
Automatic CORS headersNot injected by default. Handlers return explicit Access-Control-* headers when required.

Request-Time Component Rendering

A data-bascik-server or data-bascik-stream script can return component tags, and Bascik renders them per request with the compiler's own expansion loop. The build records every component in .bascik/dist/components.json when the site has any server or stream script; a static-only site gets no manifest and no change. The worked example is in Returning component tags, and the pass-through demo shows a misspelled tag and an escaped tag sent as written. The ✕ rows are the reasons bascik --check prints under "Components a server or stream script cannot return"; a component with one of them is left unexpanded, never rendered wrong.

See it live on Cloudflare Workers. The book catalog renders the <book-card> tags a server script returns, including a card with its own server script, and the streamed catalog streams them in batches.

CapabilityStatusNotes
Component tags in server and stream script outputRendered at request time with the component's markup, props, named and default slots, attribute inheritance, nested components, and scoped CSS, from a template the build recorded, so the result matches what the build renders for the same tag. Works on the dev server, bascik --server, and the Cloudflare adapter, with byte-identical output on Node and workerd. Each instance gets its own id (r followed by 7 hex digits), never one a build id can take, and the same id for the same route and output on every request. A custom adapter must render SiteGraph.components and return components: true, or the build warns that the tags reach clients as written.
Server and stream scripts inside a returned componentRun once per rendered instance with the same request, context, and signal, and with what is left of the page script's scripts.timeout. They receive no props from the usage tag. A nested stream script resolves with its parent: its output is part of the parent's slot. Their output may return components too, up to 4 levels deep and 64 nested scripts per response. Errors follow scripts.onServerScriptError as for page scripts.
Component CSS from script outputEmitted as a <style> block in the body, before the first instance in document order, once per response; a page with both server and stream scripts may emit it once for each. A component also used in the page's own markup gets a second, identical block. Components whose CSS embeds the instance id (url(#id), scoping.deduplicateCss: false) get one block per instance. Unscoped rules (body, *, @import) in such a component restyle the whole page. Moving the block into <head> for buffered pages is not done.
Client scripts in rendered componentsRun in place inside their instance, as in the dev server. With minify.html on they move to the end of the outermost rendered instance, not to the end of the document. Request-time instances carry no ?instance=N suffix in //# sourceURL.
Base path inside rendered componentsWith a non-root base, URLs in the component's markup, props, and slot content are rewritten as at build time. The rest of the script's output is sent as written.
Render budgetsPer script output: 1000 component instances, 4 MiB (4,194,304 characters) of rendered HTML, and rereading at most 16 times the output's length plus 33,554,432 characters (tags nested deep around long text, or left unclosed ahead of a closing tag of the same name, are reread once per nested tag). Per response: 64 nested component scripts, 4 levels deep. Over a budget the script fails with a RequestRenderError naming the route and the component file, and scripts.onServerScriptError decides the response as for a script that throws: under 'error' a 500 for a server script and a logged, empty slot for a stream script; 'warn' logs it and leaves an empty slot; 'ignore' leaves an empty slot without a log. A failed during <stage> render failure is decided the same way. Output with no component tag in it is never measured.
Tags that are not renderedA tag with no component file (a typo or a third-party element), an escaped tag (&lt;book-card&gt;), and a tag inside <script>, <style>, <textarea>, a comment, or an attribute value are sent as written. The dev server logs a typo or an unsupported component once per route and tag; names in components.external are never logged. Directive scripts in script output never run on the server.
Escaped visitor values in props, bindings, and inherited attributesA prop is decoded once and escaped again where it lands, so it is text or one attribute value. A receiver on a <script> or <style> is never filled. A prop bound to a URL attribute becomes about:invalid when it holds a script URL such as javascript:, or a data: URL (image slots keep data:image/...), the rule html from @bascik/bascik/html applies too; any other URL, another site's included, is kept as the browser takes it (see Loading scripts and styles from other sites); a prop is never bound to on*, srcdoc, <meta http-equiv>, or an SVG animation's attributeName, nor to a style that runs script or imports CSS. An inherited value has the quote character it is written in encoded and is otherwise kept as written, with one exception described in Attributes That Become URLs on the Root. Bascik warns, naming the component file, for each refusal. The same rules apply to a tag written in a page at build time. See Security.
Content Security Policy hashesWith generate.cspHashes: true, the csp-hashes.json entry of every route with a server or stream script also lists the style hash and first-instance script hashes of every component that can be rendered at request time, since any of them can be returned. Components with per-instance CSS, or client scripts that reference their own scoped ids or names, cannot be covered by a hash-only policy; bascik --check lists them under "Components without stable CSP hashes at request time". See Content Security Policy.
Prop bound to a scoped attribute (attr-binding-to-scoped-attribute)A data-bascik-attr-* binding onto id, class, style, fill, stroke, mask, clip-path, filter, or marker-* (and onto name or usemap when scoping.attributes.name is on), or onto for, headers, href, or xlink:href on an element whose value the build does not rewrite (for outside <label> and <output>, headers outside <td> and <th>, href outside <a>, <area>, and <use>). The build injects these values before scoping, which a template cannot repeat. Restructure: bind a data-* attribute instead (data-bascik-attr-data-tone="tone") and style it from a scoped class (.card[data-tone="sale"]), or return one component per variant.
Server or stream script that depends on the instance (instance-scoped-server-script)The script's body embeds the instance id (a string that names one of the component's scoped ids or form field names), or the component has such a script and its classes are scoped per instance (its stylesheet uses url(#id), or scoping.deduplicateCss: false), so one staged script cannot serve every instance. A url(#id) component with no server or stream script renders, with one style block per instance. Restructure: refer to classes rather than ids and name values in the script's strings, move id-dependent markup out of the script into the template, or move the url(#id) graphic into a child component or a global stylesheet.
Build script that reads the page (build-script-reads-page-context)A data-bascik-build script in the component read BASCIK_PAGE_PATH, BASCIK_PAGE_FILE, or BASCIK_ROUTE. The template is recorded once, with no page, so its output cannot differ per page. Restructure: use data-bascik-build-once for page-independent output, pass the page-dependent value in as a prop, or compute it in a server script from request.url.
Bound src on a directive script (attr-src-on-directive-script)data-bascik-attr-src on a data-bascik-build, data-bascik-build-once, data-bascik-server, data-bascik-stream, or data-bascik-routes script decides the module per usage, which a template cannot hold. Restructure: give the script a fixed src, or make one component per module.
Component the template recorder cannot reproduce (scoping-template-unavailable)Scoping planned a CSS @import or wrote to the console, a custom minify.js split a scoped name, a build script printed data-bascik-prop-*, data-bascik-slot, data-bascik-attr-*, or data-bascik-preserve markup, or printed a new root element that would take the usage's inherited attributes. The --check line says which. Restructure: move the @import to a global stylesheet, keep prop and slot receivers in the template instead of printing them, keep the root element in the template, or use a minifier that keeps names whole.

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
Escaped or non-ASCII class name.md\:flex {}, .w-1\/2 {}, .\31 0 {}, .café {}The escape is decoded to find the class it names (md:flex, w-1/2, 10), so the matching class attribute token is scoped to the same name; the selector keeps its escape. A class the CSS does not declare stays global, so class="md" is not scoped by a .md\:flex rule. The whitespace that ends a hex escape belongs to the name: .a\1 b is one class, never .a\1 with a descendant b. A CRLF there counts as one newline, as CSS reads it, so a stylesheet saved with Windows line endings scopes the same classes.
Class named b_el_… or b_id_….b_el_p {}, .b_id_btn {}Reserved: Bascik names the class it generates for an element selector b_el_<component>_<tag> and for an #id selector b_id_<component>_<id>. In a component stylesheet such a class is scoped like any other (bascik__<component>__b_el_p), but the same name used as a global class, in a page's class= attribute or a site stylesheet, can match a generated class and style a component's elements. The css-reserved-class-prefix lint rule checks component stylesheets only and warns there, so the habit is caught early; a site stylesheet or a page class= attribute that uses the generated shape is not linted. Rename the class (.el-p, .item__p). Authored .el__p and .id__btn are ordinary classes: they compile to bascik__<component>__el__p, which no element or #id rule produces. See The Naming Scheme.
Standalone element selectorp {}Converted to the generated class b_el_<component>_<tag> (hashed under minify.identifiers) and injected only on elements with the exact tag name in component HTML, including indented selectors inside at-rules such as @media and inline <style> blocks. A selector for header does not match a custom element such as <header-link>, and hyphenated type selectors such as code-block are left unchanged rather than split at the hyphen. Note on specificity: converting element selectors (0, 0, 1) to class selectors (0, 1, 0) is an intentional zero-runtime build-time transformation that guarantees robust class isolation across components without requiring runtime shadow DOM. Earlier releases named the class bascik__<component>__el__<tag>, which an authored .el__p class could also produce; readable names changed for every site, while hashed names keep their b plus 11 character shape.
Element pseudo-classp:hover {}Element converted to class; pseudo-class preserved: .b_el_<component>_p:hover {}
Element pseudo-elementp::before {}Element converted to class; pseudo-element preserved: .b_el_<component>_p::before {}
@keyframes name@keyframes spin {}Name scoped; animation: and animation-name: references updated to match, including a declaration that follows a nested rule. Only whole names match, so a never rewrites part of a-b or of another scoped name. In shorthands, keywords fill available timing, direction, fill-mode, iteration, and play-state slots before being treated as names. Only linear(), cubic-bezier(), and steps() consume the easing slot; calculated durations do not. Unitless numeric iteration counts, including scientific notation, consume the iteration slot without confusing time units. Prefer explicit animation-name when a name supplied through CSS variables cannot be identified statically.
@keyframes stops40% {}, 0%, 100% {}, from {}Stop selectors are left as written, including percentage lists split across lines, so the browser keeps every stop.
@media query@media (max-width: 600px) {}Media condition untouched, including comma-separated media lists (screen, print) and preludes wrapped across lines (and, not or a media type at the start of a line). Class names and element selectors inside are scoped normally.
@supports@supports (display: grid) { .foo {} }Condition untouched, including one wrapped across lines; class names inside @supports blocks are scoped normally.
@layer@layer base { .foo {} }Layer names are scoped in declaration blocks, single-name or comma-list ordering statements, and the layer(name) of a remote @import, including leading-hyphen names such as --utils. Each segment of a dotted name is scoped, so @layer a.b and @layer a { @layer b {} } name the same sublayer. Only whole names match, so a never rewrites part of a-b or of another scoped name.
@container@container sidebar (min-width: …) {}Every container name declared via container-name: or the container: shorthand is scoped (container-name: a b scopes both); @container name (…) queries, including each query of a comma-separated list, updated to match. Only whole names match, so a never rewrites part of a-b or of another scoped name. Unnamed queries, none and CSS-wide keywords untouched.
CSS custom properties--brand: #d3ff8d / var(--brand)Declarations and all var() references in the same file scoped together, and in the template's style attributes (style="--size: 2rem"). var(--prop, fallback) keeps the fallback and scopes the name. Only whole names match, so --a never rewrites part of --a-b or of another scoped name. A -- inside a name is not a declaration: the BEM class .btn--primary:hover stays a class, and a global var(--primary) stays global. A style on a custom element tag (a page's usage tag, or a child component tag inside a template) is passed through as written.
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. An element after a combinator (.a > p, .a>p) is scoped with or without surrounding spaces.
: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>. A media attribute other than all (including screen) becomes an @media wrapper, so the rules still apply only where the original <style> would.
CSS #id selector#btn {}Converted to a component-scoped class selector (.b_id_<component>_btn {}, shared by every instance because the id itself is renamed per instance) using a context-aware lookahead that correctly distinguishes selector position from hex color values. The generated class is injected onto the HTML element across development and production builds with identifier minification, inside @media and paired stylesheets. Specificity drops from (0,1,0,0) to (0,0,1,0). String literals, comments, and attribute-selector values (a[href="#tab"]) are never treated as ID selectors; their contents are preserved byte-for-byte. Two components whose rules would generate the same class (an underscore in a component name or an id: site-nav_x with #btn and site-nav with #x_btn both give b_id_site-nav_x_btn) stop the build with an error naming both rules. Earlier releases named the class bascik__<component>__id__<id>; readable names changed for every site, while hashed names keep their shape.
[id] / [id="…"] attribute selector[id] {}Ids are renamed per instance, so an attribute selector on them cannot be scoped without DOM wrapping. Stripped wherever it appears in a selector (.a[id], .a:not([id]), :is(.a, [id="x"])), inside @media and @supports too. Only that item of a comma-separated list is dropped (commas inside :is() are not separators), and a dropped rule takes its nested blocks with it. [id-ref] and [idx] are other attributes and are kept. bascik --check and the editor report every stripped form.
Attribute selector[data-foo="bar"] {}Passed through untouched, not scoped. A standalone selector, including [data-state]:not(.disabled), can apply globally because an exclusion class does not anchor it. Use a positive scoped class such as .card[data-state] or [data-state] .card. Attribute-selector values containing # (e.g. a[href="#tab"]) or . in quoted and unquoted forms (a[href$=.pdf]) remain literal and are not rewritten as selector syntax.
Compound / descendant element selectorsdiv p {}, .card p {}, #panel p {}, .list > li {}Every element name in the chain is converted to a component-scoped class and injected onto the matching elements in the component's own template, whether anchored by an element, a class, :is(.a, .b) / :where(.a, .b), or a #id selector. Descendant, child (>), adjacent (+), and general sibling (~) combinators work, as do longer chains with pseudo-classes and the universal selector (nav a, #panel > h3 + p, p + p, ul li *, ul li:first-child img, ul a:hover img). Scoped in dev and production, including with identifier minification. The rules never match page markup or a child component's root element; see Styling a Child Component from Its Parent. Element names inside :is(), :where(), and :has() are a separate case, listed below.
Parent element selector vs. child component rootnav a {} with <nav-link> inside <nav>A parent's element selector matches only elements written in the parent's own template, never a child component's root element. Components are isolated by design. To style a child's root from the parent, put a class that the parent's CSS defines on the child's usage tag (<nav-link class="item">); Bascik scopes it to the parent and merges it onto the child's root. See Attribute Inheritance.
Element selector vs. elements a script createstd {} with document.createElement('td')An element selector is scoped by adding a generated class to the elements written in the component's template, including the content of a <template>. An element a script creates has no such class, so the rule never matches it, with no warning. Clone a <template> that the component contains, or give the element a class the stylesheet defines and set it with classList.add or className. See From Vue.
Selector aimed at a preserved elementcode {}, .note code {}, .inline {} on <code class="inline">, querySelector('.inline')A scoping.preserve tag, a data-bascik-preserve element, and everything inside them keep their literal class and id, while the component's selectors are still renamed, so a rule or class query that only those elements answer never matches. The build, the dev server, the editor, and bascik --check warn (css-preserved-target, js-preserved-target) at the selector. Put the rule in a global stylesheet, or take the element out of scoping.preserve or its data-bascik-preserve directive. A class a scoped element also carries still works.
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) {}. Anchored on a scoped class and a descendant combinator (.markdown :is(p, ul, ol)), the unconverted names match every descendant of that element, slotted content included, which is the usual intent for rendered Markdown, so css-is-element-names does not flag that form.
Element selector vs. slotted content.prose h2 {} with <h2> passed through a slot.prose h2 becomes .prose .<scoped h2>, and the scoped class is added only to h2 elements in the component's own template. Content passed through a slot (including rendered Markdown a build script prints inside the usage tag) keeps plain tags, so the rule does not match it. Write .prose :is(h2) {}: Bascik leaves element names inside :is() as written, the wrapper class stays scoped, and specificity is the same as .prose h2. See Markdown.
CSS nesting, class selectors& .child {}Class selectors inside nesting are scoped normally.
CSS nesting, element selectors& p {}, & > h2 {}, > h2 {}, p {}Fully supported for explicit nesting (& p {}, & > h2 {}, &>h2 {}, & + li {}, & ~ span {}), & with a pseudo-class or attribute before the element (&:hover p {}, &[open] > summary {}), and 2023 relaxed direct nesting (> h2 {}, + li {}, ~ span {}, direct nested element selectors without &).
@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 deduplicationComponent CSS is normally injected once per type. Components containing resolvable url(#id) references emit per-instance CSS so each stylesheet targets that instance's scoped ID.
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. A space before : that is a descendant combinator (.a :hover, .a :is(h2)) is kept; only declaration colons are tightened. The space that ends a hex escape is kept apart from a combinator after it (.\31 p). A CRLF or a lone CR after an escape is one newline, so .\31 and a CRLF before p minify to .\31 p, as with LF.
minify.htmlMinifies HTML output. Style (and pre/textarea/script) raw text is shielded first, so CSS CDO/CDC tokens and comment-like strings inside those elements survive while ordinary document comments are stripped.
CommentsOrdinary HTML comments (<!-- ... -->) are stripped from the page head and body. Comment-like content inside <pre>, <textarea>, <style>, and scripts is preserved. The development server and minify.html: false keep comments; a component tag named inside a comment, in a page or a component template, stays text and is never expanded.
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.
@importLocal file imports (@import "./file.css") are inlined recursively at build time and scoped to the component. Remote URLs are kept, and are hoisted to the top of the stylesheet, after any top-level @layer a, b; statement before them; layer(name) is scoped. A missing file becomes a not found comment and later rules are kept. An @import missing its ; stays where it is, as the browser reads it (one invalid rule), instead of moving part of the next rule up with it. @import text in comments and strings is ignored. In dev, editing an imported file rebuilds every page whose components import it.
@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. Only whole names match, so a never rewrites part of a-b or of another scoped name.
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. Only whole names match, and 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 whole anchor names declared in the component's own CSS are scoped, and external anchor references are left untouched.

Names Shared by Every Instance

Container names, layer names, anchor names, view transition names, keyframes, and custom properties declared in component CSS are scoped per component type: every instance of a component shares one name, and no other component can see it. That keeps components apart, but it changes how these features combine.

PatternExampleStatusNotes
Child component querying a parent's named container@container sidebar (…) in a child of side-barThe parent's sidebar is renamed to the parent, so the child's query never matches. Use an unnamed query, @container (min-width: 30rem), which targets the nearest container, or declare the named container in a global stylesheet.
Global layer order including component layers@layer reset, components; in a page with @layer components {} in a componentThe component's layer becomes its own private layer and is not placed by the global statement. Put base styles in layers in the global stylesheet and leave component CSS unlayered; unlayered rules win over layered ones.
anchor-name with several instances on a page.trigger { anchor-name: --menu; }Every instance declares the same scoped name, so each positioned element anchors to the last one. Use the implicit anchor of popover and popovertarget, or set the name per instance from the script: el.style.setProperty('anchor-name', `--${el.id}`).
view-transition-name with several instances on a page.card { view-transition-name: card; }Two elements with one name abort the transition. Use it on components that appear once per page, or name instances from the script: el.style.setProperty('view-transition-name', el.id). The same component on two pages shares its name, so it morphs across a navigation.
Site-wide tokens declared in a component:root { --brand: … } in component CSSThe property is renamed to the component, so no other component sees it. Declare tokens in a global stylesheet and only read them in components.

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.
JavaScript literal preservation in minify.js (built-in)Strings, nested template literals (including ${} expressions that contain templates, strings, regexes, and comments), and regex literals are kept byte-for-byte. Inline script bodies are read from original HTML spans, so comment-looking text and raw-text tags inside strings stay unchanged. Prefix ++ and -- may have a next-line operand; only postfix updates end a completed expression. A / after ), ], a quote, a backtick, or a postfix ++/-- (count++ / 2) is division, except after an if, while, for, or with header, where it starts a regex statement. A / after } is treated as a possible regex only when it closes on the same line. A / after else or an expression keyword such as return or typeof starts a regex. A hashbang (#!) first line is kept as written, line break included, so the script after it is not commented out.
Braceless if/while/for body on its own line in minify.js (built-in)if (a === "x") followed by a body on the next line, as Prettier writes long conditions, stays one statement: no ; is inserted after the header, even when the condition contains a string, template, or regex literal, so a following else still parses.
Semicolon-free scripts in minify.js (built-in)A line break becomes a ; exactly where JavaScript's automatic semicolon insertion ends the statement, so the minified script runs the same program as the unminified one in development. A line starting with ( or [ continues the value before it as a call or index (foo()\n(bar) is foo()(bar)), except after return, break, continue, throw, yield, a postfix ++/--, or an arrow function body. A line starting with a string or regex literal after a value starts a new statement (const a = 1\n"use strict"); a template literal after a value stays a tagged template. A line ending in typeof, in, instanceof, new, case, or another operator keyword continues on the next line.
JavaScript MIME type minificationInline scripts with no type or text/javascript, module, application/javascript, text/ecmascript, and application/ecmascript are minified when minify.js is enabled. External scripts are left empty and unchanged.
<script type="application/json"> (and any non-JS type)Left completely untouched.
Companion script references (<script src="nav-bar.js">)Only a relative src names a companion. It is resolved against the component's folder, and a bare file name such as nav-bar.js or ./nav-bar.js matches a companion anywhere in that folder. A URL (https://cdn.example.com/nav-bar.js, //cdn.example.com/nav-bar.js) or a root-absolute path (/vendor/nav-bar.js) is another file and is left as written for the browser to load, even when its file name matches a companion.
Referenced .ts / .mts companion scripts (<script src="counter.ts">)Erasable TypeScript is stripped automatically when the companion is inlined, with no configuration and before any minify.js step. Line structure is preserved so //# sourceURL line numbers stay accurate. Non-erasable syntax (enum, parameter properties, runtime namespaces) fails the build with the file path under the default compiler. A directive script's src= (data-bascik-build, data-bascik-server, and the others) is not a companion: it is never inlined, and a scripts.typescript function compiles it with runtime: "server" only.
scripts.typescript compiler selectiontrue (default) is Node strip-only mode; a function is a bring-your-own compiler (esbuild, swc, tsc) receiving (code, { sourcePath, kind }) and returning JavaScript, which unlocks enum, decorators, and downleveling; false disables the transform so pre-compiled output passes through untouched. Applies to both browser paths, in dev and build, on the main thread and in workers. A compiler that throws or returns non-JavaScript fails the build with the source path.
<script type="text/typescript"> (inline, page or component)Stripped to JavaScript and emitted as an ordinary <script> (the type attribute is removed), then scoped and optionally minified like any other inline script. Never applied to data-bascik-* directive scripts.
TypeScript syntax in an unmarked <script>Left byte-for-byte on the JavaScript path because browsers do not execute TypeScript there. When Bascik can positively identify erasable TypeScript in such a block, type="module" and other JavaScript types included, it logs a warning naming the file and the fix (type="text/typescript" or a companion .ts). Valid JavaScript is never rewritten or warned about.
Module-shaped .ts companion or type="text/typescript" block targeting a classic scriptStripping types never removes import/export. A companion .ts inlined into a <script src="..."> with no type="module", or any type="text/typescript" block (always classic), fails the build with an actionable error naming the file if the stripped code still contains a static import/export declaration, since a classic script's IIFE wrapper would throw a SyntaxError in the browser. Fix by adding type="module" to the referencing tag, or by bundling the file first so no import/export remains. Dynamic import() and import.meta are unaffected.
<script data-bascik-build>Executed at transpile time as a Node.js ESM module. The script exports a default callable function returning an HTML string, which replaces the tag. Runs in both dev and build modes. Console methods log to the terminal for debugging. Top-level import and await are supported.
<script data-bascik-routes>Executed at build time inside bracket templates (e.g. [slug].html). The script exports a default callable function returning an array of { params, data? } objects to expand the template into concrete static HTML pages.
<script data-bascik-server>Executed on the server at request time in-process as a Node.js ESM module via ScriptRegistry. Returned markup replaces the tag dynamically. Receives a WHATWG Request, a { remoteIp } context, and { signal }, the same signature as API routes. Top-level import and await are supported. No helpers are injected or exported. Buffered: all server scripts resolve before the response is sent. A returned array is joined with no separator; a plain object, a Response, an Error, a function, a symbol or an array holding a Promise is refused: a located error in dev, an empty slot and one warning in production.
<script data-bascik-stream>Same authoring model as data-bascik-server. Receives a WHATWG Request, a { remoteIp } context, and { signal }, the same signature as API routes. Top-level import and await are supported. No helpers are injected or exported. The server commits headers and every static byte before the tag without waiting for it, then writes its output when it resolves, in document order, as a chunked response. All server scripts on the page resolve before the first byte. A stream failure can never be a 500; the slot is emitted empty. Mutually exclusive with server, build, and routes.
Class names inside data-bascik-server and data-bascik-stream script source within a componentScoped at build time like any other string in the component, so a class in the returned markup matches the component's scoped CSS in both readable and minified-identifier modes. Verified: class="result" becomes the same hashed name used in the emitted :has(> .result) selector. At request time the returned markup also gets the component's element classes, so element selectors such as p { } match it, and every class its stylesheet defines is mapped to the scoped name, including classes built while the script runs (string concatenation, values from a database). Classes the stylesheet does not define stay global, and markup inside comments, <script>, <style>, <textarea>, and attribute values is left as written.
Markup printed by a component's data-bascik-build scriptComponent build scripts run for each use in the page, after the template is scoped, so Bascik applies the component's class and element mapping to their output. That includes markup returned by an imported helper and class names computed while the script runs. Classes the component's stylesheet does not define stay global. Markup inside comments, <script>, <style>, <textarea>, and attribute values is left as written.
JavaScript syntax errors in component scriptsA component's inline <script> and companion .js/.mjs file are parsed (never run) at build time; one that does not parse stops the build with the file, line, and column. type="module" uses the module grammar, and an untyped script fails only when it parses neither as a classic script nor as a module. Scripts in pages are not parsed.
Relative import or import() in a component JavaScript scriptA component's inline <script> and companion .js/.mjs file are inlined into each page and not bundled, so import("./extra.js") or import x from "../lib/x.js" resolves against the page URL and the browser's request fails. The build warns with the file, line, and specifier (and says when the file beside the component is not published). Put the file in public/ and import its URL, such as "/js/extra.js", or bundle the script.
DevTools //# sourceURL directivesAutomatically appended to every client, build, and server <script> block. Client scripts are padded so DevTools line numbers match the component file, including when markup or <style> blocks sit above the script (unless minify.js joins the lines). Companion scripts are numbered from line 1 of their own file. The second and later instances of a component on a page get ?instance=N, so each instance is its own DevTools file. The directive is held back from minify.js (built-in or custom) and re-attached on its own line afterwards, so a minifier that drops comments or collapses the IIFE to one line can neither lose it nor glue it onto })();.
Stack trace remapping (stack-trace.ts)Build, routes, server, and stream script errors name the source file, line and column, also in components whose markup and <style> are moved before the script runs, after an import Bascik rewrote on the same line, in an eval frame's origin, and under a symlinked node_modules. Build and routes syntax errors show the line and a caret; server and stream syntax errors, which Node reports with no line, name the file and the line the script starts on. src= scripts and imported helpers keep their own paths and lines. Each error prints once, without Node.js internals or Bascik's own runner and request-handling frames.
Literal component tags inside <script>, <style>, <textarea>, or attribute valuesTreated as text, never resolved into components. Safe to mention opening and closing tags like <my-card> in JSON-LD strings, inline scripts, code examples, or HTML attribute values. An attribute value is read where the browser reads one: quotes inside an end tag's name (a typo such as </p"='x>) open none, so a component after that tag is still found.
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")classSingle and space-separated multi-class string arguments are rewritten. Scoped to the component type (document-wide across instances unless scoping.deduplicateCss is false); use getElementById or root.querySelector for per-instance lookup. getElementsByClassName("my-cls")[0] and .item(0) raise the js-document-class-query warning.
document.querySelector with .classquerySelector(".my-cls")classSingle-token class selector only. Scoped to the component type (returns the first instance's element when scoping.deduplicateCss is true); use getElementById or root.querySelector for per-instance lookup. Any class selector without an #id (".card .title", "button.btn", backtick strings) raises the js-document-class-query warning; a selector that includes a scoped #id is per-instance and is not flagged.
document.querySelectorAll with .classquerySelectorAll(".my-cls")classSingle-token class selector only. Scoped to the component type (matches all instances on the page when scoping.deduplicateCss is true); use root.querySelectorAll rooted on a per-instance id to query within one instance.
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. Every token of a compound is rewritten too: .card.active, button.btn.primary, div#panel, and #panel.open. A dot inside an attribute selector value ([data-x=a.b]) is not a class. An escaped name is one token: "#my\\.id" names the id my.id and becomes the scoped id, escaped the same way, matching getElementById("my.id") and a #my\.id rule in the component's CSS.
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")classSingle and space-separated multi-class string 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, and id and name attributes in them are scoped too. Only string and template literals are read as markup: a < in script code (if (count<max && ok) { const id = next() }) is an operator, so the code around it is left as written. Quotes inside comments and regex literals open no string: a / reads as a regex or a division by the minifier's rule, including after an if, while, for, or with header, and a string continued over a backslash and a CRLF line break stays one string.
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.
document.getElementById with a variablegetElementById(targetId)idOnly a static string literal (getElementById("my-id")) is rewritten; a variable, property, string concatenation ("pre-" + n), or template literal (even one without an interpolation) is passed through and will not match the scoped id. Custom data-* attribute values are not rewritten, so an id stored in one stays unscoped. Standard ID-reference attributes (for, form, list, popovertarget, commandfor, aria-controls, aria-labelledby, aria-describedby, href="#my-id") are rewritten, so read the target from one of those (getElementById(btn.getAttribute("popovertarget")), getElementById(label.htmlFor), or getElementById(link.getAttribute("href").slice(1)) is not flagged), or traverse from a statically looked-up element (btn.closest(…)). Raises the js-dynamic-id-lookup warning.
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

name ships as written

name is not scoped by default, so new FormData(form), form.elements.email and a form post all use the names you wrote, and radios with one name and no <form> of their own are one group across every component instance on the page. Give a component that holds a whole radio group its own <form>, or a name through a prop. With scoping.attributes.name on, names become per-instance values like bascik__comp__a1b2c3__username, and FormData keys change with them. See Component Best Practices.


HTTP Protocols, Caching & Security Standards

When running Bascik's built-in HTTP/1.1 and HTTP/2 production server (bascik --server) or dev server (bascik), responses strictly adhere to modern IETF network and caching standards:

Feature / StandardProtocol AuthorityStatusImplementation Details
HTTP/2 & ALPN NegotiationIETF RFC 9113 / RFC 9112Uses http2.createSecureServer({ allowHTTP1: true }) for ALPN negotiation with automatic HTTP/1.1 fallback. Handles HTTP/2 pseudo-headers (:status, :path, :method, :scheme).
Strong & Content-Hash ETagsIETF RFC 9110 §8.8.3Generates strong SHA-256 content-hash ETags for both HTML pages and static assets, with distinct encoding-specific ETags ("hash-br").
Conditional GET & 304 ResponsesIETF RFC 9110 §13.1.1Evaluates incoming If-None-Match request headers against generated ETags and returns 304 Not Modified with zero response body.
Content Negotiation & VaryIETF RFC 9110 §12.5.5Sends Vary: Accept-Encoding and negotiates Brotli or gzip for compressible assets. Precompressed .br/.gz sidecars are served only when stamped with matching .bmeta provenance, which bascik --build emits under http.precompress: true; otherwise compression runs on demand. Assets above 2 MiB are streamed as identity with a weak validator.
Method GuardingIETF RFC 9110 §9.1Enforces GET and HEAD requests only. Rejects other methods with 405 Method Not Allowed and sends Allow: GET, HEAD.
Strict-Transport-Security (HSTS)IETF RFC 6797Sends Strict-Transport-Security: max-age=31536000 over HTTPS: on a TLS server, an https: request on the Worker, or behind a trusted proxy (http.trustProxy with x-forwarded-proto: https). includeSubDomains and preload are opt-in, and the header can be changed or turned off with security.hsts.
Standard Security HeadersOWASP / IETF GuidelinesEvery response sends X-Content-Type-Options: nosniff, X-Frame-Options: SAMEORIGIN, Referrer-Policy: strict-origin-when-cross-origin, Cross-Origin-Opener-Policy: same-origin-allow-popups, and Cross-Origin-Resource-Policy: cross-origin; each can be changed or removed with security.headers.
Modern JavaScript MIME TypesIETF RFC 9239Serves JavaScript files (.js, .mjs, .cjs) as text/javascript; charset=utf-8 (application/javascript is legacy and deprecated per RFC 9239).
Text MIME CharsetsIANA Media TypesAll text and data MIME types (text/html, text/css, application/json, application/geo+json) include explicit charset=utf-8.
Server-Sent Events (SSE)WHATWG EventSourceEndpoint /bascik-live-reload sends Content-Type: text/event-stream; charset=utf-8, Cache-Control: no-cache, and standard double-newline frames (data: ...\n\n). Injected client includes automatic reconnect and focus re-sync.
Path Traversal SecurityWHATWG URL StandardDecodes request paths and strictly enforces boundary containment within dist/. Rejects /../ path segments with 400 Bad Request.
Sitemaps ProtocolSitemaps.org / W3CGenerates valid XML sitemaps with namespace http://www.sitemaps.org/schemas/sitemap/0.9, escaping XML metacharacters and normalizing canonical URLs.
Robots Exclusion ProtocolIETF RFC 9309Generates RFC 9309 compliant robots.txt pointing crawlers at the XML sitemap.