Introduction
Mlola UI is a component system built on native CSS tokens, owned behavior and a theme engine. It has no runtime dependencies beyond React for the React components, and none at all for the CSS and the framework-free behavior.
Why Mlola
- No utility framework. Components are styled by semantic
ml-*classes the engine generates. No Tailwind, CSS-in-JS or class-merging helpers. - Themes, not palettes. 5 canonical themes move type, geometry, depth and motion together. Your brand is one JSON file.
- You own the source. The CLI copies components into your project instead of hiding them in a package.
- Readable by machines. A generated
agents.mdand registry schema tell coding agents exactly which classes and attributes exist.
Installation
Initialize your project
Writes
mlola.config.jsonandstyles/mlola/index.css, the one stylesheet you import, and installs@mlola-ui/enginewith your package manager. Files go where your@/alias points, or intosrc/. Without an alias, as in a new Vite app, the copied files import each other by relative path, so there is nothing to set up first.npx mlola-ui initAdd components
Each one is copied, with the internal helpers it needs, into your components folder, and the packages it imports are installed. Pass
--no-installto install them yourself.npx mlola-ui add button card inputLoad the styles and pick a theme
Import the stylesheet once and set
data-themeon the root element. In a Vite app, import./styles/mlola/index.cssinsrc/main.tsxand putdata-themeon the<html>element inindex.html. Tailwind can stay: in either import order, Mlola sits above its preflight.app/layout.tsx import { Inter, JetBrains_Mono } from "next/font/google"; import "@/styles/mlola/index.css"; // The themes' type reads these variables; without them the page uses system fonts. const inter = Inter({ subsets: ["latin"], variable: "--font-inter" }); const mono = JetBrains_Mono({ subsets: ["latin"], variable: "--font-jetbrains" }); export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="en" data-theme="graphite" data-mode="system" className={`${inter.variable} ${mono.variable}`}> <body>{children}</body> </html> ); }Use them
app/page.tsx import { Button } from "@/components/ui/button"; export default function Page() { return <Button variant="primary">Save</Button>; }
Fonts and color mode
The themes set their type in Inter, Newsreader (Atelier's headings) and JetBrains Mono, through three variables: --font-inter, --font-newsreader and --font-jetbrains. Load the fonts and set the variables on the root, as the layout above does with next/font; without them the page falls back to the system's fonts. A theme of your own names its fonts in mlola.theme.json.
data-mode is light, dark, or system, which follows the reader's own setting with no script: the dark palette applies when their device asks for it.
In a Next.js project
Copied components pass eslint-config-next. The one rule to relax is @next/next/no-img-element for components/ui: an avatar shows whatever image URL it is given, which next/image cannot do without configuring every host.
// eslint.config.mjs, after the Next.js configs
{
// Mlola components show any image URL they are given, which next/image
// cannot do without per-host configuration.
files: ["components/ui/**"],
rules: { "@next/next/no-img-element": "off" },
}Without React
The stylesheet and the behavior runtime work with any server or framework. Render the markup, load the CSS, and let @mlola-ui/behavior attach keyboard and state handling to what is already in the page.
<!doctype html>
<html data-theme="graphite" data-mode="light">
<head>
<link rel="stylesheet" href="/node_modules/@mlola-ui/engine/generated/mlola.css" />
</head>
<body>
<button class="ml-button" data-variant="primary">Save</button>
<script type="module">
import { observe } from "@mlola-ui/behavior";
observe();
</script>
</body>
</html>