We use cookies to enhance your experience on the site
CodeWorlds

Shadcn/UI - Components for Next.js 15

In Metropolis Quantum, advanced research laboratories developed modular, prefabricated building elements that can be easily combined and customized as needed. Thanks to this, architects can quickly create buildings with a consistent aesthetic while maintaining flexibility in designing unique spaces. A similar role in the world of frontend development is played by the Shadcn/UI library - a collection of carefully designed and accessible user interface components that can be easily integrated and customized to meet project needs.

What is Shadcn/UI?

Shadcn/UI is a collection of UI components built with Radix UI, Tailwind CSS, and React in mind. Unlike traditional component libraries, Shadcn/UI is not installed as an npm dependency. Instead, components are copied directly into your project, giving you full control over the code and enabling easy modifications as needed.

Key Features of Shadcn/UI:

  1. No external library dependency - components are part of your code
  2. Full control over styles - easy modification of component appearance
  3. Built on Radix UI - solid, accessible primitives for interactive components
  4. Styled with Tailwind CSS - consistent and easy to customize design system
  5. Dark mode by default - built-in support for light and dark themes
  6. Strict TypeScript typing - full type support
  7. Modularity - install only the components you need
  8. Responsiveness - components adapt to different screen sizes

Instalacja i konfiguracja Shadcn/UI w Next.js 15

To get started with Shadcn/UI in a Next.js 15 project, you need to follow a few steps:

Krok 1: Instalacja CLI Shadcn/UI

1npx shadcn-ui@latest init

After running this command, the CLI will ask you to answer a few questions:

  1. Style choice - usually "Default" is a good choice
  2. Components path choice - typically "src/components/ui"
  3. Color scheme choice - you can choose default or custom
  4. Do you want to enable dark mode? - usually "yes"
  5. Component prefix choice - typically no prefix
  6. Globals.css file path - typically "app/globals.css"
  7. Place CSS variables in globals.css? - usually "yes"

Krok 2: Sprawdzenie konfiguracji Tailwind CSS

The CLI will automatically update the Tailwind CSS configuration file. It is worth checking the contents of the

tailwind.config.js
file:

1// tailwind.config.js
2/** @type {import('tailwindcss').Config} */
3module.exports = {
4  darkMode: ["class"],
5  content: [
6    './pages/**/*.{js,ts,jsx,tsx,mdx}',
7    './components/**/*.{js,ts,jsx,tsx,mdx}',
8    './app/**/*.{js,ts,jsx,tsx,mdx}',
9  ],
10  theme: {
11    container: {
12      center: true,
13      padding: "2rem",
14      screens: {
15        "2xl": "1400px",
16      },
17    },
18    extend: {
19      colors: {
20        border: "hsl(var(--border))",
21        input: "hsl(var(--input))",
22        ring: "hsl(var(--ring))",
23        background: "hsl(var(--background))",
24        foreground: "hsl(var(--foreground))",
25        primary: {
26          DEFAULT: "hsl(var(--primary))",
27          foreground: "hsl(var(--primary-foreground))",
28        },
29        secondary: {
30          DEFAULT: "hsl(var(--secondary))",
31          foreground: "hsl(var(--secondary-foreground))",
32        },
33        destructive: {
34          DEFAULT: "hsl(var(--destructive))",
35          foreground: "hsl(var(--destructive-foreground))",
36        },
37        muted: {
38          DEFAULT: "hsl(var(--muted))",
39          foreground: "hsl(var(--muted-foreground))",
40        },
41        accent: {
42          DEFAULT: "hsl(var(--accent))",
43          foreground: "hsl(var(--accent-foreground))",
44        },
45        popover: {
46          DEFAULT: "hsl(var(--popover))",
47          foreground: "hsl(var(--popover-foreground))",
48        },
49        card: {
50          DEFAULT: "hsl(var(--card))",
51          foreground: "hsl(var(--card-foreground))",
52        },
53      },
54      borderRadius: {
55        lg: "var(--radius)",
56        md: "calc(var(--radius) - 2px)",
57        sm: "calc(var(--radius) - 4px)",
58      },
59      keyframes: {
60        "accordion-down": {
61          from: { height: 0 },
62          to: { height: "var(--radix-accordion-content-height)" },
63        },
64        "accordion-up": {
65          from: { height: "var(--radix-accordion-content-height)" },
66          to: { height: 0 },
67        },
68      },
69      animation: {
70        "accordion-down": "accordion-down 0.2s ease-out",
71        "accordion-up": "accordion-up 0.2s ease-out",
72      },
73    },
74  },
75  plugins: [require("tailwindcss-animate")],
76}

Krok 3: Sprawdzenie zmiennych CSS

The CLI will also add CSS variables to the

globals.css
file:

