An ecosystem for building Lit-based web components with first-class DX: decorators, SCSS, typed events, HMR, optional signals, etc.
Kasstor is a set of libraries and tooling around Lit for building custom element libraries and applications. It keeps the Lit programming model (templates, reactive properties, LitElement) and adds decorators, SCSS support, typed custom events, Hot Module Replacement, better SSR and initial-render performance, etc.—so you ship standard web components with less boilerplate and a smoother development experience than using Lit alone.
| With plain Lit | With Kasstor |
|---|---|
@customElement('tag') only registers the tag; Shadow DOM, styles, and config are manual per component |
@Component({ tag, styles, shadow?, globalStyles?, metadata? }) — registration, Shadow Root config (mode, delegatesFocus, formAssociated), SCSS, optional no-Shadow, global styles, metadata (kstMetadata), HMR in one place |
| CSS-in-JS or separate CSS files, no preprocessing | SCSS/SASS via Vite (import styles from './component.scss?inline' for variables, mixins, nesting); otherwise pass styles as a string |
Custom events with ad-hoc detail types and manual dispatch |
@Event() + EventEmitter<T> — typed events and emit() with full type safety |
| Components are loaded only via direct import | More loading options — direct import, or declarative lazy loading with the lazyLoad directive (load when the element is attached in a Lit template); requires the library to register loaders via registerCustomElementLoaders from @genexus/kasstor-core |
| Full page reload on every change in dev | HMR — component and style hot reload via @genexus/vite-plugin-kasstor |
No built-in “react to property changes” hook beyond willUpdate |
@Observe() — run logic when specific properties change, with SSR-friendly lifecycle |
| Default LitElement rendering; no optimization when many components mount at once | Lower TBT (Total Blocking Time) — optimized first paint when many components mount; initial render path is tuned so large trees block the main thread less and become interactive sooner (better LCP/TBT); in other words, better Lighthouse Performance scores |
Lit has willUpdate but no “first update” hook; SSR-safe init before first paint is verbose |
firstWillUpdate — runs once before the first update; supports SSR-friendly initialization (e.g. data needed before first render) without extra boilerplate |
| Using the component without Shadow DOM requires custom setup | No-Shadow — shadow: false in @Component; you can style the component the same way as with Shadow (use the component tag name instead of the :host selector in your styles) |
| SSR and hydration need extra wiring (guards, lifecycle, directives) | SSR — firstWillUpdate, @Observe lifecycle, and a renderByPlatform directive to conditionally render content on the server that may differ on the client or be client-only; initialization and reactive side effects are easier to make SSR-safe (global styles are client-only and not supported on the server) |
Your components still follow the Lit model: KasstorElement extends LitElement, and you use html, @property, @state, etc., as usual. Kasstor adds structure (e.g. @Component), styling (SCSS), events (@Event), directives (lazyLoad, renderByPlatform), tooling (HMR), etc., on top.
Setup for a Vite project (recommended): install dependencies, configure Vite and TypeScript, then add a minimal component.
npm i @genexus/kasstor-core
npm i -D @genexus/vite-plugin-kasstor vite typescript sassYou do not need to install @genexus/kasstor-build or @genexus/kasstor-insights when using the Vite plugin: the plugin integrates build tooling and optional performance insights for you.
A minimal layout. Use the .lit.ts (or .lit.js) extension for component files so the plugin can apply HMR and build-time analysis.
project/
├── src/
│ ├── components/
│ │ ├── counter.lit.ts
│ │ └── counter.scss
│ ├── main.ts
│ └── vite-env.d.ts ← required for ?inline imports
├── index.html
├── tsconfig.json
├── vite.config.ts
└── package.json
Add the plugin in vite.config.ts:
import { defineConfig } from "vite";
import { kasstor } from "@genexus/vite-plugin-kasstor";
export default defineConfig({
plugins: [kasstor()]
});Kasstor uses TypeScript decorators (the legacy/experimental kind), not the current ECMAScript standard decorators. Your tsconfig.json must enable them and use the right class field semantics so that @Component, @Event, @Observe, and @property work correctly.
Required:
experimentalDecorators: true— enables TypeScript decorators.useDefineForClassFields: false— keeps the class field semantics that Lit and Kasstor decorators expect (property descriptors on the prototype). Withtrue, TypeScript uses standard field semantics and decorators can break.
A working baseline (adjust paths and options to your project):
{
"compilerOptions": {
"target": "es2022",
"experimentalDecorators": true,
"useDefineForClassFields": false,
"module": "es2022",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
// Bundler mode
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": false,
"outDir": "dist/",
// Necessary to not add the src folder inside the dist
"rootDir": "./src",
// Linting
"allowUnreachableCode": false,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"noImplicitReturns": true,
"noImplicitAny": true,
"noImplicitThis": true,
"noImplicitOverride": true,
"noUncheckedSideEffectImports": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"strict": true,
"verbatimModuleSyntax": true
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "vite.config.ts", "**/*.e2e.ts"]
}If you use a different build tool or emit TypeScript yourself, keep at least experimentalDecorators: true and useDefineForClassFields: false.
Create src/vite-env.d.ts with the following content so TypeScript recognizes Vite-specific import suffixes like ?inline (used when importing SCSS/CSS files):
// eslint-disable-next-line spaced-comment
/// <reference types="vite/client" />Without this file, TypeScript will report an error on any import styles from './component.scss?inline' line.
A minimal counter: decorator, SCSS, and a typed event.
counter.scss
:host {
display: flex;
align-items: center;
gap: 12px;
}
button {
padding-block: 10px;
padding-inline: 20px;
font-size: 16px;
border-radius: 4px;
cursor: pointer;
}
span {
min-inline-size: 40px;
text-align: center;
}counter.lit.ts
import { Component, KasstorElement } from "@genexus/kasstor-core/decorators/component.js";
import { Event, type EventEmitter } from "@genexus/kasstor-core/decorators/event.js";
import { html } from "lit";
import { property } from "lit/decorators.js";
import styles from "./counter.scss?inline";
/**
* Simple counter with increment/decrement and a typed change event.
* @access public
*/
@Component({ tag: "app-counter", styles })
export class AppCounter extends KasstorElement {
/** Current count value. */
@property({ type: Number }) count = 0;
/** Fired when the count changes; detail contains the new value. */
@Event() protected countChanged!: EventEmitter<number>;
#onIncrement = () => {
this.count++;
this.countChanged.emit(this.count);
};
#onDecrement = () => {
this.count--;
this.countChanged.emit(this.count);
};
override render() {
return html`
<button @click=${this.#onDecrement}>−</button>
<span>${this.count}</span>
<button @click=${this.#onIncrement}>+</button>
`;
}
}Usage in another Lit component
html`<app-counter
count="0"
@countChanged=${(e: CustomEvent<number>) => console.log(e.detail)}
></app-counter>`;If you are starting from scratch, create an index.html at the project root and a src/main.ts that imports your component.
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>My App</title>
<script type="module" src="./src/main.ts"></script>
</head>
<body>
<app-counter count="0"></app-counter>
</body>
</html>src/main.ts
Importing the component file runs @Component, which registers <app-counter> as a custom element. Without this import the browser does not know the tag and it will not render.
import "./components/counter.lit.js";Ensure your project is configured to use ES modules and includes a development script:
{
"type": "module",
"scripts": {
"dev": "vite --open"
}
}Run:
npm run devVite will start the dev server with HMR enabled and open your browser automatically.
-
Build tool: Kasstor is designed to work best with Vite. The plugin provides HMR, build integration, and optional performance insights; you do not need to install
@genexus/kasstor-buildor@genexus/kasstor-insightsseparately when using it. Other bundlers can use@genexus/kasstor-core(and related packages) but without the integrated HMR and plugin features. -
Base class: Components using
@Componentmust extendKasstorElement(notLitElementdirectly). -
Tag name: The component
tagmust be a valid custom element name with a hyphen (e.g.app-counter,my-button). -
File naming: For HMR and build analysis, use the
.lit.ts/.lit.jsconvention; the Vite plugin can be configured to use other patterns if needed.
| Package | Description |
|---|---|
| @genexus/kasstor-core | Core runtime: decorators (@Component, @Event, @Observe), directives (lazyLoad, renderByPlatform), and KasstorElement base class for Lit components. |
| @genexus/vite-plugin-kasstor | Vite plugin: HMR for .lit.ts components and SCSS, build integration, and optional performance insights. No need to install kasstor-build or kasstor-insights when using this plugin. |
| @genexus/kasstor-signals | Reactive signals and computed values; optional state layer for Lit. Use the watch directive in templates for pin-point updates—only the bound parts re-render when a signal changes, without triggering a full component re-render. |
| @genexus/kasstor-webkit | Shared utilities: internationalization (i18n), array helpers, typeahead, and frame-sync helpers for apps and component libraries. |
| @genexus/kasstor-insights | Performance monitoring and benchmarking for Lit apps. Included via the Vite plugin when insights option is enabled; no need to install separately when using Vite. |
| @genexus/kasstor-build | Library analysis and optional file generation (types, readmes). Used by the Vite plugin under the hood; no need to install separately when using Vite. |
Install only what you need; @genexus/kasstor-core plus the Vite plugin (@genexus/vite-plugin-kasstor) is the usual starting point.
git clone https://github.com/genexuslabs/kasstor
cd kasstor
bun iFrom the repo root, bun dev runs all package dev builds. To build a single package:
-
@genexus/kasstor-build —
bun dev:build -
@genexus/kasstor-core —
bun dev:core -
@genexus/kasstor-insights —
bun dev:insights -
@genexus/kasstor-signals —
bun dev:signals -
@genexus/vite-plugin-kasstor —
bun dev:vite-plugin -
@genexus/kasstor-webkit —
bun dev:webkit
Or run everything at once:
bun devFrom the repo root, bun run build runs all package builds. To build a single package:
-
@genexus/kasstor-build —
bun build:build -
@genexus/kasstor-core —
bun build:core -
@genexus/kasstor-insights —
bun build:insights -
@genexus/kasstor-signals —
bun build:signals -
@genexus/vite-plugin-kasstor —
bun build:vite-plugin -
@genexus/kasstor-webkit —
bun build:webkit
Or run everything at once:
bun run buildbun run test- Unit only:
bun run test:unit - E2E only:
bun run test:e2e - For tests that run in both browser and Node, use the
.shared-test.tsextension.
Kasstor is open source and we appreciate issue reports and pull requests. See CONTRIBUTING.md for more information.