v1.0.0

HTML components.
Zero runtime.

What if you could have components that were just HTML files without any special syntax? Now you can. Bascik is a build tool for HTML components with automatically scoped CSS and JS. Zero runtime. The code that ships is the code you wrote.

npm create bascik@latest
  • 0 kB framework runtime shipped
  • 0 required runtime dependencies
  • Use what you know: vanilla HTML, CSS, JS
  • No imports or exports: components just work

No imports. No exports. Just use the tag.

Create a file named after your component and add your HTML, CSS, and JavaScript. Then use that tag in any page or component. Bascik finds it automatically, with no import, export, or registration step.

site-nav
html
<!-- src/pages/index.html -->
<!DOCTYPE html>
<html lang="en">
<body>
  <site-nav />
  <h1>Welcome</h1>
</body>
</html>
html
<!-- src/components/site-nav.html -->
<style>
  .nav {
    display: flex;
    gap: 24px;
    padding: 16px 28px;
    border-bottom: 3px solid #d3ff8d;
  }
  .link { color: #d3ff8d; }
  .link:hover { text-decoration: underline; }
</style>
<nav class="nav">
  <a class="link" href="/">Home</a>
  <a class="link" href="/why-bascik">Why Bascik</a>
  <a class="link" href="/getting-started">Docs</a>
</nav>
Output is shown unminified (HTML, CSS, JS, and identifier names) for readability.
html
<!-- dist/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
  <style>
    .bascik__site-nav__nav {
      display: flex;
      gap: 24px;
      padding: 16px 28px;
      border-bottom: 3px solid #d3ff8d;
    }
    .bascik__site-nav__link { color: #d3ff8d; }
    .bascik__site-nav__link:hover { text-decoration: underline; }
  </style>
</head>
<body>
  <nav class="bascik__site-nav__nav">
    <a class="bascik__site-nav__link" href="/">Home</a>
    <a class="bascik__site-nav__link" href="/components">Components</a>
    <a class="bascik__site-nav__link" href="/slots">Slots</a>
  </nav>
  <h1>Welcome</h1>
</body>
</html>

Live Example

One component. Multiple isolated instances.

Bascik scopes CSS class names and DOM selectors at build time. Each instance gets its own namespace with no runtime JS, no Shadow DOM, and no virtual DOM.

demo-counter
html
<!-- src/pages/index.html -->
<!DOCTYPE html>
<html lang="en">
<body>
  <demo-counter data-bascik-prop-label="Instance A" />
  <demo-counter data-bascik-prop-label="Instance B" />
</body>
</html>
html
<!-- src/components/demo-counter/demo-counter.html -->
<div class="ctr">
  <span class="ctr-label" data-bascik-prop-label>
    Counter
  </span>
  <span class="ctr-count" id="count">0</span>
  <div class="ctr-btns">
    <button class="ctr-dec" id="dec">−</button>
    <button class="ctr-inc" id="inc">+</button>
  </div>
</div>
<script src="demo-counter.ts"></script>
css
/* src/components/demo-counter/demo-counter.css */
.ctr {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 16px;
}
.ctr-count {
  font-size: 2.4rem;
  font-weight: 700;
  color: #d3ff8d;
}
.ctr-dec, .ctr-inc {
  width: 40px;
  height: 40px;
  border-radius: 6px;
  cursor: pointer;
}
ts
// src/components/demo-counter/demo-counter.ts
const count = document.getElementById('count') as HTMLElement;
const dec   = document.getElementById('dec') as HTMLButtonElement;
const inc   = document.getElementById('inc') as HTMLButtonElement;
let n: number = 0;

dec.addEventListener('click', () => {
  n--;
  count.textContent = String(n);
});
inc.addEventListener('click', () => {
  n++;
  count.textContent = String(n);
});
Output is shown unminified (HTML, CSS, JS, and identifier names) for readability.
html
<!-- dist/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
  <style>
    .bascik__demo-counter__ctr {
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 16px;
    }
    .bascik__demo-counter__ctr-count {
      font-size: 2.4rem;
      font-weight: 700;
      color: #d3ff8d;
    }
    .bascik__demo-counter__ctr-dec,
    .bascik__demo-counter__ctr-inc {
      width: 40px;
      height: 40px;
      border-radius: 6px;
      cursor: pointer;
    }
  </style>
</head>
<body>
  <div class="bascik__demo-counter__ctr">
    <span class="bascik__demo-counter__ctr-label">Instance A</span>
    <span class="bascik__demo-counter__ctr-count"
          id="bascik__demo-counter__a1b2__count">0</span>
    <div class="bascik__demo-counter__ctr-btns">
      <button class="bascik__demo-counter__ctr-dec"
              id="bascik__demo-counter__a1b2__dec">−</button>
      <button class="bascik__demo-counter__ctr-inc"
              id="bascik__demo-counter__a1b2__inc">+</button>
    </div>
  </div>
  <script>
    (function() {
      const count = document.getElementById("bascik__demo-counter__a1b2__count");
      const dec   = document.getElementById("bascik__demo-counter__a1b2__dec");
      const inc   = document.getElementById("bascik__demo-counter__a1b2__inc");
      let n = 0;
      dec.addEventListener("click", () => { n--; count.textContent = String(n); });
      inc.addEventListener("click", () => { n++; count.textContent = String(n); });
    })();
  </script>
</body>
</html>
Multiple instances of a component on the same page
Instance A 0
Instance B 0

Your components work just like native HTML components.

No synthetic lifecycle hooks, no virtual DOM diffing, and no proprietary event wrappers. Pass slots, attributes, and regular HTML tags just like you would with native browser elements.

dialog-box
html
<!-- src/pages/index.html -->
<dialog-box>
  <span data-bascik-slot="header">Account Settings</span>
  <p>Your components accept slots, attributes, and regular HTML tags.</p>
  <button type="button">Save Changes</button>
</dialog-box>
html
<!-- src/components/dialog-box.html -->
<style>
  .dialog {
    border: 1px solid #3a3d40;
    border-radius: 8px;
    padding: 20px;
    background: #1e2022;
  }
  .header {
    border-bottom: 1px solid #2e3032;
    padding-bottom: 10px;
    margin-bottom: 14px;
    font-weight: 700;
  }
</style>

<div class="dialog">
  <div class="header">
    <div data-bascik-slot="header">
      <span>Notice</span>
    </div>
  </div>
  <div class="body">
    <div data-bascik-slot>
      <p>Default content if no slot content was provided.</p>
    </div>
  </div>
</div>
Output is shown unminified (HTML, CSS, JS, and identifier names) for readability.
html
<!-- dist/index.html -->
<style>
  .bascik__dialog-box__dialog {
    border: 1px solid #3a3d40;
    border-radius: 8px;
    padding: 20px;
    background: #1e2022;
  }
  .bascik__dialog-box__header {
    border-bottom: 1px solid #2e3032;
    padding-bottom: 10px;
    margin-bottom: 14px;
    font-weight: 700;
  }
</style>

<div class="bascik__dialog-box__dialog">
  <div class="bascik__dialog-box__header">
    <span>Account Settings</span>
  </div>
  <div class="bascik__dialog-box__body">
    <p>Your components accept slots, attributes, and regular HTML tags.</p>
    <button type="button">Save Changes</button>
  </div>
</div>

Native Feel

Standard DOM & Slots

Default slots, named slots, and fallback content flatten at build time. The output is clean, standard markup that any browser understands.

Read the Slots guide →

Build time, request time, or streamed live. Zero client JS.

Standard Node.js right in your HTML. Run scripts at build time for SSG, per request on the server, or stream HTML chunks progressively over an open HTTP connection without a client framework runtime.

data-bascik-build

Build Scripts

Run Node.js once at build time. Fetch data, query databases, or render Markdown directly into your static pages before shipping.

Build Scripts guide →
data-bascik-server

Server Scripts

Run on each request. Read cookies, check auth headers, and buffer responses. Aborts with a 500 before headers commit if an error happens.

Server Scripts guide →
data-bascik-stream

Stream Scripts

Flush the static shell and placeholder UI immediately, then stream resolved HTML chunks over the open HTTP connection as backend work finishes.

Stream Scripts guide →
src/pages/dashboard.html
html
<!-- 1. Skeletons and footer flush in the very first byte. Zero wait. -->
<div class="stream-layout">
  <div class="skeleton" role="status">Loading live metrics...</div>

  <!-- 2. Async backend finishes, flushes straight into the open connection -->
  <div class="slot">
    <script data-bascik-stream>
      import { escape } from '@/lib/server.ts';

      export default async function (request, context, { signal }) {
        const stats = await fetchDatabaseStats({ signal });
        return `<article class="card"><h3>${escape(stats.cluster)}</h3><p>99.99% Uptime</p></article>`;
      }
    </script>
  </div>
</div>
css
/* 3. Pure CSS: :has() swaps the skeleton out the moment HTML arrives */
.stream-layout:has(.card) .skeleton {
  display: none;
}

/* 4. CSS flex order: footer sent in chunk 1, but sits visually at the bottom */
footer {
  order: 99;
}
Output is shown unminified (HTML, CSS, JS, and identifier names) for readability.

Progressive streaming without a client framework

Browsers have parsed chunked HTML since the 90s. When you stream HTML with Bascik, the browser displays the page as it arrives. No React hydration, no virtual DOM diffing, and no client-side fetch polling.

  • Immediate first paint: Static shell and skeleton placeholders flush right away.
  • Zero layout shift: CSS :has() swaps out the skeleton in place when the HTML chunk arrives.
  • Early-paint footer: Send the footer in chunk 1, position it visually with CSS order.
A browser loading a dashboard page. A loading skeleton is shown first, and after about two seconds a card reading 99.99% uptime replaces it in place.
A real page from the dev server. The server holds the card back for two seconds on purpose so the swap is easy to see. The skeleton and shell arrive first, with no client-side fetching.

Save a file. The page is already updated.

Edit a page or a component and the dev server rebuilds only what changed, then refreshes the browser. Change one component and every page that uses it changes with it.

An editor beside a browser showing a new Bascik project. A heading is edited and saved, and the browser updates. A border color in a component is edited and saved, and every card on the page changes.
Recorded for real: the starter site from npm create bascik, edited in VS Code and served by the Bascik dev server. Nothing here is mocked.

Page builds in milliseconds. Zero runtime overhead.

The dev server and the production build run the exact same pipeline, and it is tuned for performance. Component scoping runs once, then every instance reuses the result. Build script output is cached, so only scripts whose inputs changed run again. The dev server serves files from memory. Unchanged assets are symlinked instead of copied. Watch the terminal: the server is up almost instantly, and edits are back in the browser before you have switched windows. See how the dev server works.

A terminal running bascik on a new project. The server URL is pinned to the top row as soon as the server is up, four pages transpile in a fraction of a second, and after one file is saved only that page is rebuilt, in a few milliseconds.
A new npm create bascik project, recorded for real on a laptop: the server is up almost immediately, its address stays pinned to the top row, and saving one file rebuilds only that page.
Builds in Milliseconds
Sites on average take just a couple seconds or less to boot
Instantaneous Assets
Assets are symlinked by default to save on disk space and time.

Built for developer joy.

Component modularity and automatic scoping without the framework friction, complex setups, or runtime overhead.

Zero Configuration

Works instantly

No bundler setups, complex configs, or transpiler chains. Run bascik and start building instantly with standard Node.js.

Zero Dependencies

Lean in production

Zero production dependencies at build and server runtime. A single optional dependency (chokidar) powers local dev file watching.

Zero Learning Curve

Standard HTML & CSS

No JSX, custom template syntax, or framework hooks. Write pure HTML and CSS with standard DOM APIs. Everything you know just works.

Zero Runtime Overhead

Automatic isolation

Component styles and DOM queries are scoped automatically at build time. Enjoy total component isolation with zero added JavaScript.

Up and running in under a minute.

One command scaffolds your project and starts the dev server. No config required to get going.

$ npm create bascik@latest
Read the Getting Started Guide →