v2.0 is here! New components and themes are coming soon! Stay tuned.
TWJ Labs UI

Manual Setup

Your first document

Getting started with twjlabs/ui is a breeze. This guide will walk you through the steps to set up your first project using our component library.

If you're using the CLI, please head to the Getting Started page.

Installation

Install the required dependencies via npm or yarn

npm install clsx tailwind-merge

if you were using twjlabs/ui cli, it shouldve been pretty easy for you but guess you want to fuck up your mental health

So, first we need to copy and paste the following code in your main css file globals.css if you are using Next.js or main.css for react

Copy to globals.css

globals.css
@import "tailwindcss";

@theme {
  /* =========================================
     1. CORE COMPONENT TOKENS
     ========================================= */

  /* Backgrounds & Foregrounds */
  --color-background: var(--color-background);
  --color-foreground: var(--color-foreground);

  /* Surfaces (Cards, Modals, Dropdowns) */
  --color-card: var(--color-card);
  --color-card-foreground: var(--color-card-foreground);
  --color-popover: var(--color-popover);
  --color-popover-foreground: var(--color-popover-foreground);

  /* Brand Colors */
  --color-primary: var(--color-primary);
  --color-primary-foreground: var(--color-primary-foreground);
  --color-primary-dark: var(--color-primary-dark);

  --color-secondary: var(--color-secondary);
  --color-secondary-foreground: var(--color-secondary-foreground);

  /* State Colors */
  --color-muted: var(--color-muted);
  --color-muted-foreground: var(--color-muted-foreground);

  --color-accent: var(--color-accent);
  --color-accent-foreground: var(--color-accent-foreground);

  --color-destructive: var(--color-destructive);
  --color-destructive-foreground: var(--color-destructive-foreground);

  /* Boundaries & Form Elements */
  --color-border: var(--color-border);
  --color-input: var(--color-input);
  --color-ring: var(--color-ring);

  /* =========================================
     2. LEGACY / CUSTOM TOKENS (Preserved)
     ========================================= */
  --color-gradient-one: var(--color-gradient-one);
  --color-gradient-two: var(--color-gradient-two);
  --color-gradient-three: var(--color-gradient-three);
  --color-gr-three: var(--color-gr-three); /* Alias for gradient-three */
  --color-surface: var(--color-surface);   /* Legacy alias for card/background */

  /* =========================================
     3. TYPOGRAPHY & SHAPE
     ========================================= */
  --radius-theme: var(--radius-theme); 
  --font-theme: var(--font-family);
  
  --font-modern: var(--font-manrope);
  --font-elegant: var(--font-lora);
  --font-futuristic: var(--font-orbitron);
  --font-playful: var(--font-fredoka);
  --font-brutalist: var(--font-roboto-condensed);
  --font-organic: var(--font-nunito);
  --font-vintage: var(--font-grenze-gotisch);
}

/* =========================================
   4. THEME DEFINITIONS
   ========================================= */
