Components

Components are the core building block in Bascik. Each component is a .html file in src/components/. The file name becomes the tag name used in your pages and in other components.

Your First Component

A component is a file of vanilla HTML. Save it as src/components/hello-card.html, then write <hello-card /> in any page, as many times as you like. The file name is the tag name, so there is nothing to import or register.

At build time Bascik swaps each tag for the component's markup, and the page ships as vanilla HTML. Components can use other components the same way.

src/components/hello-card.html

One file. One tag.

Plain HTML, ready to reuse.

Bascik replaces the custom tag at build time and ships the finished markup.

html
<!-- src/pages/index.html -->
<!doctype html>
<html lang="en">
  <body>
    <hello-card />
  </body>
</html>
html
<article>
  <p>One file. One tag.</p>
  <h3>Plain HTML, ready to reuse.</h3>
  <p>
    Bascik replaces the custom tag at build time and ships the finished markup.
  </p>
</article>
Output is shown unminified (HTML, CSS, JS, and identifier names) for readability.
html
<!-- dist/index.html -->
<!doctype html>
<html lang="en">
  <body>
    <article>
      <p>One file. One tag.</p>
      <h3>Plain HTML, ready to reuse.</h3>
      <p>
        Bascik replaces the custom tag at build time and ships the finished
        markup.
      </p>
    </article>
  </body>
</html>

Add Styles in the Same File

Put a <style> block above the markup. Bascik scopes it to the component at build time, so article and .body style only hello-card, never the rest of the page, and short class names are safe. The build renames the selectors and the elements they match, and moves the CSS into the page's <head>.

More on the next page. Scoped Styles covers exactly how class names and selectors are namespaced and what CSS patterns are supported.

src/components/hello-card.html

One file. One tag.

Plain HTML, ready to reuse.

Bascik replaces the custom tag at build time and ships the finished markup.

html
<style>
  article {
    width: min(100%, 32rem);
    padding: 24px;
    background: var(--elevated);
    border: 1px solid var(--border);
    border-top: 3px solid var(--accent);
    border-radius: 6px;
  }

  .kicker {
    margin: 0 0 10px;
    color: var(--accent);
    font-family: var(--font-mono);
    font-size: 0.75rem;
    font-weight: 700;
    text-transform: uppercase;
  }

  h3 {
    margin: 0 0 8px;
    color: var(--text);
    font-size: 1.1rem;
  }

  .body {
    margin: 0;
    color: var(--text-muted);
    font-size: 0.9rem;
  }
</style>
<article>
  <p class="kicker">One file. One tag.</p>
  <h3>Plain HTML, ready to reuse.</h3>
  <p class="body">
    Bascik replaces the custom tag at build time and ships the finished markup.
  </p>
</article>
Output is shown unminified (HTML, CSS, JS, and identifier names) for readability.
html
<!-- dist/index.html -->
<!doctype html>
<html lang="en">
  <head>
    <style>
      .b_el_hello-card_article {
        width: min(100%, 32rem);
        padding: 24px;
        background: var(--elevated);
        border: 1px solid var(--border);
        border-top: 3px solid var(--accent);
        border-radius: 6px;
      }
      .bascik__hello-card__kicker {
        margin: 0 0 10px;
        color: var(--accent);
        font-family: var(--font-mono);
        font-size: 0.75rem;
        font-weight: 700;
        text-transform: uppercase;
      }
      .b_el_hello-card_h3 {
        margin: 0 0 8px;
        color: var(--text);
        font-size: 1.1rem;
      }
      .bascik__hello-card__body {
        margin: 0;
        color: var(--text-muted);
        font-size: 0.9rem;
      }
    </style>
  </head>
  <body>
    <article class="b_el_hello-card_article">
      <p class="bascik__hello-card__kicker">One file. One tag.</p>
      <h3 class="b_el_hello-card_h3">Plain HTML, ready to reuse.</h3>
      <p class="bascik__hello-card__body">
        Bascik replaces the custom tag at build time and ships the finished
        markup.
      </p>
    </article>
  </body>
