Basecoat ships the shadcn/ui design system as plain HTML and CSS
A components library built with Tailwind CSS that works with any web stack.
At a glance
- What is it?
- Basecoat reimplements shadcn/ui without React, Radix or any framework runtime, splitting structure from visual style into separate CSS layers. It suits server-rendered and static sites where a component library with a runtime dependency is the wrong shape.
- Who is it for?
- Adopt Basecoat if your pages are rendered outside React, especially with Nunjucks or Jinja, and you want shadcn/ui visuals without a runtime dependency. Do not adopt it if your team is already invested in shadcn/ui through React and Radix, where the component sources you would be copying do not exist here.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 21 days ago.
- What is it written in?
- Mainly MDX, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on November 13, 2026, and from our analysis. They are not legal advice.
Star history
- hunvreus/basecoat4,442 ★ · +115
| Repository | 2026-09-23 | 2026-11-13 |
|---|---|---|
| hunvreus/basecoat | 4.3k | 4.4k |
115 Stars in the last 51 days, measured from the daily GitHub snapshot. Compare with other projects
Editorial analysis
Basecoat reimplements shadcn/ui without a framework runtime
Basecoat exists because shadcn/ui assumes a JavaScript runtime you do not always have. Its stated purpose is a Tailwind CSS, vanilla HTML/CSS/JavaScript implementation of the shadcn/ui design system, providing shadcn-style components for any web stack without React, Radix, or framework runtime dependencies. Components are semantic HTML first. Behaviour lives in small vanilla JavaScript files, and the visual layer arrives as CSS.
That last point is the decision worth pausing on. shadcn/ui hands you component source files that you paste into your own repository and compile through your own React tree. Basecoat does the opposite: you consume a published package, `basecoat-css`, and you link a stylesheet. The upside is that a Django, Laravel or Astro page can use the same menu, dialog and combobox markup as a React app without shipping a bundler runtime to the browser. The cost is that the components are not yours to fork in place; changes go through the package or through overriding the CSS.
The CSS is split into base, components and style packs
Structure and appearance live in different directories, and understanding that split tells you most of what Basecoat does. `src/css/base/base.css` holds shared tokens and semantic utilities. `src/css/components/*.css` holds component layout, structure, accessibility selectors and behaviour hooks. `src/css/styles/*.css` holds the visual side: color, radius, shadow, typography, spacing, variants and state styles.
Five kinds of generated entrypoints are committed into the repository for transparency and for package imports. `src/css/basecoat.css` is the default backward compatible Vega bundle. `src/css/basecoat-base.css` is base plus components with no style pack at all. `src/css/basecoat-components.css` imports components only. Then there is one entrypoint per style pack and a matching `.cdn.css` wrapper for CDN use. These files are produced by `scripts/generate-css-entrypoints.js` and by the build scripts, not written by hand.
The generated entrypoints being committed is a deliberate choice, and it has a consequence. You can read the exact import chain before installing anything, which is unusual for a CSS package, and the CSS-first split means a component's accessibility selectors and state styles are written once, outside any React prop.
Eight style packs, and Vega is the one you get by default
Basecoat ships standalone style packs named Vega, Nova, Maia, Lyra, Mira, Luma, Sera and Rhea. Importing the bare package gets you the default bundle, which the CSS architecture section describes as the backward compatible Vega bundle. If you want a different look you import the pack by name, and if you want none of them you import the styleless base alongside your own `style-acme.css`.
Here is a detail a skim misses. The README presents the default import as a neutral act of installing, but `src/css/basecoat.css` is a named style pack wearing the default's clothes. A project that installs Basecoat and never chooses a pack has chosen Vega, and a project that wants only structure and semantics has to know that `basecoat-base` exists and skips the pack deliberately.
A second detail: `package.json` carries `"version": "1.1.0"`, which matches the newest tagged release, 1.1.0.
Installing basecoat-css and importing one bundle
The package is published to npm as `basecoat-css`, and installation is a single command. It pulls in the generated output and the template files, since `package.json` ships only `dist/` and `templates/`.
npm install basecoat-cssYour stylesheet then imports Tailwind CSS v4 and Basecoat. Start with the default bundle, which is Vega:
@import "tailwindcss";
@import "basecoat-css";Swap the second line to pick a specific style pack:
@import "tailwindcss";
@import "basecoat-css/nova";If you want the components with your own visual language, import the styleless base and layer your own stylesheet after it. That gives you the layout and accessibility selectors without Vega's color and radius decisions:
@import "tailwindcss";
@import "basecoat-css/base";
@import "./style-acme.css";The docs at basecoatui.com/installation cover the CDN path and the template paths, and Nunjucks and Jinja templates are included for server-rendered pages. Note that the package entry points in `exports` include both a default CSS file and per-component JavaScript files such as `./dropdown-menu` and `./dropdown-menu.min`, so you can load only the behaviour you use.
window.basecoat.initAll and what force actually resets
Behaviour is driven through a small global object, `window.basecoat`. `init(name)` initialises uninitialised components for one registered component. `initAll()` sweeps every registered component. `refresh(element)` asks an already initialised component to rescan dynamic children, but only where the component supports refresh. Components clean up their own listeners through internal destroy hooks when an initialised root leaves the DOM.
The interesting argument is `force`. Calling `init(name, { force: true })` or `initAll({ force: true })` destroys and reinitialises matching components, and the documentation gives the reason: a framework or navigation library restores previously initialised DOM from a cache. Force mode resets transient component state, which it names as open menus, focus state and active interactions.
That last sentence is the catch. Force is the fix for restored markup, and it is also the thing that will close an open menu or drop focus at the moment you call it. In a client-side router that fires `initAll` on every navigation, it is a blunt instrument.
Failure modes when a framework takes back the DOM
Basecoat's components are described as semantic HTML first, and that choice creates one class of bug that React users do not have. Nothing stops a server template, a client-side navigation library, or a browser extension from swapping markup after a component initialised. Basecoat's answer is `refresh` and `force`, which means the components can recover, provided someone calls the right one.
Where it goes wrong is when nobody calls either. A dropdown inserted into an already initialised page by a partial load has no event listeners until `init(name)` runs for that component or `initAll()` sweeps the page. Whether `refresh(element)` covers your case depends on the component supporting refresh, and Basecoat does not claim every component does. The README does not document any automatic MutationObserver behaviour.
There is a second, quieter limit. Because appearance is a style pack rather than component source, overriding a component means writing CSS that outranks the pack. The README points to basecoatui.com/customization and says Basecoat is compatible with shadcn/ui themes, but a heavy customisation still means writing CSS against files you did not write.
How Basecoat differs from shadcn/ui and from daisyUI
Against shadcn/ui the difference is where the code lives. shadcn/ui gives you component source inside your repository, which you edit directly and which runs inside React with Radix primitives underneath. Basecoat gives you a dependency, which you version and import, and which runs against plain DOM nodes with its own listeners. Choose Basecoat when your rendering target is a server template, a static site or a framework you did not choose for its component model. Choose shadcn/ui when you want to change a component's behaviour in source and already have React.
Against daisyUI the difference is that daisyUI attaches its components to Tailwind plugin classes and you apply those classes in your markup, while Basecoat ships prebuilt HTML structure plus stylesheets that target that structure. That means markup you did not write, and a bundle that already contains the CSS.
One operational difference matters for adoption decisions. Basecoat's docs and site live in this repository under `site/`, described as a ReallySimpleDocs/Astro documentation app, and the development section gives the commands for both the docs site and the package build. Contributors work against the same tree, which makes reading the source of a component a realistic alternative to reading the docs.
Editorial conclusion
Adopt Basecoat if your pages are rendered outside React, especially with Nunjucks or Jinja, and you want shadcn/ui visuals without a runtime dependency. Do not adopt it if your team is already invested in shadcn/ui through React and Radix, where the component sources you would be copying do not exist here. Before committing, open the style pack page for the pack you plan to ship and check that the components you actually use are covered, because the bundle you import by default is the Vega pack rather than a neutral base.
Frequently asked questions
What is Basecoat?
Basecoat is a Tailwind CSS, vanilla HTML/CSS/JavaScript implementation of the shadcn/ui design system. It provides shadcn-style components without React, Radix, or framework runtime dependencies.
How do I install Basecoat and load a style pack?
Run `npm install basecoat-css`, then import `tailwindcss` and `basecoat-css` in your stylesheet. To choose a different pack, import it by name such as `basecoat-css/nova`.
Which style packs does Basecoat ship, and which one is the default?
The packs are Vega, Nova, Maia, Lyra, Mira, Luma, Sera and Rhea. The default import gives the Vega bundle, and `basecoat-css/base` imports components with no style pack at all.
How do I reinitialize Basecoat components after client-side navigation?
Call `window.basecoat.initAll({ force: true })`, which destroys and reinitialises matching components. Force mode is meant for frameworks or navigation libraries that restore previously initialised DOM from a cache, and it resets open menus, focus state and active interactions.
Does Basecoat need React or a framework runtime?
No. It is built on semantic HTML with small vanilla JavaScript for behaviour, and it ships Nunjucks and Jinja templates for server-rendered pages.
How do I run the Basecoat documentation site locally?
Install dependencies with `npm i` and `npm --prefix site i`, then run `npm run docs:dev` for the ReallySimpleDocs/Astro site. Package assets are built with `npm run build` and the static docs site with `npm run docs:build`.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/hunvreus-basecoat)Star history chart for your README
[](https://hysenlabs.com/projects/hunvreus-basecoat)Was this useful?