Migrating from shadcn/ui
Mlola UI works like shadcn/ui in the way that matters: the CLI copies components into your project and the source is yours. What changes is underneath. There is no Tailwind, no Radix and no class merging: a class names the element, data-* attributes carry variant and state, and every color, size and duration is a --ml-* token derived from the theme. You can move one component at a time.
What changes
| shadcn/ui | Mlola UI |
|---|---|
npx shadcn@latest init | npx mlola-ui init |
npx shadcn@latest add button | npx mlola-ui add button |
components.json | mlola.config.json |
components/ui | components/ui (the same default alias) |
Tailwind CSS | The engine stylesheet, @mlola-ui/engine. No build plugin. |
cn(), clsx, tailwind-merge, cva | Nothing: variants and state are data-* attributes, not class strings. |
Radix UI primitives | Nothing: each component owns its behavior, from the same logic as the framework-free runtime. |
lucide-react | @mlola-ui/icons |
next-themes and the .dark class | data-mode="light" or "dark" on the root, and data-theme for the character. |
Run both while you move
Nothing collides: Mlola classes start with ml- and its tokens with --ml-, and its styles live in cascade layers named mlola. The engine places them above Tailwind's preflight whichever stylesheet loads first, so its components look right. To also keep Mlola below the utilities, so a utility you still use on a Mlola element wins until you remove it, declare the order once, before either import.
/* app.css: one line first, then both stylesheets */
@layer theme, base, mlola, components, utilities;
@import "tailwindcss";
@import "@mlola-ui/engine";This order is tested with Tailwind CSS 4.
Theme variables
In shadcn/ui you write each variable by hand, in both modes. In Mlola you never write a palette token: a theme is a few decisions, and the engine derives every role with its contrast guaranteed in light and dark. When your own CSS needs a color, read the role:
| shadcn/ui | Mlola UI |
|---|---|
--background | --ml-background |
--foreground | --ml-text |
--card, --card-foreground | --ml-surface, --ml-text |
--popover, --popover-foreground | --ml-surface-elevated, --ml-text |
--primary, --primary-foreground | --ml-primary, --ml-primary-foreground. Primary-colored text on the page reads --ml-primary-text. |
--secondary, --secondary-foreground | --ml-background-subtle, --ml-text (what variant="secondary" uses) |
--muted, --muted-foreground | --ml-background-subtle, --ml-text-muted |
--accent, --accent-foreground | --ml-fill-hover, --ml-text |
--destructive | --ml-danger, with --ml-danger-foreground and --ml-danger-text |
--border | --ml-border, or --ml-border-subtle for dividers |
--input | --ml-control-border |
--ring | --ml-focus, the color; --ml-ring is the whole focus halo |
--radius | --ml-radius-xs to --ml-radius-lg by the size of the thing, and --ml-radius-pill |
--chart-1 to --chart-5 | --ml-chart-1 to --ml-chart-6 |
--sidebar and its set | The planes: the app shell's sidebar reads --ml-background-subtle, --ml-fill-hover and --ml-fill-active. |
| — | --ml-success, --ml-warning and --ml-info, each with -foreground and -text |
Dark mode is an attribute, and so is the theme:
<!-- shadcn/ui: a class, usually set by next-themes -->
<html class="dark">
<!-- Mlola UI: the character and the mode -->
<html data-theme="graphite" data-mode="dark">Variants and state
In React, variants stay props. Color moves to one shared word, tone, which means the same thing on every component that has it:
// shadcn/ui
<Button variant="destructive" size="sm">Delete</Button>
<Button variant="ghost" size="icon" aria-label="Close"><X /></Button>
<Badge variant="secondary">Draft</Badge>
<Alert variant="destructive">…</Alert>
toast.error("Could not save");
// Mlola UI
<Button variant="danger" size="sm">Delete</Button>
<Button variant="subtle" size="icon" aria-label="Close"><IconX /></Button>
<Badge tone="neutral">Draft</Badge>
<Alert tone="danger">…</Alert>
toast.danger("Could not save");In HTML, or any template language, the same thing is an attribute:
<button class="ml-button" data-variant="danger" data-size="sm">Delete</button>
<span class="ml-badge" data-tone="neutral">Draft</span>A variant of your own is a value and a rule, not a class string:
/* shadcn/ui: a new key in buttonVariants, a string of utilities */
/* Mlola UI: the source is yours. Add "brand" to ButtonVariant,
then one rule beside the others in button.css, reading tokens */
.ml-button[data-variant="brand"] {
background: var(--ml-info);
color: var(--ml-info-foreground);
}Components
Every shadcn/ui component and where it goes. Items marked Pro are part of Mlola Pro.
| shadcn/ui | Mlola UI | Notes |
|---|---|---|
| Accordion | Accordion | The same parts: AccordionItem, AccordionTrigger, AccordionContent. |
| Alert | Alert | variant="destructive" becomes tone="danger"; success, warning and info are there too. |
| Alert Dialog | Modal | role="alertdialog": announced at once, and the backdrop does not dismiss it. |
| Aspect Ratio | Native CSS aspect-ratio | |
| Avatar | Avatar | Sizes xs to xl, a presence status, and AvatarGroup. |
| Badge | Badge | Color is tone (neutral, primary, info, success, warning, danger); variant is soft, solid or outline. |
| Breadcrumb | Breadcrumb | |
| Button | Button | default becomes primary, destructive becomes danger, ghost becomes subtle; size default becomes md. loading replaces a spinner. |
| Button Group | Segmented Controlml-cluster | A segmented control for one choice; the cluster primitive to lay buttons out. |
| Calendar | Calendar | mode is single, multiple or range. |
| Card | Card | The same parts: CardHeader, CardTitle, CardDescription, CardContent, CardFooter. |
| Carousel | Carousel | |
| Chart | Chart ProLine Chart ProBar Chart ProDonut Chart Pro | Native SVG, with no charting library. |
| Checkbox | Checkbox | |
| Collapsible | Accordionor a native <details> | |
| Combobox | Combobox | |
| Command | Command Menu | |
| Context Menu | Context Menu | |
| Data Table | TableData Grid Pro | The table has the same parts; the data grid adds sorting, selection and editing. |
| Date Picker | Date Picker | One component, with mode single or range. |
| Dialog | Modal | ModalHeader, ModalBody and ModalFooter. |
| Drawer | Sheet | side="bottom". |
| Dropdown Menu | Dropdown Menu | Items are data passed as items, not children. |
| Empty | Empty State | |
| Field, Form, Label | Input | Field wires the label, hint and error for you. Keep your form library: Input forwards its ref. |
| Hover Card | Hover Card | |
| Input | Input | |
| Input OTP | OTP Input | |
| Kbd | Kbd | Shortcut spells a combination for the visitor's platform. |
| Menubar | Dropdown Menu | One dropdown menu per menu. |
| Navigation Menu | App Shell | For application navigation; there is no site mega-menu yet. |
| Pagination | Pagination | |
| Popover | Popover | |
| Progress | Progress | Also CircularProgress. |
| Radio Group | Radio Group | |
| Resizable | Resizable | |
| Scroll Area | Native scrolling; scroll areas inside components already use thin, themed scrollbars | |
| Select | Select | Options are data; searchable, clearable and multiple are props. |
| Separator | ml-divider | |
| Sheet | Sheet | |
| Sidebar | App Shell | Sidebar, SidebarSection and SidebarItem, with a drawer on phones. |
| Skeleton | Skeleton | |
| Slider | Slider | |
| Sonner | Toast | <Toaster /> and toast(), with success, warning, info, loading and promise; toast.error becomes toast.danger. |
| Switch | Toggle | Exported as Switch. |
| Table | Table | The same parts: TableHeader, TableBody, TableRow, TableHead, TableCell, TableCaption. |
| Tabs | Tabs | The same parts, plus variant (default, pills, enclosed) and orientation. |
| Textarea | Textarea | |
| Toggle | Button | variant="subtle" or "secondary" with aria-pressed. |
| Toggle Group | Segmented Control | |
| Tooltip | Tooltip | The text is the content prop; there is no TooltipContent. |
| Typography | ml-headingml-ledeml-eyebrowml-fine-print |
Mlola also has components shadcn/ui does not: Tag Input, Number Input, Time Picker, Color Picker, Stepper, Timeline, Rating, Dropzone, Tour, Virtual List.
An order that works
- Initialize. Run
npx mlola-ui init. To let Tailwind utilities keep winning while you move, add the layer line above to the stylesheet that imports Tailwind. - Bring the brand. Paste your
--primaryinto the Studio as the brand color, thennpx mlola-ui theme pull <id>. Or writemlola.theme.jsonyourself and render it offline withnpx mlola-ui theme build, or start from a canonical theme. - Swap the leaves. Button, Badge, Input, Card and the other parts with no children of their own. Most are a new import and a renamed prop.
- Swap the overlays. Dialog, Sheet, Popover, Dropdown Menu, Select and Tooltip. Their items and content become props.
- Remove what is left. When nothing imports Radix, cva, clsx, tailwind-merge or lucide-react any more, uninstall them. Tailwind can stay for your own markup.
A coding agent can do most of this. Give it the Mlola MCP server, then ask it to move one component at a time and to run check_markup on what it writes.
shadcn/ui, Tailwind CSS and Radix are the work of their authors. Mlola UI is not affiliated with them; the names are used only to describe a migration.