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:
asChild— a Radix UI pattern (which shadcn/ui wraps) that tellsTooltipTriggerto merge its behavior onto its child (badge) instead of rendering an extra wrapper<div>. You'll seeasChildthroughout iKanban wherever a trigger needs to wrap an existing styled element.- 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. - Conditional Tailwind classes inline:
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 orclassName={`gap-1 ${mode === 'both' ? 'border-green-500 text-green-600 dark:text-green-400' : ''} ${className || ''}`}styled-componentsneeded.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:
- Which Radix primitive it wraps (check the imports at the top).
- 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.