UI & Component Library
Relevant source files
The following files were used as context for generating this wiki page:The project's UI and component library is structured within a monorepo, featuring a dedicated shared UI package and its consumption in the main web application. The architecture is centered around creating reusable, themeable, and accessible components using modern frontend technologies like React, Radix UI, and Tailwind CSS. Component management is streamlined using Shadcn UI.
This system is divided into two main parts: a foundational, shared component library located in packages/ui and the consumer application in apps/web, which uses and extends the shared library with its own specific components. This separation promotes code reuse and a consistent design language across the platform.
Architecture Overview
The UI architecture follows a classic monorepo pattern where a dedicated package, @contract-based-monorepo/ui, houses the shared UI components. The web application, @contract-based-monorepo/web, lists the UI package as a workspace dependency, allowing it to import and use the shared components seamlessly.
This diagram illustrates the dependency relationship between the web application and the ui package.
Styling and Theming
Styling is handled by Tailwind CSS, with a theming system built on CSS variables for easy customization and support for both light and dark modes.
CSS Variables
Both the shared ui package and the web app define a set of CSS variables in their respective stylesheets. These variables control colors, borders, radii, and other stylistic properties. The dark mode is enabled by applying a .dark class to a parent element, which swaps the values of the CSS variables.
packages/ui/src/styles/globals.css (Base Theme)
:root {
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--border: oklch(0.922 0 0);
--radius: 0.625rem;
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--primary: oklch(0.985 0 0);
--border: oklch(0.269 0 0);
}
Sources: packages/ui/src/styles/globals.css:7-50
apps/web/src/styles.css (App-specific Theme)
The web app defines its own set of variables, which may override or supplement the base theme.
:root {
--background: oklch(1 0 0);
--foreground: oklch(0.141 0.005 285.823);
--primary: oklch(0.21 0.006 285.885);
--border: oklch(0.92 0.004 286.32);
--radius: 0.625rem;
}
.dark {
--background: oklch(0.141 0.005 285.823);
--foreground: oklch(0.985 0 0);
--primary: oklch(0.985 0 0);
--border: oklch(0.274 0.006 286.033);
}
Sources: apps/web/src/styles.css:23-66
Key Dependencies for Styling
The following libraries are crucial for the styling system:
| Dependency | Purpose | Source |
|---|---|---|
class-variance-authority | Creates variants for component styles (e.g., button sizes, colors). | packages/ui/package.json:16 |
clsx | A tiny utility for constructing className strings conditionally. | packages/ui/package.json:17 |
tailwind-merge | Merges Tailwind CSS classes without style conflicts. | packages/ui/package.json:21 |
tailwindcss-animate | Adds enter/exit animations for Tailwind CSS. | apps/web/src/styles.css:3 |
Component Structure
Components are built as React components, leveraging Radix UI for accessibility primitives and a combination of utility functions for styling.
Primitive Composition
Components are composed from unstyled, accessible primitives provided by Radix UI. For example, the Switch component uses SwitchPrimitive.Root and SwitchPrimitive.Thumb. The cn utility function (a combination of clsx and tailwind-merge) is used to apply styles.
This diagram shows the composition of the Switch component.
Here is the implementation of the Switch component, demonstrating this pattern:
// File: apps/web/src/components/ui/switch.tsx
import { cn } from "@/lib/utils";
import * as SwitchPrimitive from "@radix-ui/react-switch";
import type * as React from "react";
function Switch({
className,
...props
}: React.ComponentProps<typeof SwitchPrimitive.Root>) {
return (
<SwitchPrimitive.Root
data-slot="switch"
className={cn(
"peer inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs outline-none transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input dark:data-[state=unchecked]:bg-input/80",
className,
)}
{...props}
>
<SwitchPrimitive.Thumb
data-slot="switch-thumb"
className={cn(
"pointer-events-none block size-4 rounded-full bg-background ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0 dark:data-[state=checked]:bg-primary-foreground dark:data-[state=unchecked]:bg-foreground",
)}
/>
</SwitchPrimitive.Root>
);
}
Sources: apps/web/src/components/ui/switch.tsx:1-33
Component Management with Shadcn UI
The project uses Shadcn UI not as a component library, but as a tool to add and manage components. Both the packages/ui and apps/web directories contain a components.json file, indicating separate Shadcn UI configurations.
Configuration Comparison
The configurations differ in key areas, suggesting different use cases for each set of components. The packages/ui is configured for React Server Components ("rsc": true), while apps/web is configured for client components ("rsc": false).
| Configuration | packages/ui/components.json | apps/web/components.json |
|---|---|---|
$schema | https://ui.shadcn.com/schema.json | https://ui.shadcn.com/schema.json |
style | new-york | new-york |
rsc | true | false |
css | src/styles/globals.css | src/styles.css |
baseColor | neutral | zinc |
aliases.components | @contract-based-monorepo/ui/components | @/components |
aliases.utils | @contract-based-monorepo/ui/lib/utils | @/lib/utils |
Sources: packages/ui/components.json:1-16, apps/web/components.json:1-17
Component Usage
The apps/web application contains examples of how these UI components are used to build features. The demo.FormComponents.tsx file provides a clear demonstration of composing a form using components like Button, Input, Select, Slider, and Switch.
Form Field Data Flow
Each form component, like TextField, interacts with a form state management solution (TanStack Form) via a React context (useFieldContext). The component's state (value, errors, touched status) is derived from the form context, and user interactions (change, blur) update the central form state.
This flow diagram illustrates the data flow for the TextField component.
This is demonstrated in the TextField implementation:
// File: apps/web/src/components/demo.FormComponents.tsx
export function TextField({
label,
placeholder,
}: {
label: string;
placeholder?: string;
}) {
const field = useFieldContext<string>();
const errors = useStore(field.store, (state) => state.meta.errors);
return (
<div>
<Label htmlFor={label} className="mb-2 font-bold text-xl">
{label}
</Label>
<Input
value={field.state.value}
placeholder={placeholder}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
/>
{field.state.meta.isTouched && <ErrorMessages errors={errors} />}
</div>
);
}
Sources: apps/web/src/components/demo.FormComponents.tsx:44-62