From Static to Dynamic: Building a Modern Web Experience with Web Components
From Static to Dynamic: Building a Modern Web Experience with Web Components Web development has evolved from hand‑crafted HTML pages to massive single‑page applications (SPAs) powered by heavy JavaScript frameworks. W
From Static to Dynamic: Building a Modern Web Experience with Web Components
Web development has evolved from hand‑crafted HTML pages to massive single‑page applications (SPAs) powered by heavy JavaScript frameworks. While frameworks like React, Vue, and Angular dominate the conversation, there’s a quieter revolution happening under the hood: Web Components paired with CSS custom properties (variables). Together they let you write reusable, framework‑agnostic UI pieces that work anywhere—static sites, SPAs, or server‑rendered pages.
In this post we’ll explore why Web Components deserve a spot in your toolbox, walk through a practical example of a custom <user-card> element, and show how CSS variables keep your design system flexible without adding runtime JavaScript bloat. By the end, you’ll have a production‑ready component you can drop into any project, regardless of the stack you’re using.
Why Web Components Matter in 2024
- Native Browser Support – All modern browsers ship with the Custom Elements, Shadow DOM, and HTML Templates APIs. No polyfills required for the majority of users.
- Framework Agnostic – A component built once can be used in a vanilla HTML page, a React tree, an Angular module, or a Svelte app without adapters.
- Encapsulation – Shadow DOM isolates styles and markup, preventing accidental CSS bleed‑through that often plagues large codebases.
- Performance – Because the component lives as native DOM, the browser can optimize rendering and layout better than a virtual DOM abstraction.
Despite these benefits, many developers shy away because they think components are “hard to test” or “lack tooling.” The truth is the opposite: modern tooling (Vite, Snowpack, Lit, and even plain ES modules) makes authoring, testing, and bundling a breeze.
Building a Reusable <user-card> Component
Let’s create a small but useful component that displays a user’s avatar, name, and a short bio. We’ll use Lit (a lightweight library that helps work with the Shadow DOM) to keep the code concise, but the final output is just a native custom element.
1. Project scaffolding
# Create a new directory
mkdir user-card-demo && cd user-card-demo
# Initialize a Vite project (vanilla JS)
npm init vite@latest . --template vanilla
# Install Lit
npm install lit
2. Defining the component (src/UserCard.js)
// src/UserCard.js
import { LitElement, html, css } from 'lit';
import { property, state } from 'lit/decorators.js';
class UserCard extends LitElement {
// Public attributes
@property({ type: String }) name = 'Anonymous';
@property({ type: String }) avatar = '';
@property({ type: String }) bio = '';
static styles = css`
:host {
display: block;
max-width: 280px;
border: 1px solid var(--card-border, #ddd);
border-radius: var(--card-radius, 8px);
overflow: hidden;
font-family: var(--font-family, system-ui, sans-serif);
background: var(--card-bg, #fff);
box-shadow: var(--card-shadow, 0 2px 6px rgba(0,0,0,0.1));
}
.header {
display: flex;
align-items: center;
padding: 1rem;
background: var(--header-bg, #f5f5f5);
}
.avatar {
width: 48px;
height: 48px;
border-radius: 50%;
margin-right: 0.75rem;
background: var(--avatar-bg, #ccc) no-repeat center/cover;
}
.name {
font-size: 1.1rem;
font-weight: 600;
color: var(--name-color, #333);
}
.bio {
padding: 0.75rem 1rem;
font-size: 0.9rem;
line-height: 1.4;
color: var(--bio-color, #555);
}
`;
render() {
return html`
<div class="header">
<div class="avatar" style="background-image:url(${this.avatar})"></div>
<div class="name">${this.name}</div>
</div>
<div class="bio"><slot>${this.bio}</slot></div>
`;
}
}
customElements.define('user-card', UserCard);
export default UserCard;
Key takeaways
-
Shadow DOM – The component’s styles live inside
static styles, guaranteeing they won’t clash with page styles. -
CSS Custom Properties – Notice the
var(--card-border, #ddd)pattern. The component provides sensible defaults but lets any page override them, keeping your design system fluid. -
Slots – By exposing a
<slot>we let consumers inject richer markup into the bio area if they wish.
3. Using the component (index.html)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>User Card Demo</title>
<script type="module" src="/src/UserCard.js"></script>
<style>
/* Global design tokens */
:root {
--card-bg: #fafafa;
--card-border: #e0e0e0;
--card-radius: 12px;
--card-shadow: 0 4px 12px rgba(0,0,0,0.08);
--header-bg: #fff;
--name-color: #222;
--bio-color: #444;
--font-family: 'Helvetica Neue', Arial, sans-serif;
}
</style>
</head>
<body>
<h1>Web Component Showcase</h1>
<user-card
name="Jane Doe"
avatar="https://i.pravatar.cc/150?img=5"
bio="Frontend engineer who loves CSS art and open‑source."
></user-card>
<!-- Overriding a token for a single card -->
<user-card
name="Sam Smith"
avatar="https://i.pravatar.cc/150?img=12"
style="--card-bg:#fffbe6; --card-border:#ffd54f"
>
<p>Full‑stack developer, coffee addict, and occasional jazz saxophonist.</p>
</user-card>
</body>
</html>
When you run npm run dev (Vite’s dev server), you’ll see two cards with different looks—thanks to the CSS variables defined at the page level and even per‑instance overrides. No extra JavaScript is required after the initial component registration.
Styling Without Bloat: Harnessing CSS Variables
CSS variables are more than a convenience for theming; they’re a performance win. Because they’re resolved by the browser’s CSS engine, changing a variable (e.g., switching to a dark theme) doesn’t trigger a JavaScript re‑render. In the <user-card> example we:
- Declared defaults inside the component (
var(--card-bg, #fff)). - Exposed a global token map in
:rootthat the whole site can update with a single CSS class toggle.
/* Dark theme toggle */
body.dark-mode {
--card-bg: #2b2b2b;
--card-border: #444;
--name-color: #eee;
--bio-color: #ccc;
}
Toggling dark-mode on the <body> instantly darkens every card without touching JavaScript.
Testing the Component
Even though the component is native, you still want a solid test suite. Here’s a quick Jest + @testing-library/web component setup:
npm install --save-dev jest @testing-library/dom @testing-library/jest-dom
// tests/UserCard.test.js
import '@testing-library/jest-dom';
import { fireEvent } from '@testing-library/dom';
import '../src/UserCard.js';
test('renders name and avatar', () => {
const el = document.createElement('user-card');
el.name = 'Test User';
el.avatar = 'https://example.com/avatar.png';
document.body.append(el);
const nameEl = el.shadowRoot.querySelector('.name');
const avatarEl = el.shadowRoot.querySelector('.avatar');
expect(nameEl).toHaveTextContent('Test User');
expect(avatarEl).toHaveStyle(`background-image: url(${el.avatar})`);
});
Running npx jest confirms the component behaves as expected, and because we’re testing the Shadow DOM directly we avoid flaky CSS selector issues.
Deploying to Production
When you’re ready to ship, Vite’s build command bundles the component into a single, minified ES module:
npm run build
The output dist/assets/user-card.[hash].js can be served via any static host (Netlify, Vercel, Cloudflare Pages). Since the component is an ES module, you can also import it from a CDN like esm.sh or jsDelivr:
<script type="module" src="https://esm.sh/[email protected]"></script>
This eliminates the need for a build step on the consumer side—a big win for content‑heavy sites that prefer Markdown‑first workflows.
When Not to Use Web Components
- Heavy state management – If your UI requires a global store (Redux‑style) across many components, a full framework may still be easier.
- SSR‑first projects – While you can pre‑render custom elements, the ecosystem around SSR for Web Components isn’t as mature as for React or Next.js.
That said, for most UI widgets—cards, modals, tabs, tooltips—Web Components hit the sweet spot of reusability and performance.
Future‑Proofing Your Frontend
The Web Platform continues to mature. Upcoming proposals like CSS Cascade Layers and Declarative Shadow DOM will make component composition even cleaner. By investing in native components now, you’ll be ready to adopt these enhancements without refactoring a massive framework layer.
Recommended Tools
SuperCDN – Fast Global CDN with Free Tier – Serve your compiled component assets instantly worldwide. Their free tier is generous enough for hobby projects, and the analytics dashboard helps you track bandwidth usage in real time.
CodeSnap – Instant Code Screenshot Generator – Turn any code block into a beautifully styled image for social media or documentation. It supports custom themes, line numbers, and even animated GIFs, making it perfect for showcasing snippets like the ones above.
Happy coding, and may your components stay lightweight and your users stay delighted!
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.