Building Beautiful UIs with Tailwind CSS and shadcn/ui

May 22, 20255 min readNext Starter Team

Discover how to create stunning, accessible user interfaces using Tailwind CSS and the shadcn/ui component library in your Next.js applications.

Building Beautiful UIs with Tailwind CSS and shadcn/ui

Building Beautiful UIs with Tailwind CSS and shadcn/ui

Creating beautiful, consistent, and accessible user interfaces has never been easier thanks to the powerful combination of Tailwind CSS and shadcn/ui. In this guide, we'll explore how to leverage these tools to build stunning React applications.

Why Tailwind CSS + shadcn/ui?

This combination offers several advantages:

  • 🎨 Design Consistency: Pre-built components with consistent styling
  • ⚡ Developer Productivity: Copy-paste components that just work
  • ♿ Accessibility: Built-in accessibility features
  • 🎯 Customization: Easy to customize and extend
  • 📱 Responsive: Mobile-first responsive design

Setting Up Your Environment

1. Install Tailwind CSS

npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p

Configure your tailwind.config.js:

/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    './pages/**/*.{js,ts,jsx,tsx,mdx}',
    './components/**/*.{js,ts,jsx,tsx,mdx}',
    './app/**/*.{js,ts,jsx,tsx,mdx}',
  ],
  theme: {
    extend: {
      colors: {
        border: 'hsl(var(--border))',
        input: 'hsl(var(--input))',
        ring: 'hsl(var(--ring))',
        background: 'hsl(var(--background))',
        foreground: 'hsl(var(--foreground))',
        primary: {
          DEFAULT: 'hsl(var(--primary))',
          foreground: 'hsl(var(--primary-foreground))',
        },
        secondary: {
          DEFAULT: 'hsl(var(--secondary))',
          foreground: 'hsl(var(--secondary-foreground))',
        },
        // ... more colors
      },
    },
  },
  plugins: [],
};

2. Initialize shadcn/ui

npx shadcn@latest init

This will set up the necessary configuration files and install core dependencies.

Essential Components

Buttons

The Button component is the foundation of most interactions:

import { Button } from "@/components/ui/button"

export default function ButtonDemo() {
  return (
    <div className="space-x-2">
      <Button variant="default">Default</Button>
      <Button variant="secondary">Secondary</Button>
      <Button variant="destructive">Destructive</Button>
      <Button variant="outline">Outline</Button>
      <Button variant="ghost">Ghost</Button>
      <Button variant="link">Link</Button>
    </div>
  )
}

Cards

Perfect for organizing content:

import {
  Card,
  CardContent,
  CardDescription,
  CardFooter,
  CardHeader,
  CardTitle,
} from "@/components/ui/card"

export default function CardDemo() {
  return (
    <Card className="w-96">
      <CardHeader>
        <CardTitle>Feature Highlight</CardTitle>
        <CardDescription>
          Discover what makes this feature special
        </CardDescription>
      </CardHeader>
      <CardContent>
        <p>Your content goes here...</p>
      </CardContent>
      <CardFooter>
        <Button className="w-full">Get Started</Button>
      </CardFooter>
    </Card>
  )
}

Forms

Building accessible forms is straightforward:

import { Button } from "@/components/ui/button"
import {
  Form,
  FormControl,
  FormDescription,
  FormField,
  FormItem,
  FormLabel,
  FormMessage,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import { useForm } from "react-hook-form"

export default function ContactForm() {
  const form = useForm()

  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
        <FormField
          control={form.control}
          name="username"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Username</FormLabel>
              <FormControl>
                <Input placeholder="Enter your username" {...field} />
              </FormControl>
              <FormDescription>
                This is your public display name.
              </FormDescription>
              <FormMessage />
            </FormItem>
          )}
        />
        <Button type="submit">Submit</Button>
      </form>
    </Form>
  )
}

Advanced Patterns

Theme Switching

Implement dark/light mode easily:

"use client"

import { Moon, Sun } from "lucide-react"
import { useTheme } from "next-themes"
import { Button } from "@/components/ui/button"

export function ThemeToggle() {
  const { setTheme, theme } = useTheme()

  return (
    <Button
      variant="ghost"
      size="icon"
      onClick={() => setTheme(theme === "light" ? "dark" : "light")}
    >
      <Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
      <Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
      <span className="sr-only">Toggle theme</span>
    </Button>
  )
}

Custom Color Palette

Extend your design system:

/* globals.css */
:root {
  --background: 0 0% 100%;
  --foreground: 222.2 84% 4.9%;

  --card: 0 0% 100%;
  --card-foreground: 222.2 84% 4.9%;

  --popover: 0 0% 100%;
  --popover-foreground: 222.2 84% 4.9%;

  --primary: 222.2 47.4% 11.2%;
  --primary-foreground: 210 40% 98%;

  --secondary: 210 40% 96%;
  --secondary-foreground: 222.2 84% 4.9%;

  --accent: 210 40% 96%;
  --accent-foreground: 222.2 84% 4.9%;

  --destructive: 0 84.2% 60.2%;
  --destructive-foreground: 210 40% 98%;

  --border: 214.3 31.8% 91.4%;
  --input: 214.3 31.8% 91.4%;
  --ring: 222.2 84% 4.9%;

  --radius: 0.5rem;
}

