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.
<!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-css/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, admin and marketing alike, already generated. It is about 103 kB before compression and roughly 18 kB gzipped, and it needs no Tailwind, no PostCSS and no build.
Dark mode is a class
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.<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 type="button" class="inline-flex cursor-pointer items-center justify-center gap-2 whitespace-nowrap 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-[var(--vui-icon-size)] [&_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-[var(--vui-control-h)] rounded-lg px-4">
Save
</button>
<button type="button" class="inline-flex cursor-pointer items-center justify-center gap-2 whitespace-nowrap 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-[var(--vui-icon-size)] [&_svg]:shrink-0 border border-border bg-background text-foreground [&_svg]:text-[var(--vui-button-glyph,var(--muted-foreground))] hover:[&_svg]:text-[var(--vui-button-glyph-hover,var(--foreground))] h-[var(--vui-control-h)] rounded-lg px-4">
Cancel
</button>Badge
Status pills. The success and warning variants already carry their dark-mode colours.
<span class="vui-badge border-transparent bg-success/10 text-success">Active</span>
<span class="vui-badge border-transparent bg-muted text-muted-foreground">Draft</span>
<span class="vui-badge 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.
<section class="vui-card overflow-hidden">
<header class="vui-dialog-header">
<h2 class="vui-dialog-title">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.
<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="vui-select-trigger">
<option>Platform</option>
<option>Growth</option>
</select>
<input type="checkbox" class="vui-checkbox" />Table
Bordered rows with a muted header, matching the datatable the React package ships.
<div class="vui-card 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="vui-badge border-transparent bg-success/10 text-success">Active</span>
</td>
</tr>
</tbody>
</table>
</div>Menu
The bordered-row list standard. Separators between rows, none trailing.
<div class="w-56 overflow-hidden rounded-md border border-border bg-popover text-sm shadow-md">
<button type="button" class="vui-dropdown-item">Rename</button>
<button type="button" class="vui-dropdown-item">Duplicate</button>
<button type="button" class="vui-dropdown-item">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.
<div class="vui-overlay-in fixed inset-0 z-[70] flex items-center justify-center p-4 bg-foreground/25 supports-[backdrop-filter]:backdrop-blur-md">
<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-[var(--vui-shadow-xl)]">
<div class="vui-dialog-header">
<h2 class="vui-dialog-title">Invite teammate</h2>
</div>
<div class="vui-dialog-body">Anything goes here.</div>
<div class="vui-dialog-footer">
<button type="button" class="inline-flex cursor-pointer items-center justify-center gap-2 whitespace-nowrap 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-[var(--vui-icon-size)] [&_svg]:shrink-0 border border-border bg-background text-foreground [&_svg]:text-[var(--vui-button-glyph,var(--muted-foreground))] hover:[&_svg]:text-[var(--vui-button-glyph-hover,var(--foreground))] h-[var(--vui-control-h)] rounded-lg px-4">Cancel</button>
<button type="button" class="inline-flex cursor-pointer items-center justify-center gap-2 whitespace-nowrap 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-[var(--vui-icon-size)] [&_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-[var(--vui-control-h)] rounded-lg px-4">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-trapand@click.outsidecover 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