</html>

Move Styles to a Companion File

When the <style> block grows, move its contents into a .css file with the same base name, beside the component. Bascik merges it automatically:

text
src/components/
  hello-card.html
  hello-card.css

hello-card.html is back to markup only:

html
<article>
  <p class="kicker">One file. One tag.</p>
  <h3>Plain HTML, ready to reuse.</h3>
  <p class="body">
    Bascik replaces the custom tag at build time and ships the finished markup.
  </p>
</article>

hello-card.css holds the rules, without the <style> tag:

css
article {
  width: min(100%, 32rem);
  padding: 24px;
  /* ...the rest of the rules from the <style> block */
}

The output is the same as with the <style> block. Which one you use is a matter of preference; Companion CSS and Script Files covers the rules for both.

Add JavaScript

Add a <script> block for interactive behavior. Use id on any element you need to target from JS and reach it with getElementById.

Styles omitted for clarity. This demo component includes a companion .css file that styles the card. The CSS is not shown in the source view so the demo stays focused on the JavaScript.

See also. Scoped JavaScript explains how Bascik rewrites selectors so multiple instances of the same component on the same page stay fully independent.

src/components/comp-toggle/

Bascik assembles components at build time and ships vanilla HTML files with no framework runtime.

html
<comp-toggle />
html
<div class="toggle-wrap">
  <p>
    Bascik assembles components at build time and ships vanilla HTML files with
    no framework runtime.
  </p>
  <div id="detail" hidden>
    <p>
      No JavaScript is added to the page. Every script in the output was written
      by you.
    </p>
  </div>
  <button id="btn" type="button">Read more</button>
</div>
js
const btn = document.getElementById("btn");
const detail = document.getElementById("detail");
btn.addEventListener("click", () => {
  detail.hidden = !detail.hidden;
  btn.textContent = detail.hidden ? "Read more" : "Show less";
});
Output is shown unminified (HTML, CSS, JS, and identifier names) for readability.
html
<div class="bascik__comp-toggle__toggle-wrap">
  <p class="b_el_comp-toggle_p">
    Bascik assembles components at build time and ships vanilla HTML files with
    no framework runtime.
  </p>
  <div id="bascik__comp-toggle__d65f1c88__detail" hidden>
    <p class="b_el_comp-toggle_p">
      No JavaScript is added to the page. Every script in the output was written
      by you.
    </p>
  </div>
  <button
    class="b_el_comp-toggle_button"
    id="bascik__comp-toggle__d65f1c88__btn"
    type="button"
  >
    Read more
  </button>
</div>
<script>
  (function () {
    const btn = document.getElementById("bascik__comp-toggle__d65f1c88__btn");
    const detail = document.getElementById(
      "bascik__comp-toggle__d65f1c88__detail",
    );
    btn.addEventListener("click", () => {
      detail.hidden = !detail.hidden;
      btn.textContent = detail.hidden ? "Read more" : "Show less";
    });
  })();
  //# sourceURL=src/components/comp-toggle/comp-toggle.html
</script>
css
.bascik__comp-toggle__toggle-wrap {
  padding: 24px;
  background: var(--elevated);
  border: 1px solid var(--border);
  border-radius: var(--r);
  max-width: 420px;
}

.bascik__comp-toggle__toggle-wrap .b_el_comp-toggle_p {
  color: var(--text-muted);
  margin: 0 0 16px;
}

.bascik__comp-toggle__toggle-wrap .b_el_comp-toggle_button {
  display: inline-block;
  padding: 8px 18px;
  background: var(--accent);
  color: #18191b;
  border: none;
  border-radius: var(--r-sm);
  font-size: 0.875rem;
  font-weight: 600;
  cursor: pointer;
}

HTML, CSS, and JavaScript Together

All three can live in a single file. Follow the component convention order: place <style> blocks above markup, then markup, then <script> blocks below markup.