@layer utilities {

  /* --- MODERN (Clean, Indigo, Standard) --- */
  .theme-modern {
    --color-background: #ffffff;
    --color-foreground: #09090b;

    --color-card: #ffffff;
    --color-card-foreground: #09090b;
    --color-popover: #ffffff;
    --color-popover-foreground: #09090b;

    --color-primary: #6366f1;         /* Indigo 500 */
    --color-primary-foreground: #ffffff;
    --color-primary-dark: #4f46e5;    /* Indigo 600 */

    --color-secondary: #f4f4f5;       /* Zinc 100 */
    --color-secondary-foreground: #18181b;

    --color-muted: #f4f4f5;
    --color-muted-foreground: #71717a;

    --color-accent: #f4f4f5;          /* Used for hover states */
    --color-accent-foreground: #18181b;

    --color-destructive: #ef4444;
    --color-destructive-foreground: #ffffff;

    --color-border: #e4e4e7;
    --color-input: #e4e4e7;
    --color-ring: #6366f1;

    --color-surface: #f0f0f0;         /* Legacy */
    --color-gradient-one: #6366f1;
    --color-gradient-two: #8b5cf6;
    --color-gradient-three: #a78bfa;
    --color-gr-three: #a78bfa;

    --font-family: var(--font-manrope);
    --radius-theme: 0.5rem;
  }

  /* --- MINIMALIST (Monochrome, High Contrast) --- */
  .theme-minimalist {
    --color-background: #ffffff;
    --color-foreground: #0f172a;

    --color-card: #ffffff;
    --color-card-foreground: #0f172a;
    --color-popover: #ffffff;
    --color-popover-foreground: #0f172a;

    --color-primary: #18181b;         /* Zinc 900 */
    --color-primary-foreground: #fafafa;
    --color-primary-dark: #000000;

    --color-secondary: #f4f4f5;
    --color-secondary-foreground: #18181b;

    --color-muted: #f4f4f5;
    --color-muted-foreground: #a1a1aa;

    --color-accent: #f4f4f5;
    --color-accent-foreground: #18181b;

    --color-destructive: #dc2626;
    --color-destructive-foreground: #fafafa;

    --color-border: #e4e4e7;
    --color-input: #e4e4e7;
    --color-ring: #18181b;

    --color-surface: #ffffff;
    --color-gradient-one: #d4d4d8;
    --color-gradient-two: #a1a1aa;
    --color-gradient-three: #71717a;
    --color-gr-three: #71717a;

    --font-family: var(--font-inter); /* Standard Sans */
    --radius-theme: 0.25rem;
  }

  /* --- ELEGANT (Serif, Warm, Purple/Gold) --- */
  .theme-elegant {
    --color-background: #faf7f2;      /* Cream */
    --color-foreground: #2d2424;

    --color-card: #ffffff;
    --color-card-foreground: #2d2424;
    --color-popover: #ffffff;
    --color-popover-foreground: #2d2424;

    --color-primary: #7c3aed;         /* Violet */
    --color-primary-foreground: #ffffff;
    --color-primary-dark: #6d28d9;

    --color-secondary: #f3e8ff;       /* Light Purple */
    --color-secondary-foreground: #4c1d95;

    --color-muted: #f5f3f0;
    --color-muted-foreground: #8b7e7e;

    --color-accent: #f3e8ff;
    --color-accent-foreground: #4c1d95;

    --color-destructive: #9f1239;
    --color-destructive-foreground: #fff1f2;

    --color-border: #e5e0da;
    --color-input: #e5e0da;
    --color-ring: #7c3aed;

    --color-surface: #ffffff;
    --color-gradient-one: #7c3aed;
    --color-gradient-two: #d946ef;
    --color-gradient-three: #fca5a5;
    --color-gr-three: #fca5a5;

    --font-family: var(--font-lora);
    --radius-theme: 0.375rem;
  }

  /* --- FUTURISTIC (Dark, Neon Cyan, Tech) --- */
  .theme-futuristic {
    --color-background: #0a0f14;
    --color-foreground: #e2ecf3;

    --color-card: #111820;
    --color-card-foreground: #e2ecf3;
    --color-popover: #111820;
    --color-popover-foreground: #e2ecf3;

    --color-primary: #06b6d4;         /* Cyan 500 */
    --color-primary-foreground: #000000;
    --color-primary-dark: #0891b2;

    --color-secondary: #1e293b;
    --color-secondary-foreground: #e2ecf3;

    --color-muted: #1e293b;
    --color-muted-foreground: #94a3b8;

    --color-accent: #0f172a;
    --color-accent-foreground: #22d3ee;

    --color-destructive: #ef4444;
    --color-destructive-foreground: #ffffff;

    --color-border: #1e293b;
    --color-input: #1e293b;
    --color-ring: #06b6d4;

    --color-surface: #111820;
    --color-gradient-one: #06b6d4;
    --color-gradient-two: #3b82f6;
    --color-gradient-three: #6366f1;
    --color-gr-three: #6366f1;

    --font-family: var(--font-orbitron);
    --radius-theme: 2px;
  }

  /* --- PLAYFUL (Pastel, Rounded, Red/Yellow) --- */
  .theme-playful {
    --color-background: #fffbee;
    --color-foreground: #2b2b2b;

    --color-card: #ffffff;
    --color-card-foreground: #2b2b2b;
    --color-popover: #ffffff;
    --color-popover-foreground: #2b2b2b;

    --color-primary: #ff6b6b;         /* Soft Red */
    --color-primary-foreground: #ffffff;
    --color-primary-dark: #ee5253;

    --color-secondary: #ffd93d;       /* Yellow */
    --color-secondary-foreground: #2b2b2b;

    --color-muted: #fff0f0;
    --color-muted-foreground: #7d7d7d;

    --color-accent: #48dbfb;          /* Cyan accent */
    --color-accent-foreground: #ffffff;

    --color-destructive: #ff6b6b;
    --color-destructive-foreground: #ffffff;

    --color-border: #ffd93d;
    --color-input: #fff0f0;
    --color-ring: #ff6b6b;

    --color-surface: #ffffff;
    --color-gradient-one: #ff6b6b;
    --color-gradient-two: #ffd93d;
    --color-gradient-three: #48dbfb;
    --color-gr-three: #48dbfb;

    --font-family: var(--font-fredoka);
    --radius-theme: 1rem;
  }

  /* --- BRUTALIST (Bold, Sharp, Primary Blue) --- */
  .theme-brutalist {
    --color-background: #ffffff;
    --color-foreground: #000000;

    --color-card: #f3f3f3;
    --color-card-foreground: #000000;
    --color-popover: #ffffff;
    --color-popover-foreground: #000000;

    --color-primary: #2563eb;         /* Blue 600 */
    --color-primary-foreground: #ffffff;
    --color-primary-dark: #1d4ed8;

    --color-secondary: #bef264;       /* Lime */
    --color-secondary-foreground: #000000;

    --color-muted: #e5e5e5;
    --color-muted-foreground: #525252;

    --color-accent: #bef264;
    --color-accent-foreground: #000000;

    --color-destructive: #000000;
    --color-destructive-foreground: #ffffff;

    --color-border: #000000;
    --color-input: #ffffff;
    --color-ring: #000000;

    --color-surface: #f8f8f8;
    --color-gradient-one: #2563eb;
    --color-gradient-two: #000000;
    --color-gradient-three: #bef264;
    --color-gr-three: #bef264;

    --font-family: var(--font-roboto-condensed);
    --radius-theme: 0px;
  }

  /* --- ORGANIC (Nature, Green, Soft) --- */
  .theme-organic {
    --color-background: #f4f8f5;
    --color-foreground: #2c4f2e;

    --color-card: #ffffff;
    --color-card-foreground: #2c4f2e;
    --color-popover: #ffffff;
    --color-popover-foreground: #2c4f2e;

    --color-primary: #558b2f;         /* Green 800 */
    --color-primary-foreground: #ffffff;
    --color-primary-dark: #33691e;

    --color-secondary: #dcedc8;       /* Light Green */
    --color-secondary-foreground: #33691e;

    --color-muted: #e8f5e9;
    --color-muted-foreground: #558b2f;

    --color-accent: #f1f8e9;
    --color-accent-foreground: #2c4f2e;

    --color-destructive: #c62828;
    --color-destructive-foreground: #ffffff;

    --color-border: #c5e1a5;
    --color-input: #ffffff;
    --color-ring: #558b2f;

    --color-surface: #ffffff;
    --color-gradient-one: #558b2f;
    --color-gradient-two: #8bc34a;
    --color-gradient-three: #dcedc8;
    --color-gr-three: #dcedc8;

    --font-family: var(--font-nunito);
    --radius-theme: 1.5rem;
  }

  /* --- VINTAGE (Sepia, Paper, Brown) --- */
  .theme-vintage {
    --color-background: #fdf6e3;
    --color-foreground: #5c4033;

    --color-card: #f4ecd8;
    --color-card-foreground: #5c4033;
    --color-popover: #f4ecd8;
    --color-popover-foreground: #5c4033;

    --color-primary: #8d6e63;         /* Brown 400 */
    --color-primary-foreground: #ffffff;
    --color-primary-dark: #6d4c41;

    --color-secondary: #efebe9;
    --color-secondary-foreground: #4e342e;

    --color-muted: #d7ccc8;
    --color-muted-foreground: #8d6e63;

    --color-accent: #efebe9;
    --color-accent-foreground: #3e2723;

    --color-destructive: #8c2f39;
    --color-destructive-foreground: #ffffff;

    --color-border: #bcaaa4;
    --color-input: #ffffff;
    --color-ring: #8d6e63;

    --color-surface: #ffffff;
    --color-gradient-one: #8d6e63;
    --color-gradient-two: #a1887f;
    --color-gradient-three: #d7ccc8;
    --color-gr-three: #d7ccc8;

    --font-family: var(--font-grenze-gotisch);
    --radius-theme: 0.5rem;
  }
}

