Design system
Docs
Foundations

Theming and styling channels

How a consumer changes what a primitive looks like without forking it: token overrides, className, and xstyle

@nyte-ai/ui divides styling responsibility three ways, and each primitive exposes exactly the channels that division needs.

LayerOwnsMechanism
Base UIInteraction, focus, ARIA, data attributesHeadless components
@nyte-ai/uiReusable component stylingStyleX stylex.create reading tokens
The appLayout, typography, product identity, compositesToken overrides, className, xstyle, own components

Token overrides

Override on an ancestor. The app root is the right place; a preview container is also fine, and that is how the examples on this site are themed independently of the docs chrome.

.my-app {
  color-scheme: dark;
  --nyte-color-avatar-orange-background: #5f2a06;
  --nyte-color-avatar-orange-foreground: #ffb27d;
}

Override only the tokens that carry identity. Overriding --nyte-control-height-md to fix one layout is a layout problem; solve it with className.

The three props every styled primitive accepts

Every component in packages/ui/src/components/ui accepts these and merges them in this order.

PropTypePurpose
classNamestringConsumer layout, including Tailwind utilities. Appended after the StyleX class.
styleReact.CSSPropertiesInline overrides. Spread after StyleX's inline style, so it wins.
xstyleXStyleA deliberate StyleX override. Passed as the last argument to stylex.props, so it wins by StyleX's merge rules.

XStyle is stylex.StyleXArray of compiled styles, booleans, and null; you can pass one style, an array, or a conditional. The merge itself is mergeStyleProps in packages/ui/src/style.ts:

export function mergeStyleProps(base, className, style) {
  return {
    className: [base.className, className].filter(Boolean).join(" ") || undefined,
    style: { ...base.style, ...style },
  };
}

StyledProps<P> is the helper type that strips className/style from a Base UI props type and adds the three channels back. Use it when wrapping a new part.

unstyled

Button, Input, and Textarea accept unstyled. When true the component renders the Base UI part with no StyleX class at all, keeping only the interaction layer and the data-slot attribute. On Button, unstyled: true also forbids variant and size at the type level.

unstyled is for product surfaces that supply a complete xstyle treatment of their own. It is not for "I need a slightly different button"; that is a token override or xstyle.

Data attributes

Every primitive sets data-slot="<part>" so a consumer can target a part from CSS without knowing the StyleX class. Variant-bearing primitives also expose their appearance:

ComponentAttributes
Buttondata-slot="button", data-variant, data-size (omitted when unstyled)
Avatardata-slot="avatar", data-size, data-tone
AvatarImagedata-slot="avatar-image"
AvatarFallbackdata-slot="avatar-fallback"
Inputdata-slot="input"
Textareadata-slot="textarea"
DropdownMenuItemdata-slot="dropdown-menu-item", data-variant, data-inset
DropdownMenuLabeldata-inset

Base UI adds its own state attributes on top (data-disabled, data-popup-open, data-highlighted, data-checked, …). Those are listed on each component page.

Headless subpaths

For anything without a styled wrapper, import the Base UI namespace from its subpath and style it in the app:

import { Popover } from "@nyte-ai/ui/popover";

Import from the subpath, not from @base-ui/react, so the Base UI version stays behind the shared package. See Headless.

Dark mode

Tokens use light-dark(), so a primitive follows whichever color-scheme is in effect. There is no .dark class in the primitives. @nyte-ai/ui/styles.css does declare @custom-variant dark (&:is(.dark *)) for Tailwind consumers that want class-based variants in their own utilities; it does not affect the primitives.