Wely

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.

Why Wely?

Building frontend components today typically means choosing a heavy framework and wiring up a dozen tools. Wely replaces that with a single unified toolkit.

The problem

How Wely solves it

StepWhat you doWhat Wely handles
Configurewely initwely.config.ts (or add manually)Centralized config via ctx.config
Createwely create w-card --props title:StringScaffolds component file and barrel index
Developwely devHot-reloading playground
StyleUse Tailwind classes in templatesCompiled and injected into Shadow DOM
FetchcreateClient({ baseURL })HTTP client with interceptors, timeout
Statectx.resource() / ctx.use(store)Async resources + shared stores
Testwely testVitest with zero extra config
Buildwely buildES + UMD bundles
Exportwely export ../other-project/libCopies output to any folder
Documentwely docsGenerates COMPONENTS.md

One CLI, one config file — from scaffolding to production.

Bundle Size

Wely produces minimal bundles. Runtime includes Lit, our API (defineComponent, store, resource, fetch), and Tailwind CSS. All sizes are minified + gzipped.

BuildSize (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.

Quick Start

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

Dev Mode & Playground

wely dev starts a Vite dev server with hot reload. On first run (when no vite.config exists), the CLI creates the playground files:

FilePurpose
index.htmlPlayground page with #app container
src/playground/main.tsEntry that imports components and renders each automatically
src/styles/tailwind.cssTailwind 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.

Defining a Component

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>

Component Composition

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>
  `
}

Live Demo — PokéAPI

Fetching data with ctx.resource(). Data from PokéAPI.

API Surface

defineComponent(def)

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.

The ctx object

PropertyDescription
ctx.elHost HTMLElement
ctx.propsReadonly attribute-synced properties
ctx.stateAuto-reactive state (mutations trigger re-render)
ctx.actionsBound 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.configRead-only config from wely.config.ts

createClient(config?)

Zero-dependency HTTP client (native fetch). Axios-like: get, post, put, patch, delete, onRequest, onResponse, onError. Interceptors, timeout, query params, JSON.

createResource(fetcher, options?)

Async data primitive. Use via ctx.resource(). Tracks data, loading, error. Methods: fetch(), refetch(), abort(), mutate(), reset().

createStore(def)

Shared reactive state. state: () => ({...}), actions: { name(state, ...args) {...} }. Use ctx.use(store) to subscribe.

Configuration

Define in wely.config.ts with defineConfig(). Read with getConfig(), useConfig(key), or ctx.config. Supports import.meta.env.VITE_*.

Re-exported from Lit

html, css, nothing

devInfo — Dev tools attributes

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.

componentsDir — Components folder

Override the default src/wely-components via package.json:

{
  "wely": { "componentsDir": "src/components" }
}

Used by create, sync, list, docs, build, dev.

CLI

Runs in the current directory. New projects: wely initnpm installwely createwely 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/

Build Output

CommandOutputUse case
wely build (no vite.config)wely.bundle.*.jsConsumer project — runtime + your own components
wely build --chunkswely.chunked.es.js + chunks/*.jsVendor, runtime, components split — cache-friendly
wely build (with vite.config)wely.es.js, wely.umd.jsLibrary — 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.

Styling

Tailwind CSS v4 is integrated:

Minimal setup: wely init + wely build — CLI creates tailwind.css with correct @source on first run. Bundle consumers: no config — Tailwind baked in.

Browser Support

BrowserMinimum
Chrome73+
Edge79+
Firefox101+
Safari16.4+

Custom Elements v1, Shadow DOM, adoptedStyleSheets, Proxy

Portability

Wely components are fully portable — build once, use anywhere. They are standard Web Components; no framework runtime at the consumer side.

AspectBehavior
OutputCustom Elements v1, Shadow DOM — no framework-specific bundle
Consumer runtimeZero Wely runtime — components are plain DOM elements
Drop-inPlain HTML, React, Vue, Angular, Svelte, Astro, Eleventy, any DOM environment
FormatsES module + UMD — bundlers or classic <script>
Deploymentwely export <path> copies output to any project

Same component, same API — works in all environments without modification.

Design Principles

Tech Stack

LayerTool
LanguageTypeScript
RenderingLit (internal)
StylingTailwind CSS v4
Dev / BuildVite
TestingVitest + jsdom
OutputES module + UMD

Project Structure

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)