src/components/comp-alert/

Scheduled maintenance Sunday, 2am–4am UTC.

html
<comp-alert />
html
<style>
  .alert {
    display: flex;
    align-items: flex-start;
    gap: 12px;
    padding: 14px 16px;
    border: 1px solid #f59e0b;
    border-radius: 8px;
    background: #fffbeb;
  }
  .alert[hidden] {
    display: none;
  }
  .alert-body {
    flex: 1;
    margin: 0;
    font-size: 0.9rem;
    color: #92400e;
  }
  .alert-close {
    background: none;
    border: none;
    cursor: pointer;
    color: #b45309;
    font-size: 1.2rem;
    line-height: 1;
    padding: 0;
  }
</style>
<div class="alert" id="alert">
  <p class="alert-body">Scheduled maintenance Sunday, 2am–4am UTC.</p>
  <button id="close" class="alert-close" type="button" aria-label="Dismiss">
    ×
  </button>
</div>
<script>
  document.getElementById("close").addEventListener("click", () => {
    document.getElementById("alert").hidden = true;
  });
</script>
Output is shown unminified (HTML, CSS, JS, and identifier names) for readability.
html
<!-- dist/index.html -->
<!DOCTYPE html>
<html lang="en">
  <head>
    <style>
      .bascik__comp-alert__alert {
        display: flex;
        align-items: flex-start;
        gap: 12px;
        padding: 14px 16px;
        border: 1px solid #f59e0b;
        border-radius: 8px;
        background: #fffbeb;
      }
      .bascik__comp-alert__alert[hidden] {
        display: none;
      }
      .bascik__comp-alert__alert-body {
        flex: 1;
        margin: 0;
        font-size: 0.9rem;
        color: #92400e;
      }
      .bascik__comp-alert__alert-close {
        background: none;
        border: none;
        cursor: pointer;
        color: #b45309;
        font-size: 1.2rem;
        line-height: 1;
        padding: 0;
      }
    </style>
  </head>
  <body>
    <div
      class="bascik__comp-alert__alert"
      id="bascik__comp-alert__a10a5b2a__alert"
    >
      <p class="bascik__comp-alert__alert-body">
        Scheduled maintenance Sunday, 2am–4am UTC.
      </p>
      <button
        id="bascik__comp-alert__a10a5b2a__close"
        class="bascik__comp-alert__alert-close"
        type="button"
        aria-label="Dismiss"
      >
        ×
      </button>
    </div>
    <script>
      (function () {
        document
          .getElementById("bascik__comp-alert__a10a5b2a__close")
          .addEventListener("click", () => {
            document.getElementById(
              "bascik__comp-alert__a10a5b2a__alert",
            ).hidden = true;
          });
      })();
      //# sourceURL=src/components/comp-alert.html
    </script>
  </body>
</html>
css
.bascik__comp-alert__alert {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  padding: 14px 16px;
  border: 1px solid #f59e0b;
  border-radius: 8px;
  background: #fffbeb;
}

.bascik__comp-alert__alert[hidden] {
  display: none;
}

.bascik__comp-alert__alert-body {
  flex: 1;
  margin: 0;
  font-size: 0.9rem;
  color: #92400e;
}

.bascik__comp-alert__alert-close {
  background: none;
  border: none;
  cursor: pointer;
  color: #b45309;
  font-size: 1.2rem;
  line-height: 1;
  padding: 0;
}

Companion CSS and Script Files

Choosing between inline <style> or <script> blocks and companion .css or .ts/.mts/.js/.mjs files is a matter of personal preference.

For inline component blocks, keep <style> above the component markup and <script> below the markup. This convention is checked as an advisory warning (component-structure-order) by the linter, in your editor and in bascik --check.

Components do not need literal HTML markup. A component can consist entirely of a <script data-bascik-build> block that returns its markup, optionally with a <style> block. When there is no literal markup, the ordering warning does not apply.

