Lit-HTML is a library for creating lightweight components, all Lit components are standard web components, wich are supported by modern browsers, and can be used along other JS frameworks. It provides state management, doesn't use JSX, but has a simillar syntax and uses vanilla JS for templating, capable of better performance when compared to other frameworks that use the virtual DOM; all of this without a compilation step. When talking about Lit-HTML, i'm referencing all the packages of the Lit "ecosystem", but there's different packages; "Lit" refers to the whole library, Lit-HTML is a standalone package, as described here, but i decided to switch names because Lit-HTML sounded more clear to me. Keep distinctions in mind.
To install and configure Lit, follow the oficial doc; If you're interested in using it along a module-bundler i recommend this template for vite with TypeScript, this can be easily altered to use JS only, if wished, i did it because i consider TS unnecessary for developing UI in a non framework-no-node environment, anyway, suit yourself.
To inject JS values into your component, template literals must be used, the template literals are parsed as parts, after they're loaded, your component is rendered to the DOM; If there are changes in the component's properties, only the parts that changed will be updated, not causing excessive content re-adding. Being light-weight (very tiny bundle size, around 5kb minified and compressed) and performatic, i chose it for structuring and componentizing things, therefore multiple components are in use all around hard software.
The following is an example of the potential of Lit-HTML, its a "declarative form", i say declarative because its structure is built upon values declared on how the consumer wants it to be built; this values were declared as an object. The form-component is presented below and its source code after:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="https://cdn.tailwindcss.com"></script>
<title>Form component</title>
</head>
<body>
<component-form></component-form> <!-- Using the component -->
<script type="module">
// "nothing" can be used for conditional rendering; in child expressions, undefined, null, '', and nothing all behave the same and render no nodes.
import { LitElement, html, nothing } from "lit";
// This object will determine the form structure, inputs and some validation behaviours.
const exampleForm = {
title: `"Declarative" form example`,
formId: "example_form",
action: "#",
method: "get",
submitText: "Click to submit",
fields: [
{
id: "user_name",
name: "user[name]",
type: "text",
label: "Name",
placeholder: "Your name...",
colSize: "lg:col-span-6",
minLength: 2,
maxLength: 128,
required: true
},
{
id: "user_email",
name: "user[email]",
type: "email",
label: "E-mail",
placeholder: "Your e-mail...",
colSize: "lg:col-span-6",
minLength: 3,
maxLength: 128,
required: false
},
{
id: "user_password",
name: "user[password]",
type: "password",
label: "Password",
placeholder: "Your password...",
colSize: "lg:col-span-6",
minLength: 3,
maxLength: 128,
required: false
},
{
id: "user_confirm_password",
name: "user[confirm_password]",
type: "password",
label: "Confirm password",
placeholder: "Confirm your password...",
colSize: "lg:col-span-6",
minLength: 3,
maxLength: 128,
required: false
},
{
id: "user_age",
name: "user[age]",
type: "number",
label: "Age",
placeholder: "Your age...",
colSize: "lg:col-span-3",
min: 1,
max: 200,
required: false
},
{
id: "user_favorite_food",
name: "user[favorite_food]",
type: "select",
label: "Favorite food",
placeholder: "Your favorite food...",
colSize: "lg:col-span-3",
defaultOptionText: "Select your favorite food",
options: [
{ key: "pasta", value: "Pasta" },
{ key: "beef_and_cream", value: "Beef and cream" },
{ key: "fried_chicken", value: "Fried chicken" },
{ key: "ravioli", value: "Ravioli" },
{ key: "ice_cream", value: "Ice cream" },
{ key: "other", value: "Other" }
],
required: false,
},
{
id: "user_credit_card",
name: "user[credit_card]",
type: "text",
label: "Credit card",
placeholder: "Your credit card number(lol)...",
colSize: "lg:col-span-3",
minLength: 16,
maxLength: 19,
required: false
},
{
id: "user_credit_card_expiration",
name: "user[credit_card_expiration]",
type: "date",
label: "Credit card expiration Date",
placeholder: "Credit card expiration date...",
colSize: "lg:col-span-3",
min: "2024-12-07", // Must be set as yyyy/mm/dd; MDN reference: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/date#additional_attributes;
max: "2025-12-07",
required: false
},
{
id: "user_favorite_color",
name: "user[favorite_color]",
type: "color",
label: "Favorite color",
placeholder: "Favorite color (hex value)...",
colSize: "w-fit",
required: false
},
{
id: "user_comment",
name: "user[comment]",
type: "textarea",
label: "Commentary",
placeholder: "A very cool comment...",
colSize: "lg:col-span-12",
minLength: "4",
maxLength: "128",
required: false
},
{
name: "error_input",
label: "(Purposely inserted error)", // Error showing in read at the end of the form - purposely inserted with the purpose of showing how an input without id should not be set.
type: "text",
placeholder: "Error input...",
}
]
}
class ComponentForm extends LitElement {
static properties = {
_formPayload: {state: true},
_userFavoriteColor: {state: true}
}
constructor() {
super();
this._userFavoriteColor = ""; // Set property initial value on constructor when needed.
}
createRenderRoot() {return this} // Lazy workaround to avoid using the shadowDOM.
setColor(event) {
this._userFavoriteColor = event.target.value;
}
/**
* Handle form submit, renders the form payload.
*/
handleFormSubmit(event) {
event.preventDefault(); // Prevent submition.
// Get all elements that succesfully rendered as inputs in the form, then render them as the payload text on key-value format.
const data = Object.fromEntries(new FormData(event.target));
this._formPayload = JSON.stringify(data, null, "\t");
}
render() {
// Import variables to make form in a "declarative" manner.
const {title, formId, action, method, submitText, fields} = exampleForm;
// Set form component.
return html `
<form id="${formId}" action="${action}" method="${method}" @submit=${this.handleFormSubmit} class="border-blue-500 border-2">
<h2 class="text-center font-bold mt-8">${title}</h2>
<div class="grid grid-cols-12 gap-4 pb-8 mt-4">
${
// Creates form fields (label+inputs). Inputs are rendered differently depending on type.
fields.map(data => {
const {id, label} = data;
// Show warning at field if id is not set.
if (!id) return html `<div class="flex flex-col col-span-12 border-b-2 bg-red-500 mt-2">Id must be set for ${label || "this form item"} label/input!</div>`;
const {type, name, placeholder, colSize, maxLength, minLength, min, max, options, required, defaultOptionText} = data;
return html `
<div class="col-span-12 ${colSize} flex flex-col mt-4 px-4">
<label for="${id}">
${label ? html `<div class="inline font-bold">${label}</div>` : nothing}
${required ? html `<div class="inline font-bold text-red-500"> *</div>` : nothing}
</label>
${
(type == "text" || type == "number" || type == "email" || type == "password" || type == "date")
? html
`<input
id="${id}"
class="block border-b-2 border-blue-500 outline-none mt-2"
name="${name || nothing}"
type="${type || nothing}"
placeholder="${placeholder || nothing}"
required="${required || nothing}"
minlength="${minLength || nothing}"
maxlength="${maxLength || nothing}"
min="${min || nothing}"
max="${max || nothing}"
>`
: (type == "color")
? html
`<div class="flex mt-2">
<input @change="${this.setColor}" id="${id}" class="cursor-pointer" name="${name}" .value="${this._userFavoriteColor}" type="color">
<input @change="${this.setColor}" class="block border-b-2 border-blue-500 outline-none mt-2 ml-4" .value="${this._userFavoriteColor}" type="text" placeholder="${placeholder || nothing}">
</div`
: (type == "select")
? html
`<select id="${id}" class="border-blue-500 border-2 mt-2" name="${name}" selected>
<option disabled selected value="">${defaultOptionText}</option>
${options ? options.map(({key, value}) => html `<option value="${key}">${value}</option>`) : nothing}
</select>`
: (type == "textarea")
? html
`<textarea
id="${id}"
class="outline mt-2"
name=${name || nothing}
placeholder=${placeholder || nothing}
required="${required || nothing}"
minlength=${minLength || nothing}
maxlength=${maxLength || nothing}
></textarea>`
: nothing
}
</div>
`;
})
}
</div>
</form>
<button form="${formId}" class="block mx-auto border-2 border-black px-3 py-2 bg-gray-200 hover:bg-gray-300 mt-8" type="submit">${submitText}</button>
<p class="text-center font-bold underline mt-8">Click submit to see how form payload looks like</p>
<pre class="block w-min mx-auto mt-8">${this._formPayload}</pre>
`;
}
}
customElements.define("component-form", ComponentForm);
</script>
</body>
</html>
If you're a new devloper, i recommend to avoid using any kind of library or component for developing simple and static pages; after you realize it doesn't make sense to repeat yourself so much by copypaste, then it makes sense to use some library like Lit-HTML, static site generators such as the ones recommended at the bottom of the page at neocitites tutorials and crazy alternatives like React without node (not a very good one actually, for containing deprecated libraries). For more information about alternatives take a look at my Web components tutorial - and of course, remind yourself that nearly all my tips and guides here are provided considering the main focus to be: developing for a static-no-serverside-environment, that means no back-end, no node, no php, only the browser and its APIs.
After seeing a component and Lit's potential, if any of this awakened your interest in making static websites with a library, start by taking it easy, after making things in plain HTML and realizing it will repeat, then make a new component, like a navbar, sidenavs, footer, your page title, et cetera. Remember it's possible that you don't need a component because it doesn't really make sense to turn an only once static part of HTML into a whole new component, so only add things as they become needed.
Read more about Lit-HTML in this very nice Medium article.