npm

Getting started

Plain HTML, no build step

One stylesheet link and the markup below. No npm install, no bundler, no framework. This is the same CSS and the same class strings the React and Vue components render, so a hand-written page sits alongside them without looking like a different product.

How do I add VUI to a plain HTML page?

Link the compiled stylesheet. That is the whole setup.

index.html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <link rel="stylesheet" href="https://unpkg.com/@viliha/vui-theme/dist/vui.css" />
  </head>
  <body class="bg-background text-foreground">
    <!-- paste any snippet from this page -->
  </body>
</html>

The file carries the design tokens, the base layer, the motion utilities and every utility class VUI's own components use, already generated. It is about 75 kB before compression, and it needs no Tailwind, no PostCSS and no build.

Dark mode is a class

Add class="dark" to <html> and everything flips, because every colour is a token. Persist the choice in localStorage and set it before first paint to avoid a flash.
theme toggle, the whole thing
<script>
  // Before first paint, so the page never flashes the wrong theme.
  const stored = localStorage.getItem("theme");
  const dark = stored ? stored === "dark" : matchMedia("(prefers-color-scheme: dark)").matches;
  document.documentElement.classList.toggle("dark", dark);
</script>

Markup

Copy any of these. They are generated from the same class strings the components render, so they cannot drift out of date with the library.

Button

Every variant is the same base plus one variant class and one size class.

button.html
<button type="button" class="inline-flex cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 border border-transparent bg-[var(--button-primary)] text-[var(--button-primary-foreground)] shadow-[var(--button-shadow)] hover:bg-[var(--button-primary-hover)] [&_svg]:border-[var(--button-primary-hover)] h-8 px-3">
  Save
</button>

<button type="button" class="inline-flex cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 border border-border bg-background text-foreground [&_svg]:text-muted-foreground hover:[&_svg]:text-foreground h-8 px-3">
  Cancel
</button>

Badge

Status pills. The success and warning variants already carry their dark-mode colours.

badge.html
<span class="inline-flex items-center gap-1 rounded-md border px-2 py-0.5 font-normal border-transparent bg-emerald-50 text-emerald-700 dark:bg-emerald-950 dark:text-emerald-400">Active</span>
<span class="inline-flex items-center gap-1 rounded-md border px-2 py-0.5 font-normal border-transparent bg-muted text-muted-foreground">Draft</span>
<span class="inline-flex items-center gap-1 rounded-md border px-2 py-0.5 font-normal border-transparent bg-destructive/10 text-destructive dark:bg-destructive/20">Overdue</span>

Card

The section card the whole design system is built from: bordered, with a muted header and a body.

card.html
<section class="rounded-lg border border-border bg-card text-card-foreground overflow-hidden">
  <header class="border-b border-border bg-muted/40 px-5 py-3">
    <h2 class="text-base font-semibold tracking-tight">Team</h2>
  </header>
  <div class="px-5 py-4 text-sm leading-relaxed">
    Anything goes here.
  </div>
</section>

Input, select and checkbox

Add aria-invalid to an input and the border and focus ring turn destructive on their own.

form.html
<label class="flex items-center gap-2 text-sm leading-none font-medium select-none" for="email">
  Email
</label>
<input id="email" type="email" placeholder="you@example.com" class="flex h-8 w-full rounded-md border border-input bg-background px-2.5 py-1 transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background aria-invalid:border-destructive disabled:cursor-not-allowed disabled:opacity-50" />

<select class="flex h-8 w-full items-center justify-between gap-2 rounded-md border border-input bg-background px-2.5 transition-colors hover:bg-accent/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background">
  <option>Platform</option>
  <option>Growth</option>
</select>

<input type="checkbox" class="size-4 shrink-0 cursor-pointer rounded border-input accent-[var(--button-primary)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background" />

Table

Bordered rows with a muted header, matching the datatable the React package ships.

table.html
<div class="rounded-lg border border-border bg-card text-card-foreground vui-scroll overflow-x-auto">
  <table class="w-full border-collapse text-sm">
    <thead>
      <tr class="border-b border-border bg-muted/40 text-left">
        <th class="px-4 py-2.5 font-medium">Name</th>
        <th class="px-4 py-2.5 font-medium">Status</th>
      </tr>
    </thead>
    <tbody>
      <tr class="border-b border-border transition-colors last:border-b-0 hover:bg-accent/50">
        <td class="px-4 py-2.5">Acme Retail</td>
        <td class="px-4 py-2.5">
          <span class="inline-flex items-center gap-1 rounded-md border px-2 py-0.5 font-normal border-transparent bg-emerald-50 text-emerald-700 dark:bg-emerald-950 dark:text-emerald-400">Active</span>
        </td>
      </tr>
    </tbody>
  </table>
</div>

The bordered-row list standard. Separators between rows, none trailing.

menu.html
<div class="w-56 overflow-hidden rounded-md border border-border bg-popover text-sm shadow-md">
  <button type="button" class="flex w-full cursor-pointer items-center gap-2 border-b border-border px-3 py-2 text-left last:border-b-0 hover:bg-accent hover:text-accent-foreground">Rename</button>
  <button type="button" class="flex w-full cursor-pointer items-center gap-2 border-b border-border px-3 py-2 text-left last:border-b-0 hover:bg-accent hover:text-accent-foreground">Duplicate</button>
  <button type="button" class="flex w-full cursor-pointer items-center gap-2 border-b border-border px-3 py-2 text-left last:border-b-0 hover:bg-accent hover:text-accent-foreground">Delete</button>
</div>

Dialog

Markup only: opening, closing and the focus trap are yours. In Laravel, Alpine's x-show and x-trap do it in two attributes.

dialog.html
<div class="vui-overlay-in fixed inset-0 z-[70] flex items-center justify-center bg-foreground/25 p-4">
  <div role="dialog" aria-modal="true" aria-label="Invite" class="vui-pop-in w-full max-w-md overflow-hidden rounded-lg border border-border bg-background shadow-xl">
    <div class="border-b border-border bg-muted/40 px-5 py-3">
      <h2 class="text-base font-semibold tracking-tight">Invite teammate</h2>
    </div>
    <div class="max-h-[70vh] overflow-y-auto px-5 py-4 text-sm leading-relaxed">Anything goes here.</div>
    <div class="flex items-center justify-end gap-2 border-t border-border bg-muted/40 px-5 py-3">
      <button type="button" class="inline-flex cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 border border-border bg-background text-foreground [&_svg]:text-muted-foreground hover:[&_svg]:text-foreground h-8 px-3">Cancel</button>
      <button type="button" class="inline-flex cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 border border-transparent bg-[var(--button-primary)] text-[var(--button-primary-foreground)] shadow-[var(--button-shadow)] hover:bg-[var(--button-primary-hover)] [&_svg]:border-[var(--button-primary-hover)] h-8 px-3">Send invite</button>
    </div>
  </div>
</div>

What about the interactive parts?

The markup is here; the behaviour is yours. A dialog needs opening, closing and a focus trap, and a dropdown needs positioning and outside clicks. Three ways to get that without a framework:

  • Alpine.js in about ten lines: x-data, x-show, x-trap and @click.outside cover most of it.
  • Native elements. <dialog> gives you a modal with a focus trap and Escape for free; style it with the dialog classes above. <details> makes an accordion.
  • HTMX for the server round trips, with the classes applied to whatever it swaps in.

Being straight about the limit

There is no component package for plain HTML, and this page is not pretending otherwise. You get the design system and the markup; you write the behaviour. If that is more than you want to do, the React package has all of it built.