Create a .css file or companion script files alongside the .html file if you prefer to keep your styles or JavaScript separate. A companion .css file with the same base name (feature-card.css beside feature-card.html) is merged automatically. Companion script files explicitly referenced via <script src="counter.ts"></script> are resolved, inlined, and scoped at build time. Browsers ignore inline content in a <script> that has src, so Bascik drops any such content when it inlines the companion and prints a warning. A directive script with src= (data-bascik-build, data-bascik-server, and the others) is never inlined: Bascik runs that file as a Node.js module, so a scripts.typescript function compiles it with runtime: "server" only. A .ts or .mts companion has its erasable TypeScript stripped automatically before scoping, with no configuration; see TypeScript in Component Scripts.

Directory Isolation Rule: Path resolution for companion files is strictly scoped to the component's directory or base filename. A component inside src/components/demo-counter/ can only reference script or style files located inside its own folder. It cannot access files across other component directories. In a flat folder, a component's companions are the scripts named after it: card.ts and extras such as card-extra.ts or card.utils.ts. A script named after another component (card-header.ts beside card-header.html) belongs to that component, unless card references it with <script src>. A card-header component in its own folder owns only the scripts in that folder, so a flat card-header.ts stays with card.

Only a relative src names a companion: nav-bar.js, ./nav-bar.js, or lib/helper.js, resolved against the component's folder (a bare file name matches a companion anywhere in that folder). A URL such as https://cdn.example.com/widgets/nav-bar.js or //cdn.example.com/nav-bar.js, or a root-absolute path such as /vendor/nav-bar.js, is left as written for the browser to load, even when its file name matches a companion, so a wrapper component can share its name with the library it loads.

The directive family also includes data-bascik-attr-{attribute}="{propName}" for sending a prop value to an attribute on an element inside the component, and data-bascik-preserve for keeping selected id, name (when name scoping is on), or class values literal. Use content props for text, attribute props for values such as href, src, and aria-label, slots for consumer-authored markup, and Preserve Scoping only when an external system must bypass normal scoping.

src/components/feature-card/

Build-time Components

Bascik resolves tags and ships vanilla HTML.

html
<feature-card
  data-bascik-prop-title="Build-time Components"
  data-bascik-prop-desc="Bascik resolves tags and ships vanilla HTML."
>
</feature-card>
html
<article>
  <h3 data-bascik-prop-title></h3>
  <p data-bascik-prop-desc></p>
</article>
css
article {
  padding: 24px;
  background: #242628;
  border: 1px solid #3a3d40;
  border-radius: 10px;

  h3 {
    color: #f0f1f2;
  }
  p {
    font-size: 0.875rem;
    color: #8d929e;
  }

  &:hover {
    border-color: rgba(211, 255, 141, 0.35);
    box-shadow: 0 0 0 1px rgba(211, 255, 141, 0.12);
  }
}
Output is shown unminified (HTML, CSS, JS, and identifier names) for readability.
html
<article class="b_el_feature-card_article">
  <h3 class="b_el_feature-card_h3">Build-time Components</h3>
  <p class="b_el_feature-card_p">
    Bascik resolves tags and ships vanilla HTML.
  </p>
</article>
css
.b_el_feature-card_article {
  padding: 24px;
  background: #242628;
  border: 1px solid #3a3d40;
  border-radius: 10px;

  .b_el_feature-card_h3 {
    color: #f0f1f2;
  }
  .b_el_feature-card_p {
    font-size: 0.875rem;
    color: #8d929e;
  }

  &:hover {
    border-color: rgba(211, 255, 141, 0.35);
    box-shadow: 0 0 0 1px rgba(211, 255, 141, 0.12);
  }
}

Subfolder Layout

Organize components into subfolders whenever it helps keep your project tidy. You can group by feature, section, or per-component folders containing companion files:

text
src/components/
  marketing/
    promo-card.html
    promo-card.css
  admin/
    user-row.html
  alert-box/
    alert-box.html
    alert-box.css
    alert-box.ts

