
Security News
CVE Volume Surges Past 48,000 in 2025 as WordPress Plugin Ecosystem Drives Growth
CVE disclosures hit a record 48,185 in 2025, driven largely by vulnerabilities in third-party WordPress plugins.
Function Lit elements with reactive attributes, properties, and state. Light DOM by default for all your progressive-enhancement needs.
Bundlers:
npm install funlit lit-html
Browsers:
<script type="importmap">
{
"imports": {
"funlit": "https://unpkg.com/funlit",
"lit-html": "https://unpkg.com/lit-html",
"lit-html/": "https://unpkg.com/lit-html/"
}
}
</script>
<script type="module">
import { define, attr, html } from 'funlit';
function MyStepper(host) {
const count = attr(host, 'count', 0, { parse: Number });
function decrement() {
count.value--;
}
function increment() {
count.value++;
}
return () => html`
<button @click=${decrement}>-</button>
${count}
<button @click=${increment}>+</button>
`;
}
define('my-stepper', MyStepper);
</script>
<my-stepper></my-stepper>
<my-stepper count="10"></my-stepper>
<script type="module">
const stepper = document.createElement('my-stepper');
stepper.count = 20;
document.body.append(stepper);
</script>
This package reexports html, svg, and nothing from lit-html as a convenience. Anything else you might need (such as directives) should be imported from lit-html itself.
Alias: defineElement
tagName {string} Custom-element tag name to register.init {(FunlitElement) => () => TemplateResult} An initialization function that receives a host element instance, implements core features, and optionally returns a renderer function.Returns: {FunlitElementConstructor}
Defines a new custom element with the given tag name and init function. Returns the newly-created custom-element class constructor.
The init function is only called once per instance of the element (the host) the first time the host is connected. The function is passed a reference to the host which can be used to define attributes, properties, and state, as well as anything else you'd like. You can think of init like the constuctor, but it doesn't run until the host has been added to a document. May return a render function.
The render function will be called on every update-render cycle. May return a lit-html TemplateResult to be used to render an updated Light DOM subtree, or Shadow DOM subtree if host.attachShadow() has been called.
const MyCounterElement = define('my-counter', (host) => {
const count = attr(host, 'count', 0, { parse: Number });
function increment() {
count.value++;
}
host.addEventListener('update', console.log);
return () => html`
${count}
<button @click=${increment}>+</button>
`;
});
You may then use the element in HTML:
<my-counter count="10"></my-counter>
Elements may also be created programmatically via the constructor:
const counter = new MyCounterElement(); // does not fire `init`
conuter.count = 10;
document.body.append(counter); // fires `init`
Or, via document.createElement:
const counter = document.createElement('my-counter'); // does not fire `init`
conuter.count = 10;
document.body.append(counter); // fires `init`
Alias: defineAttribute
host {FunlitElement} Host element.key {string} Name of the property which will serve as the attribute's accessor.value {any} (default: null) Optional initial value of the attribute if one is not provided in markup or imperatively.options {object} (default: {})
attribute {string} (default: hyphenated key) Optionally specify the exact attribute name if you don't like the one autogenerated from the key.boolean {boolean} (default: false) Whether the attribute is a boolean.parse {(string) => value} Optional function to parse the attribute value to the property value.stringify {(value) => string} Optional function to stringify the property value for rendering.Returns: {{ value; toString: () => string }}
Defines a new public property on the host. Any change to the property will trigger an update-render cycle. The property is initialized with and will watch for changes to the related attribute's value. Changes to the property will not be reflected back to the DOM attribute (this is intentional for performance and security). Returns a mutable value ref.
define('my-counter', (host) => {
const count = attr(host, 'count', 0, { parse: Number });
function incrementByValue() {
count.value++;
}
function incrementByProperty() {
host.count++;
}
return () => html`
${count}
<button @click=${incrementByValue}>+ value</button>
<button @click=${incrementByProperty}>+ property</button>
`;
});
You may set the value via markup.
<my-counter count="10"></my-counter>
Or, programmatically:
const counter = document.createElement('my-counter');
conuter.count = 10;
document.body.append(counter);
Alias: defineProperty
host {FunlitElement} Host element.key {string} Name of the property.value {any} (default: null) Optional initial value of the property, if one is not provided imperatively.options {object} (default: {})
stringify {(value) => string} Optional function to stringify the property value for rendering.Returns: {{ value; toString: () => string }}
Defines a new public property on the host. Any change to the property will trigger an update-render cycle. Returns a mutable value ref.
define('my-counter', (host) => {
const count = prop(host, 'count', 0);
function incrementByValue() {
count.value++;
}
function incrementByProperty() {
host.count++;
}
return () => html`
${count}
<button @click=${incrementByValue}>+ value</button>
<button @click=${incrementByProperty}>+ property</button>
`;
});
You may set the value programmatically:
const counter = document.createElement('my-counter');
conuter.count = 10;
document.body.append(counter);
Alias: defineValue
host {FunlitElement} Host element.value {any} (default: undefined) Optional initial value of the property.options {object} (default: {})
stringify {(value) => string} Optional function to stringify the value for rendering.Returns: {{ value; toString: () => string }}
Defines a new private state value. Any change to the value will trigger an update-render cycle. Returns a mutable value ref.
define('my-counter', (host) => {
const count = val(host, 0);
function increment() {
count.value++;
}
return () => html`
${count}
<button @click=${increment}>+</button>
`;
});
Native lifecycle callbacks are emitted as non-bubbling adopt, connect, and disconnect events. There is no attributechange event emitted as attribute changes are handled with attr() and cannot be defined using the observedAttributes property.
define('my-element', (host) => {
host.addEventListener('adopt', () => { /* ... */ });
host.addEventListener('connect', () => { /* ... */ });
host.addEventListener('disconnect', () => { /* ... */ });
host.addEventListener('update', () => { /* ... */ });
});
The .update() method is automatically called any time the host is connected or a defined attribute, property, or value changes, but may also be called directly. Updates are batched so it's safe to trigger any number of updates at a time without causing unnecessary rerenders. Will trigger a non-bubbling update event. Returns a Promise that resolves after the resulting rerender happens.
The connect and update event handlers may make use of host.updateComplete to run code before or after a render.
define('my-element', (host) => {
async function refresh() {
// before render
await host.update();
// after render
}
host.addEventListener('connect', async () => {
// before render
await host.updateCompleted;
// after render
});
host.addEventListener('update', async () => {
// before render
await host.updateCompleted;
// after render
});
return () => html`
${new Date()}
<button @click=${refresh}>Refresh</button>
`;
});
You can define elements using TypeScript, if you're into that sort of thing.
import { define, attr, prop, val, html } from 'funlit';
export const FunTypesElement = define<{
foo: number;
bar: string;
doSomething: () => void;
}>('fun-types', (host) => {
// must be a number
const foo = attr(host, 'foo', 123, { parse: Number });
// must be a string
const bar = prop(host, 'bar', 'abc');
// infers a boolean
const baz = val(host, true);
// could be a bigint later
const big = val<bigint | undefined>(host);
host.doSomething = () => { /* ... */ };
console.log(foo.value); // number
console.log(bar.value); // string
console.log(baz.value); // boolean
console.log(big.value); // bigint | undefined
return () => html`
<div>foo: ${foo}</div>
<div>bar: ${bar}</div>
<div>baz: ${baz}</div>
<div>big: ${big}</div>
`;
});
declare global {
interface HTMLElementTagNameMap {
'fun-types': InstanceType<typeof FunTypesElement>;
}
}
const a = new FunTypesElement();
console.log(a.foo); // number
console.log(a.bar); // string
console.log(a.doSomething); // function
console.log(a.update); // function
const b = document.createElement('fun-types');
console.log(b.foo); // number
console.log(b.bar); // string
console.log(a.doSomething); // function
console.log(b.update); // function
MIT © Shannon Moeller
FAQs
Function Lit elements.
We found that funlit demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
CVE disclosures hit a record 48,185 in 2025, driven largely by vulnerabilities in third-party WordPress plugins.

Security News
Socket CEO Feross Aboukhadijeh joins Insecure Agents to discuss CVE remediation and why supply chain attacks require a different security approach.

Security News
Tailwind Labs laid off 75% of its engineering team after revenue dropped 80%, as LLMs redirect traffic away from documentation where developers discover paid products.