package | bundle size (plain -> gzip) |
---|---|
@lift-html/tiny | |
@lift-html/core | |
@lift-html/solid (includes solid-js) | |
@lift-html/solid (includes solid-js and useAttributes) |
Lift HTML is a new way to build your JavaScript applications, especially if all that you know is isomorphic libraries like React, Vue, Preact, Angular, Lit, Svelte, SolidJS, Ember and Qwik. It's going to be less new if you have experience with primerally server-side frameworks like Rails, Django, Laravel. Those are built with expectation that you are going to write a lot of HTML+CSS and plop a couple of script tags here are there.
With lift-html you can start with as low overhead as 150 bytes
(@lift-html/tiny
) to get type safety when declaring your custom elements and
simplified API (I tested that every web component used in
Astro website could be built with @lift-html/tiny
).
If you jump up to @lift-html/core
, in the less than 600 bytes you get HMR (Hot
Module Replacement) support, full support of web components features like
formAssociated
and observedAttributes
with type safety and nice API: init
and onCleanup
callbacks instead of constructor
, connectedCallback
,
adoptedCallback
, disconnectedCallback
(I yet to find an example of a web
component that can't be written with @lift-html/core
).
After this you may enjoy a buffet of opt-in (and tree-shakeable) features like
@lift-html/incentive
that gives you
Hotwire Stimulus or
GitHub Catalyst-like API
to work with targets inside of your components. Or various integrations to make
your attributes reactive like @lift-html/solid
that also gives you ability to
use APIs like createSignal
and createEffect
inside of your components;
@lift-html/svelte
that gives you ability to use Runes like state$
and
effect$
inside of your components.
lift-html is a tiny library for building HTML Web Components, components that are meant to enhance existing HTML on the page instead of rendering it on the client or hydrating it.
Code for liftHtml
is public domain see more in the Vendoring section.
<!-- @lift-html/solid -->
<my-button>
<button disabled>
Loading...
</button>
</my-button>
<script type="module">
import { liftSolid } from "https://esm.sh/@lift-html/solid";
import { createEffect, createSignal } from "https://esm.sh/solid-js";
// define a custom element
const MyButton = liftSolid("my-button", {
init() {
const button = this.querySelector("button");
if (!button) throw new Error("<my-button> must contain a <button>");
button.disabled = false;
const [count, setCount] = createSignal(0);
button.onclick = () => setCount(count() + 1);
createEffect(() => {
button.textContent = `Clicks: ${count()}`;
});
},
});
</script>
via codepen, total code size 3.41kb gzip and with no-build 8.94kb
<!-- @lift-html/core -->
<my-button>
<button disabled>
Loading...
</button>
</my-button>
<script type="module">
import { liftHtml } from "https://esm.sh/@lift-html/core";
// define a custom element
const MyButton = liftHtml("my-button", {
init() {
const button = this.querySelector("button");
if (!button) throw new Error("<my-button> must contain a <button>");
button.disabled = false;
let count = 0;
updateCount();
button.onclick = () => {
count++;
updateCount();
};
function updateCount() {
button.textContent = `Clicks: ${count}`;
}
},
});
</script>
via codepen, total code size 563 bytes gzip and with no-build 574 bytes gzip
Web Components are a browser primitive, kind of like document.createElement
.
You are not expected to use them directly because of the amount of boilerplate
and DX issues. But similarly to document.createElement
, there are plently of
usecases that require you to get your hands dirty with the native API.
lift-html
is an attempt to create tiny wrapper around web components that
solves enough of DX issues without introducing a completely new paradigm. If I
would put it in a single sentence, it would be: "When using lift-html I don't
want to feel like I'm writing a component (web or otherwise), I just want to
write HTML, CSS and JS/TS".
To achieve that we had to depart a bit from the web components vibe. Namely:
-
we don't use class syntax
Classes make your code overly concerned with lifecycles. Imagine a scenario of adding some sort of computed property, now with classes you start by adding a property on the class to store a value, adding a getter on the class to access it, adding a callback method to react and update that value and a call in constructor to hook that callback to the lifecycle. All of those are going to be spread over your whole class, mixing with other features together in buckets (class properties, class methods, getters, setters, public APIs, callbacks, constructor initializers, connectedCallback initializers, destructors) and your only option to share the logic is to use Mixins, which have their issues with performance and type safety. With function syntax I can haveconst thing = myThing(this)
and for the most part not worry about howmyThing
is implemented. This is pretty much the same argument as was used to introduce hooks in React. This does come with a bit of theoretical loss of performance and flexibility compared to writing everything by hand, but that option is always there if you need it. On a side node, if you like classes and typescript be sure to check out this approach by Joe Pea (author of @lume/element) we allow you to re-define component implementation at runtime
One thing that we are changing around web components is that we allow you to register your components multiple times. The main use case for that is HMR, since it will allow you to re-run your `init` function for a component that is already on the page. One obvious limitation that still stands is that you can't change observed attributes or formAssociated values. The details will depend on what sort of dev server you are using, for more information read HMR section of the docs.
But otherwise if you are looking at vanilla HTML Web Component and lift-html one you might notice that they are 100% the same.
ThemeToggle Example: vanilla vs lift-html (click to expand)
Here's vanilla HTML Web Component from Astro source code:
<script>
class ThemeToggle extends HTMLElement {
constructor() {
super();
const button = this.querySelector('button')!;
/** Set the theme to dark/light mode. */
const setTheme = (dark: boolean) => {
document.documentElement.classList[dark ? 'add' : 'remove']('theme-dark');
button.setAttribute('aria-pressed', String(dark));
};
// Toggle the theme when a user clicks the button.
button.addEventListener('click', () => setTheme(!this.isDark()));
// Initialize button state to reflect current theme.
setTheme(this.isDark());
}
isDark() {
return document.documentElement.classList.contains('theme-dark');
}
}
customElements.define('theme-toggle', ThemeToggle);
</script>
Now compare that to one using lift-html:
<script>
import { liftHtml } from "@lift-html/core";
liftHtml("theme-toggle", {
init() {
const button = this.querySelector('button')!;
/** Set the theme to dark/light mode. */
const setTheme = (dark: boolean) => {
document.documentElement.classList[dark ? 'add' : 'remove']('theme-dark');
button.setAttribute('aria-pressed', String(dark));
};
// Toggle the theme when a user clicks the button.
button.addEventListener('click', () => setTheme(!isDark()));
// Initialize button state to reflect current theme.
setTheme(isDark());
function isDark() {
return document.documentElement.classList.contains('theme-dark');
}
}
});
</script>
If you can hardly notice the difference, that's the point. Apart from a couple
super
and this
missing, the code is the same. The biggest difference is that
we are using init
instead of constructor
because it's actually considered
wrong to access DOM from the
constructor. And if you are a pedant you also noticed that isDark
is now just
a function instead of a method.
Another note on implementation, this code is technically safe to run on the
server because it doesn't reference global HTMLElement
class and
customElements.define
method. It's also safe to run liftHtml
multiple times,
the last implementation will win.
But the same can't be said about components using traditional web components
frameworks. Your lift-html
will likely look differently compared to web
component authored in another web component framework. This is intentional, if
you want to use framework components, just use framework components. For example
a lot of code in other web components frameworks will have parts that look like
this:
class X extends MyFramework {}
and@element
ordefine()
- all that is done insideliftHtml
@attribute
- you can passobservedAttributes
toliftHtml
and use helpers likeuseAttributes
with your favorite flavor of reactivityshadow: true
- you can just directly callthis.attachShadow({mode: 'open'})
ininit
if you need it@state
- that part is outside of the scope of@lift-html/core
, you are free to build something of your own or use@lift-html/solid
or@lift-html/signal
for thatstatic styles = ...
just use<style>
tagrender() { ... }
render where? in HTML Web Components you get your markup generated on the server, so it's already rendered and you can just work with it withthis.querySelector
So as you can see we are not taking over any of the concerns of frameworks like
asset management, templating or state management. The assumption here is that
you are already using some sort of backend service that generated your markup
and lift-html
is your solution to add interactivity to it, and you are free to
choose what flavor of reactivity (if any) you need for that. This does mean that
if the types of interactivity you are building requires a framework you are
probably going to use lift-html
just as a loader for those framework
components, see more in Interoperability section.
TODO
You can render lift-html
components using your favorite framework with
type-safety. We recommend creating framework specific wrapper for your
components to generate light dom markup.
Packages to better integrate with other frameworks are planned, goal here is for
you to be able to make lift-html
component wrappers out of existing React and
Solid components. The use cases:
- lazy load React component once some condition is met (like user scrolls to it)
- render Solid component (with SSR) as
lift-html
component so that you can share it on npm and are able to render it from any (js based) server like Astro or React.
TODO
HMR in lift-html is very simple. Every time you register a component with
liftHtml
it will overwrite the previous implementation. So use whatever live
reloading tool you like, copy component into the dev tools, whatever it should
all work. In the example repo see Vite and Astro examples using
import.meta.hot?.accept();
.
TODO
We are planning to have a command like npx @lift-html/cli
that will save
lift-html
code as a single file in your project, which is perfect for a
zero-dependency or no-build projects. Code for liftHtml
is public domain, so
once you have it in your project you can do whatever you want with it. Which
could give you an opportunity to remove features you don't use (like HMR) or add
something that is missing.
Tier | Frameworks |
---|---|
S tier | vanilla HTML Web Components |
A tier | Enhance.dev (enhance function inspired the shape of liftHtml ), 11ty+WebC, Brisa |
B tier | Atomico (great API, SSR support, JSX, no shadow dom by default, great integration with framework components), @lume/element (biggest WC framework based on solid ecosystem), @microsoft/fast-element (API makes sense, typescript support, SSR support planned, but shadow dom based 😬) |
C tier | Lit, solid-element (has no docs), Svelte and basically all other frameworks that have web components support and can produce web components as an output sorted by bundle size, that includes Angular, Vue, React, etc. |
F tier | Lightning Web Components (I refuse to believe that this is a real thing) |
Honorable mentions | Stencil (very popular, only under React, Vue, Preact, Angular, Lit and Svelte, above Solid and Ember), Haunted (hooks based but uses lit-html, from matthewp, co-creator of Astro), CanJS (from author of @r2wc/react-to-web-component), hyperhtml+heresy+µhtml (from WebReflection), Hybrids (never heard of it), Minze (never heard of it) |
Fallen warriors | Web Components v0, Polymer (the OG, that's how I remember web components introduced, felt like a good idea at the time), Slim.js (last update 3 years ago), SkateJS (last update 6 years ago), |