@layer base {
  body {
    @apply bg-background text-foreground border-border;
  }
}

Create ui-theme context

Now you need to create a folder in your root directory named contexts and inside that folder, create a file named ui-theme-context.tsx and copy-paste the below code into that file

contexts/ui-theme-context.tsx
"use client";

import React, { createContext, useContext, useState } from "react";
import type { Theme } from "@/twj-lib/types"; 

interface ThemeContextType {
  theme: Theme;
  setTheme: (theme: Theme) => void;
}

const ThemeContext = createContext<ThemeContextType | undefined>(undefined);

export function ThemeProvider({
  children,
  initialTheme = "modern", 
}: {
  children: React.ReactNode;
  initialTheme?: Theme;
}) {
  // ✅ 1. Initialize State with the prop. 
  // This happens once on mount. You don't need useEffect to set this.
  const [theme, setTheme] = useState<Theme>(initialTheme);

  // ❌ REMOVED: The useEffect was causing the double-render/sync error.
  
  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

export function useTheme() {
  const context = useContext(ThemeContext);
  if (context === undefined) {
    throw new Error("useTheme must be used within a ThemeProvider");
  }
  return context;
}

copy the below code into twj-lib/tw.tsx

twj-lib/tw.tsx
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs))
}

copy the below code into twj-lib/types.ts

