Styling
Learn how to style Zayne UI components with Tailwind CSS, vanilla CSS, or any approach you prefer
Zayne UI is headless — components ship with zero visual styling by default. A few components include minimal structural CSS (e.g., flexbox layout on DragScroll.List), but everything visual is yours to define.
You have three tools for styling:
className— Add classes to any component, just like a native elementunstyled— Opt out of even the minimal structural styles on a per-component basis- Data attributes — Target components from external CSS via
data-scope,data-part, anddata-slot
Using with Tailwind CSS v4
Zayne UI includes an optional Tailwind CSS v4 preset. Import it in your global CSS as shown in the Installation Guide:
@import "tailwindcss";
@import "@zayne-labs/ui-react/css/preset.css";Theme tokens
The preset registers CSS custom properties scoped under zu-* that you can use throughout your project:
@theme {
--color-zu-foreground: oklch(0.145 0 0);
--color-zu-accent: oklch(0.967 0.001 286.375);
--color-zu-accent-foreground: oklch(0.205 0 0);
--color-zu-muted-foreground: oklch(0.556 0 0);
--color-zu-destructive: oklch(0.577 0.245 27.325);
--color-zu-primary: oklch(0.21 0.04 265.75);
--color-zu-primary-foreground: oklch(0.985 0 0);
--color-zu-ring: oklch(0.705 0.015 286.067);
}
:where(.dark, [data-theme="dark"]) {
--color-zu-foreground: oklch(0.985 0 0);
--color-zu-primary: oklch(0.92 0.004 286.32);
/* ... */
}Use these tokens in your Tailwind classes:
<button className="bg-zu-primary text-zu-primary-foreground hover:bg-zu-primary/90">Submit</button>Dark mode activates automatically with the .dark class or data-theme="dark" attribute on any
ancestor element.
Animations
The preset includes named animations used by components internally, but you can also use them directly:
| Token | Description | Used by |
|---|---|---|
animate-shake | Error shake / jiggle effect | Form.ErrorMessage |
animate-fade-up | Fade in from below | General use |
animate-fade-down | Fade in from above | General use |
animate-files-in | File item entry animation | DropZone.FileItem |
animate-progress-out | Progress bar exit animation | DropZone progress |
Utilities
The preset adds a scrollbar-hidden utility that hides scrollbars across all browsers while keeping the content scrollable:
<DragScroll.List className="scrollbar-hidden flex gap-4 overflow-x-auto" />Styling example
Here's a fully styled DropZone using Tailwind and the preset tokens:
import { DropZone } from "@zayne-labs/ui-react/ui/drop-zone";
<DropZone.Root className="w-full max-w-xl">
<DropZone.Area
className="flex flex-col items-center gap-3 rounded-lg border-2 border-dashed
border-gray-300 p-12 transition-colors hover:bg-gray-50
data-drag-over:border-zu-primary data-drag-over:bg-zu-primary/5"
>
<p className="font-medium text-gray-700">Drop files here</p>
<DropZone.Trigger className="rounded-md bg-zu-primary px-6 py-2 text-white hover:opacity-90">
Select Files
</DropZone.Trigger>
</DropZone.Area>
</DropZone.Root>;Notice how data-drag-over: targets the data-drag-over attribute that DropZone.Area sets automatically when files are hovering.
The unstyled prop
Some UI components apply minimal structural CSS by default (like flex layout, cursor styles, or scrollbar hiding). If you want full control, pass unstyled to remove those base styles:
/* Default — includes structural CSS (e.g., display:flex, cursor:grab) */
<DragScroll.List className="gap-4 overflow-x-auto" />;
/* Unstyled — no structural CSS at all, you define everything */
<DragScroll.List unstyled={true} className="flex cursor-grab gap-4 overflow-x-auto" />;The unstyled prop is available on UI component parts that have base styles (like DragScroll.Root, DragScroll.List, DragScroll.Prev, DragScroll.Next). Utility components have no base styles, so they don't need it.
Using without Tailwind
Pre-compiled styles
If you're not using Tailwind, import the compiled CSS bundle in your entry file:
@import "@zayne-labs/ui-react/style.css";Vanilla CSS via data attributes
Every component renders three data attributes for CSS targeting. This means you never need to rely on internal class names or DOM structure:
| Attribute | Description | Example values |
|---|---|---|
data-scope | Identifies the component family | "card", "dropzone", "drag-scroll" |
data-part | Identifies the specific part | "root", "area", "title", "list" |
data-slot | Combined scope + part | "card-header", "drag-scroll-list" |
Use them to style components from a standalone CSS file:
/* Target a specific component part */
[data-scope="drag-scroll"][data-part="list"] {
display: flex;
gap: 1rem;
overflow-x: auto;
cursor: grab;
scrollbar-width: none; /* Firefox */
}
[data-scope="drag-scroll"][data-part="list"]:active {
cursor: grabbing;
}
/* Or use the combined slot for shorter selectors */
[data-slot="drag-scroll-list"] {
scrollbar-width: none;
}
/* Target stateful attributes */
[data-scope="drag-scroll"][data-part="list"][data-dragging="true"] {
cursor: grabbing;
user-select: none;
}
[data-scope="drop-zone"][data-part="area"][data-drag-over] {
border-color: var(--color-zu-primary);
background: oklch(0.97 0.01 265);
}Using data-slot is a convenient shorthand when you don't need the specificity of data-scope +
data-part. Both approaches target the same elements.
Stateful data attributes
Many components set additional data attributes to reflect their current state. These are documented on each component's page, but here's a summary of the most useful ones:
| Attribute | Component(s) | Description |
|---|---|---|
data-dragging | DragScroll.List | "true" when the user is actively dragging |
data-disabled | DragScroll.Prev/Next | "true" when there's nothing more to scroll |
data-drag-over | DropZone.Area | Present when files are hovering over the zone |
data-invalid | DropZone.Area, Form.* | Present when validation fails |
data-status | DropZone.FileItem | "idle", "uploading", "success", or "error" |
data-state | DropZone.FileList | "active" when files exist, "inactive" when empty |
data-animation-phase | Presence child | "enter" or "exit" (animation variant) |
data-transition-phase | Presence child | "enter" or "exit" (transition variant) |
data-mounted | Presence child | Whether the element is currently mounted |
Last updated on