Lateral EMDR
lateralemdr.com (opens in a new tab)A SaaS website and dashboard built with Bascik and the Netlify adapter. The dashboard uses stream scripts and server scripts.
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 bascikHow it works
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.
<!-- src/pages/index.html -->
<!DOCTYPE html>
<html lang="en">
<body>
<site-nav />
<h1>Welcome</h1>
</body>
</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> <!-- 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="/why-bascik">Why Bascik</a>
<a class="bascik__site-nav__link" href="/getting-started">Docs</a>
</nav>
<h1>Welcome</h1>
</body>
</html> Live Example
Scoping
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. A class that the component CSS never mentions, like ctr-label below, stays global.
<!-- 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> <!-- src/components/demo-counter/demo-counter.html -->
<div class="ctr">
<span class="ctr-label" data-bascik-prop-label> Counter </span>
<output class="ctr-count" id="count" aria-live="polite">0</output>
<div class="ctr-btns">
<button class="ctr-dec" id="dec" type="button" aria-label="Decrease">
−
</button>
<button class="ctr-inc" id="inc" type="button" aria-label="Increase">
+
</button>
</div>
</div>
<script src="demo-counter.ts"></script> /* 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;
} // 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);
}); <!-- 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="ctr-label">Instance A</span>
<output
class="bascik__demo-counter__ctr-count"
id="bascik__demo-counter__aefe6402__count"
aria-live="polite"
>0</output
>
<div class="ctr-btns">
<button
class="bascik__demo-counter__ctr-dec"
id="bascik__demo-counter__aefe6402__dec"
type="button"
aria-label="Decrease"
>
−
</button>
<button
class="bascik__demo-counter__ctr-inc"
id="bascik__demo-counter__aefe6402__inc"
type="button"
aria-label="Increase"
>
+
</button>
</div>
</div>
<div class="bascik__demo-counter__ctr">
<span class="ctr-label">Instance B</span>
<output
class="bascik__demo-counter__ctr-count"
id="bascik__demo-counter__eea6e425__count"
aria-live="polite"
>0</output
>
<div class="ctr-btns">
<button
class="bascik__demo-counter__ctr-dec"
id="bascik__demo-counter__eea6e425__dec"
type="button"
aria-label="Decrease"
>
−
</button>
<button
class="bascik__demo-counter__ctr-inc"
id="bascik__demo-counter__eea6e425__inc"
type="button"
aria-label="Increase"
>
+
</button>
</div>
</div>
<script>
(function () {
const count = document.getElementById(
"bascik__demo-counter__aefe6402__count",
);
const dec = document.getElementById(
"bascik__demo-counter__aefe6402__dec",
);
const inc = document.getElementById(
"bascik__demo-counter__aefe6402__inc",
);
let n = 0;
dec.addEventListener("click", () => {
n--;
count.textContent = String(n);
});
inc.addEventListener("click", () => {
n++;
count.textContent = String(n);
});
})();
//# sourceURL=src/components/demo-counter/demo-counter.ts
</script>
<script>
(function () {
const count = document.getElementById(
"bascik__demo-counter__eea6e425__count",
);
const dec = document.getElementById(
"bascik__demo-counter__eea6e425__dec",
);
const inc = document.getElementById(
"bascik__demo-counter__eea6e425__inc",
);
let n = 0;
dec.addEventListener("click", () => {
n--;
count.textContent = String(n);
});
inc.addEventListener("click", () => {
n++;
count.textContent = String(n);
});
})();
//# sourceURL=src/components/demo-counter/demo-counter.ts?instance=2
</script>
</body>
</html> Composition
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.
<!-- src/pages/index.html -->
<dialog-box>
<div data-bascik-slot="header"><h2>Account Settings</h2></div>
<p>Your components accept slots, attributes, and regular HTML tags.</p>
<button type="button">Save Changes</button>
</dialog-box> <!-- 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>
<section class="dialog">
<header class="header">
<div data-bascik-slot="header">
<h2>Notice</h2>
</div>
</header>
<div class="body">
<div data-bascik-slot>
<p>Default content if no slot content was provided.</p>
</div>
</div>
</section> <!-- 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>
<section class="bascik__dialog-box__dialog">
<header class="bascik__dialog-box__header">
<h2>Account Settings</h2>
</header>
<div class="body">
<p>Your components accept slots, attributes, and regular HTML tags.</p>
<button type="button">Save Changes</button>
</div>
</section> Native Feel
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 →Full-Stack Scripts
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.
Run Node.js once at build time. Fetch data, query databases, or render Markdown directly into your static pages before shipping.
Build Scripts guide →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 →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 →<!-- 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 { html } from "@bascik/bascik/html";
export default async function (request, context, { signal }) {
const stats = await fetchDatabaseStats({ signal });
return html`<article class="card">
<h3>${stats.cluster}</h3>
<p>99.99% Uptime</p>
</article>`;
}
</script>
</div>
</div> /* 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;
} <!-- Final document after the streamed result arrives; the script itself is server-side. -->
<div class="stream-layout">
<div class="skeleton" role="status">Loading live metrics...</div>
<div class="slot">
<article class="card">
<h3>Primary Cluster</h3>
<p>99.99% Uptime</p>
</article>
</div>
</div> .stream-layout:has(.card) .skeleton {
display: none;
}
footer {
order: 99;
} 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.
:has() swaps out the skeleton in place when the HTML chunk arrives. order. 
Live Reload
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.

npm create bascik, edited in VS Code and served by the Bascik dev server. Nothing here is mocked. Build Performance
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, and you can opt in to symlinking unchanged assets instead of copying them. 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.

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. assets.symlink: true to link unchanged static assets in development instead of copying them. Developer Experience
Component modularity and automatic scoping without the framework friction, complex setups, or runtime overhead.
No bundler setups, complex configs, or transpiler chains. Run bascik and start building with standard Node.js. A production build only needs your site’s URL, set once.
Zero required dependencies at build and server runtime. Two optional ones: chokidar for dev file watching and parse5 for the HTML validation in bascik --check.
No JSX, custom template syntax, or framework hooks. Write pure HTML and CSS with standard DOM APIs. Everything you know just works.
Component styles and DOM queries are scoped automatically at build time. Enjoy total component isolation with zero added JavaScript.
Showcase
Choosing a new tool is a real decision. These sites already use Bascik in production, from a SaaS app with a server-rendered dashboard to an online shop that takes payments.
What ships is HTML, JavaScript, and CSS you can inspect yourself. Turn off minification, test the behavior, and step through your code with familiar debugging tools.
A SaaS website and dashboard built with Bascik and the Netlify adapter. The dashboard uses stream scripts and server scripts.
An independent merchandise shop built with Bascik and the Cloudflare adapter. It uses Stripe Checkout for payments and Printful for shipping and fulfillment, with orders stored in Cloudflare D1.
Docs
Each feature has a dedicated page with examples and a complete reference.
Core
Build UI with reusable HTML components. No imports or registration needed. Just write the custom tag.
CSS
Class names, element selectors, @media, and @keyframes are all automatically scoped per component.
JS
DOM queries in component scripts are rewritten to match scoped IDs and class names at build time.
Slots
Pass inner HTML into components. Named slots target specific zones with fallback content supported.
Build
Run arbitrary Node.js at build time with data-bascik-build script blocks. Fetch data, render Markdown, generate anything.
Config
Override defaults in bascik.config.ts. Toggle scoping, identifier minification, and code minification per project.
Speed
No framework bundle or hydration payload. Your content, assets, and markup determine the final Lighthouse scores.
Switching
Moving from React, Next.js, Eleventy, or Astro. Concept mapping and worked examples.
Ready to build?
One command scaffolds your project and starts the dev server. No config required to get going.