twj-lib/types.ts
export type Theme = 'modern' | 'elegant' | 'futuristic' | 'playful' | 'brutalist' | 'organic' | 'minimalist' | 'vintage';

export const themes: Theme[] = ['modern', 'elegant', 'futuristic', 'playful', 'brutalist', 'organic', 'minimalist', 'vintage'];

export interface TWJComponentsProps {
    theme?: Theme;
}

Create AI Context

You need to create another file in the contexts folder named ai-context.tsx and copy-paste the below code into that file

contexts/ai-context.tsx
"use client"

import { TWJComponentsProps } from "@/twj-lib/types";
// AIContextProvider.tsx
import React, { createContext, useContext, useEffect, useRef, useState, useCallback } from 'react';

// 2. The definition of a component tool
export type AIAction = (...args: any[]) => Promise<void> | void;

export interface AIRegisteredComponent {
  id: string;
  description: string;
  // We store the actual functions here, but we won't send this object to the AI directly
  actions: Record<string, AIAction>; 
}

// 3. YOUR INTERFACE (The State) - This is what we feed the LLM
export interface AIContextState extends TWJComponentsProps {
  route: {
    path: string;
    params?: Record<string, string>; // Made optional for flexibility
  };

  pageContent: {
    rawText: string;
    lastUpdated: number;
  };

  // We only expose the NAMES of actions to the state to keep it clean/serializable
  activeComponents: Map<string, {
    id: string;
    description: string;
    availableActions: string[]; 
  }>;

  assistantPersona: 'helpful_guide' | 'technical_debugger' | 'sales_rep';

  knowledgeBase?: string;
}

// 4. The Context Value (State + Methods)
export interface AIContextValue {
  state: AIContextState;
  
  // Methods to control the system
  registerComponent: (comp: AIRegisteredComponent) => void;
  unregisterComponent: (id: string) => void;
  executeAction: (componentId: string, actionName: string, actionValue?:any) => Promise<void>;
  setPersona: (persona: AIContextState['assistantPersona']) => void;
}

const AIContext = createContext<AIContextValue | null>(null);

interface AIProviderProps extends TWJComponentsProps {
  children: React.ReactNode;
  currentPath?: string; 
  initialPersona?: AIContextState['assistantPersona'];

