Step 4 — TailwindCSS v4 + shadcn/ui Components

iKanban styles every component with TailwindCSS v4 utility classes, and builds its UI primitives on shadcn/ui — which, unlike a normal npm package, generates component source code directly into your repo rather than shipping a compiled library. That's why Badge, Dialog, Tooltip, and friends live at src/components/ui/ as plain, editable .tsx files instead of coming from node_modules.

components.json — how new components land

{
  "tailwind": { "config": "tailwind.config.js", "css": "src/index.css", "baseColor": "slate" },
  "aliases": { "components": "@/components", "utils": "@/lib/utils" }
}

Running npx shadcn-ui@latest add <component> reads this file and writes a new file under @/components/ui/, already wired to the @/ alias from Step 3. This is why CLAUDE.md's core rule #4 says shadcn/ui only for new UI primitives — hand-rolling a modal or dropdown means losing this generated, consistent styling contract.

Composing primitives: back to AgentModeBadge

Step 2 showed the TypeScript side of AgentModeBadge.tsx. Here's the composition of shadcn primitives underneath it:

import { Badge } from '@/components/ui/badge';
import {
  Tooltip,
  TooltipContent,
  TooltipProvider,
  TooltipTrigger,
} from '@/components/ui/tooltip';

// ...
return (
  <TooltipProvider>
    <Tooltip>
      <TooltipTrigger asChild>{badge}</TooltipTrigger>
      <TooltipContent>
        <p>{t(config.descriptionKey)}</p>
      </TooltipContent>
    </Tooltip>
  </TooltipProvider>
);

Three things worth noticing:

  1. asChild — a Radix UI pattern (which shadcn/ui wraps) that tells TooltipTrigger to merge its behavior onto its child (badge) instead of rendering an extra wrapper <div>. You'll see asChild throughout iKanban wherever a trigger needs to wrap an existing styled element.
  2. Composition over configuration — instead of one <Badge tooltip="..." tooltipPosition="top"> component with a dozen props, iKanban composes small pieces (Tooltip + TooltipTrigger + TooltipContent) the way you'd nest HTML tags. This scales better as requirements grow.
  3. Conditional Tailwind classes inline:
    className={`gap-1 ${mode === 'both' ? 'border-green-500 text-green-600 dark:text-green-400' : ''} ${className || ''}`}
    
    Tailwind classes are just strings, so plain template literals and ternaries are enough to make styling conditional on props or state — no separate CSS file or styled-components needed. dark: prefixes apply only in dark mode, handled automatically by Tailwind's dark-mode variant.

Checkpoint

Find one other shadcn/ui component under vibe-frontend/src/components/ui/ (e.g. dialog.tsx or alert.tsx) and identify:

  1. Which Radix primitive it wraps (check the imports at the top).
  2. One dark:-prefixed Tailwind class controlling its dark-mode appearance.

Continue to Step 5 — Local & Global State with Zustand, where we move from stateless display components to components that hold and update their own state.