Acronis UIKit
Guides

Architecture

How @acronis-platform/ui-react is built — Base UI primitives, CVA variants, Tailwind v4, and the design-token pipeline.

Architecture

Overview

@acronis-platform/ui-react is a React-only component library built on Base UI primitives, styled with Tailwind CSS v4 over design tokens generated from the Acronis design system. It is the next-generation successor to the legacy @acronis-platform/shadcn-uikit.

Design philosophy

Base UI first

  • Interactive/stateful primitives come from @base-ui/react, a direct dependency (the legacy library mixed in Radix; ui-react does not).
  • For element composition and polymorphism, components use Base UI's useRender hook with mergeProps and expose a render prop — not Radix Slot / asChild.
  • Accessibility (keyboard navigation, focus management, ARIA) is inherited from the Base UI primitives and checked in Storybook via @storybook/addon-a11y.

Token-driven theming

  • Color, typography, and component metrics resolve to --ui-* CSS custom properties from @acronis-platform/tokens-pd.
  • Values are never hand-authored in components. Light/dark is handled by the tokens' light-dark() + color-scheme, driven by the [data-theme] attribute — no rebuild to switch themes or brands.

Variants via CVA

  • Visual variants are declared with class-variance-authority and exposed through VariantProps. Classes are merged with cn() (clsx + tailwind-merge).

The token pipeline

Theming flows in one direction, from the design source to the rendered component:

@acronis-platform/design-tokens   (DTCG JSON — source of truth)
        │  built by tools/style-dictionary

@acronis-platform/tokens-pd        (generated CSS + Tailwind presets, --ui-* vars)
        │  @import + @theme bridge

@acronis-platform/ui-react         (component utilities reference the tokens)

Change a color in design-tokens and rebuild tokens-pd; every consumer follows automatically. See Packages for each tier.

Styling: two ways to reference a token

src/styles/index.css imports the base token tier and each component's tier, then bridges shared semantic tokens onto Tailwind color names in an @theme inline block. Components pick one of two patterns by how shared the token is:

  • Shared semantic vocabulary → bridged name. Colors reused across many components use the short Tailwind name: bg-primary, text-foreground, border-border.
  • Component-local tokens → direct reference with an arbitrary-value utility: bg-[var(--ui-button-primary-container-color-idle)], hover:text-[var(--ui-button-secondary-label-color-hover)].

Stateful colors wire each state to its own token (hover:*-hover, disabled:*-disabled) so brand overrides are honored per state.

Monorepo structure

PathPackageRole
packages/ui-react/@acronis-platform/ui-reactThe component library (this docs site)
packages/ui-legacy/@acronis-platform/shadcn-uikitLegacy library (deprecated)
packages/tokens-pd/@acronis-platform/tokens-pdGenerated CSS + Tailwind presets
packages/design-tokens/@acronis-platform/design-tokensDTCG token source
packages/icons-react/@acronis-platform/icons-reactGenerated React icon components
apps/docs/This documentation site (Next.js + Fumadocs)

Component file layout

Each component lives in its own directory:

packages/ui-react/src/components/ui/<component>/
├── <component>.tsx              # the component (CVA variants, forwardRef)
├── <component>.figma.tsx        # optional — Figma Code Connect mapping
├── index.ts                     # barrel export
├── __tests__/<component>.test.tsx   # Vitest + React Testing Library
└── __stories__/<component>.stories.tsx  # Storybook (also visual-regression cases)

Component anatomy

A representative component wires CVA variants to tokens and exposes a render prop for composition:

import * as React from 'react';
import { mergeProps } from '@base-ui/react/merge-props';
import { useRender } from '@base-ui/react/use-render';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils';

const buttonVariants = cva('inline-flex items-center justify-center …', {
  variants: {
    variant: {
      default: 'bg-[var(--ui-button-primary-container-color-idle)] …',
      secondary: '…',
      ghost: '…',
    },
  },
  defaultVariants: { variant: 'default' },
});

export interface ButtonProps
  extends React.ButtonHTMLAttributes<HTMLButtonElement>,
    VariantProps<typeof buttonVariants> {
  render?: useRender.RenderProp;
}

const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
  ({ className, variant, render, ...props }, ref) =>
    useRender({
      render,
      ref,
      defaultTagName: 'button',
      props: mergeProps<'button'>(
        { className: cn(buttonVariants({ variant, className })) },
        props
      ),
    })
);
Button.displayName = 'Button';

export { Button, buttonVariants };

Build & distribution

  • Built with Vite 6 in library mode; the published output is ES modules with preserved module structure for tree-shaking.
  • .figma.tsx, .test.tsx, and .stories.tsx files are excluded from the bundle.
  • Stories double as visual-regression cases, screenshotted in Docker and compared against committed baselines in CI (light and dark).

Best practices

  • Lean on Base UI primitives for interactive components — accessibility for free.
  • Variants via CVA, typed with VariantProps; export both the component and its *Variants.
  • Never hard-code a color — every value resolves to a --ui-* token.
  • forwardRef for ref-accepting components; render prop for polymorphism (not asChild).
  • TypeScript strict — define explicit prop interfaces at the module boundary.
Edit on GitHub

On this page