  knowledgeBase?: string;
}

export const AIContextProvider: React.FC<AIProviderProps> = ({ 
  children, 
  currentPath = typeof window !== 'undefined' ? window.location.pathname : '/',
  initialPersona = 'helpful_guide',
  knowledgeBase='',
  ...rest // Capture TWJ props
}) => {
  
  // --- INTERNAL STATE (The Shadow Registry) ---
  // We use a Ref for the functions to avoid re-renders when actions technically change identity
  // but logically stay the same.
  const actionRegistry = useRef<Map<string, AIRegisteredComponent>>(new Map());

  // --- PUBLIC STATE (What the AI Sees) ---
  const [state, setState] = useState<AIContextState>({
    ...rest,
    route: { path: currentPath, params: {} },
    pageContent: { rawText: '', lastUpdated: Date.now() },
    activeComponents: new Map(),
    assistantPersona: initialPersona,
    knowledgeBase: knowledgeBase
  });

  // 1. AWARENESS: Register Components
  const registerComponent = useCallback((comp: AIRegisteredComponent) => {
    // A. Update the Shadow Registry (Logic)
    actionRegistry.current.set(comp.id, comp);

    // B. Update the Public State (Visibility)
    setState((prev) => {
      const newMap = new Map(prev.activeComponents);
      newMap.set(comp.id, {
        id: comp.id,
        description: comp.description,
        availableActions: Object.keys(comp.actions) // Only store strings!
      });
      return { ...prev, activeComponents: newMap };
    });
  }, []);

  const unregisterComponent = useCallback((id: string) => {
    actionRegistry.current.delete(id);
    setState((prev) => {
      const newMap = new Map(prev.activeComponents);
      newMap.delete(id);
      return { ...prev, activeComponents: newMap };
    });
  }, []);

  // 2. ACTION: The Executor
  const executeAction = useCallback(async (componentId: string, actionName: string, actionValue?:any) => {
    const comp = actionRegistry.current.get(componentId);
    
    if (!comp) {
      console.warn(`[AI] Component not found: ${componentId}`);
      return;
    }

    const action = comp.actions[actionName];
    if (action) {
      console.log(`[AI] Executing: ${componentId} > ${actionName} with value:`, actionValue);
      await action(actionValue);
    } else {
      console.warn(`[AI] Action '${actionName}' not found on ${componentId}`);
    }
  }, []);

  // 3. SIGHT: DOM Observer
  useEffect(() => {
    if (typeof window === 'undefined') return;

    let timeoutId: NodeJS.Timeout;

    const scrapeDOM = () => {
      const idleCallback = (window as any).requestIdleCallback || ((cb: any) => setTimeout(cb, 1));
      idleCallback(() => {
        // Basic scraping strategy
        const text = document.body.innerText
          .replace(/<script\b[^>]*>([\s\S]*?)<\/script>/gm, "") // remove scripts
          .replace(/\s+/g, ' ')
          .trim()
          .slice(0, 10000); // Limit context size for tokens

        setState(prev => {
            if (prev.pageContent.rawText === text) return prev;
            return {
                ...prev,
                pageContent: { rawText: text, lastUpdated: Date.now() }
            };
        });
      });
    };

    const observer = new MutationObserver(() => {
      clearTimeout(timeoutId);
      timeoutId = setTimeout(scrapeDOM, 1000); // 1s debounce
    });

    observer.observe(document.body, { childList: true, subtree: true, characterData: true });
    scrapeDOM();

    return () => {
      observer.disconnect();
      clearTimeout(timeoutId);
    };
  }, [state.route.path]); // Re-scrape when path changes

  // 4. ROUTE: Update state when prop changes
  useEffect(() => {
    setState(prev => ({
      ...prev,
      route: { ...prev.route, path: currentPath }
    }));
  }, [currentPath]);

  // 🆕 Update state if the prop changes dynamically (e.g. if you fetch docs)
  useEffect(() => {
    setState(prev => ({ ...prev, knowledgeBase }));
  }, [knowledgeBase]);

  const setPersona = (persona: AIContextState['assistantPersona']) => {
    setState(prev => ({ ...prev, assistantPersona: persona }));
  };

  return (
    <AIContext.Provider value={{ state, registerComponent, unregisterComponent, executeAction, setPersona }}>
      {children}
    </AIContext.Provider>
  );
};

