DEV Community

Cover image for The World's First Web Components Library in the Style of shadcn — With Automatic Code Injection
Alexander
Alexander

Posted on

The World's First Web Components Library in the Style of shadcn — With Automatic Code Injection

Okay, that headline sounds pretty cocky, I know. But as far as I've been able to Google — this really is the first attempt at something like this. If I'm wrong — drop a comment, I'd love to check out alternatives. Meanwhile, let me tell you what this beast is and why it even exists.

The Backstory, or How I Got Here

It all started with microfrontends. You know, when you have one project, but inside it lives Vue, React, and maybe some legacy jQuery that nobody wants to touch because "it works, don't touch it."

So you're sitting there, a designer brings you a mockup of a new button. A beautiful button, with a gradient, with hover effects, everything just right. And you realize that now you're going to have to:

  • Write this button for Vue
  • Write the same button for React
  • Write it one more time for that legacy code
  • Pray that they look the same

And this happens every single time. Every. Single. Time.

At some point I thought: "What if I write the component once and use it everywhere?" Revolutionary thought, right? Actually no, Web Components have been around for a while. But somehow nobody made a decent DX for them, like shadcn/ui has.

What is shadcn and Why It Matters

For those who don't know: shadcn/ui isn't really a library in the traditional sense. It's more like a collection of components that you copy into your project. Not install as a dependency, but literally copy.

Why this is awesome:

  • Full control over the code
  • No version conflicts
  • Customize however you want
  • Delete what you don't need, add what you do

The problem is that shadcn is written for React. And I needed something that works everywhere. Literally everywhere.

Meet CapsuleUI

So, I went ahead and made the same thing, but for Web Components. It's called CapsuleUI, and it works roughly like this:

# Initialize the project
npx @zizigy/capsule init

# Add a component
npx @zizigy/capsule add Button
Enter fullscreen mode Exit fullscreen mode

That's it. After that, you get a @capsule folder in your project, and inside — a ready-to-use button component. Not a link to node_modules, not some magic — just files with code that you can open, read, and modify.

What It Looks Like in Use

<capsule-button variant="primary" size="lg">
  Click me
</capsule-button>
Enter fullscreen mode Exit fullscreen mode

Yeah, that simple. This works in React:

function App() {
  return (
    <div>
      <capsule-button variant="primary">
        Hello from React
      </capsule-button>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

This works in Vue:

<template>
  <capsule-button variant="primary">
    Hello from Vue
  </capsule-button>
</template>
Enter fullscreen mode Exit fullscreen mode

This works in vanilla HTML:

<!DOCTYPE html>
<html>
<head>
  <link rel="stylesheet" href="@capsule/global.css">
  <script type="module" src="@capsule/index.js"></script>
</head>
<body>
  <capsule-button variant="primary">
    Hello from 2005
  </capsule-button>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

And the coolest part — it's the same component. Not three different implementations, but one. The only one. Unique.

But Wait, Web Components...

Yeah, I know what you're going to say. "Web Components are complicated", "Shadow DOM is a pain", "How do you style them?", "What about SSR?".

Let's go through this.

Complexity

Components are written with Lit. If you don't know — it's a lightweight library from Google for creating Web Components. It does all the dirty work for you: reactivity, templating, lifecycle. The code ends up clean and understandable.

Shadow DOM

Yeah, we use it. But it's not a problem, it's a feature. Component styles are isolated — they won't break your site, and your site won't break them. For customization there's CSS Custom Properties and ::part().

Styling

All components use CSS variables. Want to change the button color? Here you go:

:root {
  --capsule-color-primary: #8b5cf6;
}
Enter fullscreen mode Exit fullscreen mode

Want to completely rewrite the styles? The files are in your project, open and edit them.

SSR — Here's Where It Gets Interesting

Okay, there's a nuance here. Web Components work with SSR. The server will render the component tag itself — it'll be in the HTML:

<capsule-button variant="primary">Click me</capsule-button>
Enter fullscreen mode Exit fullscreen mode

But if the component itself generates HTML inside Shadow DOM (for example, a complex structure with nested elements), that HTML won't be in SSR. The component tag will be there, the Shadow DOM content — not. This is normal behavior for Web Components, and usually it's not a problem, because the content still renders on the client.

For most cases this works great. If you absolutely need full SSR with Shadow DOM content — there's Declarative Shadow DOM, but that's a whole other story.

The VSCode Feature I'm Proud Of

You know what always pissed me off about Web Components? No autocomplete. You write <my-component and your IDE goes: "I have no idea what this is, good luck bro."

In CapsuleUI this works. When you add a component, the CLI automatically updates your VSCode settings. And your IDE starts understanding custom elements!

You write <capsule-button — you get autocomplete for variant, size, disabled. Hover over it — see the documentation. Like it's a built-in HTML tag, only better.

This works through html.customData in VSCode settings. The CLI generates JSON with descriptions of all components and their attributes, and the IDE picks it up. Magic? No, just proper DX.

VSCode Data Generator — For the Advanced

But what if you updated a component, added new attributes, and want to update autocomplete? Or you're working with your own components folder?

For this there's the generate command:

# Generates vscode.data.json for a specific folder
npx @zizigy/capsule generate --dir ./my-components

# Or for a specific component folder
npx @zizigy/capsule generate --dir @capsule/components/capsule-button

# You can specify the output directory
npx @zizigy/capsule generate --dir ./src/components --out ./vscode-config
Enter fullscreen mode Exit fullscreen mode

This command parses JavaScript and CSS files of components, extracts all attributes, their types and possible values, and generates vscode.data.json for autocomplete. Clever? I think so.

What's Under the Hood

Okay, let's get a bit technical for those who are interested.

Project Structure After Initialization

@capsule/
├── components/
│   └── capsule-button/
│       ├── button.js
│       ├── button.style.css
│       └── register.js
├── global.css
└── index.js
Enter fullscreen mode Exit fullscreen mode

Everything is logical and clear. Want to find button styles? Open button.style.css. Want to change the logic? Open button.js. No magic, no hidden files in node_modules.

Reactivity

Components are fully reactive thanks to Lit. Change an attribute — the component redraws:

const button = document.querySelector('capsule-button');
button.variant = 'secondary'; // Updates automatically
button.disabled = true; // This too
Enter fullscreen mode Exit fullscreen mode

This works with both JavaScript and frameworks. Vue automatically binds attributes, React does too (with some nuances, but solvable).

Custom Prefixes

Don't like capsule-? You can use your own:

npx @zizigy/capsule add Button --prefix ui
Enter fullscreen mode Exit fullscreen mode

And you'll get <ui-button> instead of <capsule-button>. Handy if you already have your own namespace.

Modules — A Different Story

Besides components there are also modules. Like, additional functionality that isn't a component, but often needed.

For example, a form validation module:

npx @zizigy/capsule module add form
Enter fullscreen mode Exit fullscreen mode

After that you get a validator with a bunch of ready-made rules:

import { CapsuleValidator, CapsuleRules } from '@capsule/modules/form';

const validator = new CapsuleValidator({
  email: [CapsuleRules.required(), CapsuleRules.email()],
  password: [CapsuleRules.required(), CapsuleRules.minLength(8)]
});

const result = validator.validate({
  email: 'test@example.com',
  password: '123'
});

// result.isValid === false
// result.errors.password === ['Minimum length: 8 characters']
Enter fullscreen mode Exit fullscreen mode

Again — it's your code. Want to add your own rule? Add it. Want to remove unnecessary ones? Remove them.

The Project's Philosophy

CapsuleUI isn't a ready-made design system. It's more like a constructor for building your own.

Components are intentionally minimalistic in styling. Basic states exist, but they're easily overridden. Because I have no idea what your design is. Maybe you have Material Design, maybe Tailwind-style, maybe something completely unique.

The idea is to give you a working foundation that you'll customize for yourself. Not force you to "fight" with someone else's styles, trying to override them.

Real Use Cases

Let me give you a few examples where this actually comes in handy.

Microfrontends

You have a project where part is Vue 3, part is React 18, and somewhere there's legacy jQuery. And you need all buttons to look the same. With CapsuleUI this is solved by adding one component — it works everywhere.

Unified Design System

You work at a big company, and you have a bunch of different projects on different technologies. But everyone should use a unified design. Instead of maintaining separate libraries for each framework, you can make one set of Web Components.

Quick Prototypes

Need to quickly throw together a prototype, but don't want to drag in a whole framework? Web Components work in vanilla HTML. Add components — get working markup.

CMS Integration

Many CMSs (WordPress, Drupal, etc.) allow using custom HTML. But they don't understand React or Vue. But Web Components? Sure thing. Your content editor can use your components.

Who Is This For?

Honestly, CapsuleUI is a niche tool. It's not for every project.

It's perfect if:

  • You have microfrontends with different frameworks
  • You want to create your own design system on Web Components
  • You need components that work everywhere
  • You want full control over the code

It probably isn't for you if:

  • You have a monolith on one framework (easier to take shadcn/ui for React or an analogue for Vue)
  • You need a ready-made design system out of the box (take Vuetify, MUI, Ant Design)
  • You don't want to bother with customization

What's Next?

Right now the library has a basic set of components that covers the main needs. But this is just the beginning.

Planned:

  • More components (modals, dropdowns, date pickers)
  • Improved documentation
  • Integration examples with different frameworks
  • Possibly, CLI for generating your own components

If you have ideas or suggestions — write in issues on GitHub. I really do read and respond.

How to Try It

# Create a new project (or use an existing one)
mkdir my-project && cd my-project

# Initialize
npx @zizigy/capsule init

# Add components
npx @zizigy/capsule add Button
npx @zizigy/capsule add Alert
npx @zizigy/capsule add Tabs

# See the list of available components
npx @zizigy/capsule list

# If you updated a component, regenerate VSCode data
npx @zizigy/capsule generate --dir @capsule/components/capsule-button
Enter fullscreen mode Exit fullscreen mode

GitHub: github.com/ZiZIGY/CapsuleUI

npm: @zizigy/capsule

Documentation: capsuleui

Instead of a Conclusion

You know, when I started this project, I just wanted to solve a specific problem at work. And in the end I got something I'm really proud of.

Web Components have long been "the technology of the future that never comes." But now they work in all browsers, there are great tools like Lit, and, I think, it's time to give them proper developer experience.

CapsuleUI is my attempt to do this. Maybe not perfect. Maybe with bugs (if you find any — write, I'll fix them). But it works, and it solves a real problem.

I'll be happy about feedback, stars on GitHub, and, of course, contributors. Because one person can't do everything alone, and a good open-source project is always a team effort.

Thanks for reading to the end. You're awesome! ✨

Top comments (0)