Skip to main content
The application uses Tailwind CSS v4 with a utility-first approach for styling. All components leverage Tailwind’s utility classes combined with custom CSS variables for consistent theming.

Tailwind CSS configuration

The project uses Tailwind CSS v4 with the new CSS-first configuration approach.

PostCSS setup

postcss.config.mjs
const config = {
  plugins: ["@tailwindcss/postcss"],
};

export default config;

CSS imports

app/globals.css
@import "tailwindcss";
@import "tw-animate-css";
Tailwind CSS v4 doesn’t require a traditional config file. Configuration is done directly in CSS using the @theme directive.

Utility patterns

The cn() utility

All components use the cn() helper to merge Tailwind classes with conditional logic:
lib/utils.ts
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs))
}
Example usage in message bubbles:
components/chat/message-bubble.tsx
const bubbleClasses = cn(
  "relative rounded-3xl px-4 py-2 shadow-sm transition-colors",
  isOutgoing
    ? "rounded-br-lg bg-bubble-outgoing text-foreground"
    : "rounded-bl-lg bg-bubble-incoming text-foreground"
)
The cn() function prevents class conflicts by intelligently merging Tailwind utilities. Later classes override earlier ones.

Component styling patterns

Message bubble styling

Message bubbles use rounded corners with asymmetric radius for a chat-like appearance:
components/chat/message-bubble.tsx
return (
  <div
    className={cn("flex gap-2", isOutgoing ? "justify-end" : "justify-start")}
    data-message-id={message.id}
  >
    <div className={cn("max-w-[82%] md:max-w-[68%]", isOutgoing && "items-end")}>
      <div className={bubbleClasses}>
        <p className="text-sm leading-snug text-foreground/90 whitespace-pre-wrap">
          {message.text}
        </p>
        <footer className="mt-1 flex items-center gap-1 text-[11px] text-muted-foreground">
          <span>{timeLabel}</span>
        </footer>
      </div>
    </div>
  </div>
)
Key styling techniques:
  • Asymmetric rounded corners: rounded-3xl rounded-br-lg for outgoing, rounded-3xl rounded-bl-lg for incoming
  • Max-width constraints: max-w-[82%] md:max-w-[68%] for responsive bubble sizing
  • Opacity modifiers: text-foreground/90 for subtle text color

Chat header styling

The header uses backdrop blur and semi-transparent backgrounds for depth:
components/chat/chat-header.tsx
<header className="flex h-20 items-center justify-between border-b border-border/60 bg-background/80 backdrop-blur px-4">
  <Avatar className="h-11 w-11 border border-border/60">
    <AvatarImage src={contact.avatarUrl} alt={contact.name} />
    <AvatarFallback>{initials(contact.name)}</AvatarFallback>
  </Avatar>
  
  <span
    className={cn(
      "absolute -right-0.5 -bottom-0.5 h-2.5 w-2.5 rounded-full border-2 border-background",
      isOnline ? "bg-accent" : "bg-muted"
    )}
  />
</header>
The backdrop-blur utility requires a semi-transparent background to show the blur effect properly.
The sidebar features glassmorphism effects with blur and transparency:
components/chat/sidebar.tsx
<aside className="flex h-full w-full flex-col border-r border-sidebar-border/70 bg-sidebar/80 backdrop-blur-xl">
  <Input
    className="h-10 rounded-full border border-sidebar-border bg-sidebar/30 pl-9 text-sm focus-visible:ring-2 focus-visible:ring-primary/40"
    placeholder="Search chats"
  />
  
  <button
    className={cn(
      "flex w-full items-center gap-3 rounded-2xl px-4 py-3 text-left transition",
      isActive
        ? "bg-sidebar-accent/70 text-sidebar-foreground shadow-sm"
        : "hover:bg-sidebar-accent/40"
    )}
  >
    {/* Chat preview content */}
  </button>
</aside>

Custom utility classes

Base layer styles

Global styles are applied in the @layer base directive:
app/globals.css
@layer base {
  * {
    @apply border-border outline-ring/50;
  }
  body {
    @apply bg-background text-foreground;
  }
}
This ensures all elements have consistent border colors and focus outlines.

Typography utilities

The application uses the Inter font family configured as a CSS variable:
app/layout.tsx
import { Inter } from "next/font/google";

const inter = Inter({
  variable: "--font-inter",
  subsets: ["latin"],
  display: "swap",
});

export default function RootLayout({ children }) {
  return (
    <html lang="en" suppressHydrationWarning>
      <body className={`${inter.variable} font-sans antialiased bg-app-surface`}>
        {children}
      </body>
    </html>
  )
}
app/globals.css
@theme inline {
  --font-sans: var(--font-inter);
}

Responsive design

Breakpoint usage

The application uses Tailwind’s default breakpoints with mobile-first design:
<div className={cn("max-w-[82%] md:max-w-[68%]")}>
  {/* Bubbles are wider on mobile, narrower on desktop */}
</div>

Conditional rendering

Components handle mobile vs desktop layouts using media queries and conditional classes:
components/chat/chat-header.tsx
{onBack ? (
  <Button
    size="icon"
    variant="ghost"
    onClick={onBack}
    className="mr-1 h-9 w-9 text-muted-foreground"
  >
    <CaretLeft className="h-5 w-5" />
    <span className="sr-only">Back to chats</span>
  </Button>
) : null}

Component variants

Button variants

The Button component uses class-variance-authority (CVA) for variant management:
components/ui/button.tsx
import { cva, type VariantProps } from "class-variance-authority"

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 hover:bg-primary/90",
        destructive: "bg-destructive text-white hover:bg-destructive/90",
        outline: "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground",
        ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
        link: "text-primary underline-offset-4 hover:underline",
      },
      size: {
        default: "h-9 px-4 py-2 has-[>svg]:px-3",
        sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
        lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
        icon: "size-9",
      },
    },
  }
)
Usage examples:
<Button className="rounded-full bg-primary text-primary-foreground">
  <PaperPlaneTilt className="h-5 w-5" weight="fill" />
  <span className="sr-only">Send message</span>
</Button>
The has-[>svg] selector automatically adjusts padding when buttons contain SVG icons.

Accessibility styling

Screen reader classes

Use sr-only for visually hidden but accessible content:
<span className="sr-only">Send message</span>

Focus states

All interactive elements have visible focus indicators:
app/globals.css
* {
  @apply outline-ring/50;
}
<Button className="focus-visible:ring-2 focus-visible:ring-primary/40">
  Click me
</Button>

ARIA attributes

Combine ARIA attributes with conditional styling:
<div
  className="inline-flex items-center gap-2 rounded-full bg-black/5 px-3 py-1"
  aria-live="polite"
  aria-label="typing indicator"
>
  <span className="sr-only">typing</span>
  {/* Visual indicator */}
</div>

Opacity and transparency

Tailwind’s opacity modifiers create depth and hierarchy:
// Text with 90% opacity
<p className="text-foreground/90">Message text</p>

// Background with 80% opacity
<div className="bg-background/80 backdrop-blur" />

// Border with 60% opacity  
<div className="border-border/60" />

Shadow utilities

The application uses subtle shadows for depth:
// Message bubbles
<div className="shadow-sm transition-colors" />

// Send button
<Button className="shadow-lg transition hover:bg-primary/90" />

// Sidebar buttons
<button className="bg-sidebar-accent/70 shadow-sm" />

Build docs developers (and LLMs) love