Tutorial Membuat Portfolio Developer dengan Next.js dan Shadcn UI
Senin, 22 Des 2025
Memiliki portfolio yang profesional adalah keharusan bagi setiap developer. Dalam tutorial ini, saya akan memandu kamu membuat portfolio modern menggunakan Next.js dan Shadcn UI—library komponen yang customizable, accessible, dan tanpa runtime overhead.
Kenapa Shadcn UI?
Shadcn UI berbeda dari component library lainnya:
- Customizable - Kamu memiliki kode komponennya, bukan dependency
- Accessible - Dibangun di atas Radix UI primitives
- No Runtime - Tidak ada bundle size tambahan dari library
- Beautiful by Default - Design system yang sudah teruji
Apa yang Akan Kita Bangun?
Portfolio dengan fitur:
- Hero section dengan animasi
- Projects/Portfolio showcase
- About section
- Contact form
- Dark mode toggle
- Responsive design
Prasyarat
- Node.js versi 18 atau lebih baru
- Text editor (VS Code recommended)
- Pengetahuan dasar React dan TypeScript
Step 1: Setup Project Next.js
Buka terminal dan jalankan command berikut:
npx create-next-app@latest my-portfolio
cd my-portfolio
Saat ditanya, pilih opsi berikut:
- TypeScript: Yes
- ESLint: Yes
- Tailwind CSS: Yes
- src/ directory: Yes
- App Router: Yes
- Import alias: @/*
Step 2: Install dan Konfigurasi Shadcn UI
Jalankan command untuk inisialisasi Shadcn UI:
npx shadcn@latest init
Pilih konfigurasi berikut:
- Style: Default
- Base color: Slate
- CSS variables: Yes
Sekarang install komponen yang kita butuhkan:
npx shadcn@latest add button card input textarea badge navigation-menu
Step 3: Struktur Folder Portfolio
Buat struktur folder berikut untuk mengorganisir project:
src/
├── app/
│ ├── layout.tsx
│ ├── page.tsx
│ └── globals.css
├── components/
│ ├── layout/
│ │ ├── header.tsx
│ │ └── footer.tsx
│ ├── sections/
│ │ ├── hero.tsx
│ │ ├── projects.tsx
│ │ ├── about.tsx
│ │ └── contact.tsx
│ ├── ui/
│ │ └── (shadcn components)
│ └── theme-provider.tsx
├── lib/
│ └── utils.ts
└── data/
└── projects.ts
Buat folder yang diperlukan:
mkdir -p src/components/layout src/components/sections src/data
Step 4: Setup Theme Provider untuk Dark Mode
Install next-themes untuk dark mode:
npm install next-themes
Buat file src/components/theme-provider.tsx:
"use client"
import * as React from "react"
import { ThemeProvider as NextThemesProvider } from "next-themes"
import { type ThemeProviderProps } from "next-themes"
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
}
Update src/app/layout.tsx:
import type { Metadata } from "next"
import { Inter } from "next/font/google"
import "./globals.css"
import { ThemeProvider } from "@/components/theme-provider"
import { Header } from "@/components/layout/header"
import { Footer } from "@/components/layout/footer"
const inter = Inter({ subsets: ["latin"] })
export const metadata: Metadata = {
title: "John Doe - Full Stack Developer",
description: "Portfolio of John Doe, a passionate full stack developer",
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="id" suppressHydrationWarning>
<body className={inter.className}>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
<Header />
<main>{children}</main>
<Footer />
</ThemeProvider>
</body>
</html>
)
}
Step 5: Membuat Header dengan Theme Toggle
Install komponen dropdown menu dan icon:
npx shadcn@latest add dropdown-menu
npm install lucide-react
Buat file src/components/layout/header.tsx:
"use client"
import Link from "next/link"
import { Button } from "@/components/ui/button"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { Moon, Sun, Menu } from "lucide-react"
import { useTheme } from "next-themes"
export function Header() {
const { setTheme } = useTheme()
return (
<header className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<div className="container flex h-16 items-center justify-between">
<Link href="/" className="font-bold text-xl">
JohnDoe
</Link>
<nav className="hidden md:flex items-center gap-6">
<Link href="#projects" className="text-sm font-medium hover:text-primary">
Projects
</Link>
<Link href="#about" className="text-sm font-medium hover:text-primary">
About
</Link>
<Link href="#contact" className="text-sm font-medium hover:text-primary">
Contact
</Link>
</nav>
<div className="flex items-center gap-2">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
<Sun className="h-5 w-5 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-5 w-5 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">Toggle theme</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setTheme("light")}>
Light
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("dark")}>
Dark
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("system")}>
System
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
</header>
)
}
Step 6: Membuat Hero Section

Buat file src/components/sections/hero.tsx:
import { Button } from "@/components/ui/button"
import { ArrowRight, Github, Linkedin, Twitter } from "lucide-react"
import Link from "next/link"
export function Hero() {
return (
<section className="container py-24 md:py-32">
<div className="flex flex-col items-center text-center gap-8">
<div className="space-y-4">
<h1 className="text-4xl md:text-6xl font-bold tracking-tight">
Hi, I'm <span className="text-primary">John Doe</span>
</h1>
<p className="text-xl md:text-2xl text-muted-foreground max-w-2xl">
Full Stack Developer yang passionate dalam membangun
aplikasi web modern dan user-friendly.
</p>
</div>
<div className="flex gap-4">
<Button asChild size="lg">
<Link href="#projects">
Lihat Projects <ArrowRight className="ml-2 h-4 w-4" />
</Link>
</Button>
<Button variant="outline" size="lg" asChild>
<Link href="#contact">Hubungi Saya</Link>
</Button>
</div>
<div className="flex gap-4 mt-4">
<Button variant="ghost" size="icon" asChild>
<Link href="https://github.com" target="_blank">
<Github className="h-5 w-5" />
</Link>
</Button>
<Button variant="ghost" size="icon" asChild>
<Link href="https://linkedin.com" target="_blank">
<Linkedin className="h-5 w-5" />
</Link>
</Button>
<Button variant="ghost" size="icon" asChild>
<Link href="https://twitter.com" target="_blank">
<Twitter className="h-5 w-5" />
</Link>
</Button>
</div>
</div>
</section>
)
}
Step 7: Membuat Projects Section

Pertama, buat data projects di src/data/projects.ts:
export const projects = [
{
id: 1,
title: "E-Commerce Platform",
description: "Full-stack e-commerce dengan Next.js, Prisma, dan Stripe payment integration.",
image: "/images/projects/ecommerce.jpg",
tags: ["Next.js", "Prisma", "Stripe", "PostgreSQL"],
github: "https://github.com",
demo: "https://demo.com",
},
{
id: 2,
title: "Task Management App",
description: "Aplikasi manajemen tugas real-time dengan drag and drop functionality.",
image: "/images/projects/taskapp.jpg",
tags: ["React", "Node.js", "Socket.io", "MongoDB"],
github: "https://github.com",
demo: "https://demo.com",
},
{
id: 3,
title: "AI Chat Application",
description: "Chat application dengan AI assistant menggunakan OpenAI API.",
image: "/images/projects/aichat.jpg",
tags: ["Next.js", "OpenAI", "Vercel AI SDK", "Tailwind"],
github: "https://github.com",
demo: "https://demo.com",
},
]
Buat file src/components/sections/projects.tsx:
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Github, ExternalLink } from "lucide-react"
import { projects } from "@/data/projects"
import Image from "next/image"
import Link from "next/link"
export function Projects() {
return (
<section id="projects" className="container py-24">
<div className="space-y-4 text-center mb-12">
<h2 className="text-3xl md:text-4xl font-bold">Projects</h2>
<p className="text-muted-foreground max-w-2xl mx-auto">
Beberapa project yang telah saya kerjakan
</p>
</div>
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
{projects.map((project) => (
<Card key={project.id} className="overflow-hidden group">
<div className="relative h-48 overflow-hidden">
<Image
src={project.image}
alt={project.title}
fill
className="object-cover transition-transform group-hover:scale-105"
/>
</div>
<CardHeader>
<CardTitle>{project.title}</CardTitle>
<CardDescription>{project.description}</CardDescription>
</CardHeader>
<CardContent>
<div className="flex flex-wrap gap-2">
{project.tags.map((tag) => (
<Badge key={tag} variant="secondary">
{tag}
</Badge>
))}
</div>
</CardContent>
<CardFooter className="gap-2">
<Button variant="outline" size="sm" asChild>
<Link href={project.github} target="_blank">
<Github className="h-4 w-4 mr-2" /> Code
</Link>
</Button>
<Button size="sm" asChild>
<Link href={project.demo} target="_blank">
<ExternalLink className="h-4 w-4 mr-2" /> Demo
</Link>
</Button>
</CardFooter>
</Card>
))}
</div>
</section>
)
}
Step 8: Membuat About Section
Buat file src/components/sections/about.tsx:
import { Badge } from "@/components/ui/badge"
import Image from "next/image"
const skills = [
"JavaScript", "TypeScript", "React", "Next.js",
"Node.js", "PostgreSQL", "MongoDB", "Prisma",
"Tailwind CSS", "Git", "Docker", "AWS"
]
export function About() {
return (
<section id="about" className="bg-muted/50 py-24">
<div className="container">
<div className="grid md:grid-cols-2 gap-12 items-center">
<div className="relative aspect-square max-w-md mx-auto">
<Image
src="/images/profile.jpg"
alt="John Doe"
fill
className="object-cover rounded-2xl"
/>
</div>
<div className="space-y-6">
<h2 className="text-3xl md:text-4xl font-bold">About Me</h2>
<div className="space-y-4 text-muted-foreground">
<p>
Halo! Saya John, seorang Full Stack Developer dengan 5+ tahun
pengalaman membangun aplikasi web. Saya passionate dalam
menciptakan solusi digital yang tidak hanya fungsional
tetapi juga memberikan pengalaman user yang excellent.
</p>
<p>
Saat ini saya fokus pada teknologi modern seperti Next.js,
TypeScript, dan cloud services. Di waktu luang, saya senang
berkontribusi ke open source dan menulis artikel teknis.
</p>
</div>
<div className="space-y-4">
<h3 className="text-xl font-semibold">Tech Stack</h3>
<div className="flex flex-wrap gap-2">
{skills.map((skill) => (
<Badge key={skill} variant="outline">
{skill}
</Badge>
))}
</div>
</div>
</div>
</div>
</div>
</section>
)
}
Step 9: Membuat Contact Form
Buat file src/components/sections/contact.tsx:
"use client"
import { useState } from "react"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Textarea } from "@/components/ui/textarea"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Mail, Send } from "lucide-react"
export function Contact() {
const [isLoading, setIsLoading] = useState(false)
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault()
setIsLoading(true)
// Implementasi pengiriman form
// Bisa menggunakan API route atau service seperti Formspree
await new Promise((resolve) => setTimeout(resolve, 1000))
setIsLoading(false)
alert("Pesan terkirim! Saya akan segera menghubungi Anda.")
}
return (
<section id="contact" className="container py-24">
<div className="max-w-xl mx-auto">
<Card>
<CardHeader className="text-center">
<div className="mx-auto w-12 h-12 bg-primary/10 rounded-full flex items-center justify-center mb-4">
<Mail className="h-6 w-6 text-primary" />
</div>
<CardTitle className="text-2xl">Hubungi Saya</CardTitle>
<CardDescription>
Ada project menarik? Mari diskusi dan bekerja sama!
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<label htmlFor="name" className="text-sm font-medium">
Nama
</label>
<Input
id="name"
placeholder="John Doe"
required
/>
</div>
<div className="space-y-2">
<label htmlFor="email" className="text-sm font-medium">
Email
</label>
<Input
id="email"
type="email"
placeholder="[email protected]"
required
/>
</div>
</div>
<div className="space-y-2">
<label htmlFor="subject" className="text-sm font-medium">
Subject
</label>
<Input
id="subject"
placeholder="Project Collaboration"
required
/>
</div>
<div className="space-y-2">
<label htmlFor="message" className="text-sm font-medium">
Pesan
</label>
<Textarea
id="message"
placeholder="Ceritakan tentang project Anda..."
rows={5}
required
/>
</div>
<Button type="submit" className="w-full" disabled={isLoading}>
{isLoading ? (
"Mengirim..."
) : (
<>
Kirim Pesan <Send className="ml-2 h-4 w-4" />
</>
)}
</Button>
</form>
</CardContent>
</Card>
</div>
</section>
)
}
Step 10: Membuat Footer
Buat file src/components/layout/footer.tsx:
import Link from "next/link"
import { Github, Linkedin, Twitter } from "lucide-react"
export function Footer() {
return (
<footer className="border-t py-8">
<div className="container flex flex-col md:flex-row justify-between items-center gap-4">
<p className="text-sm text-muted-foreground">
© {new Date().getFullYear()} John Doe. All rights reserved.
</p>
<div className="flex gap-4">
<Link
href="https://github.com"
target="_blank"
className="text-muted-foreground hover:text-foreground"
>
<Github className="h-5 w-5" />
</Link>
<Link
href="https://linkedin.com"
target="_blank"
className="text-muted-foreground hover:text-foreground"
>
<Linkedin className="h-5 w-5" />
</Link>
<Link
href="https://twitter.com"
target="_blank"
className="text-muted-foreground hover:text-foreground"
>
<Twitter className="h-5 w-5" />
</Link>
</div>
</div>
</footer>
)
}
Step 11: Gabungkan Semua di Homepage
Update src/app/page.tsx:
import { Hero } from "@/components/sections/hero"
import { Projects } from "@/components/sections/projects"
import { About } from "@/components/sections/about"
import { Contact } from "@/components/sections/contact"
export default function Home() {
return (
<>
<Hero />
<Projects />
<About />
<Contact />
</>
)
}
Step 12: Jalankan Project
Sekarang jalankan development server:
npm run dev
Buka http://localhost:3000 untuk melihat portfolio kamu!
Step 13: Deploy ke Vercel
Deploy portfolio ke Vercel sangat mudah:
npm install -g vercel
vercel
Atau cukup push ke GitHub dan connect repository di vercel.com. Vercel akan otomatis detect Next.js dan melakukan deployment.
Kesimpulan
Selamat! Kamu sudah berhasil membuat portfolio developer profesional dengan Next.js dan Shadcn UI. Beberapa keuntungan dari stack ini:
- Next.js 14 - App Router, Server Components, dan performa optimal
- Shadcn UI - Komponen yang accessible dan fully customizable
- TypeScript - Type safety untuk mengurangi bug
- Dark Mode - UX modern dengan next-themes
- Vercel - Deployment yang seamless dan gratis
Dari sini, kamu bisa mengembangkan lebih lanjut dengan menambahkan:
- Blog section dengan MDX
- Animasi dengan Framer Motion
- CMS untuk mengelola projects
- Analytics untuk tracking visitors
Ada pertanyaan atau ingin diskusi? Reach out ke saya di Twitter @nayakayp!