.dark {
  --background: 222.2 84% 4.9%;
  --foreground: 210 40% 98%;

  /* ... dark mode colors */
}

Responsive Components

Build mobile-first designs:

export default function ResponsiveGrid() {
  return (
    <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
      {items.map((item) => (
        <Card key={item.id} className="h-full">
          <CardHeader>
            <CardTitle className="text-sm md:text-base lg:text-lg">
              {item.title}
            </CardTitle>
          </CardHeader>
          <CardContent>
            <p className="text-xs md:text-sm text-muted-foreground">
              {item.description}
            </p>
          </CardContent>
        </Card>
      ))}
    </div>
  )
}

Component Composition

Building Complex Layouts

Combine components to create sophisticated interfaces:

import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Separator } from "@/components/ui/separator"

export default function ProductCard({ product }) {
  return (
    <Card className="overflow-hidden">
      <div className="aspect-video bg-muted" />
      <CardHeader>
        <div className="flex items-start justify-between">
          <CardTitle className="line-clamp-2">{product.name}</CardTitle>
          <Badge variant={product.inStock ? "default" : "secondary"}>
            {product.inStock ? "In Stock" : "Out of Stock"}
          </Badge>
        </div>
      </CardHeader>
      <CardContent className="space-y-4">
        <p className="text-sm text-muted-foreground line-clamp-3">
          {product.description}
        </p>
        <Separator />
        <div className="flex items-center justify-between">
          <span className="text-2xl font-bold">${product.price}</span>
          <Button disabled={!product.inStock}>
            Add to Cart
          </Button>
        </div>
      </CardContent>
    </Card>
  )
}

Performance Optimization

Bundle Size Optimization

Import only what you need:

// ❌ Don't import the entire library
import * as lucide from 'lucide-react';

// ✅ Import specific icons
import { ChevronDown, User, Settings } from 'lucide-react';

CSS-in-JS Alternative

Use Tailwind's utility classes for better performance:

// ❌ Runtime CSS-in-JS
const styles = {
  button: {
    backgroundColor: '#3b82f6',
    color: 'white',
    padding: '0.5rem 1rem',
    borderRadius: '0.25rem',
  }
}

// ✅ Tailwind utilities (compile-time)
<Button className="bg-blue-500 text-white px-4 py-2 rounded">
  Click me
</Button>

Accessibility Best Practices

Keyboard Navigation

Ensure all interactive elements are keyboard accessible:

import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"

export default function AccessibleDropdown() {
  return (
    <DropdownMenu>
      <DropdownMenuTrigger asChild>
        <Button variant="outline" aria-label="Open menu">
          Menu
        </Button>
      </DropdownMenuTrigger>
      <DropdownMenuContent>
        <DropdownMenuItem onSelect={() => console.log('Profile')}>
          Profile
        </DropdownMenuItem>
        <DropdownMenuItem onSelect={() => console.log('Settings')}>
          Settings
        </DropdownMenuItem>
      </DropdownMenuContent>
    </DropdownMenu>
  )
}

Screen Reader Support

Use semantic HTML and ARIA attributes:

export default function StatusCard({ status, message }) {
  return (
    <Card role="alert" aria-live="polite">
      <CardHeader>
        <CardTitle className="flex items-center gap-2">
          <StatusIcon status={status} aria-hidden="true" />
          <span className="sr-only">Status: </span>
          {status}
        </CardTitle>
      </CardHeader>
      <CardContent>
        <p>{message}</p>
      </CardContent>
    </Card>
  )
}

Testing Your Components

Component Testing with Jest

import { render, screen } from '@testing-library/react'
import { Button } from '@/components/ui/button'

describe('Button', () => {
  it('renders correctly', () => {
    render(<Button>Click me</Button>)
    expect(screen.getByRole('button')).toHaveTextContent('Click me')
  })

  it('applies variant styles', () => {
    render(<Button variant="destructive">Delete</Button>)
    expect(screen.getByRole('button')).toHaveClass('bg-destructive')
  })
})

Conclusion

The combination of Tailwind CSS and shadcn/ui provides a powerful foundation for building beautiful, accessible, and maintainable user interfaces. By following the patterns and best practices outlined in this guide, you'll be able to create stunning applications that delight your users.

Start building your design system today and experience the joy of consistent, beautiful UI development!


Want to explore more components? Check out the shadcn/ui documentation and discover the full range of available components.

Kickstart your product with Next SaaS

Duplicate the starter, wire up your own data, and focus on building features that matter.

Open the Starter

No credit card required