Bascik
Bascik is a build tool for HTML components. It expands custom tags such as
into the matching component file, scopes the component's CSS, IDs, classes, and scripts, and writes plain HTML pages to
. It adds zero JavaScript: every script in the output is one you wrote.
It is not a framework. There is no virtual DOM, reactive state, client router, template language, Shadow DOM, or runtime. Write HTML, CSS, and TypeScript (stripped to plain JavaScript), and let Bascik do the isolation at build time.
Pages read like a list of tags
Write pages as a list of component tags, not big blocks of HTML:
html
<body>
<site-header />
<main id="main">
<home-hero />
<pricing-section>
<plan-card data-bascik-prop-name="Home" data-bascik-prop-price="$49" />
<plan-card data-bascik-prop-name="Cafe" data-bascik-prop-price="$129" />
</pricing-section>
<faq-list />
</main>
<site-footer />
</body>
- Split a section into a component when it has a job you can name (pricing, reviews, signup form), when its markup repeats, or when the page is no longer readable at a glance.
- Do not split a piece that only makes sense inside its parent, that would need a prop passed through a component that does not use it, or that is a tiny wrapper used once. Use a slot, or keep the parent whole.
- Write each fact once. An address, opening hours, a price, or a phone number lives in one component or one data file, never typed into several. A fact several components show goes in ; data only one component renders (its FAQ entries, its plans) lives in that component's folder (rule 17). A value that is the same on every page, such as the brand name, belongs inside the component, not passed as a prop on every page. A heading group (eyebrow, heading, intro) that repeats across sections is a component with props.
Small named files are easier for people and agents to read and change, and each is a boundary for its CSS, IDs, and script. It is a balance: the
skill has the detailed guidance with good and bad examples, and
scripts/audit-components.mjs
checks it.
TypeScript by default
Write all new code in TypeScript. Use JavaScript only when the person asks for it. When refactoring TypeScript, keep it TypeScript and carry the types across.
- Browser code goes in a component's companion () or an inline
<script type="text/typescript">
. Bascik strips the types with Node's strip-only mode, so it works with no setup. Use erasable syntax only: annotations, interfaces, , , and . , namespaces with code, and parameter properties fail the build. A plain is JavaScript. A page's own is compiled and inlined too; a classic one with or fails the build (inline scripts ignore them), so drop the attribute or mark it . A script is inlined, not bundled: importing (or any relative, or path) fails the build, so bundle it with esbuild or import a published URL.
- Build, routes, server, and stream scripts are TypeScript with a or a tag marked (
<script data-bascik-server type="text/typescript">
). Bascik strips them the same way before they run, and compiles them when the page builds, so a syntax error or fails the build with file:line:column; an unmarked block is JavaScript.
- blocks are type checked in the editor (the VS Code extension or , with hover, definition and completion for imports) and, opted in, by
bascik --check --typescript
.
- Node code is too: helpers and data modules, routes, scripts, and . Node strips their types. Keep each block to a few lines that import typed helpers: for a module in the component's own folder, for one several pages share (rule 17). A helper that returns markup builds it with from and declares as its return type (rule 15).
- esbuild is the option for full TypeScript (, decorators, an older browser target): , then set to an esbuild transform. Browser code that imports npm packages needs esbuild too, as a step that bundles it into .
ts
// bascik.config.ts (illustrative: esbuild as the browser TypeScript compiler)
import { defineConfig } from "@bascik/bascik/config";
import { transform } from "esbuild";
export default defineConfig({
scripts: {
typescript: async (code, { sourcePath }) => {
const result = await transform(code, {
loader: "ts",
target: "es2020",
sourcefile: sourcePath,
});
return result.code;
},
},
});
Read
/scoped-javascript#typescript-in-component-scripts
,
/configuration#scriptstypescript
, and
/how-to/bundling-npm-packages
before going beyond erasable syntax.
Code in this skill is illustrative. Adapt tag names, paths, and values to the project instead of copying example names literally.
Recognize a Bascik project
- depends on , or the root has / , or there are files under .
- Node.js 24.21 or newer (26, the current LTS, recommended). Node runs files natively and Bascik strips browser TypeScript, so no TypeScript build step exists.
- Standard scripts: (), (), (). is always plain , also in a Cloudflare or Netlify project: never rewrite it to , , or . The host's runtime gets its own script (, ); see the skill, Deploy.
Project layout
text
src/
components/ component templates; the file name is the tag name
site-nav/
site-nav.html <site-nav>
site-nav.css optional companion stylesheet, scoped to site-nav
site-nav.ts optional companion script, loaded with <script src="site-nav.ts">
links.ts data or a helper only site-nav uses, imported with ./links.ts
pages/ one .html file per URL, plus public assets
index.html /
about.html /about
blog/post.html /blog/post
404.html served for unknown routes
css/styles.css copied to dist/css/styles.css (global, unscoped)
lib/ modules several components or pages share (import with @/lib/...)
api/ optional API route handlers in .ts (bascik --server only)
dist/ compiled output, never edited by hand, never committed
Write a component
Default pattern:
above the markup, markup in the middle, a TypeScript
below. Give every element a script must control an
and find it with
, and put a
on the root and on each control (rule 16).
html
<!-- src/components/like-button/like-button.html -->
<style>
.like {
padding: 0.5rem 1rem;
border-radius: 6px;
}
.like.active {
background: #d3ff8d;
}
</style>
<button class="like" id="btn" type="button" data-testid="like-button">
<span data-bascik-prop-label>Like</span>
</button>
<script type="text/typescript">
const btn = document.getElementById("btn") as HTMLButtonElement;
btn.addEventListener("click", () => btn.classList.toggle("active"));
</script>
html
<!-- src/pages/index.html -->
<like-button data-bascik-prop-label="Like this post" />
<like-button data-bascik-prop-label="Like this comment" />
Bascik strips the types, rewrites
to
.bascik__like-button__like
, gives each instance its own
(
bascik__like-button__<instance>__btn
), rewrites the
string and the
class to match, and wraps the script in an IIFE. For a longer script, move it to
beside the template and load it with
<script src="like-button.ts"></script>
. Both buttons work independently. To see the exact output, run
.
New components are picked up by the running dev server automatically; no restart is needed.
Rules (must follow)
- Concise, lowercase component names. Name the job, not the app: , not ; , not . Use hyphens between words, never underscores. A non-native single word works because Bascik expands it before the browser sees it, although the CLI/editor recommend hyphens for future compatibility. Never use a native HTML, SVG, or MathML element name, including obsolete names (, , , , ); use , , or instead. Names must be unique across every component root. See
/components#component-naming-and-hyphens
.
- Per-instance targeting uses . Use (or ) for elements a script controls. returns the first instance on the page, so the other instances look broken.
- Order: above the markup, below it.
- Props carry text only. values are HTML-escaped strings. Pass markup or nested components through slots ().
- Self-close components that take no slot content: , .
- HTML and CSS first. Reach for JavaScript only when HTML and CSS cannot do the job cleanly (, , , cover a lot).
- Semantic HTML. Pick the element the HTML standard defines for the job, because Bascik ships exactly the markup you write: a component's root is the semantic element itself ( renders a , a review an , a form a ), not a wrapper ; for actions and for navigation, never a clickable or ; one and one per page with no skipped heading levels; a for every control; on every image; native , , and before ARIA or JavaScript. The checklist with spec links:
read-doc.mjs /component-best-practices#use-htmls-elements
.
- Runtime state in attributes. Toggle and style
.panel[data-state="open"]
; anchor attribute selectors on a scoped class.
- Zero config. Do not create unless a requirement differs from a default, and never put in it. See the skill.
- Never write generated files into . Generated output goes to () from an existing script.
- Client scripts are not bundled. Bare imports such as fail in the browser. They work only in Node-side scripts (, ). Bundle browser packages with esbuild (
read-doc.mjs /how-to/bundling-npm-packages
).
- Pages are lists of tags. Shared tags, the header, the footer, and every section with a nameable job become components (, , , ). Pages keep their own , , and . Balance it: no tiny single-use wrappers and no props passed through components that do not use them.
- Script directives are exclusive. One tag takes at most one of , , , , . runs once per file with no page context.
- TypeScript by default. New browser and Node code is TypeScript (see "TypeScript by default" above). Use JavaScript only when the person asks for it.
- Every script and helper builds markup with . Any build, build-once, routes, server or stream script, API route, and any helper that returns markup (in or beside a component) does
import { html } from "@bascik/bascik/html"
and returns : it escapes each value for where it lands, turns URLs into , and throws where escaping cannot help. Use only for markup the project wrote and trusts or sanitized CMS HTML, for JSON-LD, and or where a string is built without . Never write your own escaper (, an replace chain) and never interpolate data into a plain template string. Give a URL attribute one hole: build the URL in code first (const href = `${siteUrl}${path}`
, then ), because refuses two holes before the URL's first () so that two values cannot spell together. A browser script cannot import it: set there. See the skill.
- A on what a test needs. Every component root gets
data-testid="<component-name>"
, and every interactive part (a button, an input, select, or textarea, a , a link a script handles, an element with an ARIA widget role or ) gets data-testid="<component-name>-<part>"
. Forms, dialogs, and landmarks (, , the page's and ) get one too. Bascik never scopes or hashes , so matches in a production build where classes and ids are hashed. Instances share their ids: a test picks one with , or the page names one with on the usage tag, which replaces the root's. Never put request or random data in a test id, and keep existing hooks (, , ) when migrating. scripts/new-component.mjs
adds the root's, and lists the gaps. See read-doc.mjs /testing/e2e-testing
.
- Colocate what one component or page uses. A module (data, helper, types) that one component uses lives in that component's folder, with its tests, and is imported with (
import { faqs } from "./faqs.ts"
). Content lives with the component that renders it: a page that also needs it, for JSON-LD say, imports it from there (@/components/faq-list/faqs.ts
). A module only one page uses sits beside that page as a file (the pages folder publishes a or file as a public asset, never a one). Only modules several components or pages share, or that config, exec scripts, or API routes also import, go in . Files in component and page folders are watched already; needs . scripts/audit-components.mjs
reports a module with one user ().
Slots, props, and attributes
| Need | Component template | Usage site |
|---|
| Default slot (rich HTML) | <div data-bascik-slot>fallback</div>
| <my-card><h2>Title</h2></my-card>
|
| Named slot | <div data-bascik-slot="header"></div>
| <div data-bascik-slot="header"><h1>Hi</h1></div>
inside the tag |
| Text prop | <p data-bascik-prop-title></p>
| <my-card data-bascik-prop-title="Hello" />
|
| Prop into an attribute | <img data-bascik-attr-src="image" data-bascik-attr-alt="alt">
| <my-card data-bascik-prop-image="/a.png" data-bascik-prop-alt="A" />
|
| Forward attributes | (nothing) | <my-card class="wide" aria-label="Promo" />
lands on the first root element |
Pass props as
data-bascik-prop-<name>="..."
on the usage tag. A plain attribute such as
is not a prop: it is forwarded to the root element and the prop keeps its default. A forwarded attribute the root already has keeps the root's value, except
: the usage tag's replaces it, so a page can name one instance. The slot element itself is replaced by the content, so named-slot wrappers disappear: wrap headings and links in a
<div data-bascik-slot="x">
rather than putting the attribute on the
or
. Details:
,
,
.
Script tags at a glance
| Tag | Runs | Use for |
|---|
| Browser | Component and page behavior (scoped, IIFE-wrapped) |
<script data-bascik-build>
| Node, at build time | Markup from files, Markdown, APIs; output replaces the tag |
<script data-bascik-build-once>
| Node, once per file | Markup shared by every use of a component or every route of a template |
<script data-bascik-routes>
| Node, at build time | Expanding templates into many pages |
<script data-bascik-server>
| Node, per request | Personalized markup (needs or a serverless target) |
<script data-bascik-stream>
| Node, per request, streamed | Slow per-request blocks that should not delay the page shell |
Node-side scripts export a default function that returns an HTML string. Component tags in that HTML render, at build time and, from server and stream scripts, per request. Details are in the
skill.
CLI
sh
bascik # dev server at http://localhost:8080 with live reload and watch
bascik --build # production build to dist/ (HTML, CSS, JS, and identifier minification on)
bascik --build --only "blog/**" # rebuild matching pages without cleaning dist/
bascik --server # serve a built dist/ (needed for server/stream scripts and API routes)
bascik --check # project checks, plus the per-file checks when @bascik/language-server is installed
bascik --check --fix # also rewrite fixable findings (needs @bascik/language-server)
bascik add <package> # copy components from an installed npm package into src/components/
bascik init # add Bascik to an existing project
npx @bascik/language-server --check # the per-file checks on their own (bascik --check already runs them)
and the linter are not two overlapping tools.
answers project questions (duplicate routes, unused components, config), and the linter answers per-file questions (attribute typos, CSS that cannot be scoped).
runs the linter too, so use it, and reach for the linter alone only on a folder outside the project.
New project:
npm create bascik@rc my-site -y
. It and
both write
with
"html.validate.scripts": false
and recommend the Bascik extension in
, merging into files the folder already has. Third-party linters such as Webhint are opt-in: neither command installs them or creates or modifies their configs. A project set up any other way (a migration, or files copied in with
) needs that VS Code setting added by hand, or VS Code joins a page's
blocks into one file and reports false errors such as "A module cannot have multiple default exports". The setting turns off only VS Code's own check of inline scripts, not Bascik's checks. Production builds need a site URL for
and
:
BASCIK_SITE_URL=https://example.com bascik --build
(or
, or
). Full CLI and error output:
.
Verify every change
Do not stop at "the file looks right". Run these from the project root:
node <skill-dir>/scripts/verify.mjs
runs (project and per-file checks) and, for component folders outside the project, the linter, then summarizes each step. Add to also run a production build.
node <skill-dir>/scripts/audit-components.mjs
reports oversized pages and components, inline sections that should be components, repeated blocks and heading groups, the same fact typed into several files, props every page passes with the same value, tiny single-use components, props passed through layers, JavaScript where TypeScript is expected, a hand-written HTML escaper or code that strips , and from a value (build the markup with instead), a module that only one component or page uses (move it beside that user), and a component build script that should be (or a build-once script that reads the page). It also warns about semantic HTML: a clickable or , a component root that is a where , , , , , or fits, a skipped heading level in a page's output, an with no , a form control with no label, and a page with no . Each finding has and what to do. Pass only when the person asked for JavaScript.
node <skill-dir>/scripts/preview.mjs '<my-card data-bascik-prop-title="Hi" />' --count 2
compiles a snippet with the project's real components and config and prints the unminified output, so you can confirm scoped names, slot placement, and script rewrites.
- Read the relevant file in after a build. A hyphenated tag still present in means no component matched it.
A page is not done until
passes and
exits 0. Fix each warning, or tell the person which one you kept and why (for example, a long legal page that is all prose). "The content differs" is not a reason to keep a repeated block: different text is what props and slots are for. Neither is "the fields belong to their form" when two forms repeat them.
is the directory containing this file, for example
.
Editor linters read source; Bascik writes
webhint, axe, html-validate, HTMLHint, and VS Code's HTML checker read
. Bascik overrules some of what they report, because the answer is only in the built page:
- Composed at build time: a missing charset or viewport (a head component adds them), an link "should be specified in the " (a head component is a fragment with no ), a missing (a build script writes it), an with no or and an empty heading whose value comes from a or prop.
- Bascik syntax: self-closed component tags, component files with no doctype, and several blocks in one file (
"html.validate.scripts": false
).
Rules:
- Never "fix" one of these by adding the tag to a source page. Open the built page in first. If the tag is there, the warning is false and a second copy would ship twice; if it is missing there too, fix the head component or build script.
- Everything else is real: a with no , with no , a literal with no . The optional documented turns off in the editor, so a literal missing shows only in and the built-site check: run them. The documented makes optional, so a literal with no shows only when html-validate checks ; never turn
element-required-attributes
off for all of , which also hides a missing .
- Do not add third-party linter configs or dependencies by default. If the person uses Webhint or another linter, read
/testing/linting#editor-linters-read-source
and adapt its copyable configs to the project's folders. Preserve existing settings. Keep Webhint configs as plain JSON, because its extension's quick fixes use . Keep the built-site config independent of the source config so source exceptions do not leak into . Other linters get a config; HTMLHint's must list its default rules too, or unnamed checks are disabled.
- The checks that decide run on built output: for Bascik syntax and WHATWG parsing, then . If the person opts into Webhint, copy the documented first, then run
npx hint -c .hintrc.dist.json $(find dist -name '*.html')
(files, not the folder; use if it is not ). Lighthouse on is another built-site audit. Webhint does not check local links: run .
- cSpell on a brand or product name is real: add the name to the project's , not to a user setting.
Every warning, its setting, and the config files:
read-doc.mjs /testing/linting#editor-linters-read-source
.
Bundled scripts
All scripts are dependency-free Node.js (24.21+) and run from the project root. Each prints
.
| Script | Purpose |
|---|
| , plus the linter on component folders outside the project, with an optional |
scripts/audit-components.mjs
| Page and component structure (too big, inline sections, repeats, tiny wrappers, prop drilling), semantic HTML (clickable divs, wrapper roots, heading order, alt, labels, ), JS where TS is expected, hand-written escapers, single-use modules, and build vs build-once |
| Compile a snippet, file, or existing page in a temporary build and print the result |
scripts/list-components.mjs
| Inventory components with their props, slots, companion files, and descriptions |
scripts/new-component.mjs
| Create a component folder that follows the naming and ordering rules |
| Search the live docs at bascik.dev for a topic |
| Print the current text of a docs page, such as |
| Copy a tested example component or part of the starter site from bascik.dev into the project ( shows them) |
| Find broken internal links, missing assets, and dead in the built , honoring |
Run
before creating a component so you reuse what exists instead of duplicating it.
Look it up in the docs
The docs at
https://bascik.dev are the reference; this skill only holds the rules. Read the page for any task below before writing code, instead of guessing:
sh
node <skill-dir>/scripts/read-doc.mjs /scripts/dynamic-routes
node <skill-dir>/scripts/search-docs.mjs "dynamic routes trailing slash"
| How do I... | Read |
|---|
| Write components, slots, props, attributes | , , , |
| Pick semantic elements, landmarks, headings, labels | /component-best-practices#use-htmls-elements
|
| Pass shared tags | |
| Use a library (petite-vue, Alpine.js, HTMX, Tailwind) | |
| Use a third-party web component | /how-to/third-party-web-components
|
| Share components between projects | /how-to/sharing-components
, /how-to/publishing-components
|
| Find answers to edge cases, fix a known problem | , |
| Use the CLI and the dev server | , |
The full page index is at
https://bascik.dev/llms.txt.
Start from an example
https://bascik.dev/example-components has tested components (tabs, dialog, popover menu, copy button, filter list, contact form, theme toggle).
also offers
, a complete site (layout components, SEO helper, Markdown blog, feed,
, contact page) whose README is at
https://github.com/bascikdev/bascik/tree/main/examples/starter-site. Copy instead of writing from scratch, then adapt:
sh
node <skill-dir>/scripts/add-example.mjs --list
node <skill-dir>/scripts/add-example.mjs tab-group
node <skill-dir>/scripts/add-example.mjs starter-site/src/lib/seo.ts
The script downloads from bascik.dev, maps files to the project's configured folders, also copies example components a file uses, and writes nothing if any file would overwrite a different one. It prints
and
instead of copying them.
Build a new site
npm create bascik@rc my-site -y
, or read the existing project first with . Add BASCIK_SITE_URL=https://example.com
to .
- Plan the pages and URLs (one purpose and one each), then the components each page is made of: the shared layout (head, header, footer), one component per section with a job, and one per repeated block. Plan repeated content (a collection in ) and anything that needs a server (API routes, per-request markup). Ask when the plan is unclear.
- Copy the foundation:
add-example.mjs starter-site
, or only the parts you need. Then replace the example name, navigation (src/components/site-header/links.ts
), colors (tokens in ), and every page's seo({ title, description })
.
- Add components (
new-component.mjs --script inline
or ), then pages as lists of their tags, and interactive examples. Load the companion skill for each area.
- Before launch: , then , , , the audit, and the token check. The starter site README's "Build and Check It" section has the manual checklist.
Companion skills
| Skill | Load it when |
|---|
| Deciding what becomes a component, writing component CSS or TypeScript, interactivity, theming, scoped names |
| Responsive WebP, native srcset and sizes, stable gallery geometry, and correct selected variants before first paint |
| Deciding whether an HTML, CSS, or JavaScript feature works in browsers and inside a component |
| Using , , , , API routes, Markdown, or a blog |
| Titles, descriptions, canonical URLs, social cards, JSON-LD, sitemap, |
| Creating or editing , monorepos, deploying, or running |
| Writing tests, debugging, validating output, or tuning Lighthouse scores |
| Moving a site from React, Next.js, Vue, Svelte, Astro, Eleventy, Hugo, or WordPress |
| Opt-in: having coding agents refactor a live site to production parity and 100 in all five Lighthouse categories |
New projects do not include
. Install it with
npx skills add bascikdev/bascik --skill bascik-refactor
.
If the companions or this skill's
folder are missing, the skill was installed as a single file. Reinstall the full set:
sh
npx skills add bascikdev/bascik
# or, without npx skills:
mkdir -p .agents/skills && curl -fsSL https://bascik.dev/assets/skills.tar.gz | tar -xz -C .agents/skills