Bascik derives the tag name from the filename only, not from the directory path. Subfolders do not create separate namespaces, so marketing/promo-card.html registers <promo-card>, admin/user-row.html registers <user-row>, and alert-box/alert-box.html registers <alert-box>.

There is nothing to import or register. Here hello-card.html lives in its own folder, and typing <hello-card> in a page is enough:

An editor above a browser. The editor shows hello-card.html in src/components/hello-card. In a page, typing the start of the tag brings up hello-card as a suggested Bascik component. Accepting it and saving renders the card in the browser. Duplicating the line and saving renders a second card

If two files anywhere in src/components/ resolve to the same tag name (e.g. marketing/card.html and admin/card.html, or card.html and card.v2.html, since the tag is the filename up to its first dot), Bascik raises a build error naming both conflicting paths.

Flexible organization. Organize src/components/ flat, in feature folders, or in per-component directories. Choose whichever layout fits your workflow and keep filenames unique across the tree.

No restart needed. The dev server watches src/components/ for new and changed files. Drop in a new .html or .css file and all affected pages re-transpile and reload automatically.

File Structure Flexibility

Bascik gives you complete freedom to structure your components however you prefer. You can mix and match different layouts across your project depending on the size and complexity of each component:

  1. HTML-only file: Just a .html file inside src/components/. No CSS/JS files needed.
  2. Single-file component: An .html file inside src/components/ containing both your markup and <style>/<script> blocks.
  3. Companion files in the root: An .html file and a .css file side-by-side in src/components/ (e.g. src/components/my-card.html and src/components/my-card.css).
  4. Subfolder organization: A folder inside src/components/ containing the matching files (e.g. src/components/my-card/my-card.html and src/components/my-card/my-card.css).

You can choose any of these arrangements at any time. There is no functionality or performance difference between them. At build time, Bascik treats them identically, extracting and scoping your styles and bundling them into the page's final compiled output.

Four ways to lay out the same component

HTML only

text
src/components/
  my-card.html

Single file

text
src/components/
  my-card.html   (markup, style, script)

Companion files

text
src/components/
  my-card.html
  my-card.css

Subfolder

text
src/components/
  my-card/
    my-card.html
    my-card.css

The tag name comes from the filename, not the folder, so every one registers the my-card tag.

Component Naming and Hyphens

Under the WHATWG HTML standard (§4.13.1.2), custom elements must contain at least one hyphen (such as <site-nav>, <hello-card>, or <my-button>).

Naming your components with hyphens ensures they never collide with native HTML tags or future web standards. Bascik validates component names when the dev server starts or a build runs, and the official Bascik VS Code Extension flags non-hyphenated names in your editor:

  • If a component shares the name of a native HTML element (such as header.html or dialog.html), the CLI warns Component "header" has the same name as a native HTML element. but still loads the component, which then replaces every <header> on your pages and most likely breaks the site. The editor does not flag native names; only the CLI does.
  • If a component filename is not hyphenated (such as card.html), Bascik compiles it for backward compatibility, but issues a warning recommending a hyphenated name like my-card.html or site-card.html.
  • A filename no tag can spell, such as my widget.html or 2col.html, can never be used, because <my widget> is read as <my> with a widget attribute. A name must start with a lowercase ASCII letter and hold only letters, digits, -, ., and _. Bascik warns with the file and a name to rename it to, such as my-widget.html.
  • Use hyphens, not underscores, between words. An underscore is legal, but #btn {} in site-nav_x.html and #x_btn {} in site-nav.html both compile to the class b_id_site-nav_x_btn, so each rule would style the other component's element. The build stops on that with an error naming both rules and their lines (rename the id or the component), and the editor and bascik --check warn on any component name with an underscore (component-name-underscore).
  • If a component file is empty (such as one just created in the editor), the CLI warns Component "stub-card" (src/components/stub-card.html) is empty, so every <stub-card> renders nothing until the file has markup. Each <stub-card> usage, and whatever it wraps, is removed from the output, and every other component on the page still expands.