1@tailwind base;
2@tailwind components;
3@tailwind utilities;
4 
5@layer base {
6  :root {
7    --background: 0 0% 100%;
8    --foreground: 222.2 84% 4.9%;
9 
10    --card: 0 0% 100%;
11    --card-foreground: 222.2 84% 4.9%;
12 
13    --popover: 0 0% 100%;
14    --popover-foreground: 222.2 84% 4.9%;
15 
16    --primary: 222.2 47.4% 11.2%;
17    --primary-foreground: 210 40% 98%;
18 
19    --secondary: 210 40% 96.1%;
20    --secondary-foreground: 222.2 47.4% 11.2%;
21 
22    --muted: 210 40% 96.1%;
23    --muted-foreground: 215.4 16.3% 46.9%;
24 
25    --accent: 210 40% 96.1%;
26    --accent-foreground: 222.2 47.4% 11.2%;
27 
28    --destructive: 0 84.2% 60.2%;
29    --destructive-foreground: 210 40% 98%;
30 
31    --border: 214.3 31.8% 91.4%;
32    --input: 214.3 31.8% 91.4%;
33    --ring: 222.2 84% 4.9%;
34 
35    --radius: 0.5rem;
36  }
37 
38  .dark {
39    --background: 222.2 84% 4.9%;
40    --foreground: 210 40% 98%;
41 
42    --card: 222.2 84% 4.9%;
43    --card-foreground: 210 40% 98%;
44 
45    --popover: 222.2 84% 4.9%;
46    --popover-foreground: 210 40% 98%;
47 
48    --primary: 210 40% 98%;
49    --primary-foreground: 222.2 47.4% 11.2%;
50 
51    --secondary: 217.2 32.6% 17.5%;
52    --secondary-foreground: 210 40% 98%;
53 
54    --muted: 217.2 32.6% 17.5%;
55    --muted-foreground: 215 20.2% 65.1%;
56 
57    --accent: 217.2 32.6% 17.5%;
58    --accent-foreground: 210 40% 98%;
59 
60    --destructive: 0 62.8% 30.6%;
61    --destructive-foreground: 210 40% 98%;
62 
63    --border: 217.2 32.6% 17.5%;
64    --input: 217.2 32.6% 17.5%;
65    --ring: 212.7 26.8% 83.9%;
66  }
67}
68 
69@layer base {
70  * {
71    @apply border-border;
72  }
73  body {
74    @apply bg-background text-foreground;
75  }
76}

Step 4: Adding Shadcn/UI Components

Now you can install selected components using the CLI:

1npx shadcn-ui@latest add button

To polecenie doda komponent Button do katalogu

components/ui
:

1// components/ui/button.tsx
2import * as React from "react"
3import { Slot } from "@radix-ui/react-slot"
4import { cva, type VariantProps } from "class-variance-authority"
5
6import { cn } from "@/lib/utils"
7
8const buttonVariants = cva(
9  "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
10  {
11    variants: {
12      variant: {
13        default: "bg-primary text-primary-foreground hover:bg-primary/90",
14        destructive:
15          "bg-destructive text-destructive-foreground hover:bg-destructive/90",
16        outline:
17          "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
18        secondary:
19          "bg-secondary text-secondary-foreground hover:bg-secondary/80",
20        ghost: "hover:bg-accent hover:text-accent-foreground",
21        link: "text-primary underline-offset-4 hover:underline",
22      },
23      size: {
24        default: "h-10 px-4 py-2",
25        sm: "h-9 rounded-md px-3",
26        lg: "h-11 rounded-md px-8",
27        icon: "h-10 w-10",
28      },
29    },
30    defaultVariants: {
31      variant: "default",
32      size: "default",
33    },
34  }
35)
36
37export interface ButtonProps
38  extends React.ButtonHTMLAttributes<HTMLButtonElement>,
39    VariantProps<typeof buttonVariants> {
40  asChild?: boolean
41}
42
43const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
44  ({ className, variant, size, asChild = false, ...props }, ref) => {
45    const Comp = asChild ? Slot : "button"
46    return (
47      <Comp
48        className={cn(buttonVariants({ variant, size, className }))}
49        ref={ref}
50        {...props}
51      />
52    )
53  }
54)
55Button.displayName = "Button"
56
57export { Button, buttonVariants }

The CLI will also automatically add a

lib/utils.ts
file containing utility functions:

1// lib/utils.ts
2import { type ClassValue, clsx } from "clsx"
3import { twMerge } from "tailwind-merge"
4 
5export function cn(...inputs: ClassValue[]) {
6  return twMerge(clsx(inputs))
7}

Main Shadcn/UI Components

Shadcn/UI offers many components, from simple buttons to advanced interaction components. Here are some of the most important ones:

Podstawowe komponenty

  1. Button - buttons with different variants
  2. Input - pola wprowadzania
  3. Checkbox - pola wyboru
  4. RadioGroup - grupy opcji radio
  5. Select - rozwijane listy wyboru
  6. Textarea - pola tekstowe wieloliniowe
  7. Label - etykiety dla kontrolek formularzy

Layout Components

  1. Card - cards for grouping content
  2. AspectRatio - maintaining element proportions
  3. ScrollArea - obszary przewijania
  4. Separator - dividing lines
  5. Sheet - wysuwane panele boczne

Komponenty nawigacji

  1. Tabs - tabs
  2. Navigation Menu - menu nawigacyjne
  3. Breadcrumb - navigation breadcrumbs
  4. Pagination - stronicowanie

Komponenty dialogowe

  1. Dialog - okna dialogowe
  2. Alert Dialog - alerts requiring confirmation
  3. Drawer - wysuwane panele
  4. Popover - popup bubbles
  5. Toast - powiadomienia

Komponenty danych

  1. Table - tabele
  2. Accordion - rozwijane sekcje
  3. Calendar - kalendarze
  4. Carousel - karuzele

Komponenty formularzy

  1. Form - comprehensive form solution
  2. Combobox - rozbudowane pola wyboru z wyszukiwaniem
  3. Slider - suwaki
  4. Switch - toggles

Usage Examples of Shadcn/UI in a Next.js 15 Project

Example 1: Login Form