export const useAIContext = () => {
  const context = useContext(AIContext);
  if (!context) throw new Error("useAIContext must be used within AIContextProvider");
  return context;
};

// ============== HOOK ==============
//=======================================



interface UseAIControlProps {
  id: string;
  description: string;
  actions: Record<string, AIAction>;
}

export const useAIControl = ({ id, description, actions }: UseAIControlProps) => {
  const { registerComponent, unregisterComponent } = useAIContext();
  
  // Keep actions stable in a ref so we don't re-register constantly
  const actionsRef = useRef(actions);
  actionsRef.current = actions;

  useEffect(() => {
    registerComponent({
      id,
      description,
      actions: actionsRef.current,
    });

    return () => {
      unregisterComponent(id);
    };
  }, [id, description, registerComponent, unregisterComponent]);
};

BRO , YOU MADE IT TILL HERE! HATS OFF TO YOU, I still dont understand why the fuck you are not using @twjlabs-cli

We're still not done lil bro, we still got a few more things to do alright

Changes in layout.tsx

There is one more step to complete the setup. You need to wrap your application with the ThemeProvider and add fonts to enable theming and global styles. Open your layout.tsx file and make the following changes:

app/layout.tsx
import './globals.css';
import { ThemeProvider } from '@/contexts/ui-theme-context'; 
import { AIContextProvider } from "@/contexts/ai-context"; 

// 1. Import the fonts
import {  
  Manrope, 
  Lora, 
  Orbitron, 
  Fredoka, 
  Roboto_Condensed, 
  Nunito,
  Grenze_Gotisch
} from "next/font/google";

// 2. Setup the font instances with specific variable names
const manrope = Manrope({ subsets: ["latin"], variable: "--font-manrope", display: "swap" });
const lora = Lora({ subsets: ["latin"], variable: "--font-lora", display: "swap" });
const orbitron = Orbitron({ subsets: ["latin"], variable: "--font-orbitron", display: "swap" });
const fredoka = Fredoka({ subsets: ["latin"], variable: "--font-fredoka", display: "swap" });
const robotoCondensed = Roboto_Condensed({ subsets: ["latin"], variable: "--font-roboto-condensed", display: "swap" });
const nunito = Nunito({ subsets: ["latin"], variable: "--font-nunito", display: "swap" });
const grenzeGotisch = Grenze_Gotisch({ subsets: ["latin"], variable: "--font-grenze-gotisch", display: "swap" });

export default function Layout({ children }: LayoutProps<'/'>) {
  return (
    // Add any theme you want as initialTheme
   <ThemeProvider initialTheme='brutalist'>  
   <AIContextProvider 
      initialPersona="technical_debugger"
      >
      <html lang="en" className={`${manrope.variable} 
            ${lora.variable} 
            ${orbitron.variable} 
            ${fredoka.variable} 
            ${robotoCondensed.variable} 
            ${nunito.variable}
            ${grenzeGotisch.variable}`
          } 
      suppressHydrationWarning>
        <body className="flex flex-col min-h-screen">
            {children}
        </body>
      </html>
      </AIContextProvider>
   </ThemeProvider>
  );
}

Ok so, first you need to import the ThemeProvider from the ui-theme-context and AIContextProvider from the ai-context. Then, import the fonts you want to use from next/font/google. Next, create instances of each font with a unique CSS variable name. Finally, wrap your application in the ThemeProvider and AIContextProvider, setting an initial theme of your choice, and apply the font variables to the <html> element.

That's it! You're now ready to start building with twjlabs/ui. Happy coding!

Walah! You have successfully set up twjlabs/ui in your project manually. Althogh i still think you are crazy for not using the CLI but hey, to each their own. Enjoy building with twjlabs/ui! 🚀

Next Steps

  • Explore the Components to see what you can use in your project.
  • Check out the Theming Guide to customize the look and feel of your application.

On this page