Choose concise lowercase names that describe a component's job, without repeating the application's name: not-found rather than shop-not-found. Use hyphens between words, never underscores.

Bascik components are expanded before the browser sees them, not registered as browser custom elements. A non-native single word such as navigation works, although the CLI and editor still recommend a hyphenated name for future compatibility. Never shorten a name to a native element: nav, head, and footer would replace those elements throughout the site. Use navigation, page-head, and site-footer instead. Browser custom elements that remain in the built output must follow the WHATWG hyphen requirement.

Native names include SVG and MathML elements (path, circle, mi, mrow) and obsolete HTML elements (blink, isindex, keygen). The public skill's component-creation helper rejects these names too; reserve them for native markup.

Component names match complete tag names only. A component named card claims <card>, but it never claims a longer tag such as <card-header>.

Void / Self-Closing Component Tags

When utilizing your components inside pages or other components, choose self-closing void syntax for any component that does not take slot children:

html
<!-- Preferred void (self-closing) syntax for components without slots -->
<site-nav />
<site-head />
<site-footer />

<!-- Paired tags (only required when passing inner slot content) -->
<hello-card>
  <p>Slot content goes here.</p>
</hello-card>

If a component does not declare a data-bascik-slot to accept inner children, always prefer self-closing/void syntax (<site-nav />, <site-footer />) to keep page markup clean, concise, and readable. A space before /> is optional, so <site-nav /> and <site-nav/> compile identically, including when nested inside another instance of the same component. Paired and self-closing forms compile to the exact same output. Children passed to a component with no data-bascik-slot are dropped, and the build prints a warning naming the component and the file the usage is in. A native <slot> element is not a Bascik slot: outside a <template> it stays in the output as written, and the build warns about it.

Pick the Root Element

A component's root element is the element that replaces its tag in the output, and the one that attributes from the usage tag land on. Make it the element for the component's job, not a <div> around it: a <site-nav> renders a <nav>, a <site-footer> a <footer>, a <customer-review> an <article>, and a <signup-form> a <form>.

html
<!-- src/components/site-nav/site-nav.html -->
<nav class="nav" aria-label="Main">
  <ul>
    <li><a href="/">Home</a></li>
    <li><a href="/about">About</a></li>
  </ul>
</nav>

Bascik never adds or removes a wrapper, so what you pick is what ships. Component Best Practices has a table of jobs and their elements, with good and bad examples built by Bascik.

Give the root data-testid="<component-name>" and each control inside it data-testid="<component-name>-<part>", so end-to-end tests can find them after a production build hashes class names and IDs. Bascik never scopes or hashes data-testid. See End-to-End Testing.

Multiple Root Elements

Unlike traditional JavaScript frameworks (such as React or Vue 2) that historically required a single root wrapper element or explicit fragment components, Bascik component templates naturally support multiple top-level HTML elements in a single .html file.

When transpiled, all root-level elements are inserted directly into the page markup in order. No unnecessary wrapper <div> or <Fragment> tags are added to your rendered HTML. Bascik's scoping engine automatically handles CSS rules, class names, IDs, element selectors, and scripts across every element in the component template.

Attribute Inheritance: If non-data-bascik-* attributes are passed on a usage tag (such as class="extra" or aria-label="Section"), Bascik merges them onto the first root HTML element in the component template.

src/components/roots-demo/

Release notes

Three top-level elements in one file.

No wrapper element is added.
html
<roots-demo />
html
<style>
  .roots-title {
    margin: 0 0 6px;
    color: #f0f1f2;
    font-size: 1.1rem;
    font-weight: 700;
  }
  .roots-lead {
    margin: 0 0 10px;
    color: #8d929e;
    font-size: 0.9rem;
  }
  .roots-note {
    padding: 8px 12px;
    color: #d3ff8d;
    border: 1px dashed #d3ff8d;
    border-radius: 6px;
    font-size: 0.85rem;
  }
