Skip to Content
UI Component Architecture

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.css

Dependency 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/classes

Rules used in this codebase:

  1. src/components/ui/* are low-level primitives; they should not import app routes.
  2. src/hooks/* consume contexts and browser APIs; they should not import app pages.
  3. src/contexts/* define shared state contracts/providers used by layout.tsx and components.
  4. 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.

The sidebar implementation shows clean layering:

  1. src/contexts/sidebar-context.tsx provides persistent config contract.
  2. src/hooks/use-sidebar-config.ts is the consumer API.
  3. src/components/ui/sidebar.tsx provides generic sidebar primitives.
  4. src/components/app-sidebar.tsx binds application navigation and Convex data.
  5. src/app/(dashboard)/layout.tsx places 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/ui and 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 in src/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:

  1. Provider placement in src/app/layout.tsx.
  2. Context shape in src/contexts.
  3. Hook API in src/hooks.
  4. Primitive component API in src/components/ui.
  5. Route composition and feature assembly in src/app.
  6. Styling/token dependencies in src/app/globals.css.
Last updated on