In the modern day it's easy to find a tech stack as it usually is just a quick npm create something
and you get a full scaffolded development setup. The same doesn't really happen in the Web Components space though. With Web Components, it's more like a all-you-can-eat buffet. You pick what you want and you go all in on it.
That's what makes it really hard especially for starting developers to find the correct toolchain to get started with developing with Web Components and being productive with them.
I'm here today to showcase my current go-to development stack and how I like to use my tools. I've used this stack in multiple production-grade setups and will continue to do so with slight variations.
In this post we'll be going through the following technologies:
So let's get started!
Lit
Lit markets itself as a
Lit is a simple library for building fast, lightweight web components.
And that is simply all it delivers. Lit is built from 3 parts:
- Lit HTML, which handles dynamic rendering in a Javascript context
- Lit Element, which provides an easy interface for creating reactive Web Components
- Helper functions, which make everything feel like a framework
Lit has been my go-to since the old Polymer days and it's still my daily driver for modern web frontends. It gets the job done and allows you to keep productive in pretty much any environment.
With Lit, it's as simple as
const name = "World";
html`Hello ${name}!`;
Suunta
This is an opinionated post, and this is an opinionated statement: Suunta is one of the best ways to handle views and state in Web Component environments.
With Suunta, you can manage SPA applications easily, while also being able to create reactive views, without having everything be a component.
What makes Suunta versatile, is that it doesn't ship a renderer, but allows the user to implement it themselves. The "sane default" here is to use Lit as the renderer, and it's just a matter of adding a one-liner function to your codebase.
Suunta provides the ease-of-use of a framework with the footprint of a helper library.
import { html } from "lit";
import { createState } from "suunta";
export function HomeView() {
const state = createState({
clicks: 0,
});
function onClick() {
state.clicks += 1;
}
return () => html` <button @click=${onClick}>Clicked ${state.clicks} times</button> `;
}
Suunta provides routing, state management and sane helpers for things like API calls. Combined with Lit, you are pretty much all set to write any modern web application.
Tailwind
Whether you like it or not, Tailwind is making the rounds. I've started adopting Tailwind to my dev setups due to co-worker pressure but it's starting to grow on me a bit.
As my current default dev setups include Vite as the default dev server / build tool, implementing a Tailwind setup is as easy as it can be.
After installing tailwindcss
and @tailwindcss/vite
, all you have to do is setup your Vite to handle tailwind, and import it into your css.
// vite.config.ts
import { defineConfig } from "vite";
import tailwindcss from "@tailwindcss/vite";
export default defineConfig({
server: {
port: 8000,
},
plugins: [tailwindcss()],
});
/* main.css */
@import "tailwindcss";
And voilá! You have a functional Tailwind setup for your regular views.
But what about web components and shadow root?
For those pesky shadow roots and encapsulations, we have to do some extra work to have tailwind pierce through them.
Luckily, it can be achieved with some modern Typescript in just a few lines of code.
Adding this helper into your project will allow you to inject Tailwind into any component inside of your project.
import { css, LitElement, unsafeCSS } from "lit";
import mainStyles from "./main.css?inline";
export type LitElementConstructor<T = typeof LitElement> = T & {
new (...args: any[]): LitElement;
};
export function withTailwind(constructor: LitElementConstructor) {
const styleInject = css`
${unsafeCSS(mainStyles)}
`;
// Append to existing array if set
if (Array.isArray(constructor.styles)) {
constructor.styles.push(styleInject);
return;
}
// If not value at all, init an array with styles
if (!constructor.styles) {
constructor.styles = [styleInject];
return;
}
// If value is set, but is singular instead of array, make it an array.
constructor.styles = [constructor.styles, styleInject];
}
After writing this helper function, it's just the matter of adding the decorator to your component class.
@customElement("my-button")
@withTailwind
export class MyButton extends LitElement {
render() {
return html`
<button
class="bg-transparent hover:bg-blue-500 text-blue-700 font-semibold hover:text-white py-2 px-4 border border-blue-500 hover:border-transparent rounded"
>
Click me
</button>
`;
}
}
And you're all setup! You have...
- Rendering in Javascript-land
- Re-usable components via Lit
- Routing and state management through Suunta
- Easy helper-class style css via Tailwind
And all without having any framework magic under the surface, just simple libraries working perfectly together.
Closing
I hope this setup provides you with a clear starter for your next web development project. I've also lefta repo with the same setup pre-setup if you want to clone that and start fiddling. https://github.com/Matsuuu/modern-tech
Top comments (0)