1// app/login/page.tsx
2"use client"
3
4import { useState } from "react"
5import { useRouter } from "next/navigation"
6import { zodResolver } from "@hookform/resolvers/zod"
7import { useForm } from "react-hook-form"
8import * as z from "zod"
9
10import { Button } from "@/components/ui/button"
11import {
12  Form,
13  FormControl,
14  FormField,
15  FormItem,
16  FormLabel,
17  FormMessage,
18} from "@/components/ui/form"
19import { Input } from "@/components/ui/input"
20import { toast } from "@/components/ui/use-toast"
21
22const formSchema = z.object({
23  email: z.string().email({
24    message: "Please enter a valid email address.",
25  }),
26  password: z.string().min(8, {
27    message: "Password must contain at least 8 characters.",
28  }),
29})
30
31export default function LoginPage() {
32  const router = useRouter()
33  const [isLoading, setIsLoading] = useState(false)
34
35  const form = useForm<z.infer<typeof formSchema>>({
36    resolver: zodResolver(formSchema),
37    defaultValues: {
38      email: "",
39      password: "",
40    },
41  })
42
43  async function onSubmit(values: z.infer<typeof formSchema>) {
44    setIsLoading(true)
45
46    try {
47      // In a real application, we would call the login API here
48      console.log(values)
49      
50      // Simulating delay
51      await new Promise(resolve => setTimeout(resolve, 1000))
52      
53      toast({
54        title: "Logged in successfully",
55        description: "Redirecting you to the main dashboard",
56      })
57      
58      router.push("/dashboard")
59    } catch (error) {
60      toast({
61        variant: "destructive",
62        title: "An error occurred",
63        description: "Unable to log in. Check your credentials and try again.",
64      })
65    } finally {
66      setIsLoading(false)
67    }
68  }
69
70  return (
71    <div className="mx-auto max-w-md space-y-6 p-6">
72      <div className="space-y-2 text-center">
73        <h1 className="text-3xl font-bold">Logowanie</h1>
74        <p className="text-gray-500 dark:text-gray-400">
75          Enter your credentials to log in to the Metropolis Quantum panel
76        </p>
77      </div>
78      <Form {...form}>
79        <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
80          <FormField
81            control={form.control}
82            name="email"
83            render={({ field }) => (
84              <FormItem>
85                <FormLabel>Email</FormLabel>
86                <FormControl>
87                  <Input type="email" placeholder="email@example.com" {...field} />
88                </FormControl>
89                <FormMessage />
90              </FormItem>
91            )}
92          />
93          <FormField
94            control={form.control}
95            name="password"
96            render={({ field }) => (
97              <FormItem>
98                <FormLabel>Password</FormLabel>
99                <FormControl>
100                  <Input type="password" {...field} />
101                </FormControl>
102                <FormMessage />
103              </FormItem>
104            )}
105          />
106          <Button type="submit" className="w-full" disabled={isLoading}>
107            {isLoading ? "Logging in..." : "Log in"}
108          </Button>
109        </form>
110      </Form>
111      <div className="text-center text-sm">
112        <a href="/forgot-password" className="text-primary hover:underline">
113          Forgot your password?
114        </a>
115      </div>
116    </div>
117  )
118}

Example 2: Panel with Tabs

