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
useRenderhook withmergePropsand expose arenderprop — not RadixSlot/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-authorityand exposed throughVariantProps. Classes are merged withcn()(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
| Path | Package | Role |
|---|---|---|
packages/ui-react/ | @acronis-platform/ui-react | The component library (this docs site) |
packages/ui-legacy/ | @acronis-platform/shadcn-uikit | Legacy library (deprecated) |
packages/tokens-pd/ | @acronis-platform/tokens-pd | Generated CSS + Tailwind presets |
packages/design-tokens/ | @acronis-platform/design-tokens | DTCG token source |
packages/icons-react/ | @acronis-platform/icons-react | Generated 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.tsxfiles 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. forwardReffor ref-accepting components;renderprop for polymorphism (notasChild).- TypeScript strict — define explicit prop interfaces at the module boundary.