Set up
Skills are installed by placing their folder where your agent loads skills.
The download comes from github.com/yw662/rikka itself, not from our server, so what you get is exactly what its author published.
Install instructions come from the README
README
synced Oct 22, 2026🌠 Rikka
Modern Web Components Toolkit · Zero Dependencies · TC39 Signals · Full TypeScript
Live Demo · Why Rikka? · Quick Start · API Reference
🤔 Why Rikka?
Three things drove this project:
-
Reactivity should be a platform standard Rikka is built on TC39 Signals (Stage 1) — a browser-vendor-supported standardization effort. When it lands in the spec, Rikka can drop the polyfill without changing your code. No proprietary reactivity, no framework-specific primitives.
-
TypeScript-first, not TypeScript-adjacent Every API in Rikka is designed for full type inference — signals, DOM helpers, custom element attributes and events. No
anyescapes, no manual type annotations needed. The types don't just exist; they carry their weight. -
LLMs need good docs too In 2026, a meaningful percentage of frontend code is written by AI agents. Rikka ships with
llms.txtandskills/documentation specifically for LLMs. Tested in Cursor: AI writes correct Rikka code on the first try.
Rikka is designed for developers who value correctness and standards, cross-framework component libraries, and AI-assisted development.
✨ Features
- 🔥 Reactive State — TC39 Signals-based reactivity (
signal,computed,effect) - 🎨 DOM Utilities — Hyperscript
h(), tag helpers,For/Show/Switchcontrol flow - 🧩 Custom Elements — Function-based
defineElement()with Shadow DOM, attributes, events - 🌐 Resource-Oriented Server —
rikka-site: declarative resource tree, content negotiation (JSON/CSV/CBOR/HTML/protobuf), edge-ready - 🎮 Live Playground — Real-time code editor component for interactive demos
- ⚡ Zero Runtime — No framework abstraction, call stacks go straight to the browser
- 🔒 Type-Safe — Full TypeScript inference for signals, DOM, and components
- 📦 Tree-Shakeable — Each package is < 15KB gzipped, use only what you need
🚀 Quick Start
Installation
Packages are published to npm. No extra registry configuration is needed.
# Install all packages
pnpm add @takanashi/rikka-signal @takanashi/rikka-dom @takanashi/rikka-elements
# Or install individually
npm install @takanashi/rikka-signal
Counter Example
import { defineElement, NumberAttr } from "@takanashi/rikka-elements";
import { div, p, button } from "@takanashi/rikka-dom";
const MyCounter = defineElement("my-counter", {
shadow: { mode: "open" },
attributes: {
count: { ...NumberAttr, default: 0 },
},
render() {
return div(
p("Count: ", this.$count),
button({ onclick: () => this.count++ }, "+"),
button({ onclick: () => this.count-- }, "-"),
);
},
});
// Usage in your app
const counter = document.createElement("my-counter");
document.body.appendChild(counter);
// Or set initial count via attribute
counter.setAttribute("count", "10");
🌐 Live Demo
Try Rikka in your browser: https://yw662.github.io/rikka/
📦 Packages
| Package | Version | Description | Size (gzip) |
|---|---|---|---|
@takanashi/rikka-signal | Reactive primitives based on TC39 Signals | ~1.6 KB | |
@takanashi/rikka-dom | DOM utilities — h(), tag shortcuts, control flow | ~6.3 KB | |
@takanashi/rikka-elements | Function-based custom element definition | ~3.3 KB | |
@takanashi/rikka-site | Resource-oriented server framework with content negotiation | — | |
@takanashi/rikka-live-playground | Live code editor Web Component | ~4.2 KB |
📖 API Reference
@takanashi/rikka-signal — Reactive Primitives
Based on TC39 Signals proposal (Stage 1).
import { signal, computed, effect } from "@takanashi/rikka-signal";
// Basic signals
const count = signal(0);
const doubled = computed(() => count.get() * 2);
effect(() => console.log(doubled.get())); // 0
count.set(5); // logs: 10
API Surface:
| rikka-signal | TC39 Signals |
|---|---|
signal(v) | new Signal.State(v) |
computed(fn) | new Signal.Computed(fn) |
effect(fn) | Signal.subtle.Watcher + Signal.Computed |
@takanashi/rikka-dom — DOM Utilities
Hyperscript h() function for creating DOM elements. Returns real DOM elements — no virtual DOM, no factory functions.
import { div, p, button, applyChild } from "@takanashi/rikka-dom";
import { signal } from "@takanashi/rikka-signal";
const count = signal(0);
const app = div(
{ style: "display: flex; gap: 0.5rem;" },
p("Count: ", count),
button({ onclick: () => count.set(count.get() + 1) }, "+"),
);
// Mount to DOM
const container = document.getElementById("app");
if (container) applyChild(container, app);
Tag shortcuts: div, p, span, a, button, input, form, ul, ol, li, h1–h6, header, footer, main, section, nav, article, aside, img, table, thead, tbody, tr, th, td, label, select, option, textarea, br, hr, slot, template.
Control Flow: For (list rendering), Show/When/Switch/Match (conditional), ReactiveRange (range helpers).
Styling: css() (CSSStyleSheet), style() (style object).
@takanashi/rikka-elements — Custom Elements
Function-based custom element definition. No base class required — just extend HTMLElement.
defineElement(tagName, config?)
// Simple attribute binding
defineElement("my-input", {
attributes: {
value: StringAttr,
},
});
// With Shadow DOM + render function
defineElement("my-counter", {
shadow: { mode: "open" },
attributes: {
count: { ...NumberAttr, default: 0 },
},
render() {
return div(
button({ onclick: () => this.count++ }, "+"),
span("Count: ", this.$count),
);
},
});
Options:
| Option | Type | Description |
|---|---|---|
shadow | ShadowRootInit | false | Shadow DOM config; false to disable |
styles | CSSStyleSheet | CSSStyleSheet[] | Stylesheets injected into Shadow DOM |
attributes | Record<string, AttributeSpec> | Reactive attributes with defaults |
events | Record<string, EventSpec> | Custom events to dispatch |
methods | Record<string, Function> | Custom methods attached to element instance |
template | HTMLTemplateElement | Template element (mutually exclusive with render) |
render | (this: El) => Element | Render function (mutually exclusive with template). Wrapped in computed — this.count is reactive (coarse-grained). |
Attribute Access: Use this.$count for fine-grained DOM bindings (only the text node updates). Use this.count for logic where re-rendering is acceptable (coarse-grained — whole render re-runs). Both are reactive; render is wrapped in computed so this.count (which calls .get() internally) is automatically tracked.
Events: Use dispatchEventName(value) to emit, onEventName(callback) to listen
Event Helper
import { event, defineElement, NumberAttr } from "@takanashi/rikka-elements";
import { div, button, span } from "@takanashi/rikka-dom";
const MyCounter = defineElement("my-counter", {
shadow: { mode: "open" },
attributes: {
count: { ...NumberAttr, default: 0 },
},
events: {
countChange: event<number>(),
},
render() {
return div(
span("Count: ", this.$count),
button({ onclick: () => this.dispatchCountChange(this.count++) }, "+"),
);
},
});
// Usage
document.body.appendChild(document.createElement("my-counter"));
// Listen for custom events
document.querySelector("my-counter")!.onCountChange = (e) => {
console.log("Count changed to:", e.detail);
};
CSS in Shadow DOM
import { css, adoptStyle } from "@takanashi/rikka-dom";
const styles = css`
.counter {
padding: 16px;
border-radius: 8px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
`;
// In render():
adoptStyle(this.shadowRoot!, styles);
@takanashi/rikka-site — Resource-Oriented Server
A resource-oriented server framework with content negotiation, built on the
same reactive primitives. Define resources as a declarative tree of
ResourceKind instances; the server handles routing, content negotiation
(JSON, JSON-LD, CSV, CBOR, HTML, …), auth, CORS, pagination, and hydration
automatically.
import {
CollectionKind,
ItemKind,
site,
FileSystemKind,
type Schema,
} from "@takanashi/rikka-site";
import type { Repr } from "@takanashi/rikka-site";
// Resources are classes extending abstract base kinds.
// Extend a Kind, then mount an instance in the site tree.
class Articles extends CollectionKind {
schema: Schema = { type: "object" };
async list(): Promise<Repr> {
return { content: [{ id: 1, title: "Hello" }] };
}
async create(ctx): Promise<Repr> {
const body = await ctx.json(); // streaming body → parsed JSON
const article = { id: nextId++, ...body };
return { content: article, meta: { location: `./${article.id}` } };
}
}
class Article extends ItemKind<{ id: string }> {
async content(ctx): Promise<Repr> {
return { content: articles[ctx.params.id] };
}
}
// Static files — FileSystemKind is a concrete class, pass config to the constructor
const app = site({
articles: new Articles(),
":id": new Article(),
"assets/": new FileSystemKind({ root: "./public" }),
});
// Same URL serves multiple formats:
// GET /articles → HTML page
// GET /articles?accept=json → JSON
// GET /articles?accept=csv → CSV
// GET /articles?accept=cbor → CBOR binary
Abstract class hierarchy: ResourceKind (abstract base) → CollectionKind,
ItemKind, SingletonKind, ReadOnlyKind, ActionKind (abstract — extend and
instantiate). Each kind constrains the HTTP semantics (allowed methods, status codes).
The site tree stores ResourceKind instances, cloned per request via Object.create.
Concrete resource adapters live in separate packages:
@takanashi/rikka-resource-filesystem (FileSystemKind — static files + WebDAV) and
@takanashi/rikka-resource-database (DatabaseKind — schema-driven CRUD).
Streaming bodies: Request and response bodies are
ReadableStream<Uint8Array>. Use await ctx.json() to parse JSON,
await ctx.text() for text, or read the raw stream directly.
Schema types: SchemaObject, SchemaArray, SchemaRaw ({ type: "raw", mime: string } for opaque binary), and schema-based formats like protobuf
(protobuf(schema) returns a transformer).
Handlers return Repr: All handlers explicitly return
Repr | Promise<Repr> — no auto-wrapping. Repr is { content, meta?, links? }.
Transformers: Value→Raw pipeline with built-in JSON, JSON-LD, CSV,
text, CBOR (binary, RFC 8949), and HTML. Binary formats produce Uint8Array
that flows through the pipeline without string conversion.
Edge-ready: Adapters for Cloudflare Workers/Pages, Deno Deploy, Vercel
Edge, and Node.js. See utils/rikka-site/README.md
for full docs.
Design philosophy — rikka-site is resource-oriented, not route-oriented. A URL identifies a resource (a noun); HTTP methods are verbs on that resource. The site tree is a declarative nesting of ResourceKind instances — keys become URL segments ("users", ":userId", ":path*" for catch-all), so there is no routing boilerplate. Each Kind is both config and factory: the tree holds the Kind (template); per-request, kind.resolve(params) creates a Resource carrying params and path. Resources are HTTP-agnostic — they return a Repr ({ content, meta }), and the server infers transport concerns (status code, Content-Type, Location, Allow) from the Repr's shape, meta, and the resource's Kind. Content negotiation is a pluggable transformer pipeline (Value → Value → Raw → Raw), not a hardcoded if (accept === "json") switch.
RESTful by construction — Each Kind constrains the HTTP semantics of its resources: CollectionKind → GET/POST, ItemKind → GET/PUT/PATCH/DELETE, SingletonKind → GET/PUT/PATCH, ReadOnlyKind → GET only, ActionKind → POST only. Unsupported methods auto-return 405 with an Allow header derived from the methods actually overridden on the resource. One URL serves many representations via ?accept=json|csv|cbor|jsonld|protobuf|html (explicit) or the Accept header with q-values (standard negotiation). Status codes are inferred from the Repr: 201 for POST + meta.location, 204 for null content, 302 for redirect (Repr.redirect()), 206 for partial content (Range header / PartialContent). The Location header is auto-set from meta.location, resolved relative to the resource path. All IANA-registered HTTP methods — including WebDAV (PROPFIND/MKCOL/COPY/MOVE), CalDAV, and DeltaV — are present as named methods on every resource; override the ones you support, the rest return 405. JSON-LD hypermedia (@context/@id/@type/@graph) is produced automatically when application/ld+json is requested.
Prefix-independent — Resources don't know or care where they're mounted. meta.location is expressed relative to the resource's own path (./42 means "a child of this resource", ../settings means "a sibling"), and the framework resolves it against the actual request path. The same ArticleKind works at /articles/:id, /api/articles/:id, or /v2/articles/:id with zero code changes — the Location header, JSON-LD @id, and internal links are all derived from the request path. Sub-trees defined once can be mounted at multiple paths without rewriting, which makes resources composable and reusable across sites.
Automatic content negotiation — Resource handlers return a structured Value plus a Schema, and the framework picks the representation. The handler never inspects Accept or branches on output format: the same list() method serves text/html, application/json, application/ld+json, text/csv, application/cbor, and application/x-protobuf (when registered) without a single if. Negotiation follows ?accept=… (explicit override) → Accept header with q-values (standard) → application/json (default for Value content; Raw content uses the resource's meta.type or a path-guessed MIME). Browsers sending Accept: text/html,… get HTML through negotiation. New formats are added by registering a transformer — no changes to existing resources.
🎮 Live Playground
Interactive code editor component for demos and documentation. Registered as <rikka-live-playground> custom element.
HTML
<rikka-live-playground
code="console.log('Hello, Rikka!')"
height="200"
title="Quick Demo"
></rikka-live-playground>
Code can also be provided as element content:
<rikka-live-playground title="Counter Example">
import { signal } from "rikka-signal"; const count = signal(0);
console.log(count.get());
</rikka-live-playground>
Tag Function (h)
import { RikkaLivePlayground } from "@takanashi/rikka-live-playground";
const playground = RikkaLivePlayground.h({
code: `console.log("Hello from h()!")`,
height: "300",
title: "h() Demo",
});
document.body.appendChild(playground);
createElement
import "@takanashi/rikka-live-playground";
const playground = document.createElement("rikka-live-playground");
playground.setAttribute("code", "console.log('Hello!')");
playground.setAttribute("height", "250");
playground.setAttribute("title", "createElement Demo");
document.body.appendChild(playground);
Inside Another Element
import { defineElement } from "@takanashi/rikka-elements";
import { RikkaLivePlayground } from "@takanashi/rikka-live-playground";
import { div } from "@takanashi/rikka-dom";
defineElement("my-demo-page", {
shadow: { mode: "open" },
render() {
return div(
RikkaLivePlayground.h({
code: `import { signal } from "rikka-signal";
const count = signal(0);
console.log(count.get());`,
title: "Signal Demo",
}),
);
},
});
Attributes:
| Attribute | Default | Description |
|---|---|---|
code | "" | Initial code content |
height | "200" | Editor area height (px) |
title | "Example" | Header title |
layout | "vertical" | Editor/preview split direction ("vertical" | "horizontal") |
panel | "both" | Visible panels ("both" | "editor" | "preview") |
theme | "auto" | Color theme ("auto" | "dark" | "light"). auto follows prefers-color-scheme; explicit values pin the theme. The resolved value is also exposed as data-theme on :host for further CSS overrides. |
Events: error — dispatched when compilation or runtime error occurs
Methods: run() — compile & execute code, reset() — restore initial code & re-run, setTheme(t) / toggleTheme() — switch between light and dark. All --pg-* CSS variables are also pushed into the preview iframe, so user code can reference them directly.
🤖 AI Coding with Rikka
If you're an LLM/agent generating rikka code, install the skills first:
npx skills add yw662/rikka
This installs all 14 task-oriented skills (in the agentskills.io format) into your agent's skill directory. Compatible with OpenCode, Claude Code, Codex, Cursor, and 67 more agents.
Manual fallback (if you can't use the CLI):
- Skills index: https://github.com/yw662/rikka/tree/main/skills/rikka/SKILL.md
- llms.txt: https://yw662.github.io/rikka/llms.txt
- Homepage index: https://yw662.github.io/rikka/skills/index.html
- Browse on skills.sh: https://skills.sh
These contain task-oriented guides (reactive-state, dom-creation, signal-binding, custom-element, common-pitfalls, etc.) that explain patterns and the most common LLM-specific mistakes. See also AGENTS.md at the project root.
🏗️ Underlying Standards
TC39 Signals API
Built on the TC39 Signals proposal (Stage 1).
Once the proposal reaches Stage 4 and ships natively, @takanashi/rikka-signal can drop the polyfill with zero API changes.
Web Components
Uses native browser APIs:
- Custom Elements v1 — Define reusable HTML elements
- Shadow DOM v1 — Style encapsulation
- CSS Nesting — Scoped styles without preprocessors
🎯 Design Principles
- No base class — Function-based API, extend
HTMLElementdirectly - Explicit over implicit — No Context API; use
closest()for ancestors - LLM-friendly — All APIs designed for clarity and predictability
- Type-safe — Full TypeScript inference throughout
- Direct DOM — Tag functions return real DOM elements, no virtual DOM abstraction
- Zero dependencies — Core packages have zero runtime dependencies
🛠️ Requirements
- TypeScript 5.0+ (strict mode recommended)
- Browsers supporting Custom Elements, Shadow DOM, CSS Nesting
- Build tool Vite, Rsbuild, or any modern bundler
📊 Quality Metrics
| Metric | Value |
|---|---|
| TypeScript Errors | 0 |
| Unit Tests | 1076 passing |
| Test Coverage | Signal / DOM / Elements / Playground |
| Bundle Size (total) | ~16KB gzip |
| Tree Shakeable | Yes |
| ESM Only | Yes |
| Side Effects | None |
🗺️ Roadmap
- v0.2 (next) — SSR support, more demos, React/Vue integration guides
- v0.5 — Component showcase, official template integrations
- v1.0 — API stable, full documentation site, official design system
See open issues for the full list.
❓ FAQ
Q: How is this different from Lit? A: Rikka is built on TC39 Signals (a proposed standard) rather than a custom reactivity model. It's function-based with no base class, and every API is designed for full TypeScript inference. Lit is more mature with a larger ecosystem — if you need that today, use Lit.
Q: Can I use Rikka in React/Vue/Angular/Svelte?
A: Yes. Rikka produces standard Web Components, which work in any framework. For React, use @lit/react to wrap them.
Q: Is Rikka production-ready? A: The core API is stable and the test suite has 1076 tests. Use it for new projects, evaluate carefully for existing projects.
Q: What about SSR? A: Web Components SSR is evolving. Rikka works well for client-side rendering today. For SSR, follow the relevant TC39/CSSWG discussions.
🤝 Contributing
Rikka is open to contributions. Pick an issue tagged good first issue to start, or open a discussion for bigger changes. We use Changesets for versioning — PRs that change behavior should include a changeset.
🔧 Development
# Clone repo
git clone https://github.com/yw662/rikka.git
cd rikka
# Install dependencies
pnpm install
# Run all checks
pnpm test:all # Full CI pipeline (~35s)
pnpm typecheck # TypeScript check only (~2s)
pnpm test # Unit tests only (~15s)
pnpm browser-test # Browser E2E tests (~15s)
# Start demo site
pnpm demo # http://localhost:3000
🙏 Acknowledgments
- TC39 Signals Proposal — Reactive primitives standard
- signal-polyfill — Signals polyfill implementation
- Rslib — Build tooling
- happy-dom — Test DOM environment