Component Structure
Understanding the compound component pattern, asChild, polymorphism, and data attributes in Zayne UI
Zayne UI uses a compound component pattern — a single import gives you a namespace of sub-components that work together, sharing state through React context without prop drilling.
Compound components
Instead of one monolithic component with dozens of props, Zayne UI splits each component into composable parts. You pick the parts you need and arrange them however you want:
import { DropZone } from "@zayne-labs/ui-react/ui/drop-zone";
<DropZone.Root onUpload={handleUpload}>
<DropZone.Area>
<p>Drag and drop files here</p>
<DropZone.Trigger>Select Files</DropZone.Trigger>
</DropZone.Area>
<DropZone.FileList>
{({ fileState }) => (
<DropZone.FileItem fileState={fileState}>
<DropZone.FileItemPreview />
<DropZone.FileItemMetadata />
<DropZone.FileItemDelete>Remove</DropZone.FileItemDelete>
</DropZone.FileItem>
)}
</DropZone.FileList>
</DropZone.Root>;Each sub-component is independently renderable. You can omit DropZone.FileList if you don't need file previews, or omit DropZone.Trigger if you only want drag-and-drop with no click-to-browse.
Import patterns
Every component supports two import styles:
The namespace import gives you dot-notation access to all sub-components:
import { DropZone } from "@zayne-labs/ui-react/ui/drop-zone";
import { Show } from "@zayne-labs/ui-react/common/show";
<DropZone.Root>...</DropZone.Root>
<Show.Root when={condition}>...</Show.Root>This is the recommended approach — it keeps component families visually grouped and makes it clear which parts belong together.
If you prefer, you can import each sub-component by its full name:
import { DropZoneRoot, DropZoneTrigger, DropZoneArea } from "@zayne-labs/ui-react/ui/drop-zone";
import { ShowRoot, ShowContent, ShowFallback } from "@zayne-labs/ui-react/common/show";
<DropZoneRoot>
<DropZoneArea>
<DropZoneTrigger>Browse</DropZoneTrigger>
</DropZoneArea>
</DropZoneRoot>The as prop — polymorphism
Most components accept an as prop that changes the underlying HTML element. TypeScript enforces that only valid attributes for the chosen element are allowed:
import { Card } from "@zayne-labs/ui-react/ui/card";
// Default — renders <article>
<Card.Root>Content</Card.Root>;
// Renders <a> with full anchor props
<Card.Root as="a" href="/dashboard">
Clickable Card
</Card.Root>;
// Renders <section>
<Card.Root as="section" aria-label="stats">
Stats
</Card.Root>;Each Card sub-component has its own default element:
| Sub-component | Default element | Accepts as |
|---|---|---|
Card.Root | <article> | ✅ |
Card.Header | <header> | ✅ |
Card.Title | <h3> | ✅ |
Card.Description | <p> | ✅ |
Card.Content | <div> | ✅ |
Card.Action | <button> | ✅ |
Card.Footer | <footer> | ✅ |
ForWithWrapper is also polymorphic — the wrapper element defaults to <ul> but can be changed:
<ForWithWrapper as="ol" each={steps}>
{(step) => <li key={step.id}>{step.label}</li>}
</ForWithWrapper>The asChild prop — slot composition
Some components support asChild, which delegates rendering to the child element instead of creating a new DOM node. The parent's props (including className, event handlers, and refs) are merged into the child:
import { Card } from "@zayne-labs/ui-react/ui/card";
import { Link } from "next/link";
/* Without asChild — wraps Link in an <article> */
<Card.Root>
<Link href="/post/1">Read more</Link>
</Card.Root>;
/* With asChild — the <Link> becomes the root, no extra wrapper */
<Card.Root asChild={true}>
<Link href="/post/1" className="block rounded-lg border p-4">
Read more
</Link>
</Card.Root>;This pattern is powered by the Slot component internally. When asChild is true, the component renders a Slot.Root instead of its default element, merging all props down.
asChild requires exactly one valid React element child. Text nodes, fragments, or multiple
elements will result in an error.
Components supporting asChild: Card.Root, Card.Header, Card.Footer, Await.Root, Await.Error
The unstyled prop
Some UI components (primarily DragScroll) apply minimal structural CSS by default — things like display: flex, cursor: grab, or scrollbar hiding. If you want to control everything yourself, pass unstyled:
/* Default — includes structural styles */
<DragScroll.List className="gap-4" />;
/* Unstyled — you provide all styles */
<DragScroll.List unstyled={true} className="flex cursor-grab gap-4 overflow-x-auto" />;Utility components have no built-in styles, so they don't need this prop.
Data attributes
Every component renders three data attributes that you can use for CSS targeting without depending on internal class names or DOM structure:
| Attribute | Purpose | Example values |
|---|---|---|
data-scope | Component family | "card", "drop-zone", "drag-scroll" |
data-part | Specific part within scope | "root", "header", "area", "list" |
data-slot | Combined scope-part | "card-header", "drag-scroll-list" |
CSS targeting
/* Target all card headers */
[data-scope="card"][data-part="header"] {
display: flex;
align-items: center;
gap: 0.5rem;
}
/* Shorthand using the combined slot */
[data-slot="card-header"] {
display: flex;
align-items: center;
gap: 0.5rem;
}
/* Combine with state attributes */
[data-scope="drop-zone"][data-part="area"][data-drag-over] {
border-color: var(--color-zu-primary);
background: oklch(0.97 0.01 265);
}Tailwind CSS targeting
In Tailwind, use the data-* modifier syntax:
<>
<DropZone.Area className="border-2 border-dashed border-gray-300 data-drag-over:border-blue-500 data-drag-over:bg-blue-50" />
<DragScroll.List className="flex gap-4 data-[dragging=true]:cursor-grabbing" />
</>Stateful data attributes
Beyond the three core attributes, many components set dynamic state attributes. These are documented on each component's page. Here are the most commonly used ones:
| Attribute | Component(s) | Values |
|---|---|---|
data-dragging | DragScroll.List | "true" / "false" |
data-disabled | DragScroll.Prev/Next | "true" / "false" |
data-drag-over | DropZone.Area | Present when files hover |
data-invalid | DropZone.Area, Form parts | Present on validation errors |
data-status | DropZone.FileItem | "idle", "uploading", "success", "error" |
data-state | DropZone.FileList | "active" / "inactive" |
data-animation-phase | Presence child | "enter" / "exit" |
data-transition-phase | Presence child | "enter" / "exit" |
data-mounted | Presence child | true / false |
data-present | Presence child | true / false |
UI vs Utility components
Path: @zayne-labs/ui-react/ui/<name>
Interactive components with internal state management, compound sub-components, and data attributes. They handle complex behaviors like drag-to-scroll, file upload, or carousel sliding.
import { Card } from "@zayne-labs/ui-react/ui/card";
import { Carousel } from "@zayne-labs/ui-react/ui/carousel";
import { DragScroll } from "@zayne-labs/ui-react/ui/drag-scroll";
import { DropZone } from "@zayne-labs/ui-react/ui/drop-zone";
import { Form } from "@zayne-labs/ui-react/ui/form";Characteristics: compound pattern, data-scope/data-part/data-slot, as prop, asChild support, optional unstyled prop
Path: @zayne-labs/ui-react/common/<name>
Lightweight rendering primitives with zero styling. They solve common React patterns (conditional rendering, list iteration, portals, error handling) declaratively.
import { Show } from "@zayne-labs/ui-react/common/show";
import { For } from "@zayne-labs/ui-react/common/for";
import { Switch } from "@zayne-labs/ui-react/common/switch";
import { Await } from "@zayne-labs/ui-react/common/await";
import { Presence } from "@zayne-labs/ui-react/common/presence";
import { ErrorBoundary } from "@zayne-labs/ui-react/common/error-boundary";
import { Teleport } from "@zayne-labs/ui-react/common/teleport";
import { ClientGate } from "@zayne-labs/ui-react/common/client-gate";
import { Slot } from "@zayne-labs/ui-react/common/slot";
import { SuspenseWithBoundary } from "@zayne-labs/ui-react/common/suspense-with-boundary";Characteristics: no styling, render-only logic, some use compound pattern (Show, Switch, Await, Slot), others are standalone (For, Presence, ErrorBoundary, Teleport, ClientGate)
Last updated on