Define UI components once, as typed data, and render them to vanilla HTML/CSS/JS or to React, Vue, and Svelte components, with styling methodologies (BEM, Tailwind) applied as pluggable extensions.
// button.ts
import { defineTemplate } from '@js-template-engine/types';
export default defineTemplate({
type: 'component',
name: 'Button',
props: {
label: { type: 'string', required: true },
variant: { type: "'primary' | 'secondary'", default: 'primary' },
},
children: [
{
type: 'element',
tag: 'button',
attributes: { class: ['button'], type: 'button' },
conditionalAttributes: [
{
condition: "variant === 'primary'",
attributes: { class: ['button--primary'] },
},
],
events: [{ name: 'click', handler: 'handleClick' }],
children: [{ type: 'text', expression: 'label' }],
},
],
});npx js-template-engine render button.ts # → Button.html
npx js-template-engine render button.ts --framework react # → Button.tsx
npx js-template-engine render button.ts --framework vue # → Button.vue
npx js-template-engine render button.ts --framework svelte # → Button.svelteThe React output, for a taste:
interface ButtonProps {
label: string;
variant?: 'primary' | 'secondary';
}
export function Button(props: ButtonProps) {
const { label, variant = 'primary' } = props;
return (
<button
className={'button' + (variant === 'primary' ? ' button--primary' : '')}
type="button"
onClick={handleClick}
>
{label}
</button>
);
}Tools like Mitosis occupy the "write once, compile everywhere" space by compiling a restricted JSX dialect. This engine takes the opposite approach:
| Mitosis | js-template-engine | |
|---|---|---|
| Authoring surface | Code (a static JSX subset) | Data (typed TS objects / JSON) |
| Intermediate format | Internal, not an authoring format | The data is the format |
| Styling | Out of scope | First-class: BEM, Tailwind, output strategies |
| Default output | Framework targets | Vanilla HTML/CSS/JS, zero extensions required |
Because templates are plain serializable data, any program can assemble, merge, validate, and transform them - a generator, a CMS, a design tool, or an LLM - far more naturally than it could synthesize JSX source. That is what makes scaffold-ui-kit possible: maintain one source of truth, ship component libraries for every framework.
| Package | Purpose |
|---|---|
js-template-engine |
The CLI: render and validate |
@js-template-engine/core |
The engine: validation, HTML/CSS/JS output, extension contract |
@js-template-engine/types |
The template format: TypeScript types + JSON Schema |
@js-template-engine/extension-react |
React function components (.tsx) |
@js-template-engine/extension-vue |
Vue single-file components (.vue) |
@js-template-engine/extension-svelte |
Svelte components (.svelte) |
@js-template-engine/extension-bem |
BEM class contribution (block__element--modifier) |
@js-template-engine/extension-tailwind |
Tailwind utility-class contribution |
HTML-first. process(template) produces working HTML, CSS, and
JavaScript with no extensions involved, a zero-dependency rendering that
doubles as the semantic baseline every framework target follows.
Extensions are passed directly. No registry, no string keys, no magic:
import { process } from '@js-template-engine/core';
import { react } from '@js-template-engine/extension-react';
import { bem } from '@js-template-engine/extension-bem';
const result = process(template, { extensions: [react(), bem()] });
// result.files → [{ path: 'Button.tsx', content: '...' }]The core has zero framework knowledge; framework support lives entirely in extensions, and third-party extensions plug in through the same interface.
Concepts live on nodes. Events, conditions, iterations, slots, and styling are embedded in the template nodes themselves. Eight node types cover the whole format. Dynamic values are JavaScript expressions carried as opaque strings; the engine emits them into the target syntax and never evaluates them.
Unified output strategies. Styles and scripts share one vocabulary -
inline, in-file, separate-file - so the same template can produce a
single self-contained file or separate .css/.js artifacts.
# Render a template from the command line:
npx js-template-engine render button.ts --framework react --styling bemThe getting-started guide walks through the template format - props, expressions, conditionals, iteration, slots, events, styles - and every render target.
scaffold-ui-kit is a separate project built on this engine. It scaffolds a multi-framework component library from a single set of data templates, builds each target, and ships a consumer CLI that copies components into projects with no engine dependency:
npx scaffold-ui-kit my-ui-kitMIT