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
const config = {
plugins: [ "@tailwindcss/postcss" ],
};
export default config ;
CSS imports
@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:
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
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:
@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:
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 >
)
}
@theme inline {
--font-sans: var(--font-inter);
}
Responsive design
Breakpoint usage
The application uses Tailwind’s default breakpoints with mobile-first design:
Message bubbles
Image preview
< 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
The Button component uses class-variance-authority (CVA) for variant management:
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 >
< Button size = "icon" variant = "ghost" className = "h-10 w-10 text-muted-foreground" >
< Smiley className = "h-5 w-5" />
</ Button >
< Button
variant = "outline"
className = "h-10 w-10 rounded-full border-sidebar-border/80 bg-sidebar"
>
< Plus className = "h-5 w-5" />
</ 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:
* {
@ 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" />