Xiod Ui
Published by imkkingshuk in xiodui
What this skill does
Build React UIs with XiodUI (the `xiod-ui` npm package) — 90 accessible components, 19 colour palettes, built on Base UI and Tailwind CSS 4. Use when installing xiod-ui, importing or composing its components (Button, Dialog, Field, Form, Select, Toast, ColorPicker, Sidebar, and the rest), wiring dark mode or a palette, swapping a component's built-in icon, fixing an import that does not resolve, or migrating an app from shadcn/ui, HeroUI, MUI, Ant Design, Mantine, or Chakra UI to XiodUI. Also us
Add Xiod Ui to your agent
Review the source and files first. When you are ready, copy the prompt instruction or use the CLI command supported by your environment.
Install with a prompt
Paste this into a compatible coding agent:
add this skill "xiod-ui" from https://github.com/imkkingshuk/xioduiInstall with the CLI
Run this command in a controlled environment after reviewing the repository:
npx skills add https://github.com/imkkingshuk/xiodui --skill xiod-uiSkill instructions
XiodUI
A React component library shipped only as an npm package. No CLI, no init
command, no copy-the-source-into-your-repo flow. Installing the package installs
every component.
- 90 components, each on its own import subpath
- Built on Base UI (bundled — never install it yourself)
- Styled with Tailwind CSS 4 and CSS custom properties
- 19 palettes + a neutral base, light and dark for each
- React 19.3+, Tailwind CSS 4.3+, Node 20+
Install
npm install xiod-ui react react-dom
Tailwind CSS 4 as a dev dependency, if the project does not have it:
npm install -D tailwindcss @tailwindcss/postcss
Then, in the global stylesheet (app/globals.css, src/index.css, …):
@import "tailwindcss";
@import "xiod-ui/styles";
Those two lines are the entire setup. There is no config file and no
Tailwind plugin to register. xiod-ui/styles supplies the colour tokens, the
dark variant, the base reset, and a @source directive that tells Tailwind to
scan the package so the utility classes components reference actually get
generated. Skip the import and every component renders unstyled.
Base UI and XiodIcons arrive as dependencies of xiod-ui. Install xiod-icons
directly only when importing icons yourself.
Import
One component family per subpath. The slug is the kebab-case file name:
import { Button } from "xiod-ui/button";
import { Dialog, DialogPopup, DialogTitle } from "xiod-ui/dialog";
import { InputOtp } from "xiod-ui/input-otp";
import { ThemeProvider, useTheme } from "xiod-ui/theme-provider";
Standalone hooks live under xiod-ui/hooks/<name>. There are two modules:
import { useCopyToClipboard } from "xiod-ui/hooks/use-copy-to-clipboard";
import { useIsMobile, useMediaQuery } from "xiod-ui/hooks/use-media-query";
const { copyToClipboard, isCopied } = useCopyToClipboard({ timeout: 2000 });
const wide = useMediaQuery("lg"); // min-width: 1024px
const narrow = useMediaQuery("max-md"); // below 800px
const tablet = useMediaQuery({ min: "sm", max: "lg", pointer: "coarse" });
const isMobile = useIsMobile(); // same as useMediaQuery("max-md")
Named breakpoints follow Tailwind except md, which is 800px, not 768. Pass
a number ({ max: 768 }) to match Tailwind's md: exactly. Both hooks return
false during server rendering.
Hooks tied to one component (useSidebar, useTheme, useCarousel, …) come
from that component's subpath and are listed on its reference page.
There is no root barrel. import { Button } from "xiod-ui" throws
ERR_PACKAGE_PATH_NOT_EXPORTED — the exports map has no . entry, on purpose.
If unsure which subpath a component lives on, check references/components/
below, or read node_modules/xiod-ui/dist/components/ — the installed package
is the source of truth.
Class merging
Components accept className and merge it last-wins. The library uses the
cn package (MIT, a compiled clsx + tailwind-merge). To use the same helper:
npm install cn
import { cn } from "cn";
xiod-ui does not re-export it. Never build class strings by concatenation or
template interpolation — conflicting Tailwind utilities will both survive.
Compose
Components are compound: a root plus named parts, all from the same subpath.
import { Button } from "xiod-ui/button";
import {
Dialog,
DialogClose,
DialogDescription,
DialogFooter,
DialogHeader,
DialogPanel,
DialogPopup,
DialogTitle,
DialogTrigger,
} from "xiod-ui/dialog";
import { Field, FieldLabel } from "xiod-ui/field";
import { Form } from "xiod-ui/form";
import { Input } from "xiod-ui/input";
export function EditProfile() {
return (
<Dialog>
<DialogTrigger render={<Button variant="outline" />}>
Open Dialog
</DialogTrigger>
<DialogPopup className="sm:max-w-sm">
<Form className="contents">
<DialogHeader>
<DialogTitle>Edit profile</DialogTitle>
<DialogDescription>
Make changes to your profile here.
</DialogDescription>
</DialogHeader>
<DialogPanel className="grid gap-4">
<Field>
<FieldLabel>Name</FieldLabel>
<Input defaultValue="Optimus Prime" type="text" />
</Field>
</DialogPanel>
<DialogFooter>
<DialogClose render={<Button variant="ghost" />}>
Cancel
</DialogClose>
<Button type="submit">Save</Button>
</DialogFooter>
</Form>
</DialogPopup>
</Dialog>
);
}
render, not asChild
This is the single most common mistake. XiodUI follows Base UI's composition
API: to make a component render as a different element or component, pass
render a React element (or a function returning one). The parent's props
and behaviour are merged into it.
// ✅ Trigger renders as a Button
<DialogTrigger render={<Button variant="outline" />}>Open</DialogTrigger>
// ✅ Render as a plain tag
<Text variant="heading3" render={<h2 id="install" />}>Install</Text>
// ❌ shadcn/Radix pattern — `asChild` does not exist here
<DialogTrigger asChild><Button>Open</Button></DialogTrigger>
Children stay where they are — they go on the outer component, not inside the
element passed to render.
Variants
Most components take variant and size. Button, for example:
<Button>Default</Button>
<Button variant="secondary" size="sm">Secondary</Button>
<Button variant="destructive">Delete</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="destructive-outline">Careful</Button>
<Button variant="link">Link</Button>
<Button size="icon" aria-label="Copy"><Copy /></Button>
Sizes: xs sm default lg xl, plus icon-xs … icon-xl for square
icon buttons. Every component's exact prop table is in
references/components/<slug>.md.
Fields and validation
Field wires label, control, description, and error together with the right
ARIA relationships. Put the control inside it and pass state on the Field:
<Field invalid>
<FieldLabel>Subdomain</FieldLabel>
<Input defaultValue="my workspace" type="text" />
<FieldError match>Subdomains cannot contain spaces.</FieldError>
</Field>
<Field disabled>
<FieldLabel>Plan</FieldLabel>
<Input defaultValue="Enterprise" type="text" />
<FieldDescription>Contact support to change plans.</FieldDescription>
</Field>
Toasts
Toasts are imperative. Mount the provider once near the root:
// app/providers.tsx
"use client";
import { ToastProvider } from "xiod-ui/toast";
export function Providers({ children }: { children: React.ReactNode }) {
return <ToastProvider position="bottom-right">{children}</ToastProvider>;
}
Then call the manager from any client code — an event handler, a server action's result, a plain function:
import { toastManager } from "xiod-ui/toast";
toastManager.add({
title: "Event has been created",
description: "Monday, January 3rd at 6:00pm",
});
toastManager only shows toasts while a ToastProvider is mounted. For toasts
anchored to an element, mount AnchoredToastProvider and call
anchoredToastManager instead.
xiod-ui/morphic-toast is a separate toast system with a gooey merge
animation. Mount <MorphicToaster /> once, then call morphicToast:
import { morphicToast } from "xiod-ui/morphic-toast";
morphicToast.success({ title: "Saved", description: "All changes synced." });
The two systems are independent: toastManager does not reach
MorphicToaster, and morphicToast does not reach ToastProvider.
Icons
import { ArrowRight, Check, Copy } from "xiod-icons";
Named exports only. Icons inherit currentColor and size from the surrounding
component.
Replacing built-in icons
Every icon a component renders itself is replaceable.
To change one place, pass a prop. A part with one icon takes icon; a part with
several takes named props (prevIcon / nextIcon, triggerIcon / clearIcon,
copiedIcon / copyIcon). The value is any ReactNode:
<DrawerClose closeIcon={<XMarkIcon className="size-4" />} />
To change one everywhere, use IconProvider at the app root:
import { IconProvider } from "xiod-ui/icon-provider";
<IconProvider icons={{ Cancel: XMarkIcon, Check: CheckIcon }}>
<App />
</IconProvider>;
Keys come from the IconName union. Pass components, not elements — the
component sets the size and colour. Nested providers merge, the nearest winning.
A prop beats the provider, which beats the default. null renders no icon.
FileUpload's file-type icons and AgentSteps' status icons follow the content, so change those through the provider.
Theme
Dark mode
ThemeProvider and useTheme come from xiod-ui/theme-provider. The provider
toggles light/dark classes on <html>, which is exactly what the package's
dark variant keys off. Do not add next-themes.
// app/providers.tsx
"use client";
import { ThemeProvider } from "xiod-ui/theme-provider";
export function Providers({ children }: { children: React.ReactNode }) {
return <ThemeProvider defaultTheme="system">{children}</ThemeProvider>;
}
"use client";
import { useTheme } from "xiod-ui/theme-provider";
const { theme, resolvedTheme, setTheme } = useTheme();
// theme: "light" | "dark" | "system" (what the user picked)
// resolvedTheme: "light" | "dark" (never "system")
Provider props and their defaults: defaultTheme ("system"),
enableSystemTheme (true), attribute ("class"), storageKey
("theme"), defaultPalette (unset), paletteAttribute ("data-palette"),
paletteStorageKey ("palette"), nonce (unset). attribute and
paletteAttribute must differ, or the two axes overwrite each other on the
same element.
The provider applies the saved theme and palette before the first paint, on
server-rendered pages too, without touching <html> before React hydrates.
Render it inside <body> and pass nonce when a CSP requires one for inline
scripts. Nothing else: do not add a theme script to <head> or
suppressHydrationWarning to <html>, and remove both when migrating.
Write dark styles as dark: classes, or in CSS with @variant dark, including
dark token overrides: :root { @variant dark { --primary: …; } }. Never a
plain .dark { … } or html.dark … rule: it only applies once React has
hydrated, so it flashes on server-rendered pages.
Palettes
A palette is one stylesheet redefining every token plus --radius. Nineteen
ship with the package, in two families:
Core (restrained enough to ship as-is): clay sepia sage lagoon
cobalt iris roast crimson sorbet
Expressive (more personality): riso afterglow cathode ion cel
taffy mochi toxin cinder blacklight
The neutral base needs no import — it is what xiod-ui/styles already gives.
One fixed palette — import it after xiod-ui/styles; it overrides the base
tokens outright and nothing in the markup changes:
@import "tailwindcss";
@import "xiod-ui/styles";
@import "xiod-ui/themes/cobalt";
Order matters. Before xiod-ui/styles, the base tokens win.
Let users switch at runtime — import the /scoped variants, which apply
only under their own data-palette attribute, so importing several is safe:
@import "xiod-ui/styles";
@import "xiod-ui/themes/cobalt/scoped";
@import "xiod-ui/themes/lagoon/scoped";
"use client";
import { useTheme } from "xiod-ui/theme-provider";
const { palette, setPalette } = useTheme();
setPalette("cobalt");
setPalette(undefined); // back to the neutral base
setPalette writes data-palette on <html>, which is the attribute the
/scoped stylesheets key off — so runtime switching needs the scoped imports,
not the plain ones.
Theme and palette are independent axes; each palette defines both colour schemes, so there is no light×palette grid to maintain.
Adjusting tokens
Override individual tokens below the imports in the app's own stylesheet. A palette is a starting point, not a lock.
@import "xiod-ui/styles";
@import "xiod-ui/themes/cobalt";
:root {
--radius: 0.25rem;
--primary: #1d4ed8;
}
Components read only tokens, so this reaches all 90 of them at once. Prefer it
over per-component className colour overrides.
Coming from shadcn/ui
Most training data is shadcn/ui, and XiodUI is not shadcn/ui. The part names, props, state attributes, setup, and icon set all differ. Do not write a shadcn name and hope it resolves: none of the names in the left column exist in XiodUI, and a wrong name fails at build time or renders nothing.
Part names
| shadcn/ui writes | XiodUI uses |
|---|---|
DialogContent, DialogOverlay | DialogPopup, DialogBackdrop |
AlertDialogContent | AlertDialogPopup |
AlertDialogAction, AlertDialogCancel | AlertDialogClose render={<Button />} for both |
DropdownMenu, DropdownMenuTrigger, DropdownMenuContent | Menu, MenuTrigger, MenuPopup from xiod-ui/menu |
DropdownMenuItem, DropdownMenuLabel, DropdownMenuSub | MenuItem, MenuGroupLabel, MenuSub (every DropdownMenu* is Menu*) |
ContextMenuContent, MenubarContent | ContextMenuPopup, MenubarPopup |
HoverCard, HoverCardTrigger, HoverCardContent | PreviewCard, PreviewCardTrigger, PreviewCardPopup |
PopoverContent, TooltipContent, SelectContent | PopoverPopup, TooltipPopup, SelectPopup |
SelectLabel | SelectGroupLabel |
TabsTrigger, TabsContent | TabsTab, TabsPanel |
AccordionContent, CardContent, CollapsibleContent | AccordionPanel, CardPanel, CollapsiblePanel |
RadioGroup, RadioGroupItem | Radio, RadioItem from xiod-ui/radio |
Sheet, SheetContent, side="right" | Drawer, DrawerPopup, position="right" |
InputOTP, InputOTPGroup, InputOTPSlot | InputOtp, InputOtpGroup, InputOtpInput |
Toaster + toast("…") from sonner | ToastProvider + toastManager.add({ title }) |
Form, FormField, FormItem, FormControl, FormMessage | Form + Field, FieldLabel, FieldDescription, FieldError |
Spinner, Typography | Loader, Text |
Props and behaviour
asChild→render. Seerender, notasChild.- Accordion: there is no
type="single"orcollapsible. One item open at a time is the default, and any item can close. Passmultipleto allow several. - Menu items: use
onClick, notonSelect. The menu closes on click by default; passcloseOnClick={false}to keep it open. - Forms: there is no react-hook-form wrapper. Use
Form(errors,onFormSubmit) withFieldparts, as in Fields and validation. - Slider:
valueanddefaultValuetake a number for one thumb. Pass an array only for a range. - Select: pass
items={[{ label, value }]}toSelect, orSelectValueshows the raw value instead of the label. - Combobox, Autocomplete, Command: data-driven. Pass the entries as
itemson the root; the…Listpart takes a function that returns one…Itemper entry (<ComboboxList>{(item) => <ComboboxItem key={item.value} value={item}>{item.label}</ComboboxItem>}</ComboboxList>). Filtering works only throughitems. Static children, as in cmdk, don't filter.
Styling state
Components set Base UI's data attributes, not Radix's data-state:
| Radix / shadcn/ui | XiodUI |
|---|---|
data-[state=open]: | data-open: on popups, data-popup-open: on popup triggers, data-panel-open: on accordion and collapsible triggers |
data-[state=checked]: | data-checked: (data-unchecked:, data-indeterminate:) |
data-[state=active]: (tabs) | data-active: |
data-[highlighted]: | data-highlighted: |
data-[disabled]: | data-disabled: |
animate-in fade-in / animate-out | nothing — popups already animate with data-starting-style and data-ending-style |
var(--radix-popover-trigger-width) | var(--anchor-width) |
Colour tokens hold complete colours (--primary: var(--color-neutral-800)).
Use the utilities (bg-primary, text-muted-foreground) or var(--primary).
Never wrap a token in hsl(…) or oklch(…).
Migrating an existing app
To move an app from another UI library to XiodUI, find the library in
package.json, then read its guide before changing any code:
| The app depends on | Guide |
|---|---|
components.json, components/ui/*, @radix-ui/* | references/migrations/shadcn.md |
@heroui/react (v3 or v2), @nextui-org/react | references/migrations/heroui.md |
@mui/material | references/migrations/mui.md |
antd | references/migrations/antd.md |
@mantine/core | references/migrations/mantine.md |
@chakra-ui/react | references/migrations/chakra.md |
Every guide follows the same order. Do the steps in this order and finish each one before starting the next:
- Inventory. Run the guide's search commands and list every file that imports the old library. That list is the migration's scope.
- Set up XiodUI next to the old library. Install
xiod-ui, add the two CSS imports, and mountThemeProvider(andToastProviderif the app shows toasts). Leave the old library installed so the app keeps building. - Migrate one file at a time. Change its imports, part names, and props using the guide's tables, then build or typecheck before the next file. Never leave a file half on each library.
- Replace the old library's helpers. Its toast, theme, form, and icon APIs are handled in the guide's "Patterns" section.
- Remove the old library. Once its search commands return nothing, uninstall its packages, delete its providers, config files, and CSS, and build again.
- Verify. Work through the guide's checklist, including light and dark mode and a phone-width screen.
When a component has no XiodUI equivalent, the guide says what to do. Never invent a XiodUI component or prop to fill the gap; check Finding a component first.
Never do this
- Root barrel import.
from "xiod-ui"is not an export. Usefrom "xiod-ui/<slug>". - Installing
@base-ui/reactseparately. It ships insidexiod-ui. A second copy means two versions of the same context and broken portals. asChild. Userender={<Element />}.- A CLI, an
initstep, or copying component source into the project. None exist. Any instruction implying otherwise is wrong. - Redefining library tokens,
@custom-variant dark, or the base reset in the app's stylesheet. The package already declares them; a second declaration fights it. Override token values only, as shown above. - Importing from a relative path or from
components/ui/. There is no vendored copy to edit. - Guessing a slug. PascalCase, camelCase, and pluralised subpaths do not
resolve.
InputOtplives atxiod-ui/input-otp. - Hiding a built-in icon with CSS, or vendoring a component to change one.
Pass an icon prop, or set it globally with
IconProvider. - shadcn part names.
DialogContent,DropdownMenu,SheetContent,TabsTriggerand the rest do not exist. Use the table in Coming from shadcn/ui. npx shadcn add,components.json, orcnfrom@/lib/utils. There is nothing to add.cncomes from thecnpackage.lucide-reactor another icon set for a component's own icons. Built-in icons come fromxiod-icons; replace them with icon props orIconProvider, not by installing a second set.tailwindcss-animate,tw-animate-css,framer-motion, ormotionto animate components. Every component animates itself in CSS. Addinganimate-in/animate-outclasses doubles the animation.- A
tailwind.config.js, atheme.extend.colorsblock, orhsl(var(--…))colours. Tailwind 4 needs no config file, and the tokens are complete colours already. sonner,vaul,cmdk,embla-carousel-react, orreact-hook-formto get a toast, drawer, command palette, carousel, or form. XiodUI ships its own:xiod-ui/toast,xiod-ui/drawer,xiod-ui/command,xiod-ui/carousel,xiod-ui/formwithxiod-ui/field.data-[state=…]:selectors on library parts. Use the Base UI attributes in Styling state.
Remember "use client" on any file that holds state or handlers — most
interactive examples need it in the Next.js App Router.
Finding a component
All 90 slugs:
accordion agent-steps alert alert-dialog aspect-ratio autocomplete
avatar badge breadcrumb button button-group button-split calendar
card carousel checkbox circular-progress collapsible color-picker
combobox command context-menu copy-to-clipboard corner-badge
dashboard-grid date-picker dialog dot-matrix draggable drawer
empty field fieldset file-upload form frame gauge grid group icon-provider
input input-group input-otp input-payment input-phone
input-sensitive kbd kinetic-click label list-box loader marker
menu menubar message meter morphic-toast navigation-menu
number-field option-picker orb pagination popover preview-card
progress radio resizable ruler-picker scroll-area scroll-bar
select separator sidebar skeleton slider sortable switch table
tabs text textarea theme-provider timeline toast toggle
toggle-group toolbar tooltip waveform wheel-picker
For the exports, props, and anatomy of a specific component, read the matching
file under references/components/ — one per slug, e.g.
references/components/dialog.md. Read only the ones the task needs.
Full documentation with live examples: https://ui.xiod.dev/docs
Files included
- references/components/accordion.md
- references/components/agent-steps.md
- references/components/alert-dialog.md
- references/components/alert.md
- references/components/aspect-ratio.md
- references/components/autocomplete.md
- references/components/avatar.md
- references/components/badge.md
- references/components/breadcrumb.md
- references/components/button-group.md
- references/components/button-split.md
- references/components/button.md
- references/components/calendar.md
- references/components/card.md
- references/components/carousel.md
- references/components/checkbox.md
- references/components/circular-progress.md
- references/components/collapsible.md
- references/components/color-picker.md
- references/components/combobox.md
- references/components/command.md
- references/components/context-menu.md
- references/components/copy-to-clipboard.md
- references/components/corner-badge.md
- references/components/dashboard-grid.md
- references/components/date-picker.md
- references/components/dialog.md
- references/components/dot-matrix.md
- references/components/draggable.md
- references/components/drawer.md
- references/components/empty.md
- references/components/field.md
- references/components/fieldset.md
- references/components/file-upload.md
- references/components/form.md
- references/components/frame.md
- references/components/gauge.md
- references/components/grid.md
- references/components/group.md
- references/components/icon-provider.md
- references/components/input-group.md
- references/components/input-otp.md
- references/components/input-payment.md
- references/components/input-phone.md
- references/components/input-sensitive.md
- references/components/input.md
- references/components/kbd.md
- references/components/kinetic-click.md
- references/components/label.md
- references/components/list-box.md
- references/components/loader.md
- references/components/marker.md
- references/components/menu.md
- references/components/menubar.md
- references/components/message.md
- references/components/meter.md
- references/components/morphic-toast.md
- references/components/navigation-menu.md
- references/components/number-field.md
- references/components/option-picker.md
- references/components/orb.md
- references/components/pagination.md
- references/components/popover.md
- references/components/preview-card.md
- references/components/progress.md
- references/components/radio.md
- references/components/resizable.md
- references/components/ruler-picker.md
- references/components/scroll-area.md
- references/components/scroll-bar.md
- references/components/select.md
- references/components/separator.md
- references/components/sidebar.md
- references/components/skeleton.md
- references/components/slider.md
- references/components/sortable.md
- references/components/switch.md
- references/components/table.md
- references/components/tabs.md
- references/components/text.md
- references/components/textarea.md
- references/components/theme-provider.md
- references/components/timeline.md
- references/components/toast.md
- references/components/toggle-group.md
- references/components/toggle.md
- references/components/toolbar.md
- references/components/tooltip.md
- references/components/waveform.md
- references/components/wheel-picker.md
- references/migrations/antd.md
- references/migrations/chakra.md
- references/migrations/heroui.md
- references/migrations/mantine.md
- references/migrations/mui.md
- references/migrations/shadcn.md
- SKILL.md