1// app/dashboard/page.tsx
2"use client"
3
4import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
5import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
6import { Button } from "@/components/ui/button"
7
8export default function DashboardPage() {
9  return (
10    <div className="container mx-auto py-10">
11      <h1 className="text-3xl font-bold mb-6">Dashboard Metropolis Quantum</h1>
12      
13      <Tabs defaultValue="overview" className="space-y-4">
14        <TabsList className="grid w-full grid-cols-4">
15          <TabsTrigger value="overview">Overview</TabsTrigger>
16          <TabsTrigger value="analytics">Analityka</TabsTrigger>
17          <TabsTrigger value="reports">Raporty</TabsTrigger>
18          <TabsTrigger value="settings">Ustawienia</TabsTrigger>
19        </TabsList>
20        
21        <TabsContent value="overview" className="space-y-4">
22          <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
23            <Card>
24              <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
25                <CardTitle className="text-sm font-medium">Total Revenue</CardTitle>
26                <svg
27                  xmlns="http://www.w3.org/2000/svg"
28                  viewBox="0 0 24 24"
29                  fill="none"
30                  stroke="currentColor"
31                  strokeLinecap="round"
32                  strokeLinejoin="round"
33                  strokeWidth="2"
34                  className="h-4 w-4 text-muted-foreground"
35                >
36                  <path d="M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6" />
37                </svg>
38              </CardHeader>
39              <CardContent>
40                <div className="text-2xl font-bold">$45,231.89</div>
41                <p className="text-xs text-muted-foreground">
42                  +20.1% from last month
43                </p>
44              </CardContent>
45            </Card>
46            <Card>
47              <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
48                <CardTitle className="text-sm font-medium">Subskrypcje</CardTitle>
49                <svg
50                  xmlns="http://www.w3.org/2000/svg"
51                  viewBox="0 0 24 24"
52                  fill="none"
53                  stroke="currentColor"
54                  strokeLinecap="round"
55                  strokeLinejoin="round"
56                  strokeWidth="2"
57                  className="h-4 w-4 text-muted-foreground"
58                >
59                  <path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" />
60                  <circle cx="9" cy="7" r="4" />
61                  <path d="M22 21v-2a4 4 0 0 0-3-3.87M16 3.13a4 4 0 0 1 0 7.75" />
62                </svg>
63              </CardHeader>
64              <CardContent>
65                <div className="text-2xl font-bold">+2350</div>
66                <p className="text-xs text-muted-foreground">
67                  +180.1% from last month
68                </p>
69              </CardContent>
70            </Card>
71            <Card>
72              <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
73                <CardTitle className="text-sm font-medium">Sales</CardTitle>
74                <svg
75                  xmlns="http://www.w3.org/2000/svg"
76                  viewBox="0 0 24 24"
77                  fill="none"
78                  stroke="currentColor"
79                  strokeLinecap="round"
80                  strokeLinejoin="round"
81                  strokeWidth="2"
82                  className="h-4 w-4 text-muted-foreground"
83                >
84                  <rect width="20" height="14" x="2" y="5" rx="2" />
85                  <path d="M2 10h20" />
86                </svg>
87              </CardHeader>
88              <CardContent>
89                <div className="text-2xl font-bold">+12,234</div>
90                <p className="text-xs text-muted-foreground">
91                  +19% from last month
92                </p>
93              </CardContent>
94            </Card>
95            <Card>
96              <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
97                <CardTitle className="text-sm font-medium">Active Users</CardTitle>
98                <svg
99                  xmlns="http://www.w3.org/2000/svg"
100                  viewBox="0 0 24 24"
101                  fill="none"
102                  stroke="currentColor"
103                  strokeLinecap="round"
104                  strokeLinejoin="round"
105                  strokeWidth="2"
106                  className="h-4 w-4 text-muted-foreground"
107                >
108                  <path d="M22 12h-4l-3 9L9 3l-3 9H2" />
109                </svg>
110              </CardHeader>
111              <CardContent>
112                <div className="text-2xl font-bold">+573</div>
113                <p className="text-xs text-muted-foreground">
114                  +201 from last month
115                </p>
116              </CardContent>
117            </Card>
118          </div>
119          <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-7">
120            <Card className="col-span-4">
121              <CardHeader>
122                <CardTitle>Overview</CardTitle>
123              </CardHeader>
124              <CardContent className="pl-2">
125                {/* A chart could go here */}
126                <div className="h-[200px] bg-muted rounded-md flex items-center justify-center">
127                  Revenue Chart
128                </div>
129              </CardContent>
130            </Card>
131            <Card className="col-span-3">
132              <CardHeader>
133                <CardTitle>Ostatnie Transakcje</CardTitle>
134                <CardDescription>
135                  24 transactions completed this month.
136                </CardDescription>
137              </CardHeader>
138              <CardContent>
139                <div className="space-y-8">
140                  {/* Example transaction */}
141                  <div className="flex items-center">
142                    <div className="mr-4 rounded-full bg-primary/10 p-2">
143                      <svg
144                        xmlns="http://www.w3.org/2000/svg"
145                        viewBox="0 0 24 24"
146                        fill="none"
147                        stroke="currentColor"
148                        strokeLinecap="round"
149                        strokeLinejoin="round"
150                        strokeWidth="2"
151                        className="h-4 w-4 text-primary"
152                      >
153                        <path d="M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6" />
154                      </svg>
155                    </div>
156                    <div className="space-y-1">
157                      <p className="text-sm font-medium">Quantum Plan (Roczny)</p>
158                      <p className="text-xs text-muted-foreground">14 kwietnia 2024</p>
159                    </div>
160                    <div className="ml-auto font-medium">+$4,999.00</div>
161                  </div>
162                  {/* More transactions */}
163                </div>
164              </CardContent>
165            </Card>
166          </div>
167        </TabsContent>
168        
169        <TabsContent value="analytics" className="space-y-4">
170          <Card className="col-span-4">
171            <CardHeader>
172              <CardTitle>Analityka</CardTitle>
173              <CardDescription>
174                Detailed traffic and conversion analysis.
175              </CardDescription>
176            </CardHeader>
177            <CardContent>
178              <div className="h-[300px] bg-muted rounded-md flex items-center justify-center">
179                Dane Analityczne
180              </div>
181            </CardContent>
182          </Card>
183        </TabsContent>
184        
185        <TabsContent value="reports" className="space-y-4">
186          <Card>
187            <CardHeader>
188              <CardTitle>Raporty</CardTitle>
189              <CardDescription>
190                Browse and generate activity reports.
191              </CardDescription>
192            </CardHeader>
193            <CardContent className="space-y-4">
194              <div className="space-y-2">
195                <div className="flex items-center justify-between">
196                  <div>
197                    <p className="text-sm font-medium">Monthly Report (April 2024)</p>
198                    <p className="text-xs text-muted-foreground">Utworzony 01.05.2024</p>
199                  </div>
200                  <Button variant="outline" size="sm">Pobierz</Button>
201                </div>
202                <div className="flex items-center justify-between">
203                  <div>
204                    <p className="text-sm font-medium">Raport Kwartalny (Q1 2024)</p>
205                    <p className="text-xs text-muted-foreground">Utworzony 05.04.2024</p>
206                  </div>
207                  <Button variant="outline" size="sm">Pobierz</Button>
208                </div>
209              </div>
210            </CardContent>
211            <CardFooter>
212              <Button>Generuj Nowy Raport</Button>
213            </CardFooter>
214          </Card>
215        </TabsContent>
216        
217        <TabsContent value="settings" className="space-y-4">
218          <Card>
219            <CardHeader>
220              <CardTitle>Ustawienia</CardTitle>
221              <CardDescription>
222                Manage your account settings.
223              </CardDescription>
224            </CardHeader>
225            <CardContent className="space-y-4">
226              {/* Example settings */}
227              <div className="space-y-1">
228                <p className="text-sm font-medium">Powiadomienia Email</p>
229                <p className="text-xs text-muted-foreground">Receive notifications about important updates.</p>
230              </div>
231              {/* More settings */}
232            </CardContent>
233            <CardFooter>
234              <Button>Save Zmiany</Button>
235            </CardFooter>
236          </Card>
237        </TabsContent>
238      </Tabs>
239    </div>
240  )
241}

