Installation
Quick install guide for Plex UI — add the npm package, wire up the stylesheet, and configure your router in a Next.js, Vite, or Remix app
Prerequisites
Plex UI is built on top of React and Tailwind 4. To use Plex UI, you'll need to have both installed in your project, and configured with your build system.
- React 18 or 19 (install guide)
- Tailwind 4 (install guide)
Install steps
1. Install the package
npm install @plexui/ui2. Setup styles
Add the foundation styles and Tailwind layers to the top of your global stylesheet (e.g. main.css):
/* Pin the cascade-layer order so Plex UI's reset wins over Tailwind preflight
for our form controls. Without this declaration the order depends on
first-occurrence — which in Tailwind v4 means preflight overrides Plex
UI's button/input/textarea normalisations. */
@layer theme, base, components, utilities;
@import "tailwindcss";
@import "@plexui/ui/css";
/* Required for Tailwind to find class references in Plex UI components. */
@source "../node_modules/@plexui/ui";
/* The rest of your application CSS */On Vite — use the official plugin
If your bundler is Vite, drop the manual @layer line, the css.transformer override, and the <style> failsafe — @plexui/ui/vite does all three for you:
// vite.config.ts
import { defineConfig } from "vite";
import plexui from "@plexui/ui/vite";
export default defineConfig({
plugins: [plexui()],
});The plugin pins css.transformer = 'postcss' and build.cssMinify = 'esbuild' (so Vite 8's Lightning CSS minifier doesn't strip the layer declaration), and injects <style>@layer theme, base, components, utilities;</style> into <head> as a runtime failsafe before any other stylesheet.
Your main.css then only needs the imports — no @layer …; line, no other ceremony:
@import "tailwindcss";
@import "@plexui/ui/css";
@source "../node_modules/@plexui/ui";Pass plexui({ injectLayerOrder: false }) if you've already added the equivalent <style> tag to your index.html by hand.
Vite + Lightning CSS gotcha
If you're on Vite (default in Vite 5+) Lightning CSS is the active CSS transformer, and it reorders cascade layers strictly by first occurrence. In Vite 8 the minifier silently drops the bare @layer theme, base, components, utilities; declaration entirely. Symptoms when either path hits:
<button>losescursor: pointer<input>/<textarea>regains the iOS-Safari default chrome- Pill controls render square because
--radius-fullfalls through - Solid buttons render with invisible text (preflight
color: inheritwins over component CSS)
Three things together cover both Vite 5–7 and Vite 8:
-
Keep the explicit
@layerline at the top ofmain.css— it's enough on its own in Vite 5–7. -
Pin the CSS transformer + minifier so Lightning CSS doesn't strip the layer declaration on Vite 8:
// vite.config.ts export default defineConfig({ css: { transformer: 'postcss' }, build: { cssMinify: 'esbuild' }, }); -
HTML failsafe — inject the layer order through
<head>so it survives any minification path:<!-- index.html --> <head> <style>@layer theme, base, components, utilities;</style> <!-- the rest of your <head>, link/script tags etc. --> </head>Place it before any
<link rel="stylesheet">or bundler-injected stylesheet. The browser sees the layer order before any rule arrives, and your CSS imports just slot in.
Skipping Tailwind preflight (optional)
Plex UI ships its own opinionated reset in @plexui/ui/css, so Tailwind preflight is redundant. If you want only Tailwind utilities and theme, drop preflight:
@layer theme, base, components, utilities;
@import "tailwindcss/theme.css";
@import "tailwindcss/utilities.css";
@import "@plexui/ui/css";
@source "../node_modules/@plexui/ui";This sidesteps every preflight-vs-Plex-UI ordering problem.
Using the Plex radius/font scale in your Tailwind utilities (opt-in)
By default Plex UI declares its tokens in :root, so the components render correctly without touching your Tailwind theme. Your rounded-md, rounded-full, text-sm utilities keep using Tailwind's own defaults.
If you want rounded-md to render with the Plex radius (0.5rem), rounded-full to render 9999px, and your text to default to the Plex font stack, opt in with one extra import:
@layer theme, base, components, utilities;
@import "tailwindcss";
@import "@plexui/ui/css";
/* Optional — overrides Tailwind's default theme tokens with Plex's scale */
@import "@plexui/ui/styles/variables-tailwind-theme.css";
@source "../node_modules/@plexui/ui";This file uses @theme static and resets Tailwind's --radius-*, --font-*, --font-weight-*, and --breakpoint-* so they take Plex's values instead of Tailwind's defaults. Use only if you actively want that — it's a project-wide decision, not a per-page one.
Then import your stylesheet before rendering any components:
// Must be imported first to ensure Tailwind layers and style foundations are defined before any potential component styles
import "./main.css"
import { StrictMode } from "react"
import { createRoot } from "react-dom/client"
import { App } from "./App"
createRoot(document.getElementById("root")!).render(
<StrictMode>
<App />
</StrictMode>,
)3. Configure router (optional)
<PlexUIProvider> helps define your default router link component, used in components like <TextLink> and <ButtonLink>.
This provider is optional - router links can also be passed directly to components via the as prop.
// Must be imported first to ensure Tailwind layers and style foundations are defined before component styles
import "./main.css"
import { PlexUIProvider } from "@plexui/ui/components/PlexUIProvider"
import { StrictMode } from "react"
import { createRoot } from "react-dom/client"
import { Link } from 'react-router'
import { App } from "./App"
declare global {
interface PlexUIConfig {
LinkComponent: typeof Link
}
}
createRoot(document.getElementById("root")!).render(
<StrictMode>
<PlexUIProvider linkComponent={Link}>
<App />
</PlexUIProvider>
</StrictMode>,
)Start building
Your project is now ready to use Plex UI!
Here's an example of a simple reservation card, using Tailwind classes and components.
import { Badge } from "@plexui/ui/components/Badge"
import { Button } from "@plexui/ui/components/Button"
import {
Calendar,
Invoice,
Maps,
Members,
Phone,
} from "@plexui/ui/components/Icon"
export function ReservationCard() {
return (
<div className="w-full max-w-sm rounded-2xl border border-default bg-surface shadow-lg p-4">
<div className="flex items-start justify-between gap-3">
<div>
<p className="text-secondary text-sm">
Reservation
</p>
<h2 className="mt-1 heading-lg">La Luna Bistro</h2>
</div>
<Badge color="success">Confirmed</Badge>
</div>
<div>
<dl className="mt-4 grid grid-cols-[auto_1fr] gap-x-3 gap-y-2 text-sm">
<dt className="flex items-center gap-1.5 font-medium text-secondary">
<Calendar className="size-4" />
Date
</dt>
<dd className="text-right">Apr 12 · 7:30 PM</dd>
<dt className="flex items-center gap-1.5 font-medium text-secondary">
<Members className="size-4" />
Guests
</dt>
<dd className="text-right">Party of 2</dd>
<dt className="flex items-center gap-1.5 font-medium text-secondary">
<Invoice className="size-4" />
Reference
</dt>
<dd className="text-right uppercase">4F9Q2K</dd>
</dl>
</div>
<div className="mt-4 grid gap-3 border-t border-subtle pt-4 sm:grid-cols-2">
<Button variant="soft" color="secondary" block>
<Phone />
Call
</Button>
<Button color="primary" block>
<Maps />
Directions
</Button>
</div>
</div>
)
}