# hybrids: Web Components Without a Build Step

> hybrids is a JavaScript framework that defines custom elements from plain objects and pure functions, adding a store, a router, a layout engine and localization on top of the Web Components API. It is for teams that want app-scale structure without a compiler in the loop.

**hybridsjs/hybrids** — Extraordinary JavaScript UI framework with unique declarative and functional architecture

- Repository: https://github.com/hybridsjs/hybrids
- Website: https://hybrids.js.org
- Stars: 3,181 · Forks: 91
- Language: JavaScript
- License: MIT
- Published: 2026-09-24 · Updated: 2026-09-24 · Language: en
- Canonical page: https://hysenlabs.com/projects/hybridsjs-hybrids

## The problem hybrids solves: app structure on top of custom elements

The Web Components API gives you custom elements, shadow DOM and templates, but it stops there. It has no opinion about where state lives, how views are addressed by URL, how styles are shared between components, or how strings get translated. Teams that pick the platform directly end up assembling those pieces themselves, and the assembly is where most of the maintenance cost sits.

hybrids is aimed at that gap. The README describes it as a framework for "creating client-side web applications, UI components libraries, or single web components with unique mixed declarative and functional architecture." The audience is narrow and specific: developers who want to ship real elements that work in any HTML page, but who also want the conveniences a framework normally provides. A component library author and an application author are both served by the same definition format, which is not true of most frameworks in this space.

The architectural bet is that a component does not need a class. In hybrids a component is a plain object with a tag name, some property descriptors, and a render function. That object is handed to define(), and the framework wires it to a custom element under the hood. The README's footnote is honest about the limits of the purity claim: it applies to the component definition, while side effects attached to event listeners may still mutate the host element.

## How the component model, store and router fit together

The mechanism is descriptor-based. Each key on the component object becomes a property on the element, and hybrids decides how to treat it based on its value. A primitive like count: 0 becomes a reactive property with a default. A function becomes a computed value derived from the host. The render function receives the host and returns a tagged template literal from the html export.

Event handlers are wired through the same template syntax. In the README's counter example, onclick is bound to a plain function that takes the host and increments host.count. There is no synthetic event system and no binding helper; the function mutates the host directly and the framework re-renders. This is the trade-off the library makes in exchange for not having a class instance to reason about.

The store is a separate module built on the same descriptor idea. A model is an object whose keys describe fields, with id: true marking the identifier. The connection to a backend is declared under a computed key, store.connect, which takes an object with a get function. Components then declare a property as store(User) and read the result through three helpers: store.pending, store.error and store.ready. The README's example renders a loading string while pending, an error string on failure, and the user's name once ready. The README also lists async external storages, relations and offline caching as store features.

The router inverts the usual URL-first model. Instead of matching a path to a component, you declare a stack of views on the component itself under router.connect, and the router renders that stack. The README states this makes URLs optional and gives out-of-the-box support for dialogs and protected views. Links are generated with router.url(Details) rather than written by hand. That is a real difference in kind from path-matching routers, and it means the URL structure follows the view graph rather than the other way around.

## Installing hybrids and defining a first element

The package is published on npm as hybrids, and package.json declares "type": "module", so it is ESM-only and cannot be require()d from CommonJS without a loader. The engines field requires Node 18 or newer. Install it as a normal dependency:

```bash
npm install hybrids
```

A first component needs two imports, html and define. The README's counter example is the shortest complete program:

```javascript
import { html, define } from "hybrids";

function increaseCount(host) {
  host.count += 1;
}

define({
  tag: "simple-counter",
  count: 0,
  render: ({ count }) => html`
    <button onclick="${increaseCount}">
      Count: ${count}
    </button>
  `,
});
```

After that module runs, the element is registered. Use it in HTML like any other custom element, and the count attribute seeds the initial value:

```html
<simple-counter count="42"></simple-counter>
```

What you should see is a button reading "Count: 42" that increments on click. Because define() registers a real custom element, the same module works when the markup comes from a server-rendered page rather than from a JavaScript entry point. That is the property most frameworks cannot offer, and it is the reason to try hybrids before anything else.

## Layout engine and localization: the parts that are not in the README's quick look

Two features get a short section each in the README and a full page in the documentation. The layout engine lets you write layout attributes directly in templates, for example layout="column center gap:2" on a template element, or layout="grow grid:1|max" on a child. The README claims this works even without shadow DOM while still keeping style encapsulation, and shows a responsive variant with layout@768px="hidden" to hide a footer at small widths. That breakpoint-in-attribute syntax is unusual and worth reading the documentation for before using it in production, because the README does not explain how the generated rules are scoped.

Localization works by wrapping the component's rendered strings. You call localize with a locale code and a dictionary keyed by the original template text, including its interpolation placeholders. The README's example maps "Hello ${0}!" to "Witaj ${0}!" for Polish. The README also states there is a CLI tool for extracting messages from source code, and package.json confirms a bin entry named hybrids pointing at cli/index.js. The README additionally mentions dynamic messages with plural forms and HTML content, and using messages outside a template context, but does not show those APIs.

The CLI is the one piece of hybrids that runs outside the browser. Because it is declared as a bin, it is available after install as the hybrids command, and the repository has a dedicated test script for it (npm run test:cli). What the extraction produces is not described in the README, so check the CLI documentation before wiring it into a translation pipeline.

