UI Component Architecture
FluxKit UI structure, dependency direction, composition, and extension conventions.
Scope
Primary UI roots:
- src/app
- src/components
- src/hooks
- src/contexts
- src/app/globals.cssDependency Direction (Allowed Flow)
app routes/layouts
-> feature/page components (app/*/components)
-> shared UI and domain components (src/components/*)
-> primitives (src/components/ui/*)
-> hooks (src/hooks/*)
-> contexts (src/contexts/*)
Cross-cutting style system:
src/app/globals.css -> consumed by all layers via Tailwind tokens/classesRules used in this codebase:
src/components/ui/*are low-level primitives; they should not import app routes.src/hooks/*consume contexts and browser APIs; they should not import app pages.src/contexts/*define shared state contracts/providers used bylayout.tsxand components.src/app/*composes providers and page-level behavior; it is the top integration layer.
Root Composition in src/app/layout.tsx
Root layout composes global providers in a strict order: data/auth -> theme -> sidebar config -> error boundary -> children.
import type { Metadata } from "next";
import "./globals.css";
import { ThemeProvider } from "@/components/theme-provider";
import { SidebarConfigProvider } from "@/contexts/sidebar-context";
import { inter } from "@/lib/fonts";
import { ConvexClientProvider } from "@/components/providers/ConvexClientProvider";
import { getToken } from "@/lib/auth/server";
import { RootErrorBoundary } from "@/components/root-error-boundary";
import { Toaster } from "@/components/ui/sonner";
export default async function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
let token: string | null = null;
try {
token = (await getToken()) ?? null;
} catch {
token = null;
}
return (
<html
lang="en"
className={`${inter.variable} antialiased`}
suppressHydrationWarning
>
<body className={inter.className}>
<ConvexClientProvider initialToken={token}>
<ThemeProvider defaultTheme="system" storageKey="nextjs-ui-theme">
<SidebarConfigProvider>
<RootErrorBoundary>{children}</RootErrorBoundary>
</SidebarConfigProvider>
</ThemeProvider>
<Toaster />
</ConvexClientProvider>
</body>
</html>
);
}Convention: global providers are introduced in root layout first; feature layouts read from hooks/contexts instead of redefining provider state.
Primitive-First Shared UI (src/components/ui/*)
Low-level primitives wrap behavior (Radix, CVA variants, shared class contracts), then app/feature components compose them.
Example: Button primitive
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
outline:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground",
},
size: {
default: "h-9 px-4 py-2",
lg: "h-10 rounded-md px-6",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
function Button({
asChild = false,
className,
variant,
size,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "button";
return (
<Comp
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
);
}Convention: expose variants through CVA and keep consumer APIs stable (variant, size, asChild).
Context + Hook Contracts
Contexts define shape and default behavior; hooks enforce usage boundaries.
Example: theme context + hook
// src/contexts/theme-context.ts
import * as React from "react";
type Theme = "dark" | "light" | "system";
export type ThemeProviderState = {
theme: Theme;
setTheme: (theme: Theme) => void;
};
const initialState: ThemeProviderState = {
theme: "system",
setTheme: () => null,
};
export const ThemeProviderContext =
React.createContext<ThemeProviderState>(initialState);// src/hooks/use-theme.ts
import * as React from "react";
import { ThemeProviderContext } from "@/contexts/theme-context";
export const useTheme = () => {
const context = React.useContext(ThemeProviderContext);
if (context === undefined)
throw new Error("useTheme must be used within a ThemeProvider");
return context;
};Convention: any hook reading context should throw with a clear provider-boundary error.
Feature Composition in src/app
Feature layouts/pages in src/app compose shared primitives, providers, hooks, and data.
Example: dashboard layout orchestration
"use client";
import { AppSidebar } from "@/components/app-sidebar";
import { SiteHeader } from "@/components/site-header";
import { SidebarProvider, SidebarInset } from "@/components/ui/sidebar";
import { useSidebarConfig } from "@/hooks/use-sidebar-config";
import { Authenticated, Unauthenticated, AuthLoading } from "convex/react";
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
const { config } = useSidebarConfig();
return (
<Authenticated>
<SidebarProvider
className={config.collapsible === "none" ? "sidebar-none-mode" : ""}
>
<AppSidebar
variant={config.variant}
collapsible={config.collapsible}
side={config.side}
/>
<SidebarInset>
<SiteHeader />
{children}
</SidebarInset>
</SidebarProvider>
</Authenticated>
);
}Convention: app-layer files can depend on components/hooks; lower layers cannot depend on route files.
Sidebar System as a Layered Example
The sidebar implementation shows clean layering:
src/contexts/sidebar-context.tsxprovides persistent config contract.src/hooks/use-sidebar-config.tsis the consumer API.src/components/ui/sidebar.tsxprovides generic sidebar primitives.src/components/app-sidebar.tsxbinds application navigation and Convex data.src/app/(dashboard)/layout.tsxplaces it in route layout.
// src/hooks/use-sidebar-config.ts
import * as React from "react";
import {
SidebarContext,
type SidebarContextValue,
} from "@/contexts/sidebar-context";
export function useSidebarConfig(): SidebarContextValue {
const context = React.useContext(SidebarContext);
if (!context) {
throw new Error(
"useSidebarConfig must be used within a SidebarConfigProvider",
);
}
return context;
}Conventions Checklist
- Use
@/alias imports for internal modules. - Keep primitive UI in
src/components/uiand avoid application-specific business logic there. - Keep feature sections close to route segments under
src/app/(segment)/.... - Keep state contracts in
src/contexts, consumer ergonomics insrc/hooks. - Keep global design tokens in
src/app/globals.css; components consume semantic classes (bg-background,text-foreground,border-border, etc.).
What to Document When Adding UI
When documenting a new UI system in this project, capture:
- Provider placement in
src/app/layout.tsx. - Context shape in
src/contexts. - Hook API in
src/hooks. - Primitive component API in
src/components/ui. - Route composition and feature assembly in
src/app. - Styling/token dependencies in
src/app/globals.css.