</style>
<p class="roots-title">Release notes</p>
<p class="roots-lead">Three top-level elements in one file.</p>
<div class="roots-note">No wrapper element is added.</div>
Output is shown unminified (HTML, CSS, JS, and identifier names) for readability.
html
<p class="bascik__roots-demo__roots-title">Release notes</p>
<p class="bascik__roots-demo__roots-lead">
  Three top-level elements in one file.
</p>
<div class="bascik__roots-demo__roots-note">No wrapper element is added.</div>
css
.bascik__roots-demo__roots-title {
  margin: 0 0 6px;
  color: #f0f1f2;
  font-size: 1.1rem;
  font-weight: 700;
}
.bascik__roots-demo__roots-lead {
  margin: 0 0 10px;
  color: #8d929e;
  font-size: 0.9rem;
}
.bascik__roots-demo__roots-note {
  padding: 8px 12px;
  color: #d3ff8d;
  border: 1px dashed #d3ff8d;
  border-radius: 6px;
  font-size: 0.85rem;
}

Component Structure & Tag Ordering

When writing component files containing both styles and scripts alongside HTML, always place <style> tags above the HTML markup as a style guide, and always place <script> tags below the HTML markup.

Multiple Style Blocks

You can include multiple <style> blocks in a single component file. For example, you can organize styles into separate blocks for general layout, media queries, or themes.

html
<style>
  .card {
    padding: 16px;
    background: #1a1b1e;
  }
</style>

<div class="card">
  <h3>Card Title</h3>
</div>

<style media="(min-width: 768px)">
  .card {
    padding: 24px;
  }
</style>

At build time, Bascik extracts every <style> block from the component file, combines them with any companion .css file, applies class and selector scoping, and injects the resulting CSS into the document <head>. A page that leaves out <head> (HTML allows it) gets one, written right before <body> where the browser would imply it, so its component styles still ship.

Readability & Maintainability: While Bascik supports multiple <style> tags in a single component file, using multiple <style> tags (or mixing an inline <style> tag with a companion .css file) is not recommended for readability and maintainability. Choose a single stylesheet pattern per component.

Multiple Script Blocks

Component templates can also contain multiple <script> tags. Bascik handles each script according to its type and attributes:

  • Client scripts: Standard JavaScript blocks (without data-bascik-server or data-bascik-build) are each wrapped in an isolated IIFE (function() { ... })();. You can include multiple client scripts in a component template, and each receives its own scope so local variables do not bleed into other blocks.
  • Build scripts (<script data-bascik-build>): Executed during build or dev time in Node.js for each component use, with that page's context. Returned HTML replaces the tag, and component tags in it are expanded at build time; see Returning component tags.
  • Build-once scripts (<script data-bascik-build-once>): Executed once when the component is registered, without page or route context. The same returned HTML is reused at every component use, then scoped and expanded normally. Their output is cached like other build script output. See data-bascik-build-once.
  • Server and stream scripts (<script data-bascik-server>, <script data-bascik-stream>): Executed on the server at request time (Node.js or a Cloudflare Worker). They are not wrapped in browser IIFEs, and the component tags they return render at request time; see Returning component tags.
  • Data scripts (e.g., type="application/ld+json"): Preserved intact without IIFE wrapping or JavaScript minification.

Clean Code Recommendation: Using separate <script> tags for distinct, unrelated concerns within a component (for example, a modal controller vs. an analytics handler) is recommended for code readability and maintainability. Because Bascik wraps each client script in its own IIFE, local variables stay safely isolated without polluting a single monolithic script block. Avoid splitting closely related code for no reason, but use separate <script> tags whenever a component handles multiple independent interactive features.

