Lightweight Web Component framework — one defineComponent(), no class syntax, no framework lock-in.
Lit powers rendering internally but is never exposed. Developers interact exclusively through the Wely API. Output is native custom elements — fully portable, they run in plain HTML, React, Vue, Angular, Svelte, anywhere.
Building frontend components today typically means choosing a heavy framework and wiring up a dozen tools. Wely replaces that with a single unified toolkit.
| Step | What you do | What Wely handles |
|---|---|---|
| Configure | wely init → wely.config.ts (or add manually) | Centralized config via ctx.config |
| Create | wely create w-card --props title:String | Scaffolds component file and barrel index |
| Develop | wely dev | Hot-reloading playground |
| Style | Use Tailwind classes in templates | Compiled and injected into Shadow DOM |
| Fetch | createClient({ baseURL }) | HTTP client with interceptors, timeout |
| State | ctx.resource() / ctx.use(store) | Async resources + shared stores |
| Test | wely test | Vitest with zero extra config |
| Build | wely build | ES + UMD bundles |
| Export | wely export ../other-project/lib | Copies output to any folder |
| Document | wely docs | Generates COMPONENTS.md |
One CLI, one config file — from scaffolding to production.
Wely produces minimal bundles. Runtime includes Lit, our API (defineComponent, store, resource, fetch), and Tailwind CSS. All sizes are minified + gzipped.
| Build | Size (min+gzip) |
|---|---|
Runtime only (wely.es.js) | 13 KB |
| 1 component (w-button) | 13.3 KB |
| 2 components (+ w-counter) | 13.6 KB |
| 3 components (+ w-counter-card) | 14 KB |
| 5 components (+ w-pokemon-grid, w-user-list) | 15 KB |
Per-component overhead: ~0.4–0.5 KB for simple components.
Pay for what you use — Wely bundles only what you import. Add one component → ~13 KB. Add five → ~15 KB. No framework runtime at the consumer; output is native Web Components. Tree-shaking keeps the bundle minimal: unused components never land in the final file.
Minimal (new project): Only wely.config.ts + welyjs — no vite.config, no extra deps.
mkdir my-app && cd my-app
wely init
npm install
wely create w-hello --props msg:String
wely build # → dist/wely.bundle.*.js
wely dev # playground at localhost:5173
Full repo (Wely development):
npm install
npm run dev # playground at localhost:5173
npm run build # library → dist/wely.es.js + dist/wely.umd.js
npm run test
npm run test:run
wely dev starts a Vite dev server with hot reload. On first run (when no vite.config exists), the CLI creates the playground files:
| File | Purpose |
|---|---|
index.html | Playground page with #app container |
src/playground/main.ts | Entry that imports components and renders each automatically |
src/styles/tailwind.css | Tailwind entry with @source for component templates |
Auto-rendering: The playground uses getAllComponents() to list every registered component and renders each in its own section. When a component has props defined, the playground shows them (e.g. Props: start: Number, label: String) so you can see which attributes to pass. When you add a new component with wely create, it appears in the playground immediately (HMR). No manual HTML edits needed.
Works everywhere: With vite.config (Wely repo), the existing playground is used. Without it (consumer project), the CLI scaffolds the minimal setup and uses vite.dev.config.ts from the package.
Every component is a plain object passed to defineComponent():
import { defineComponent, html } from 'welyjs'
defineComponent({
tag: 'w-counter',
props: { start: Number },
state() { return { count: 0 } },
setup(ctx) { ctx.state.count = ctx.props.start ?? 0 },
actions: {
increment(ctx) { ctx.state.count++ },
decrement(ctx) { ctx.state.count-- },
reset(ctx) { ctx.state.count = ctx.props.start ?? 0 },
},
render(ctx) {
return html`
<button @click=${ctx.actions.decrement}>-</button>
<span>${ctx.state.count}</span>
<button @click=${ctx.actions.increment}>+</button>
<button @click=${ctx.actions.reset}>Reset</button>
`
},
})
Use it: <w-counter start="5"></w-counter>
Wely components are native Custom Elements — nest them by using the tag name:
render(ctx) {
return html`
<div class="border rounded-lg p-4">
<h3>${ctx.props.title}</h3>
<w-counter start=${ctx.props.start ?? 0}></w-counter>
<w-button label="Action" @w-click=${ctx.actions.onClick}></w-button>
</div>
`
}
ctx.emit('event-name', payload))createStore() + ctx.use(store))Fetching data with ctx.resource(). Data from PokéAPI.
Registers a native custom element. Fields: tag, props, devInfo, styles, state(), actions, setup(ctx), render(ctx), connected(ctx), disconnected(ctx). Actions receive (ctx, event?) — with @input, @click, etc., event.target gives the element.
| Property | Description |
|---|---|
ctx.el | Host HTMLElement |
ctx.props | Readonly attribute-synced properties |
ctx.state | Auto-reactive state (mutations trigger re-render) |
ctx.actions | Bound action map — handlers receive (ctx, event); event.target for the element |
ctx.update() | Manually request re-render |
ctx.emit(event, payload?) | Dispatch CustomEvent |
ctx.resource(fetcher, opts?) | Async resource bound to lifecycle |
ctx.use(store) | Subscribe to shared store |
ctx.config | Read-only config from wely.config.ts |
Zero-dependency HTTP client (native fetch). Axios-like: get, post, put, patch, delete, onRequest, onResponse, onError. Interceptors, timeout, query params, JSON.
Async data primitive. Use via ctx.resource(). Tracks data, loading, error. Methods: fetch(), refetch(), abort(), mutate(), reset().
Shared reactive state. state: () => ({...}), actions: { name(state, ...args) {...} }. Use ctx.use(store) to subscribe.
Define in wely.config.ts with defineConfig(). Read with getConfig(), useConfig(key), or ctx.config. Supports import.meta.env.VITE_*.
html, css, nothing
When enabled (default), each component gets data-wely-version and data-wely-mounted attributes — visible in browser DevTools. Version comes from defineConfig({ version: '1.0.0' }) or per-component override.
// Default: attributes added
defineComponent({ tag: 'w-card', render: () => html`...` })
// Disable
defineComponent({ tag: 'w-secret', devInfo: false, render: () => html`...` })
// Override version per component
defineComponent({ tag: 'w-card', devInfo: { version: '2.0.0' }, render: () => html`...` })
Set version in wely.config.ts for global devInfo version.
Override the default src/wely-components via package.json:
{
"wely": { "componentsDir": "src/components" }
}
Used by create, sync, list, docs, build, dev.
Runs in the current directory. New projects: wely init → npm install → wely create → wely build. Wely brings Vite and Tailwind — no extra deps.
# Setup
wely init # wely.config.ts + package.json
npm install
# Components
wely create w-card --props title:String
wely create w-user --props name:String,age:Number --actions refresh,delete
wely sync
wely list
wely docs
wely docs --out docs/api.md
# Build (minimal project: bundle by default; full repo: library)
wely build
wely build --bundle
wely build --chunks # vendor, runtime, components split
wely build --all
wely build --export ../app/public/vendor/wely
# Deploy
wely export ../other-project/lib
wely export ./out --no-build
wely export ../lib --clean
# Dev & test
wely dev
wely test
wely test --run
# GitHub Pages
wely page # → docs/
| Command | Output | Use case |
|---|---|---|
wely build (no vite.config) | wely.bundle.*.js | Consumer project — runtime + your own components |
wely build --chunks | wely.chunked.es.js + chunks/*.js | Vendor, runtime, components split — cache-friendly |
wely build (with vite.config) | wely.es.js, wely.umd.js | Library — runtime only (packaging) |
Bundle: drop-in script. Library: import { defineComponent, html } from 'welyjs'.
Chunked build: wely build --chunks splits output into vendor (Lit), runtime (Wely API), and components. Use <script type="module" src="wely.chunked.es.js"></script>. Copy the entire dist/ folder (including chunks/) when deploying.
Size optimization: Default uses esbuild minify. For smaller bundles, use a custom vite.config with minify: 'terser' and terserOptions: { compress: { drop_console: true } } — terser yields ~5–15% smaller output.
Tailwind CSS v4 is integrated:
tailwind.css normallystyles: css`:host { ... }`Minimal setup: wely init + wely build — CLI creates tailwind.css with correct @source on first run. Bundle consumers: no config — Tailwind baked in.
| Browser | Minimum |
|---|---|
| Chrome | 73+ |
| Edge | 79+ |
| Firefox | 101+ |
| Safari | 16.4+ |
Custom Elements v1, Shadow DOM, adoptedStyleSheets, Proxy
Wely components are fully portable — build once, use anywhere. They are standard Web Components; no framework runtime at the consumer side.
| Aspect | Behavior |
|---|---|
| Output | Custom Elements v1, Shadow DOM — no framework-specific bundle |
| Consumer runtime | Zero Wely runtime — components are plain DOM elements |
| Drop-in | Plain HTML, React, Vue, Angular, Svelte, Astro, Eleventy, any DOM environment |
| Formats | ES module + UMD — bundlers or classic <script> |
| Deployment | wely export <path> copies output to any project |
Same component, same API — works in all environments without modification.
defineComponent() call, no classesactions pattern separates logic from templates| Layer | Tool |
|---|---|
| Language | TypeScript |
| Rendering | Lit (internal) |
| Styling | Tailwind CSS v4 |
| Dev / Build | Vite |
| Testing | Vitest + jsdom |
| Output | ES module + UMD |
src/
runtime/ defineComponent, config, registry, fetch, resource, store
components/ w-counter, w-button, w-counter-card, w-pokemon-grid, w-user-list
styles/ tailwind.css
playground/ main.ts
index.html Playground
wely.config.ts App config
vite.config.ts Vite + Vitest
page/ GitHub Pages landing (this page)