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:

  1. className — Add classes to any component, just like a native element
  2. unstyled — Opt out of even the minimal structural styles on a per-component basis
  3. Data attributes — Target components from external CSS via data-scope, data-part, and data-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:

app/globals.css
@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:

@zayne-labs/ui-react/css/theme.css
@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:

TokenDescriptionUsed by
animate-shakeError shake / jiggle effectForm.ErrorMessage
animate-fade-upFade in from belowGeneral use
animate-fade-downFade in from aboveGeneral use
animate-files-inFile item entry animationDropZone.FileItem
animate-progress-outProgress bar exit animationDropZone 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:

app/globals.css
@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:

AttributeDescriptionExample values
data-scopeIdentifies the component family"card", "dropzone", "drag-scroll"
data-partIdentifies the specific part"root", "area", "title", "list"
data-slotCombined scope + part"card-header", "drag-scroll-list"

Use them to style components from a standalone CSS file:

styles.css
/* 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:

AttributeComponent(s)Description
data-draggingDragScroll.List"true" when the user is actively dragging
data-disabledDragScroll.Prev/Next"true" when there's nothing more to scroll
data-drag-overDropZone.AreaPresent when files are hovering over the zone
data-invalidDropZone.Area, Form.*Present when validation fails
data-statusDropZone.FileItem"idle", "uploading", "success", or "error"
data-stateDropZone.FileList"active" when files exist, "inactive" when empty
data-animation-phasePresence child"enter" or "exit" (animation variant)
data-transition-phasePresence child"enter" or "exit" (transition variant)
data-mountedPresence childWhether the element is currently mounted
Edit on GitHub

Last updated on

On this page