Example 3: Dialog with Form

1// components/quantum-project-dialog.tsx
2"use client"
3
4import { useState } from "react"
5import { Button } from "@/components/ui/button"
6import {
7  Dialog,
8  DialogContent,
9  DialogDescription,
10  DialogFooter,
11  DialogHeader,
12  DialogTitle,
13  DialogTrigger,
14} from "@/components/ui/dialog"
15import { Input } from "@/components/ui/input"
16import { Label } from "@/components/ui/label"
17import { Textarea } from "@/components/ui/textarea"
18import { 
19  Select,
20  SelectContent,
21  SelectItem,
22  SelectTrigger,
23  SelectValue,
24} from "@/components/ui/select"
25import { toast } from "@/components/ui/use-toast"
26
27export function QuantumProjectDialog() {
28  const [open, setOpen] = useState(false)
29  const [isLoading, setIsLoading] = useState(false)
30  
31  const [formData, setFormData] = useState({
32    name: "",
33    description: "",
34    category: "",
35    priority: "",
36  })
37  
38  const handleChange = (field: string, value: string) => {
39    setFormData((prev) => ({ ...prev, [field]: value }))
40  }
41  
42  const handleSubmit = async (e: React.FormEvent) => {
43    e.preventDefault()
44    setIsLoading(true)
45    
46    // Symulacja zapisywania danych
47    await new Promise(resolve => setTimeout(resolve, 1000))
48    
49    toast({
50      title: "Projekt utworzony",
51      description: `Project "${formData.name}" has been successfully created.`,
52    })
53    
54    setIsLoading(false)
55    setOpen(false)
56    // Reset formularza
57    setFormData({
58      name: "",
59      description: "",
60      category: "",
61      priority: "",
62    })
63  }
64  
65  return (
66    <Dialog open={open} onOpenChange={setOpen}>
67      <DialogTrigger asChild>
68        <Button variant="default">Nowy Projekt</Button>
69      </DialogTrigger>
70      <DialogContent className="sm:max-w-[425px]">
71        <form onSubmit={handleSubmit}>
72          <DialogHeader>
73            <DialogTitle>Create New Project</DialogTitle>
74            <DialogDescription>
75              Enter project details. Click save when you are done.
76            </DialogDescription>
77          </DialogHeader>
78          <div className="grid gap-4 py-4">
79            <div className="grid gap-2">
80              <Label htmlFor="name">Nazwa projektu</Label>
81              <Input
82                id="name"
83                value={formData.name}
84                onChange={(e) => handleChange("name", e.target.value)}
85                placeholder="Nazwa projektu"
86                required
87              />
88            </div>
89            <div className="grid gap-2">
90              <Label htmlFor="description">Opis</Label>
91              <Textarea
92                id="description"
93                value={formData.description}
94                onChange={(e) => handleChange("description", e.target.value)}
95                placeholder="Describe your project"
96                required
97              />
98            </div>
99            <div className="grid grid-cols-2 gap-4">
100              <div className="grid gap-2">
101                <Label htmlFor="category">Kategoria</Label>
102                <Select
103                  value={formData.category}
104                  onValueChange={(value) => handleChange("category", value)}
105                  required
106                >
107                  <SelectTrigger id="category">
108                    <SelectValue placeholder="Choose a category" />
109                  </SelectTrigger>
110                  <SelectContent>
111                    <SelectItem value="development">Development</SelectItem>
112                    <SelectItem value="research">Badania</SelectItem>
113                    <SelectItem value="infrastructure">Infrastruktura</SelectItem>
114                    <SelectItem value="marketing">Marketing</SelectItem>
115                    <SelectItem value="other">Inne</SelectItem>
116                  </SelectContent>
117                </Select>
118              </div>
119              <div className="grid gap-2">
120                <Label htmlFor="priority">Priorytet</Label>
121                <Select
122                  value={formData.priority}
123                  onValueChange={(value) => handleChange("priority", value)}
124                  required
125                >
126                  <SelectTrigger id="priority">
127                    <SelectValue placeholder="Wybierz priorytet" />
128                  </SelectTrigger>
129                  <SelectContent>
130                    <SelectItem value="low">Niski</SelectItem>
131                    <SelectItem value="medium">Medium</SelectItem>
132                    <SelectItem value="high">Wysoki</SelectItem>
133                    <SelectItem value="critical">Krytyczny</SelectItem>
134                  </SelectContent>
135                </Select>
136              </div>
137            </div>
138          </div>
139          <DialogFooter>
140            <Button type="button" variant="outline" onClick={() => setOpen(false)}>
141              Cancel
142            </Button>
143            <Button type="submit" disabled={isLoading}>
144              {isLoading ? "Zapisywanie..." : "Save projekt"}
145            </Button>
146          </DialogFooter>
147        </form>
148      </DialogContent>
149    </Dialog>
150  )
151}

Customizing Shadcn/UI Components

One of the greatest advantages of Shadcn/UI is the ease of customizing components. Since the components are part of your code, you can modify them as needed.

Customizing Styles

You can modify the appearance of components in several ways:

  1. Changing CSS variables - update variable values in
    globals.css