Understanding the distinction between page shells (src/pages/*.html) and component templates (src/components/*.html) is key to structuring accessible layouts (see Component Best Practices for the landmark elements themselves):

  • Component Templates (src/components/*.html): Component markup is scoped at build time. Class names are namespaced, and id attributes are hashed per instance (e.g. id="bascik__comp__a1b2__btn").
  • Page Shells (src/pages/*.html): Page markup is unscoped vanilla HTML. IDs, landmarks, and attributes written directly in page files remain literal strings (id="main-content").
Where scoping starts and stops

Page shell: src/pages/about.html

You write

html
<main id="main-content"></main>

Bascik emits

html
<main id="main-content"></main>

Component template: src/components/site-nav.html

You write

html
<button id="menu"></button>

Bascik emits

html
<button id="bascik__site-nav__a1b2c3d4__menu"></button>

Shared Head Tags & Layout Component Decomposition

When structuring or migrating a site with Bascik, identify repeating HTML markup across page shells (especially shared <head> tags such as meta charset, viewport, favicons, web fonts, Open Graph tags, global CSS links, and site headers or footers) and extract them into reusable components.

Component tags can be placed inside <head> element blocks in your page shells:

html
<!-- src/components/site-head.html -->
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="stylesheet" href="/styles.css" />

Page shells can then include <site-head /> inside their <head>:

html
<!-- src/pages/index.html -->
<!DOCTYPE html>
<html lang="en">
  <head>
    <site-head />
    <title>Home - My Site</title>
  </head>
  <body>
    <site-nav />
    <main id="main-content">
      <h1>Welcome</h1>
    </main>
    <site-footer />
  </body>
</html>

At build time, Bascik expands <site-head> inside <head> and merges any component styles into the document <head> naturally.

A component inside the head element

One tag in every page shell

src/pages/index.html

html
<head>
  <site-head />
  <title>Home - My Site</title>
</head>

dist/index.html

html
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
  <link rel="stylesheet" href="/styles.css" />
  <title>Home - My Site</title>
</head>

Under WCAG 2.4.1 (Bypass Blocks), a "Skip to main content" link must be the first focusable element in the DOM so keyboard users pressing Tab can skip past header navigation links directly to the page's primary content.

Placing the skip link at the top of a <site-nav> component guarantees it is rendered at the top of every page, while targeting an unscoped id="main-content" on the page shell's <main> landmark:

html
<!-- src/components/site-nav.html (First focusable element in DOM) -->
<a href="#main-content" class="skip-link">Skip to main content</a>
<nav aria-label="Main">
  <a href="/" class="logo">Logo</a>
  <!-- Navigation links... -->
</nav>

<!-- src/pages/about.html (Page Shell: unscoped HTML landmark) -->
<!DOCTYPE html>
<html lang="en">
  <body>
    <site-nav />

    <!-- Target landmark: ID remains unscoped and literal for #main-content anchor -->
    <main id="main-content" class="docs-content">
      <h1>About Us</h1>
      <p>Page content...</p>
    </main>
  </body>
</html>

Because id="main-content" is written on the page shell (<main id="main-content">), the id is not hashed, ensuring the component's <a href="#main-content"> skip link always resolves cleanly to the main landmark.

Do not add data-bascik-preserve="id" to the skip link to "keep it working". A preserved link is never rewritten, so if its target is a scoped id inside the same component the link ends up pointing at nothing. See the reference rules in Preserve Scoping.

Third-Party Custom Elements

A hyphenated tag that is not a Bascik component, such as a library's <heading-anchors> or <model-viewer>, is passed to the browser unchanged. By default each page prints an Unresolved component tag warning for it. Declare the tag in components.external to remove the warning and the bascik --check finding. Typos are still reported.

Example Components

The Example Components section has working components to read: a header and footer, tabs, a modal dialog, a popover menu, a filter list, a copy button, a contact form, and a theme toggle. Each page shows the live component, its usage, and its full source.

Sharing and Distributing Components

Components can be shared across projects or distributed as npm packages:

  • Adding components: Use bascik add <package> to copy components from npm packages directly into your project's src/components/ directory. See Sharing Components.
  • Publishing components: Set up a package with the bascik.components directory contract to allow other developers to add your components. See Publishing Components.