## Where hybrids is the wrong choice

The most concrete limitation is that the framework's value is inseparable from the platform it targets. Custom elements have a well-known cost: registration is global, tag names cannot be unregistered, and two versions of hybrids on the same page will both try to define the same tags. Nothing in the README addresses multiple-version coexistence, and the exports map in package.json exposes a single entry point, so a page loading hybrids from two bundle versions is a situation the project does not appear to design for.

The second limitation is documentation depth. The README is a tour, not a reference. The store section shows one get function and three state helpers; it does not show how a connect adapter handles writes, how relations resolve, or what offline caching actually stores. Those answers live in the documentation site at hybrids.js.org, and the README explicitly defers to it. If you are evaluating hybrids from the repository alone, you will not find enough to judge the store.

Third, the pure-function framing has an edge. Because handlers receive the host and mutate it directly, there is no immutable data flow to inspect and no time-travel debugging story. Teams that rely on tracing state changes through a store will find less to trace here. The README's own footnote concedes that side effects attached to listeners may mutate the host, which is exactly the pattern the counter example demonstrates.

Finally, if your application is a single interactive widget on an otherwise static page, hybrids is more machinery than the task needs. The component model alone is small, but the store, router and layout engine are the reason the package exists, and you would be carrying them unused.

## How hybrids differs from Lit and from React

Lit is the closest comparison, since both compile to custom elements. Lit components are classes extending LitElement, with reactive properties declared as class fields and templates written with the same tagged-literal syntax. hybrids replaces the class with a plain object and pure functions, and its property semantics come from the shape of the descriptor rather than from a decorator or a static properties block. The practical difference shows up in composition: a hybrids component can be spread, cloned and generated as data, which a class body cannot.

React differs at a more basic level. React renders into a tree it owns and reconciles a virtual DOM; hybrids registers elements with the browser and lets the browser own the tree. That means a hybrids component can be dropped into a page that has no build step and no React root, while a React component cannot. The cost is that hybrids inherits every constraint of the platform, including global tag registration, and it has no equivalent of React's ecosystem of state libraries, dev tools and component kits. The README lists a Gitter channel and the GitHub issue tracker as the support channels, which tells you the community footprint is small.

Vue and Svelte sit between these poles: both compile templates and both can emit custom elements, but neither makes the custom element the primary unit of composition the way hybrids does. If you specifically want the browser's element registry to be your component registry, hybrids is the more direct expression of that idea.

## Maintenance, licensing and what an upgrade costs

The repository is not archived, and the last push was on 2026-09-17. The most recent release listed is v9.1.23 on 2026-08-07, while package.json on the default branch reads version 9.2.0, so the branch is ahead of the latest tagged release. The two entries before that, v9.1.22 and v9.1.21, are both dated 2026-01-07 within a minute of each other, which looks like a republish rather than two separate changes. Treat the release cadence as irregular and pin a version.

The licence is MIT, declared both in the README and in the license field of package.json. MIT permits commercial use, modification and redistribution provided the copyright notice and permission notice are retained. That is a summary of the licence text, not legal advice; read LICENSE in the repository if the distinction matters to your organisation.

Upgrade cost is dominated by the ESM-only packaging. There is no CommonJS build in the exports map, so a project still on require() has to move to ESM or add a loader before it can adopt hybrids at all. The engines field pins Node 18 as the floor, which is a low bar now but rules out older CI images. Beyond that, the surface area is small: the public API is the named exports from src/index.js plus the hybrids binary, and the types are shipped in types/index.d.ts, so npm run test:typescript in the repository is the same check you would want in your own project after an upgrade.

## Conclusion

Adopt hybrids if your team already writes custom elements or wants to, and you are willing to accept that the framework's own documentation is the only complete reference for the store and router. Do not adopt it if you need a component model that survives a framework migration without touching markup, or if you expect a large third-party ecosystem of wrappers and starters. Before committing, verify three things against the version you install: that the store.connect adapter you need is covered in the Store section of the docs, that your bundler resolves the exports map in package.json, and that the CLI produces a message catalogue your localization pipeline can consume.

## FAQ

### How do you use hybrids in a project?

Install the hybrids package from npm, import html and define, and pass a plain object with a tag, properties and a render function to define(). That registers a real custom element you can place in HTML, as the README's simple-counter example shows.

### Is hybrids a real framework?

Yes. It is published on npm as hybrids, its source lives in the hybridsjs/hybrids repository, and its documentation is at hybrids.js.org. The README describes it as a JavaScript framework for client-side web applications, component libraries and single web components.

### Is hybrids worth adopting?

It is worth it if you want app-scale features such as a store, router, layout engine and localization while keeping components as real custom elements usable in any HTML page. It is less suitable if you need a CommonJS build, since package.json declares "type": "module", or if you need the store and router APIs fully documented in the README rather than on the documentation site.

## Sources

- [hybridsjs/hybrids on GitHub](https://github.com/hybridsjs/hybrids)
- [License: MIT](https://github.com/hybridsjs/hybrids/blob/main/LICENSE)
- [Project website](https://hybrids.js.org)
- [README](https://github.com/hybridsjs/hybrids/blob/main/README.md)
- [Releases](https://github.com/hybridsjs/hybrids/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/hybridsjs-hybrids