1:root {
2  --primary: 240 5.9% 10%; /* Zmiana koloru primary */
3  --radius: 0.75rem; /* Changing corner rounding */
4}
  1. Modifying component definition - edit the component file directly
1// components/ui/button.tsx
2const buttonVariants = cva(
3  "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ring-offset-background",
4  {
5    variants: {
6      variant: {
7        default: "bg-primary text-primary-foreground hover:bg-primary/90",
8        destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
9        outline: "border border-input hover:bg-accent hover:text-accent-foreground",
10        secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
11        ghost: "hover:bg-accent hover:text-accent-foreground",
12        link: "underline-offset-4 hover:underline text-primary",
13        // Dodajemy nowy wariant
14        gradient: "bg-gradient-to-r from-blue-500 to-purple-500 text-white hover:opacity-90",
15      },
16      // Other definitions
17    },
18  }
19)
20
21// Update the interface to include the new variant
22export interface ButtonProps
23  extends React.ButtonHTMLAttributes<HTMLButtonElement>,
24    VariantProps<typeof buttonVariants> {
25  asChild?: boolean
26}
  1. Extending components - create your own components based on Shadcn/UI
1// components/quantum/quantum-card.tsx
2import { Card, CardContent, CardFooter, CardHeader } from "@/components/ui/card"
3import { cn } from "@/lib/utils"
4
5interface QuantumCardProps {
6  className?: string
7  children: React.ReactNode
8  gradient?: boolean
9  hoverable?: boolean
10}
11
12export function QuantumCard({ 
13  className, 
14  children, 
15  gradient = false,
16  hoverable = false,
17}: QuantumCardProps) {
18  return (
19    <Card 
20      className={cn(
21        "overflow-hidden",
22        hoverable && "transition-transform hover:-translate-y-1 hover:shadow-lg",
23        gradient && "bg-gradient-to-br from-blue-50 to-indigo-50 dark:from-blue-950 dark:to-indigo-950",
24        className
25      )}
26    >
27      {children}
28    </Card>
29  )
30}
31
32// Export remaining components
33export { CardContent, CardFooter, CardHeader }

Customizing Functionality

You can also extend the functionality of components:

1// components/quantum/quantum-button.tsx
2import { forwardRef } from "react"
3import { Button, ButtonProps } from "@/components/ui/button"
4import { Loader2 } from "lucide-react"
5
6interface QuantumButtonProps extends ButtonProps {
7  isLoading?: boolean
8  loadingText?: string
9  icon?: React.ReactNode
10  iconPosition?: "left" | "right"
11}
12
13export const QuantumButton = forwardRef<HTMLButtonElement, QuantumButtonProps>(
14  ({ 
15    children, 
16    isLoading, 
17    loadingText, 
18    icon, 
19    iconPosition = "left", 
20    ...props 
21  }, ref) => {
22    return (
23      <Button
24        ref={ref}
25        disabled={isLoading || props.disabled}
26        {...props}
27      >
28        {isLoading && (
29          <Loader2 className="mr-2 h-4 w-4 animate-spin" />
30        )}
31        {!isLoading && icon && iconPosition === "left" && (
32          <span className="mr-2">{icon}</span>
33        )}
34        {isLoading && loadingText ? loadingText : children}
35        {!isLoading && icon && iconPosition === "right" && (
36          <span className="ml-2">{icon}</span>
37        )}
38      </Button>
39    )
40  }
41)
42QuantumButton.displayName = "QuantumButton"

Creating Themes and Design Systems

With Shadcn/UI, you can easily create and manage your own design systems:

Defining Your Own Theme

  1. First, define CSS variables for different themes:
1/* app/globals.css */
2@tailwind base;
3@tailwind components;
4@tailwind utilities;
5 
6@layer base {
7  /* Default theme (light) */
8  :root {
9    --background: 0 0% 100%;
10    --foreground: 222.2 84% 4.9%;
11    
12    /* Kolory dla marki Quantum */
13    --quantum-blue: 210 100% 50%;
14    --quantum-purple: 270 100% 60%;
15    --quantum-teal: 180 100% 40%;
16    
17    /* Other variables */
18    /* ... */
19  }
20  
21  /* Ciemny motyw */
22  .dark {
23    --background: 222.2 84% 4.9%;
24    --foreground: 210 40% 98%;
25    
26    /* ... */
27  }
28  
29  /* Motyw neonowy */
30  .theme-neon {
31    --primary: 120 100% 50%;
32    --secondary: 300 100% 50%;
33    --accent: 60 100% 50%;
34    
35    /* ... */
36  }
37  
38  /* Motyw vintage */
39  .theme-vintage {
40    --primary: 30 80% 60%;
41    --secondary: 20 60% 40%;
42    --accent: 40 70% 50%;
43    
44    /* ... */
45  }
46}
  1. Next, create a context for theme switching:
1// components/theme-provider.tsx
2"use client"
3
4import { createContext, useContext, useEffect, useState } from "react"
5
6type Theme = "light" | "dark" | "neon" | "vintage" | "system"
7
8type ThemeProviderProps = {
9  children: React.ReactNode
10  defaultTheme?: Theme
11  storageKey?: string
12}
13
14type ThemeProviderState = {
15  theme: Theme
16  setTheme: (theme: Theme) => void
17}
18
19const initialState: ThemeProviderState = {
20  theme: "system",
21  setTheme: () => null,
22}
23
24const ThemeProviderContext = createContext<ThemeProviderState>(initialState)
25
26export function ThemeProvider({
27  children,
28  defaultTheme = "system",
29  storageKey = "quantum-ui-theme",
30  ...props
31}: ThemeProviderProps) {
32  const [theme, setTheme] = useState<Theme>(
33    () => (localStorage.getItem(storageKey) as Theme) || defaultTheme
34  )
35
36  useEffect(() => {
37    const root = window.document.documentElement
38    
39    // Remove previous theme classes
40    root.classList.remove("light", "dark", "theme-neon", "theme-vintage")
41
42    if (theme === "system") {
43      const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
44        .matches
45        ? "dark"
46        : "light"
47      root.classList.add(systemTheme)
48      return
49    }
50    
51    if (theme === "neon") {
52      root.classList.add("dark", "theme-neon")
53      return
54    }
55    
56    if (theme === "vintage") {
57      root.classList.add("light", "theme-vintage")
58      return
59    }
60
61    root.classList.add(theme)
62  }, [theme])
63
64  const value = {
65    theme,
66    setTheme: (theme: Theme) => {
67      localStorage.setItem(storageKey, theme)
68      setTheme(theme)
69    },
70  }
71
72  return (
73    <ThemeProviderContext.Provider {...props} value={value}>
74      {children}
75    </ThemeProviderContext.Provider>
76  )
77}
78
79export const useTheme = () => {
80  const context = useContext(ThemeProviderContext)
81
82  if (context === undefined)
83    throw new Error("useTheme must be used within a ThemeProvider")
84
85  return context
86}
  1. Use the theme provider in your layout:
1// app/layout.tsx
2import { ThemeProvider } from "@/components/theme-provider"
3import "./globals.css"
4
5export default function RootLayout({
6  children,
7}: {
8  children: React.ReactNode
9}) {
10  return (
11    <html lang="pl" suppressHydrationWarning>
12      <body>
13        <ThemeProvider defaultTheme="system" storageKey="quantum-theme">
14          {children}
15        </ThemeProvider>
16      </body>
17    </html>
18  )
19}

Theme Switcher Component

1// components/theme-switcher.tsx
2"use client"
3
4import { Moon, Sun, Sparkles, Clock } from "lucide-react"
5import { useTheme } from "@/components/theme-provider"
6import { Button } from "@/components/ui/button"
7import {
8  DropdownMenu,
9  DropdownMenuContent,
10  DropdownMenuItem,
11  DropdownMenuTrigger,
12} from "@/components/ui/dropdown-menu"
13
14export function ThemeSwitcher() {
15  const { theme, setTheme } = useTheme()
16
17  return (
18    <DropdownMenu>
19      <DropdownMenuTrigger asChild>
20        <Button variant="outline" size="icon">
21          <Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
22          <Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
23          <span className="sr-only">Toggle theme</span>
24        </Button>
25      </DropdownMenuTrigger>
26      <DropdownMenuContent align="end">
27        <DropdownMenuItem onClick={() => setTheme("light")}>
28          <Sun className="mr-2 h-4 w-4" />
29          <span>Jasny</span>
30        </DropdownMenuItem>
31        <DropdownMenuItem onClick={() => setTheme("dark")}>
32          <Moon className="mr-2 h-4 w-4" />
33          <span>Ciemny</span>
34        </DropdownMenuItem>
35        <DropdownMenuItem onClick={() => setTheme("neon")}>
36          <Sparkles className="mr-2 h-4 w-4" />
37          <span>Neon</span>
38        </DropdownMenuItem>
39        <DropdownMenuItem onClick={() => setTheme("vintage")}>
40          <Clock className="mr-2 h-4 w-4" />
41          <span>Vintage</span>
42        </DropdownMenuItem>
43        <DropdownMenuItem onClick={() => setTheme("system")}>
44          <span>Systemowy</span>
45        </DropdownMenuItem>
46      </DropdownMenuContent>
47    </DropdownMenu>
48  )
49}

Best Practices pracy z Shadcn/UI

1. Component Organization

Maintain a clear directory structure:

1components/
2├── ui/              # Podstawowe komponenty Shadcn/UI
3│   ├── button.tsx
4│   ├── card.tsx
5│   └──
6├── quantum/         # Your own business components
7│   ├── quantum-button.tsx
8│   ├── quantum-card.tsx
9│   └──
10└── blocks/          # Complex interface blocks
11    ├── dashboard-stats.tsx
12    ├── user-profile-form.tsx
13    └──

2. Consistent Naming

Maintain consistent naming conventions for your own components:

  • Button
    - podstawowy komponent Shadcn/UI
  • QuantumButton
    - custom extension of a base component
  • UserProfileForm
    - complex business component

3. Using Component Generators

Use the Shadcn/UI CLI to generate new components:

1npx shadcn-ui@latest add alert

4. Optimizing Reusability

For frequently used combinations, create your own higher-level components:

1// components/quantum/quantum-section.tsx
2import { cn } from "@/lib/utils"
3
4interface QuantumSectionProps {
5  title: string
6  description?: string
7  children: React.ReactNode
8  className?: string
9}
10
11export function QuantumSection({
12  title,
13  description,
14  children,
15  className,
16}: QuantumSectionProps) {
17  return (
18    <section className={cn("space-y-4", className)}>
19      <div className="space-y-1">
20        <h2 className="text-2xl font-bold">{title}</h2>
21        {description && (
22          <p className="text-muted-foreground">{description}</p>
23        )}
24      </div>
25      <div>{children}</div>
26    </section>
27  )
28}

5. Theme Management

Use CSS variables to manage themes:

1// lib/themes.ts
2export const themes = {
3  default: {
4    primary: "222.2 47.4% 11.2%",
5    // ...inne kolory
6  },
7  quantum: {
8    primary: "240 100% 50%",
9    // ...inne kolory
10  },
11  // ...inne motywy
12}
13
14export function setTheme(theme: keyof typeof themes) {
15  const root = document.documentElement
16  Object.entries(themes[theme]).forEach(([key, value]) => {
17    root.style.setProperty(`--${key}`, value)
18  })
19}

Integration with Other Tools

1. Integracja z bibliotekami formularzy

Shadcn/UI integrates well with libraries such as React Hook Form and Zod:

1npx shadcn-ui@latest add form
1// components/signup-form.tsx
2"use client"
3
4import { zodResolver } from "@hookform/resolvers/zod"
5import { useForm } from "react-hook-form"
6import * as z from "zod"
7
8import { Button } from "@/components/ui/button"
9import {
10  Form,
11  FormControl,
12  FormDescription,
13  FormField,
14  FormItem,
15  FormLabel,
16  FormMessage,
17} from "@/components/ui/form"
18import { Input } from "@/components/ui/input"
19
20const formSchema = z.object({
21  username: z.string().min(2).max(50),
22  email: z.string().email(),
23  password: z.string().min(8),
24})
25
26export function SignupForm() {
27  const form = useForm<z.infer<typeof formSchema>>({
28    resolver: zodResolver(formSchema),
29    defaultValues: {
30      username: "",
31      email: "",
32      password: "",
33    },
34  })
35
36  function onSubmit(values: z.infer<typeof formSchema>) {
37    console.log(values)
38  }
39
40  return (
41    <Form {...form}>
42      <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
43        <FormField
44          control={form.control}
45          name="username"
46          render={({ field }) => (
47            <FormItem>
48              <FormLabel>Username</FormLabel>
49              <FormControl>
50                <Input placeholder="Enter your username" {...field} />
51              </FormControl>
52              <FormDescription>
53                The name that will be displayed on your profile.
54              </FormDescription>
55              <FormMessage />
56            </FormItem>
57          )}
58        />
59        {/* Other form fields */}
60        <Button type="submit">Sign Up</Button>
61      </form>
62    </Form>
63  )
64}

2. Integracja z bibliotekami animacji

Shadcn/UI can be easily integrated with animation libraries:

1// components/animated-card.tsx
2"use client"
3
4import { motion } from "framer-motion"
5import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
6
7interface AnimatedCardProps {
8  title: string
9  children: React.ReactNode
10}
11
12export function AnimatedCard({ title, children }: AnimatedCardProps) {
13  return (
14    <motion.div
15      initial={{ opacity: 0, y: 20 }}
16      animate={{ opacity: 1, y: 0 }}
17      transition={{ duration: 0.5 }}
18    >
19      <Card>
20        <CardHeader>
21          <CardTitle>{title}</CardTitle>
22        </CardHeader>
23        <CardContent>{children}</CardContent>
24      </Card>
25    </motion.div>
26  )
27}

3. Integration with State Management

Shadcn/UI works well with state management libraries:

1// store/theme-store.ts
2import { create } from 'zustand'
3
4type Theme = 'light' | 'dark' | 'system'
5
6interface ThemeState {
7  theme: Theme
8  setTheme: (theme: Theme) => void
9}
10
11export const useThemeStore = create<ThemeState>((set) => ({
12  theme: 'system',
13  setTheme: (theme) => set({ theme }),
14}))
15
16// components/theme-toggle.tsx
17"use client"
18
19import { Moon, Sun } from "lucide-react"
20import { useThemeStore } from "@/store/theme-store"
21import { Button } from "@/components/ui/button"
22import { useEffect } from "react"
23
24export function ThemeToggle() {
25  const { theme, setTheme } = useThemeStore()
26  
27  useEffect(() => {
28    const root = document.documentElement
29    
30    if (theme === "system") {
31      const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
32        .matches ? "dark" : "light"
33      root.classList.remove("light", "dark")
34      root.classList.add(systemTheme)
35    } else {
36      root.classList.remove("light", "dark")
37      root.classList.add(theme)
38    }
39  }, [theme])
40  
41  return (
42    <Button
43      variant="outline"
44      size="icon"
45      onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
46    >
47      <Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
48      <Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
49      <span className="sr-only">Toggle theme</span>
50    </Button>
51  )
52}

Summary

Shadcn/UI is a powerful tool for creating beautiful, accessible, and consistent user interfaces in Next.js 15. Instead of being a black box that is hard to customize, Shadcn/UI provides high-quality, modifiable components that become part of your code.

The most important advantages of Shadcn/UI:

  1. Control - components are part of your code, so you have full control over them
  2. Modularity - you download only the components you need
  3. Styling - easy customization of appearance with Tailwind CSS
  4. Accessibility - components built on Radix UI that ensure a high level of accessibility
  5. Built-in dark mode - support for light and dark themes by default
  6. TypeScript - full type support

Unlike traditional UI libraries that may limit your creativity and make it difficult to customize to unique project needs, Shadcn/UI provides solid foundations on which you can build your own unique user experiences - just like the modular building system in Metropolis Quantum allows architects to create unique buildings while maintaining the consistent aesthetic of the city.

In the next chapter, we will look at the Image component and image optimization in Next.js 15, which will allow us to further improve the performance and user experience of our applications.

Go to CodeWorlds