Programming

Panduan Shadcn/ui di Next.js: Lengkap dan Praktis

M
MUGHU
39 menit baca
Panduan Shadcn/ui di Next.js: Lengkap dan Praktis
Daftar isi

shadcn/ui cocok buat Teman-Teman yang ingin membangun antarmuka React yang rapi, mudah disesuaikan, dan tetap sepenuhnya berada di dalam repositori proyek. Alih-alih memasang pustaka komponen tertutup lalu sibuk menimpa gayanya, shadcn/ui memberikan kode komponen yang bisa dibaca, diubah, dan dikembangkan sesuai kebutuhan produk.

shadcn/ui adalah kumpulan komponen antarmuka yang mudah diakses sekaligus sistem distribusi kode. Komponen ditambahkan langsung ke proyek, jadi tim memiliki kendali penuh atas struktur, gaya, perilaku, dan proses pengembangannya.

Ringkasan singkat: Tutorial ini membahas cara memasang shadcn/ui di Next.js, menambahkan komponen, membangun halaman dasbor, membuat formulir, mengatur tema gelap, menangani kesalahan umum, serta memilih antara shadcn/ui, Material UI, Chakra UI, dan pustaka komponen lain. Contohnya dirancang agar relevan untuk proyek SaaS, dasbor operasional, situs bisnis lokal, dan aplikasi internal di Indonesia.

Masalah yang Diselesaikan shadcn/ui

Membangun antarmuka modern sering terlihat gampang di awal. Kita cukup membuat tombol, kartu, formulir, dialog, dan tabel. Masalahnya baru terasa setelah produk bertambah besar dan setiap halaman mulai memakai variasi desain yang berbeda.

Beberapa masalah yang paling sering muncul antara lain:

  • Tombol di halaman pemasaran tidak konsisten dengan tombol di dasbor.

  • Modal terlihat bagus, tetapi navigasi papan ketiknya bermasalah.

  • Formulir memiliki pesan kesalahan yang berbeda-beda.

  • Tim menumpuk banyak kelas CSS untuk menimpa gaya pustaka.

  • Komponen dari beberapa pustaka memiliki API yang tidak seragam.

  • Perubahan warna merek harus dilakukan di banyak berkas.

  • Hasil buatan alat AI tidak sesuai dengan sistem desain proyek.

  • Pembaruan pustaka memunculkan perubahan tampilan yang tidak diinginkan.

Masalah tersebut bukan cuma soal estetika. Antarmuka yang tidak konsisten dapat memperlambat pengembangan, meningkatkan biaya pengujian, dan membuat pengalaman pengguna terasa kurang matang.

shadcn/ui menawarkan pendekatan berbeda. Teman-Teman mengambil komponen yang diperlukan, memasukkannya ke dalam proyek, lalu memperlakukannya sebagai kode milik tim sendiri.

Pendekatan ini penting karena:

  1. Kode mudah diperiksa. Tidak ada lapisan misterius yang sulit ditelusuri.

  2. Komponen mudah diubah. Tim dapat menyesuaikan markup, kelas, dan perilaku.

  3. Desain lebih konsisten. Komponen memakai token dan pola yang sama.

  4. Lebih ramah untuk AI. Kode lokal bisa dibaca dan disunting oleh alat bantu pengembangan.

  5. Tidak terikat tampilan bawaan. Produk tidak harus terlihat seperti semua aplikasi lain yang memakai pustaka serupa.

Dokumentasi resmi menyebut shadcn/ui sebagai fondasi untuk membangun sistem desain, bukan sekadar katalog komponen siap pakai. Teman-Teman dapat melihat daftar komponen dan konsep resminya di dokumentasi shadcn/ui.

Cara Kerja shadcn/ui

Pustaka komponen tradisional biasanya dipasang sebagai paket. Kode implementasinya tetap berada di node_modules, sedangkan aplikasi hanya mengimpor komponen tersebut.

Contohnya kira-kira seperti ini:

TSX
import { Button } from "some-ui-library"

Teman-Teman dapat memakai komponennya, tetapi perubahan mendalam sering memerlukan properti khusus, penimpaan CSS, atau pembungkus tambahan.

Pada shadcn/ui, CLI menambahkan berkas komponen ke proyek. Setelah memasang tombol, misalnya, proyek akan memiliki berkas seperti:

TEXT
components/
└── ui/
    └── button.tsx

Aplikasi tetap mengimpornya dengan cara yang familiar:

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

Perbedaannya terletak pada kepemilikan kode. Berkas button.tsx berada di repositori Teman-Teman dan bisa disunting langsung.

Anggap shadcn/ui seperti paket rancangan rumah yang sudah memperhitungkan struktur, aksesibilitas, dan tampilan awal. Teman-Teman menerima rancangannya, lalu bebas mengganti warna, bahan, tata ruang, atau menambah ruangan tanpa meminta izin kepada penyedia paket.

Pendekatan ini disebut Open Code. Menurut dokumentasi resminya, shadcn/ui dibangun di atas prinsip kode terbuka, komposisi, distribusi, tampilan awal yang baik, dan kesiapan untuk alur kerja berbantuan AI.

Fitur Utama shadcn/ui

Kode Komponen Menjadi Milik Proyek

Setiap komponen yang ditambahkan berada di dalam proyek. Ini memudahkan peninjauan kode, pelacakan perubahan melalui Git, dan penyesuaian desain.

Tim juga dapat membuat aturan internal. Contohnya, semua tombol utama wajib memiliki tinggi tertentu atau semua dialog wajib mencatat peristiwa analitik.

Komponen yang Bisa Disusun

Komponen shadcn/ui dirancang agar dapat digabungkan. Dialog, dropdown, formulir, tombol, dan kartu memakai pola yang relatif konsisten.

Komposisi membantu tim menghindari komponen raksasa dengan puluhan properti. Struktur halaman tetap jelas karena bagian-bagiannya terlihat langsung di JSX.

Aksesibilitas sebagai Fondasi

Banyak komponen shadcn/ui dibangun di atas primitive antarmuka yang menangani detail seperti:

  • Navigasi papan ketik.

  • Fokus saat dialog dibuka.

  • Atribut ARIA.

  • Hubungan label dengan kolom input.

  • Perilaku menu dan popover.

  • Penutupan dialog dengan tombol Escape.

Meski begitu, aksesibilitas bukan tombol otomatis yang langsung menyelesaikan semuanya. Tim tetap perlu menguji teks alternatif, urutan fokus, kontras warna, ukuran target sentuh, dan kualitas label.

Integrasi dengan Tailwind CSS

Tailwind Css Best Practices and Techniques - Webkul Blog

shadcn/ui memakai kelas utilitas untuk mengatur tampilan. Pendekatan ini membuat gaya komponen terlihat dekat dengan markup dan mudah disesuaikan.

Teman-Teman tidak wajib menghafal semua utilitas sejak awal. Cukup pahami pola dasarnya:

TSX
<div className="rounded-xl border bg-card p-6 text-card-foreground shadow-sm">
  Konten kartu
</div>

Kelas tersebut mengatur sudut, garis tepi, warna latar, jarak dalam, warna teks, dan bayangan.

CLI dan Registry

shadcn/ui bukan hanya sekumpulan berkas. Ia juga menyediakan CLI dan format registry untuk mendistribusikan komponen.

CLI membantu Teman-Teman:

  • Menginisialisasi konfigurasi.

  • Menambahkan komponen tertentu.

  • Memasang dependensi yang diperlukan.

  • Mengambil komponen dari registry.

  • Menjaga struktur impor tetap konsisten.

Ekosistem registry juga memungkinkan komunitas menyediakan blok, pola, dan komponen tambahan. Tetap periksa kualitas kode serta lisensinya sebelum memasukkan registry pihak ketiga ke produk.

Perbandingan shadcn/ui dengan Pustaka Komponen Lain

Tidak ada satu pilihan yang selalu paling unggul. Pilihan yang tepat bergantung pada kapasitas tim, target peluncuran, kebutuhan merek, dan seberapa jauh antarmuka akan disesuaikan.

Kriteria

shadcn/ui

Material UI

Chakra UI

Ant Design

Headless UI

Model distribusi

Kode disalin ke proyek

Paket NPM

Paket NPM

Paket NPM

Paket primitive

Kendali atas kode

Sangat tinggi

Sedang

Sedang

Sedang

Tinggi

Tampilan awal

Minimal dan modern

Material Design

Bersih dan fleksibel

Cocok untuk aplikasi bisnis

Hampir tanpa gaya

Penyesuaian mendalam

Mudah, tetapi perlu disiplin

Melalui tema dan penimpaan

Melalui properti dan tema

Melalui token dan konfigurasi

Harus membangun gaya sendiri

Kecepatan memulai

Cepat

Sangat cepat

Sangat cepat

Sangat cepat

Sedang

Risiko ketergantungan desain

Rendah

Sedang

Sedang

Sedang

Rendah

Beban pemeliharaan lokal

Lebih tinggi

Lebih rendah

Lebih rendah

Lebih rendah

Tinggi

Cocok untuk sistem desain khusus

Sangat cocok

Cocok

Cocok

Cukup cocok

Sangat cocok

Cocok untuk prototipe singkat

Cocok

Sangat cocok

Sangat cocok

Sangat cocok

Cukup cocok

Pilih shadcn/ui Jika

  • Produk membutuhkan identitas visual khusus.

  • Tim ingin memiliki kode komponen.

  • Proyek memakai React atau Next.js dan Tailwind CSS.

  • Komponen akan banyak disesuaikan.

  • Tim ingin membangun sistem desain secara bertahap.

  • Alur kerja pengembangan banyak dibantu AI.

  • Transparansi implementasi lebih penting daripada pembaruan otomatis.

Pilih Material UI Jika

  • Produk mengikuti Material Design.

  • Tim membutuhkan katalog komponen yang luas sejak hari pertama.

  • Penyesuaian tingkat rendah bukan prioritas.

  • Kecepatan membangun aplikasi administrasi lebih penting daripada identitas visual unik.

Pilih Chakra UI Jika

  • Tim menyukai API berbasis properti.

  • Proyek perlu produktif dengan konfigurasi yang relatif ringan.

  • Tim tidak ingin menyimpan implementasi setiap komponen secara lokal.

Pilih Ant Design Jika

  • Produk berupa aplikasi perusahaan dengan tabel dan formulir yang kompleks.

  • Tim membutuhkan banyak komponen bisnis siap pakai.

  • Tampilan khas Ant Design masih sesuai dengan kebutuhan produk.

Pilih Headless UI Jika

  • Tim desain dan pengembang sudah matang.

  • Semua gaya ingin dibangun sendiri.

  • Proyek membutuhkan primitive perilaku tanpa desain visual yang kuat.

Rekomendasi praktis: shadcn/ui paling menarik ketika diferensiasi desain, kepemilikan kode, dan fleksibilitas jangka panjang lebih penting daripada kenyamanan pembaruan paket terpusat.

Latar Belakang Studi Kasus: Dasbor Pesanan untuk Bisnis Lokal

Agar tutorial tidak berhenti pada tombol dan kartu kosong, kita akan memakai skenario yang realistis. Bayangkan sebuah tim kecil sedang membuat dasbor pesanan untuk usaha kuliner di Jakarta.

Usaha tersebut menerima pesanan dari beberapa wilayah, misalnya:

  • Kebayoran Baru.

  • Tebet.

  • Kemang.

  • Cilandak.

  • Menteng.

  • Kelapa Gading.

Tim operasional membutuhkan halaman untuk melihat jumlah pesanan, omzet, status pengiriman, dan daftar transaksi terbaru. Tampilan harus tetap nyaman dibuka dari laptop kasir maupun tablet.

Tantangan yang Dihadapi

Versi awal dibuat dengan elemen HTML dan kelas yang ditulis langsung di setiap halaman. Dalam beberapa minggu, muncul pola yang tidak konsisten:

  • Tombol “Simpan” memiliki tiga variasi warna.

  • Kolom input memakai ukuran yang berbeda.

  • Kartu ringkasan tidak memiliki jarak yang seragam.

  • Dialog konfirmasi sulit digunakan dengan papan ketik.

  • Tema gelap tidak berjalan di beberapa komponen.

  • Pengembang menghabiskan waktu mengulang kode yang sama.

Masalah terbesarnya bukan kurangnya komponen. Masalahnya adalah tidak adanya fondasi yang konsisten.

Pendekatan yang Dipilih

Tim memutuskan memakai shadcn/ui dengan strategi berikut:

  1. Menentukan token warna dan radius terlebih dahulu.

  2. Menambahkan komponen hanya saat dibutuhkan.

  3. Membuat komponen domain di luar folder components/ui.

  4. Menyimpan primitive shadcn/ui tetap kecil.

  5. Menguji alur papan ketik dan tampilan seluler.

  6. Mendokumentasikan variasi komponen yang disetujui.

Struktur ini menghindari dua ekstrem: menulis semuanya dari nol atau memasang terlalu banyak komponen tanpa rencana.

Getting Started: Prasyarat Sebelum Memasang shadcn/ui

Tutorial ini memakai Next.js, TypeScript, dan Tailwind CSS. Teman-Teman tidak perlu menguasai seluruh ekosistemnya, tetapi sebaiknya sudah mengenal komponen React dan perintah terminal.

Perangkat yang Dibutuhkan

Pastikan perangkat memiliki:

  • Node.js versi LTS yang masih didukung.

  • Pengelola paket seperti npm, pnpm, yarn, atau bun.

  • Penyunting kode.

  • Terminal.

  • Git untuk pencatatan perubahan.

  • Peramban modern.

Periksa Node.js dan npm:

BASH
node --version
npm --version

Contoh keluaran:

TEXT
v22.14.0
10.9.2

Nomor versi Teman-Teman mungkin berbeda. Yang penting, perintah dapat dijalankan tanpa pesan bahwa node atau npm tidak ditemukan.

Jika Node.js belum terpasang, unduh versi LTS dari situs resmi Node.js.

Pengetahuan yang Membantu

Beberapa konsep yang akan sering muncul:

  • Komponen React: fungsi yang mengembalikan antarmuka.

  • Props: data yang dikirim ke komponen.

  • State: data yang dapat berubah selama interaksi.

  • TypeScript: JavaScript dengan pemeriksaan tipe.

  • Tailwind CSS: kumpulan kelas utilitas untuk mengatur tampilan.

  • Server Component: komponen Next.js yang secara bawaan dijalankan di server.

  • Client Component: komponen yang dapat memakai state, efek, dan peristiwa peramban.

Tidak perlu menghafal definisinya. Teman-Teman akan melihat penerapannya di setiap langkah.

Step 1 — Membuat Proyek Next.js

Buka terminal, lalu jalankan:

BASH
npx create-next-app@latest warung-dashboard

Saat pertanyaan konfigurasi muncul, pilih kurang lebih seperti ini:

TEXT
Would you like to use TypeScript? Yes
Would you like to use ESLint? Yes
Would you like to use Tailwind CSS? Yes
Would you like your code inside a `src/` directory? No
Would you like to use App Router? Yes
Would you like to use Turbopack? Yes
Would you like to customize the import alias? No

Masuk ke direktori proyek:

BASH
cd warung-dashboard

Jalankan server pengembangan:

BASH
npm run dev

Buka alamat berikut:

TEXT
http://localhost:3000

Hasil yang Diharapkan

Peramban menampilkan halaman bawaan Next.js. Terminal juga menampilkan informasi bahwa server siap menerima permintaan.

TEXT
Ready in 1.2s
Local: http://localhost:3000

Mengapa Langkah Ini Penting?

shadcn/ui membutuhkan fondasi proyek yang jelas. Next.js menyiapkan sistem pembangunan, routing, TypeScript, dan Tailwind CSS agar kita tidak perlu merangkainya satu per satu.

Memilih TypeScript juga membantu saat komponen mulai menerima banyak properti. Kesalahan tipe dapat diketahui lebih cepat sebelum aplikasi dijalankan pengguna.

Kesalahan yang Sering Terjadi

Perintah npx Tidak Ditemukan

Penyebab paling umum adalah Node.js belum terpasang atau terminal belum membaca ulang variabel lingkungan.

Coba:

  1. Tutup terminal.

  2. Buka terminal baru.

  3. Jalankan kembali node --version.

  4. Pasang ulang Node.js LTS jika masih gagal.

Port 3000 Sudah Digunakan

Next.js biasanya menawarkan port lain secara otomatis. Teman-Teman juga bisa menentukan port:

BASH
npm run dev -- --port 3001

Lalu buka:

TEXT
http://localhost:3001

Folder Proyek Sudah Ada

Gunakan nama lain atau kosongkan folder sebelumnya:

BASH
npx create-next-app@latest warung-dashboard-v2

Step 2 — Menginisialisasi shadcn/ui

Pastikan terminal berada di direktori proyek, lalu jalankan:

BASH
npx shadcn@latest init

CLI akan memeriksa proyek dan meminta beberapa pilihan konfigurasi. Opsi dapat berubah mengikuti versi, tetapi umumnya mencakup gaya, warna dasar, variabel CSS, dan alias komponen.

Contoh pilihan:

TEXT
Which color would you like to use as the base color? Neutral
Would you like to use CSS variables for theming? Yes

Setelah selesai, proyek biasanya memperoleh berkas konfigurasi seperti:

TEXT
components.json

CLI juga dapat memperbarui berkas CSS global dan menambahkan fungsi utilitas.

Contoh components.json

Isi persisnya dapat berbeda, tetapi bentuk umumnya seperti ini:

JSON
{
  "$schema": "https://ui.shadcn.com/schema.json",
  "style": "new-york",
  "rsc": true,
  "tsx": true,
  "tailwind": {
    "css": "app/globals.css",
    "baseColor": "neutral",
    "cssVariables": true
  },
  "aliases": {
    "components": "@/components",
    "utils": "@/lib/utils",
    "ui": "@/components/ui",
    "lib": "@/lib",
    "hooks": "@/hooks"
  }
}

Hasil yang Diharapkan

Terminal menampilkan bahwa konfigurasi dan dependensi telah berhasil ditambahkan. Struktur proyek dapat berubah menjadi:

TEXT
warung-dashboard/
├── app/
│   ├── globals.css
│   ├── layout.tsx
│   └── page.tsx
├── lib/
│   └── utils.ts
├── components.json
├── package.json
└── tsconfig.json

Mengapa Langkah Ini Penting?

Berkas components.json memberi tahu CLI tempat menyimpan komponen, lokasi CSS, dan alias impor yang dipakai. Tanpa konfigurasi yang benar, komponen dapat masuk ke folder yang salah atau menghasilkan jalur impor yang tidak cocok.

Penggunaan variabel CSS juga penting untuk tema. Warna seperti background, foreground, dan primary dapat diganti tanpa menyunting setiap komponen.

Kesalahan yang Sering Terjadi

CLI Tidak Mengenali Kerangka Kerja

Pastikan perintah dijalankan di folder yang memiliki package.json.

BASH
ls

Di Windows PowerShell:

POWERSHELL
Get-ChildItem

Teman-Teman harus melihat package.json, folder app, dan berkas konfigurasi Next.js.

Konfigurasi Tailwind Tidak Terdeteksi

Periksa apakah Tailwind memang dipilih saat membuat proyek. Jalankan aplikasi dan pastikan kelas sederhana bekerja:

TSX
export default function Home() {
  return <main className="p-10 text-3xl font-bold">Tes Tailwind</main>
}

Jika teks tidak berubah, masalahnya berada pada konfigurasi Tailwind, bukan shadcn/ui.

Alias @/ Bermasalah

Periksa tsconfig.json. Konfigurasi umumnya memuat:

JSON
{
  "compilerOptions": {
    "paths": {
      "@/*": ["./*"]
    }
  }
}

Jika proyek memakai folder src, aliasnya mungkin perlu mengarah ke:

JSON
{
  "compilerOptions": {
    "paths": {
      "@/*": ["./src/*"]
    }
  }
}

Step 3 — Menambahkan Komponen Pertama

Kita mulai dengan tiga komponen yang sering dipakai:

BASH
npx shadcn@latest add button card badge

Setelah perintah selesai, folder berikut biasanya muncul:

TEXT
components/
└── ui/
    ├── badge.tsx
    ├── button.tsx
    └── card.tsx

Ganti isi app/page.tsx dengan kode berikut:

TSX
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import {
  Card,
  CardContent,
  CardDescription,
  CardFooter,
  CardHeader,
  CardTitle,
} from "@/components/ui/card"

export default function Home() {
  return (
    <main className="min-h-screen bg-muted/40 p-6 md:p-10">
      <div className="mx-auto max-w-3xl">
        <Card>
          <CardHeader>
            <div className="flex items-center justify-between gap-4">
              <CardTitle>Pesanan Hari Ini</CardTitle>
              <Badge>Aktif</Badge>
            </div>
            <CardDescription>
              Ringkasan pesanan dari seluruh area layanan.
            </CardDescription>
          </CardHeader>

          <CardContent>
            <p className="text-4xl font-bold">128</p>
            <p className="mt-2 text-sm text-muted-foreground">
              Naik 12% dibandingkan kemarin
            </p>
          </CardContent>

          <CardFooter>
            <Button>Lihat semua pesanan</Button>
          </CardFooter>
        </Card>
      </div>
    </main>
  )
}

Hasil yang Diharapkan

Halaman menampilkan sebuah kartu berisi:

  • Judul “Pesanan Hari Ini”.

  • Badge “Aktif”.

  • Angka 128.

  • Informasi kenaikan 12%.

  • Tombol “Lihat semua pesanan”.

Tampilannya memiliki garis tepi, jarak yang rapi, dan warna yang mengikuti token tema.

Mengapa Langkah Ini Penting?

Kita langsung memakai komponen secara nyata agar alur shadcn/ui terlihat jelas. CLI menambahkan kode, lalu halaman mengimpor komponen dari folder lokal.

Perhatikan bahwa Card terdiri dari beberapa bagian. Struktur ini lebih mudah disesuaikan daripada satu komponen kartu dengan banyak properti khusus.

Membaca Komposisi Komponen

Bagian kartu memiliki tanggung jawab masing-masing:

  • Card menjadi pembungkus.

  • CardHeader menampung judul dan deskripsi.

  • CardContent menampung data utama.

  • CardFooter menampung tindakan.

  • CardTitle memberi struktur judul.

  • CardDescription menampilkan informasi pendukung.

Kalau desain berubah, Teman-Teman dapat memindahkan bagian-bagian tersebut tanpa menulis ulang seluruh komponen.

Step 4 — Memahami dan Menyesuaikan Token Tema

Buka app/globals.css. Setelah inisialisasi, Teman-Teman akan menemukan variabel warna untuk tema terang dan gelap.

Bentuknya dapat berbeda mengikuti versi, tetapi konsepnya serupa:

CSS
:root {
  --background: oklch(1 0 0);
  --foreground: oklch(0.145 0 0);
  --card: oklch(1 0 0);
  --card-foreground: oklch(0.145 0 0);
  --primary: oklch(0.205 0 0);
  --primary-foreground: oklch(0.985 0 0);
  --muted: oklch(0.97 0 0);
  --muted-foreground: oklch(0.556 0 0);
  --border: oklch(0.922 0 0);
  --radius: 0.625rem;
}

Variabel tersebut dipakai melalui kelas semantik:

TSX
<div className="bg-background text-foreground">
  <button className="bg-primary text-primary-foreground">
    Simpan
  </button>
</div>

Mengapa Nama Semantik Lebih Baik?

Hindari menulis warna merek secara berulang seperti:

TSX
<button className="bg-blue-600 text-white">
  Simpan
</button>

Kode itu terlihat sederhana, tetapi akan merepotkan jika warna merek berubah. Lebih baik gunakan:

TSX
<button className="bg-primary text-primary-foreground">
  Simpan
</button>

Sekarang perubahan warna cukup dilakukan pada token primary.

Contoh Tema untuk Bisnis Kuliner

Misalnya, merek menggunakan warna hijau hangat. Sesuaikan variabel tema melalui generator resmi atau nilai warna yang sudah diperiksa kontrasnya.

Contoh konseptual:

CSS
:root {
  --primary: oklch(0.55 0.14 145);
  --primary-foreground: oklch(0.98 0.02 145);
  --ring: oklch(0.55 0.14 145);
  --radius: 0.75rem;
}

Jangan memilih warna hanya karena terlihat bagus di layar sendiri. Periksa kontras teks, terutama pada tombol, badge, pesan kesalahan, dan teks sekunder.

Kesalahan yang Sering Terjadi

Warna Tidak Berubah

Kemungkinan penyebabnya:

  • Variabel yang diubah bukan variabel yang dipakai komponen.

  • Berkas CSS global tidak diimpor.

  • Peramban masih menyimpan hasil lama.

  • Kelas memakai warna tetap seperti bg-blue-600.

Periksa app/layout.tsx:

TSX
import "./globals.css"

Lalu muat ulang halaman secara penuh.

Semua Teks Menjadi Sulit Dibaca

Pastikan pasangan warna tetap serasi:

  • primary dengan primary-foreground.

  • background dengan foreground.

  • card dengan card-foreground.

  • muted dengan muted-foreground.

Mengubah latar tanpa menyesuaikan warna teks adalah kesalahan tema yang cukup umum.

Step 5 — Membangun Kartu Statistik yang Bisa Dipakai Ulang

Komponen di components/ui sebaiknya tetap menjadi primitive antarmuka. Komponen yang memahami istilah bisnis, seperti omzet atau pesanan, ditempatkan di luar folder tersebut.

Buat components/stat-card.tsx:

TSX
import { ReactNode } from "react"

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

type StatCardProps = {
  title: string
  value: string
  description: string
  icon?: ReactNode
}

export function StatCard({
  title,
  value,
  description,
  icon,
}: StatCardProps) {
  return (
    <Card>
      <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
        <CardTitle className="text-sm font-medium">
          {title}
        </CardTitle>
        {icon ? (
          <span className="text-muted-foreground">{icon}</span>
        ) : null}
      </CardHeader>

      <CardContent>
        <p className="text-2xl font-bold">{value}</p>
        <CardDescription className="mt-1">
          {description}
        </CardDescription>
      </CardContent>
    </Card>
  )
}

Perbarui app/page.tsx:

TSX
import { Package, ShoppingCart, TrendingUp, Wallet } from "lucide-react"

import { StatCard } from "@/components/stat-card"

const stats = [
  {
    title: "Pesanan",
    value: "128",
    description: "+12% dibandingkan kemarin",
    icon: ShoppingCart,
  },
  {
    title: "Omzet",
    value: "Rp8.450.000",
    description: "+8,4% dalam 24 jam",
    icon: Wallet,
  },
  {
    title: "Siap Dikirim",
    value: "24",
    description: "Menunggu kurir",
    icon: Package,
  },
  {
    title: "Rata-rata Transaksi",
    value: "Rp66.016",
    description: "+3,1% minggu ini",
    icon: TrendingUp,
  },
]

export default function Home() {
  return (
    <main className="min-h-screen bg-muted/40 p-6 md:p-10">
      <div className="mx-auto max-w-6xl">
        <div className="mb-8">
          <h2 className="text-3xl font-bold tracking-tight">
            Ringkasan Operasional
          </h2>
          <p className="mt-2 text-muted-foreground">
            Data pesanan hari ini dari seluruh area layanan Jakarta.
          </p>
        </div>

        <section className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
          {stats.map((stat) => {
            const Icon = stat.icon

            return (
              <StatCard
                key={stat.title}
                title={stat.title}
                value={stat.value}
                description={stat.description}
                icon={<Icon className="size-4" />}
              />
            )
          })}
        </section>
      </div>
    </main>
  )
}

Jika lucide-react belum tersedia, pasang:

BASH
npm install lucide-react

Hasil yang Diharapkan

Halaman menampilkan empat kartu statistik. Pada layar kecil, kartu tersusun satu atau dua kolom. Pada layar besar, keempatnya muncul dalam satu baris.

Mengapa Langkah Ini Penting?

Pemisahan antara primitive dan komponen domain membuat arsitektur lebih terjaga.

Bandingkan dua folder berikut:

TEXT
components/
├── ui/
│   ├── button.tsx
│   ├── card.tsx
│   └── badge.tsx
└── stat-card.tsx

Folder ui berisi bahan bangunan umum. stat-card.tsx berisi komponen yang memahami kebutuhan dasbor.

Pola ini mencegah components/ui/card.tsx dipenuhi logika omzet, pesanan, atau wilayah layanan yang seharusnya tidak menjadi tanggung jawab primitive kartu.

Step 6 — Menambahkan Tabel Pesanan

Tambahkan komponen tabel:

BASH
npx shadcn@latest add table

Buat components/orders-table.tsx:

TSX
import { Badge } from "@/components/ui/badge"
import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from "@/components/ui/table"

type OrderStatus = "Diproses" | "Dikirim" | "Selesai"

type Order = {
  id: string
  customer: string
  area: string
  total: string
  status: OrderStatus
}

const orders: Order[] = [
  {
    id: "ORD-1048",
    customer: "Rina",
    area: "Tebet",
    total: "Rp185.000",
    status: "Diproses",
  },
  {
    id: "ORD-1047",
    customer: "Budi",
    area: "Kemang",
    total: "Rp240.000",
    status: "Dikirim",
  },
  {
    id: "ORD-1046",
    customer: "Nadia",
    area: "Menteng",
    total: "Rp126.000",
    status: "Selesai",
  },
  {
    id: "ORD-1045",
    customer: "Fajar",
    area: "Cilandak",
    total: "Rp315.000",
    status: "Selesai",
  },
]

function getStatusVariant(status: OrderStatus) {
  if (status === "Selesai") return "default"
  if (status === "Dikirim") return "secondary"

  return "outline"
}

export function OrdersTable() {
  return (
    <div className="overflow-hidden rounded-xl border bg-card">
      <Table>
        <TableHeader>
          <TableRow>
            <TableHead>Pesanan</TableHead>
            <TableHead>Pelanggan</TableHead>
            <TableHead>Area</TableHead>
            <TableHead>Status</TableHead>
            <TableHead className="text-right">Total</TableHead>
          </TableRow>
        </TableHeader>

        <TableBody>
          {orders.map((order) => (
            <TableRow key={order.id}>
              <TableCell className="font-medium">{order.id}</TableCell>
              <TableCell>{order.customer}</TableCell>
              <TableCell>{order.area}</TableCell>
              <TableCell>
                <Badge variant={getStatusVariant(order.status)}>
                  {order.status}
                </Badge>
              </TableCell>
              <TableCell className="text-right">{order.total}</TableCell>
            </TableRow>
          ))}
        </TableBody>
      </Table>
    </div>
  )
}

Tambahkan ke app/page.tsx setelah bagian statistik:

TSX
import { OrdersTable } from "@/components/orders-table"

Lalu masukkan:

TSX
<section className="mt-8">
  <div className="mb-4">
    <h3 className="text-xl font-semibold">Pesanan Terbaru</h3>
    <p className="text-sm text-muted-foreground">
      Transaksi yang baru masuk dari area layanan aktif.
    </p>
  </div>

  <OrdersTable />
</section>

Hasil yang Diharapkan

Di bawah kartu statistik akan muncul tabel pesanan. Setiap baris menampilkan nomor pesanan, pelanggan, area, status, dan total transaksi.

Mengapa Langkah Ini Penting?

Tabel bisnis sering berkembang menjadi komponen yang rumit. Nanti mungkin ada penyortiran, pencarian, pilihan baris, paginasi, dan tindakan massal.

Memulai dari tabel sederhana membantu tim menguji struktur data sebelum memasang solusi tabel yang lebih besar. Jangan langsung memakai tabel canggih jika kebutuhan saat ini hanya menampilkan sepuluh transaksi.

Masalah Responsif pada Tabel

Tabel lebar dapat melampaui layar ponsel. Bungkus tabel dengan kontainer yang dapat digulir:

TSX
<div className="overflow-x-auto">
  <Table>{/* isi tabel */}</Table>
</div>

Untuk pengalaman seluler yang lebih baik, pertimbangkan:

  • Menyembunyikan kolom yang kurang penting.

  • Menampilkan detail lewat dialog.

  • Mengubah setiap baris menjadi kartu pada layar kecil.

  • Menjaga kolom tindakan tetap mudah disentuh.

Step 7 — Menambahkan Dialog untuk Pesanan Baru

Tambahkan komponen dialog, input, dan label:

BASH
npx shadcn@latest add dialog input label textarea

Buat components/create-order-dialog.tsx:

TSX
"use client"

import { useState } from "react"

import { Button } from "@/components/ui/button"
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from "@/components/ui/dialog"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Textarea } from "@/components/ui/textarea"

export function CreateOrderDialog() {
  const [open, setOpen] = useState(false)

  function handleSubmit(event: React. FormEvent<HTMLFormElement>) {
    event.preventDefault()

    const formData = new FormData(event.currentTarget)
    const customer = formData.get("customer")

    console.log("Pesanan dibuat untuk:", customer)
    setOpen(false)
  }

  return (
    <Dialog open={open} onOpenChange={setOpen}>
      <DialogTrigger asChild>
        <Button>Tambah pesanan</Button>
      </DialogTrigger>

      <DialogContent className="sm:max-w-lg">
        <form onSubmit={handleSubmit}>
          <DialogHeader>
            <DialogTitle>Tambah pesanan</DialogTitle>
            <DialogDescription>
              Masukkan data pelanggan dan alamat pengiriman.
            </DialogDescription>
          </DialogHeader>

          <div className="grid gap-5 py-6">
            <div className="grid gap-2">
              <Label htmlFor="customer">Nama pelanggan</Label>
              <Input
                id="customer"
                name="customer"
                placeholder="Contoh: Siti Rahma"
                required
              />
            </div>

            <div className="grid gap-2">
              <Label htmlFor="phone">Nomor WhatsApp</Label>
              <Input
                id="phone"
                name="phone"
                type="tel"
                placeholder="08xxxxxxxxxx"
                required
              />
            </div>

            <div className="grid gap-2">
              <Label htmlFor="address">Alamat pengiriman</Label>
              <Textarea
                id="address"
                name="address"
                placeholder="Nama jalan, nomor rumah, dan patokan"
                required
              />
            </div>
          </div>

          <DialogFooter>
            <Button
              type="button"
              variant="outline"
              onClick={() => setOpen(false)}
            >
              Batal
            </Button>
            <Button type="submit">Simpan pesanan</Button>
          </DialogFooter>
        </form>
      </DialogContent>
    </Dialog>
  )
}

Tambahkan komponen ke bagian atas halaman:

TSX
import { CreateOrderDialog } from "@/components/create-order-dialog"

Ubah bagian judul menjadi:

TSX
<div className="mb-8 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
  <div>
    <h2 className="text-3xl font-bold tracking-tight">
      Ringkasan Operasional
    </h2>
    <p className="mt-2 text-muted-foreground">
      Data pesanan hari ini dari seluruh area layanan Jakarta.
    </p>
  </div>

  <CreateOrderDialog />
</div>

Hasil yang Diharapkan

Saat tombol “Tambah pesanan” ditekan, dialog terbuka. Fokus berpindah ke dalam dialog dan pengguna dapat mengisi formulir.

Setelah formulir dikirim:

  • Data pelanggan ditampilkan di konsol peramban.

  • Dialog tertutup.

  • Halaman tidak dimuat ulang.

Mengapa "use client" Diperlukan?

Komponen ini memakai useState dan peristiwa klik. Fitur tersebut berjalan di peramban, jadi berkas perlu ditandai sebagai Client Component.

Tanpa "use client", Next.js dapat menampilkan kesalahan karena hook React dipakai dalam Server Component.

Kesalahan yang Sering Terjadi

useState Tidak Bisa Dipakai

Pesan kesalahannya biasanya menyebut hook hanya dapat digunakan pada Client Component.

Pastikan baris pertama berkas adalah:

TSX
"use client"

Tidak boleh ada impor atau komentar sebelum direktif tersebut.

Tombol Pemicu Tidak Terlihat Benar

Gunakan asChild pada DialogTrigger:

TSX
<DialogTrigger asChild>
  <Button>Tambah pesanan</Button>
</DialogTrigger>

Tanpa asChild, pemicu dapat menghasilkan elemen tombol di dalam tombol. Struktur HTML seperti itu tidak valid dan dapat mengganggu aksesibilitas.

Dialog Langsung Tertutup

Periksa apakah tombol di dalam formulir memiliki tipe yang benar. Tombol “Batal” harus memakai:

TSX
<Button type="button">Batal</Button>

Jika type tidak ditentukan, tombol dapat dianggap sebagai tombol kirim.

Step 8 — Menambahkan Validasi Formulir

Validasi HTML seperti required cukup untuk contoh sederhana, tetapi aplikasi produksi biasanya membutuhkan aturan yang lebih jelas. Kita bisa memakai React Hook Form dan Zod.

Pasang dependensi:

BASH
npm install react-hook-form zod @hookform/resolvers

Tambahkan komponen formulir:

BASH
npx shadcn@latest add form

Buat skema data:

TSX
import { z } from "zod"

export const orderSchema = z.object({
  customer: z
    .string()
    .min(2, "Nama pelanggan minimal 2 karakter")
    .max(80, "Nama pelanggan terlalu panjang"),
  phone: z
    .string()
    .min(10, "Nomor WhatsApp terlalu pendek")
    .max(15, "Nomor WhatsApp terlalu panjang")
    .regex(/^[0-9+]+$/, "Gunakan angka atau tanda +"),
  address: z
    .string()
    .min(10, "Alamat perlu ditulis lebih lengkap")
    .max(300, "Alamat terlalu panjang"),
})

export type OrderFormValues = z.infer<typeof orderSchema>

Simpan sebagai lib/validations/order.ts.

Kemudian bentuk formulir dengan komponen shadcn/ui:

TSX
"use client"

import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"

import {
  OrderFormValues,
  orderSchema,
} from "@/lib/validations/order"
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 { Textarea } from "@/components/ui/textarea"

export function CreateOrderForm() {
  const form = useForm<OrderFormValues>({
    resolver: zodResolver(orderSchema),
    defaultValues: {
      customer: "",
      phone: "",
      address: "",
    },
  })

  function onSubmit(values: OrderFormValues) {
    console.log(values)
  }

  return (
    <Form {...form}>
      <form
        onSubmit={form.handleSubmit(onSubmit)}
        className="grid gap-5"
      >
        <FormField
          control={form.control}
          name="customer"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Nama pelanggan</FormLabel>
              <FormControl>
                <Input
                  placeholder="Contoh: Siti Rahma"
                  autoComplete="name"
                  {...field}
                />
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        />

        <FormField
          control={form.control}
          name="phone"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Nomor WhatsApp</FormLabel>
              <FormControl>
                <Input
                  type="tel"
                  inputMode="tel"
                  placeholder="08xxxxxxxxxx"
                  autoComplete="tel"
                  {...field}
                />
              </FormControl>
              <FormDescription>
                Nomor dipakai untuk konfirmasi pesanan.
              </FormDescription>
              <FormMessage />
            </FormItem>
          )}
        />

        <FormField
          control={form.control}
          name="address"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Alamat pengiriman</FormLabel>
              <FormControl>
                <Textarea
                  placeholder="Nama jalan, nomor rumah, dan patokan"
                  autoComplete="street-address"
                  {...field}
                />
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        />

        <Button type="submit" disabled={form.formState.isSubmitting}>
          {form.formState.isSubmitting
            ? "Menyimpan..."
            : "Simpan pesanan"}
        </Button>
      </form>
    </Form>
  )
}

Hasil yang Diharapkan

Jika pengguna mengirim formulir kosong, pesan kesalahan muncul di bawah kolom terkait. Jika semua data valid, objek berikut muncul di konsol:

TEXT
{
  customer: "Siti Rahma",
  phone: "081234567890",
  address: "Jl. Tebet Barat Dalam, Jakarta Selatan"
}

Mengapa Validasi Perlu Dipisahkan?

Skema Zod menjadi kontrak data yang jelas. Aturan yang sama dapat dipakai kembali pada formulir, route handler, atau server action.

Namun, validasi di peramban tidak menggantikan validasi di server. Pengguna dapat melewati JavaScript peramban atau mengirim permintaan langsung ke API.

Aturan penting: validasi di klien membantu pengalaman pengguna, sedangkan validasi di server menjaga integritas dan keamanan data. Aplikasi produksi membutuhkan keduanya.

Menangani Nomor Telepon Indonesia

Nomor telepon tampak sederhana, tetapi formatnya beragam:

  • 081234567890

  • +6281234567890

  • 6281234567890

  • Nomor dengan spasi atau tanda hubung

Jangan membuat aturan terlalu ketat tanpa memahami kebutuhan bisnis. Normalisasi format di server, lalu simpan bentuk yang konsisten.

Contoh normalisasi sederhana:

TS
export function normalizeIndonesianPhone(value: string) {
  const cleaned = value.replace(/[^\d+]/g, "")

  if (cleaned.startsWith("08")) {
    return `+62${cleaned.slice(1)}`
  }

  if (cleaned.startsWith("62")) {
    return `+${cleaned}`
  }

  return cleaned
}

Untuk sistem yang berhubungan dengan pembayaran, identitas, atau komunikasi penting, gunakan pengujian yang lebih lengkap.

Step 9 — Menambahkan Toast untuk Umpan Balik

Pengguna perlu tahu apakah tindakan berhasil atau gagal. Menutup dialog tanpa pesan dapat membuat orang bertanya-tanya apakah data benar-benar tersimpan.

Tambahkan komponen notifikasi yang tersedia pada versi shadcn/ui proyek Teman-Teman. Salah satu pilihan yang umum adalah Sonner:

BASH
npx shadcn@latest add sonner

Tambahkan Toaster pada app/layout.tsx:

TSX
import type { Metadata } from "next"
import { Toaster } from "@/components/ui/sonner"

import "./globals.css"

export const metadata: Metadata = {
  title: "Dasbor Pesanan",
  description: "Dasbor operasional pesanan bisnis lokal",
}

export default function RootLayout({
  children,
}: Readonly<{
  children: React. ReactNode
}>) {
  return (
    <html lang="id">
      <body>
        {children}
        <Toaster richColors position="top-right" />
      </body>
    </html>
  )
}

Panggil toast setelah formulir berhasil:

TSX
import { toast } from "sonner"

function onSubmit(values: OrderFormValues) {
  console.log(values)

  toast.success("Pesanan berhasil disimpan", {
    description: `Pesanan atas nama ${values.customer} sudah masuk.`,
  })
}

Tangani kegagalan dengan pesan yang bisa ditindaklanjuti:

TSX
toast.error("Pesanan belum tersimpan", {
  description: "Periksa koneksi lalu coba lagi.",
})

Hasil yang Diharapkan

Setelah data valid dikirim, notifikasi muncul di kanan atas. Pesan menyebut bahwa pesanan berhasil disimpan dan menampilkan nama pelanggan.

Mengapa Umpan Balik Harus Spesifik?

Pesan “Berhasil” terlalu umum. Pengguna perlu tahu tindakan apa yang berhasil.

Lebih baik:

TEXT
Pesanan berhasil disimpan
Pesanan atas nama Siti Rahma sudah masuk.

Daripada:

TEXT
Sukses!

Pada aplikasi operasional, pesan yang jelas mengurangi tindakan ganda. Kasir tidak perlu menekan tombol simpan berulang kali karena ragu.

Step 10 — Menghubungkan Formulir dengan Server Action

Mencetak data ke konsol belum menghasilkan aplikasi nyata. Berikut contoh server action sederhana untuk menerima dan memvalidasi data.

Buat app/actions/orders.ts:

TSX
"use server"

import { orderSchema } from "@/lib/validations/order"

export type CreateOrderState = {
  success: boolean
  message: string
}

export async function createOrder(
  input: unknown
): Promise<CreateOrderState> {
  const parsed = orderSchema.safeParse(input)

  if (!parsed.success) {
    return {
      success: false,
      message: "Data pesanan belum valid.",
    }
  }

  const order = parsed.data

  // Simpan ke basis data di sini.
  console.log("Data tervalidasi di server:", order)

  return {
    success: true,
    message: "Pesanan berhasil dibuat.",
  }
}

Panggil dari komponen klien:

TSX
import { createOrder } from "@/app/actions/orders"

async function onSubmit(values: OrderFormValues) {
  const result = await createOrder(values)

  if (!result.success) {
    toast.error("Pesanan belum tersimpan", {
      description: result.message,
    })
    return
  }

  toast.success("Pesanan berhasil disimpan")
  form.reset()
}

Mengapa Validasi Diulang di Server?

Data dari klien tidak boleh langsung dipercaya. Walaupun formulir memakai Zod, orang tetap dapat mengirim permintaan buatan sendiri.

Server action harus:

  1. Memvalidasi data.

  2. Memeriksa hak akses.

  3. Membersihkan atau menormalkan data.

  4. Menyimpan data.

  5. Mengembalikan respons yang aman.

Untuk contoh tutorial, penyimpanan basis data belum ditambahkan. Pada produk nyata, gunakan transaksi basis data jika satu tindakan mengubah beberapa tabel sekaligus.

Jangan Mengirim Kesalahan Internal ke Pengguna

Hindari:

TS
return {
  success: false,
  message: error.stack,
}

Pesan tersebut dapat membocorkan struktur internal aplikasi. Catat detailnya di sistem pemantauan, lalu tampilkan pesan yang aman:

TS
return {
  success: false,
  message: "Terjadi kendala saat menyimpan pesanan.",
}

Step 11 — Menambahkan Tema Gelap

Tema gelap bukan sekadar membalik latar menjadi hitam. Semua warna permukaan, teks, garis, grafik, dan fokus perlu tetap terbaca.

Pasang next-themes:

BASH
npm install next-themes

Buat components/theme-provider.tsx:

TSX
"use client"

import * as React from "react"
import { ThemeProvider as NextThemesProvider } from "next-themes"

export function ThemeProvider({
  children,
  ...props
}: React. ComponentProps<typeof NextThemesProvider>) {
  return (
    <NextThemesProvider {...props}>
      {children}
    </NextThemesProvider>
  )
}

Bungkus aplikasi di app/layout.tsx:

TSX
import { ThemeProvider } from "@/components/theme-provider"
import { Toaster } from "@/components/ui/sonner"

export default function RootLayout({
  children,
}: Readonly<{
  children: React. ReactNode
}>) {
  return (
    <html lang="id" suppressHydrationWarning>
      <body>
        <ThemeProvider
          attribute="class"
          defaultTheme="system"
          enableSystem
          disableTransitionOnChange
        >
          {children}
          <Toaster richColors position="top-right" />
        </ThemeProvider>
      </body>
    </html>
  )
}

Tambahkan dropdown atau tombol tema. Pasang dropdown:

BASH
npx shadcn@latest add dropdown-menu

Buat components/theme-toggle.tsx:

TSX
"use client"

import { Laptop, Moon, Sun } from "lucide-react"
import { useTheme } from "next-themes"

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

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

  return (
    <DropdownMenu>
      <DropdownMenuTrigger asChild>
        <Button
          variant="outline"
          size="icon"
          aria-label="Pilih tema"
        >
          <Sun className="size-4 dark:hidden" />
          <Moon className="hidden size-4 dark:block" />
        </Button>
      </DropdownMenuTrigger>

      <DropdownMenuContent align="end">
        <DropdownMenuItem onClick={() => setTheme("light")}>
          <Sun className="mr-2 size-4" />
          Terang
        </DropdownMenuItem>

        <DropdownMenuItem onClick={() => setTheme("dark")}>
          <Moon className="mr-2 size-4" />
          Gelap
        </DropdownMenuItem>

        <DropdownMenuItem onClick={() => setTheme("system")}>
          <Laptop className="mr-2 size-4" />
          Ikuti perangkat
        </DropdownMenuItem>
      </DropdownMenuContent>
    </DropdownMenu>
  )
}

Tempatkan ThemeToggle di bagian atas halaman.

Hasil yang Diharapkan

Pengguna dapat memilih:

  • Tema terang.

  • Tema gelap.

  • Tema yang mengikuti pengaturan perangkat.

Pilihan disimpan oleh next-themes, jadi tema tetap sama setelah halaman dimuat ulang.

Mengatasi Hydration Warning

Tema pengguna baru diketahui di peramban. Karena itu, hasil render server dan klien dapat berbeda.

Atribut berikut membantu menangani perubahan kelas tema:

TSX
<html lang="id" suppressHydrationWarning>

Namun, jangan memakai suppressHydrationWarning untuk menyembunyikan semua masalah render. Gunakan hanya pada bagian yang memang memiliki perbedaan terkontrol.

Step 12 — Mengatur Struktur Proyek agar Mudah Dipelihara

Setelah komponen bertambah, struktur folder mulai berpengaruh terhadap produktivitas tim. Salah satu susunan yang cukup aman adalah:

TEXT
app/
├── actions/
│   └── orders.ts
├── globals.css
├── layout.tsx
└── page.tsx

components/
├── ui/
│   ├── badge.tsx
│   ├── button.tsx
│   ├── card.tsx
│   ├── dialog.tsx
│   ├── input.tsx
│   └── table.tsx
├── create-order-dialog.tsx
├── create-order-form.tsx
├── orders-table.tsx
├── stat-card.tsx
├── theme-provider.tsx
└── theme-toggle.tsx

lib/
├── validations/
│   └── order.ts
└── utils.ts

Aturan Praktis Penempatan Komponen

Gunakan components/ui untuk:

  • Button.

  • Input.

  • Dialog.

  • Badge.

  • Card.

  • Primitive lain yang tidak memahami bisnis.

Gunakan folder komponen biasa untuk:

  • Tabel pesanan.

  • Kartu omzet.

  • Formulir pelanggan.

  • Navigasi dasbor.

  • Pemilih cabang.

  • Ringkasan pengiriman.

Gunakan lib untuk:

  • Validasi.

  • Pemformatan.

  • Klien basis data.

  • Fungsi autentikasi.

  • Utilitas yang tidak merender antarmuka.

Mengapa Jangan Mengubah Semua Primitive?

Karena kode shadcn/ui berada di proyek, godaan pertama adalah mengubah setiap berkas. Itu tidak selalu perlu.

MUGHU menyarankan urutan keputusan berikut:

  1. Coba susun komponen yang ada.

  2. Gunakan className untuk penyesuaian lokal.

  3. Tambahkan variant jika pola dipakai berulang.

  4. Ubah primitive jika perilaku dasarnya memang perlu berubah.

  5. Dokumentasikan perubahan yang memengaruhi seluruh produk.

Pendekatan ini menjaga komponen tetap mudah dipahami.

Step 13 — Membuat Variant Tombol Khusus

Misalnya, bisnis membutuhkan tombol untuk tindakan berisiko. shadcn/ui biasanya sudah menyediakan variant destructive, tetapi kita dapat mempelajari cara variant bekerja.

Buka components/ui/button.tsx. Teman-Teman akan melihat pola variant yang kurang lebih seperti ini:

TSX
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 shadow-xs hover:bg-primary/90",
        destructive:
          "bg-destructive text-white shadow-xs hover:bg-destructive/90",
        outline:
          "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground",
        secondary:
          "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
        ghost:
          "hover:bg-accent hover:text-accent-foreground",
        link:
          "text-primary underline-offset-4 hover:underline",
      },
      size: {
        default: "h-9 px-4 py-2",
        sm: "h-8 rounded-md px-3",
        lg: "h-10 rounded-md px-6",
        icon: "size-9",
      },
    },
    defaultVariants: {
      variant: "default",
      size: "default",
    },
  }
)

Tambahkan variant hanya jika benar-benar mewakili pola desain yang stabil. Contohnya, tombol persetujuan:

TSX
approval:
  "bg-emerald-600 text-white shadow-xs hover:bg-emerald-700 focus-visible:ring-emerald-600/30",

Lalu gunakan:

TSX
<Button variant="approval">
  Setujui pesanan
</Button>

Mengapa Variant Lebih Baik daripada Kelas Berulang?

Bandingkan:

TSX
<Button className="bg-emerald-600 text-white hover:bg-emerald-700">
  Setujui
</Button>

Jika pola tersebut muncul di 20 tempat, perubahan desain harus dilakukan 20 kali. Variant memusatkan keputusan visual.

Namun, jangan membuat variant untuk setiap konteks kecil:

TEXT
greenButton
dashboardGreenButton
orderGreenButton
specialGreenButton

Nama variant harus menjelaskan makna, bukan sekadar warnanya. approval lebih tahan terhadap perubahan desain dibandingkan green.

Step 14 — Menambahkan Status Kosong dan Skeleton

Aplikasi tidak selalu memiliki data. Tabel kosong tanpa penjelasan dapat terlihat seperti kerusakan.

Tambahkan skeleton:

BASH
npx shadcn@latest add skeleton

Buat components/dashboard-skeleton.tsx:

TSX
import { Skeleton } from "@/components/ui/skeleton"

export function DashboardSkeleton() {
  return (
    <div className="space-y-8">
      <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
        {Array.from({ length: 4 }).map((_, index) => (
          <div
            key={index}
            className="rounded-xl border bg-card p-6"
          >
            <Skeleton className="h-4 w-24" />
            <Skeleton className="mt-4 h-8 w-32" />
            <Skeleton className="mt-3 h-3 w-40" />
          </div>
        ))}
      </div>

      <div className="rounded-xl border bg-card p-6">
        <Skeleton className="h-6 w-40" />
        <Skeleton className="mt-6 h-10 w-full" />
        <Skeleton className="mt-3 h-10 w-full" />
        <Skeleton className="mt-3 h-10 w-full" />
      </div>
    </div>
  )
}

Buat status kosong untuk pesanan:

TSX
import { Inbox } from "lucide-react"

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

export function EmptyOrders() {
  return (
    <div className="flex min-h-72 flex-col items-center justify-center rounded-xl border border-dashed bg-card p-8 text-center">
      <div className="flex size-12 items-center justify-center rounded-full bg-muted">
        <Inbox className="size-5 text-muted-foreground" />
      </div>

      <h3 className="mt-4 font-semibold">
        Belum ada pesanan
      </h3>

      <p className="mt-2 max-w-sm text-sm text-muted-foreground">
        Pesanan baru akan muncul di sini. Tambahkan pesanan
        secara manual jika pelanggan memesan lewat telepon.
      </p>

      <Button className="mt-5">
        Tambah pesanan
      </Button>
    </div>
  )
}

Mengapa Status Antarmuka Ini Penting?

Satu halaman setidaknya perlu memikirkan empat kondisi:

Kondisi

Tujuan Antarmuka

Memuat

Menunjukkan bahwa proses sedang berjalan

Berhasil

Menampilkan data atau konfirmasi

Kosong

Menjelaskan bahwa belum ada data

Gagal

Menjelaskan masalah dan langkah berikutnya

Komponen yang hanya dirancang untuk kondisi berhasil biasanya terasa rapuh saat dipakai di dunia nyata.

Hindari Skeleton yang Terlalu Lama

Skeleton berguna untuk proses singkat. Jika permintaan membutuhkan waktu lama, tampilkan informasi yang lebih jelas atau opsi untuk mencoba ulang.

Animasi tanpa penjelasan selama puluhan detik tidak membuat sistem terasa cepat. Ia hanya membuat pengguna menunggu dengan dekorasi.

Step 15 — Menguji Aksesibilitas dan Navigasi Papan Ketik

Komponen yang memakai primitive aksesibel tetap perlu diuji dalam konteks halaman. Susunan komponen, teks, dan gaya khusus dapat menciptakan masalah baru.

Daftar Uji Papan Ketik

Coba alur berikut tanpa mouse:

  1. Tekan Tab sampai tombol “Tambah pesanan” terfokus.

  2. Tekan Enter.

  3. Pastikan dialog terbuka.

  4. Tekan Tab untuk berpindah antarbidang.

  5. Tekan Shift + Tab untuk bergerak mundur.

  6. Tekan Escape untuk menutup dialog.

  7. Pastikan fokus kembali ke tombol pemicu.

Periksa Label Formulir

Semua input harus memiliki label yang jelas:

TSX
<Label htmlFor="phone">Nomor WhatsApp</Label>
<Input id="phone" name="phone" />

Placeholder bukan pengganti label. Placeholder menghilang saat pengguna mulai mengetik dan sering memiliki kontras rendah.

Periksa Nama Tombol Ikon

Tombol yang hanya memiliki ikon membutuhkan nama aksesibel:

TSX
<Button
  variant="outline"
  size="icon"
  aria-label="Pilih tema"
>
  <Sun className="size-4" />
</Button>

Tanpa aria-label, pembaca layar mungkin hanya mengumumkan “button” tanpa menjelaskan fungsinya.

Periksa Kontras dan Fokus

Pastikan fokus terlihat:

TSX
className="focus-visible:ring-2 focus-visible:ring-ring"

Jangan menghapus outline tanpa memberikan indikator pengganti:

CSS
/* Hindari jika tidak ada pengganti yang jelas */
button:focus {
  outline: none;
}

Step 16 — Menjaga Performa Aplikasi

shadcn/ui memberi kendali besar, tetapi performa tetap bergantung pada cara aplikasi dibangun.

Tambahkan Komponen Secukupnya

Jangan memasang seluruh katalog hanya karena tersedia. Tambahkan saat diperlukan:

BASH
npx shadcn@latest add button card dialog

Pendekatan bertahap membuat repositori lebih mudah diperiksa dan mengurangi kode yang tidak dipakai.

Jaga Client Component Tetap Kecil

Jangan menandai seluruh halaman dengan "use client" hanya karena satu tombol membutuhkan state.

Lebih baik:

TEXT
Server page
├── Server-rendered heading
├── Server-rendered statistics
└── Client dialog button

Daripada:

TEXT
Entire dashboard as one Client Component

Server Component membantu mengurangi JavaScript yang dikirim ke peramban dan dapat mengambil data langsung di server.

Hindari Impor Ikon Berlebihan

Impor ikon yang dipakai:

TSX
import { Moon, Sun } from "lucide-react"

Hindari pola yang mengimpor seluruh paket sebagai objek jika tidak diperlukan.

Ukur, Jangan Menebak

Gunakan alat peramban dan laporan pembangunan untuk memeriksa:

  • Ukuran bundel.

  • Pergeseran tata letak.

  • Waktu respons server.

  • Waktu interaksi.

  • Permintaan jaringan.

  • Komponen yang dirender ulang terlalu sering.

Optimasi tanpa pengukuran dapat menghabiskan waktu pada bagian yang dampaknya kecil.

Step 17 — Menangani Pembaruan Komponen

Model Open Code memiliki konsekuensi: komponen lokal tidak selalu mengikuti perubahan terbaru secara otomatis.

Ini sekaligus kekuatan dan tanggung jawab.

Kelebihannya

  • Pembaruan paket tidak tiba-tiba mengubah tampilan komponen lokal.

  • Tim dapat mengendalikan waktu migrasi.

  • Penyesuaian tetap berada di repositori.

  • Semua perubahan dapat ditinjau melalui Git.

Kekurangannya

  • Perbaikan dari sumber tidak selalu masuk otomatis.

  • Tim perlu membandingkan perubahan.

  • Komponen yang banyak dimodifikasi lebih sulit diselaraskan.

  • Dokumentasi internal menjadi penting.

Strategi Pembaruan yang Aman

  1. Catat komponen yang telah dimodifikasi.

  2. Buat cabang khusus pembaruan.

  3. Periksa perubahan CLI sebelum menimpa berkas.

  4. Gunakan git diff.

  5. Jalankan tes visual dan interaksi.

  6. Uji tema terang serta gelap.

  7. Gabungkan hanya setelah perubahan dipahami.

Sebelum menjalankan perintah yang berpotensi menimpa berkas, simpan perubahan:

BASH
git add .
git commit -m "chore: save UI before component update"

Setelah pembaruan:

BASH
git diff

Jangan menyetujui perubahan hanya karena aplikasi masih bisa dikompilasi. Periksa perilaku fokus, ukuran, variant, dan tampilan responsif.

Implementasi Studi Kasus: Hasil yang Dicapai

Dalam studi kasus dasbor pesanan, perpindahan ke shadcn/ui dinilai dari sisi teknis dan operasional. Angka berikut merupakan ilustrasi target yang masuk akal, bukan tolok ukur universal.

Metrik

Sebelum Standardisasi

Setelah Implementasi

Perubahan

Waktu membuat halaman operasional

3–4 hari

1,5–2,5 hari

Lebih cepat sekitar 35–50%

Variasi tombol tak terdokumentasi

11 variasi

5 variant resmi

Berkurang 55%

Kode gaya berulang

Tinggi

Lebih terpusat

Berkurang secara nyata

Masalah fokus pada dialog

Sering ditemukan

Lebih terkendali

Risiko turun

Waktu perubahan warna merek

Beberapa jam

Kurang dari 1 jam

Lebih efisien

Komponen domain yang bisa dipakai ulang

Sedikit

8 komponen

Konsistensi meningkat

Perbaikan tersebut tidak terjadi hanya karena memasang CLI. Dampak terbesar datang dari keputusan arsitektur:

  • Token ditetapkan lebih awal.

  • Primitive dan komponen bisnis dipisahkan.

  • Variant diberi nama berdasarkan makna.

  • Komponen hanya ditambahkan saat dibutuhkan.

  • Kondisi kosong, memuat, gagal, dan berhasil dirancang bersama.

  • Perubahan komponen ditinjau melalui Git.

Nilai Bisnis yang Terlihat

Bagi tim produk, manfaat utamanya bukan sekadar kartu yang lebih cantik. Nilainya muncul dalam bentuk:

  • Waktu implementasi fitur lebih mudah diperkirakan.

  • Peninjauan desain lebih cepat.

  • Pengembang tidak mengulang keputusan visual.

  • Masalah konsistensi berkurang sebelum pengujian.

  • Perubahan merek dapat diterapkan melalui token.

  • Tim memiliki fondasi yang dapat berkembang bersama produk.

Untuk agensi web di Jakarta, Bandung, Surabaya, Yogyakarta, atau kota lain, pola ini juga membantu saat menangani beberapa proyek klien. Fondasi dapat digunakan ulang, sementara warna, tipografi, dan komponen domain tetap disesuaikan untuk tiap merek.

Kesalahan Umum Saat Memakai shadcn/ui

Menganggap shadcn/ui sebagai Paket yang Tidak Boleh Disentuh

Tujuan Open Code adalah memberi kendali kepada tim. Teman-Teman boleh mengubah komponen.

Namun, perubahan sebaiknya punya alasan jelas. Jangan mengubah struktur hanya karena ingin membuat kode terlihat berbeda.

Menaruh Semua Komponen di components/ui

Folder components/ui bukan tempat untuk seluruh aplikasi.

Kurang tepat:

TEXT
components/ui/order-payment-summary.tsx
components/ui/jakarta-delivery-map.tsx
components/ui/customer-loyalty-card.tsx

Lebih rapi:

TEXT
components/ui/card.tsx
components/orders/order-payment-summary.tsx
components/delivery/jakarta-delivery-map.tsx
components/customers/customer-loyalty-card.tsx

Memasang Terlalu Banyak Komponen Sekaligus

Memasang semua komponen membuat repositori penuh berkas yang belum tentu digunakan. Tim juga menjadi sulit membedakan mana komponen aktif dan mana yang sekadar ikut terpasang.

Tambahkan sesuai kebutuhan fitur.

Mengubah Warna Tetap di Banyak Tempat

Hindari:

TSX
className="bg-[#0f7a45] text-[#ffffff]"

Jika warna tersebut mewakili merek, letakkan pada token:

TSX
className="bg-primary text-primary-foreground"

Mengandalkan Placeholder sebagai Label

Kurang tepat:

TSX
<Input placeholder="Nomor telepon" />

Lebih baik:

TSX
<Label htmlFor="phone">Nomor telepon</Label>
<Input id="phone" placeholder="08xxxxxxxxxx" />

Melupakan Kondisi Gagal

Jangan hanya membuat toast sukses. API dapat gagal karena jaringan, sesi kedaluwarsa, validasi server, atau basis data.

TSX
try {
  const result = await createOrder(values)

  if (!result.success) {
    toast.error("Pesanan belum tersimpan", {
      description: result.message,
    })
    return
  }

  toast.success("Pesanan berhasil disimpan")
} catch {
  toast.error("Koneksi bermasalah", {
    description: "Periksa jaringan lalu coba lagi.",
  })
}

Mempercayai Semua Registry Pihak Ketiga

Ekosistem shadcn/ui memiliki banyak komponen, blok, dan template dari komunitas. Ada yang gratis, berbayar, terbuka, atau memakai lisensi khusus.

Sebelum memasang komponen pihak ketiga:

  • Baca sumber kodenya.

  • Periksa dependensi tambahan.

  • Pastikan lisensi sesuai.

  • Uji aksesibilitas.

  • Periksa permintaan jaringan.

  • Hindari kode yang meminta kredensial tanpa alasan jelas.

  • Tinjau apakah komponen cocok dengan versi React dan Tailwind proyek.

Katalog komunitas seperti Cult UI dan penyedia blok lain dapat mempercepat pekerjaan, tetapi tanggung jawab keamanan tetap berada pada tim yang memasukkannya ke produk.

Troubleshooting shadcn/ui

Modul @/components/ui/button Tidak Ditemukan

Periksa apakah komponen sudah ditambahkan:

BASH
npx shadcn@latest add button

Pastikan berkas tersedia:

TEXT
components/ui/button.tsx

Periksa pula alias di tsconfig.json dan components.json.

cn Tidak Ditemukan

Komponen shadcn/ui sering memakai fungsi cn dari lib/utils.ts.

Contoh:

TS
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs))
}

Jika berkas hilang, periksa hasil inisialisasi atau buat kembali dengan dependensi yang sesuai:

BASH
npm install clsx tailwind-merge

Gaya Komponen Tidak Muncul

Periksa:

  • CSS global diimpor di layout.

  • Tailwind berjalan.

  • Variabel tema tersedia.

  • Jalur komponen benar.

  • Server pengembangan sudah dimulai ulang.

  • Tidak ada kesalahan sintaks di CSS.

Jalankan:

BASH
npm run dev

Jika masalah tetap ada, hapus cache pembangunan:

BASH
rm -rf .next
npm run dev

Pada PowerShell:

POWERSHELL
Remove-Item -Recurse -Force .next
npm run dev

Kelas Dinamis Tailwind Tidak Terdeteksi

Pola berikut dapat bermasalah:

TSX
<div className={`bg-${color}-500`} />

Tailwind mungkin tidak dapat menemukan nama kelas lengkap saat memindai sumber.

Gunakan pemetaan eksplisit:

TSX
const colorClasses = {
  green: "bg-green-500",
  red: "bg-red-500",
  blue: "bg-blue-500",
}

<div className={colorClasses[color]} />

Hydration Mismatch

Penyebab umum:

  • Membaca window saat render server.

  • Menggunakan tanggal atau angka acak langsung saat render.

  • Tema server dan klien berbeda.

  • Struktur HTML tidak valid.

  • Tombol berada di dalam tombol.

Hindari:

TSX
<p>{Math.random()}</p>

Jika nilai hanya tersedia di peramban, baca setelah komponen terpasang atau pindahkan logika ke Client Component yang tepat.

Dialog Tidak Bisa Dibuka

Periksa bahwa komponen interaktif berada dalam Client Component jika memakai state. Pastikan juga DialogTrigger berada di dalam Dialog.

TSX
<Dialog>
  <DialogTrigger asChild>
    <Button>Buka</Button>
  </DialogTrigger>
  <DialogContent>Isi</DialogContent>
</Dialog>

Variant TypeScript Ditolak

Jika Teman-Teman menambahkan variant baru, pastikan variant dimasukkan ke konfigurasi buttonVariants dan tipe tombol masih merujuk ke konfigurasi tersebut.

Mulai ulang server TypeScript atau penyunting kode bila tipe lama masih tersimpan.

Tampilan Berbeda Setelah Menyalin Contoh Lama

shadcn/ui berkembang dari waktu ke waktu. Nama kelas, dependensi, struktur tema, dan implementasi komponen dapat berubah.

Utamakan dokumentasi yang sesuai dengan versi proyek. Jangan mencampur contoh lama dan konfigurasi terbaru tanpa memeriksa perbedaannya.

Tips Praktis agar shadcn/ui Tetap Rapi

Tetapkan Token Sebelum Memoles Halaman

Sebelum mengubah setiap kartu, sepakati:

  • Warna utama.

  • Warna destruktif.

  • Warna teks sekunder.

  • Radius.

  • Bayangan.

  • Jarak dasar.

  • Ukuran judul.

  • Aturan fokus.

Keputusan ini mengurangi revisi kecil yang berulang.

Buat Daftar Variant yang Disetujui

Contoh dokumentasi internal:

Komponen

Variant

Penggunaan

Button

default

Tindakan utama

Button

secondary

Tindakan pendamping

Button

outline

Tindakan netral

Button

ghost

Tindakan ringan

Button

destructive

Menghapus atau membatalkan permanen

Badge

default

Status aktif

Badge

secondary

Status informatif

Badge

outline

Status menunggu

Daftar sederhana ini menjaga desainer dan pengembang memakai bahasa visual yang sama.

Bungkus Pola Bisnis, Bukan Semua Elemen

Buat komponen jika pola:

  • Dipakai berulang.

  • Memiliki aturan bisnis.

  • Sulit ditulis dengan benar.

  • Membutuhkan pengujian khusus.

  • Perlu konsistensi lintas halaman.

Jangan membuat pembungkus yang tidak memberi nilai:

TSX
function MyButton(props) {
  return <Button {...props} />
}

Pembungkus seperti itu hanya menambah lapisan tanpa menyederhanakan penggunaan.

Gunakan Git sebagai Pengaman

Sebelum menambah atau memperbarui komponen:

BASH
git status
git add .
git commit -m "chore: save work before adding UI component"

Setelah CLI berjalan:

BASH
git diff

Karena kode disalin ke proyek, git diff adalah alat penting untuk memahami apa yang berubah.

Dokumentasikan Keputusan, Bukan Hanya Komponen

Dokumentasi yang berguna menjelaskan:

  • Kapan memakai dialog dan kapan memakai sheet.

  • Kapan tombol harus destructive.

  • Cara menulis pesan kesalahan.

  • Aturan ukuran target sentuh.

  • Kapan tabel berubah menjadi kartu di ponsel.

  • Bagaimana format rupiah dan tanggal ditampilkan.

Komponen saja tidak cukup untuk membentuk sistem desain. Sistem desain juga membutuhkan aturan penggunaan.

Ulasan Seimbang: Kelebihan dan Kekurangan shadcn/ui

Kelebihan

  • Kepemilikan kode penuh. Tim dapat memeriksa dan mengubah implementasi.

  • Tampilan awal yang matang. Banyak komponen sudah terlihat rapi tanpa banyak penyesuaian.

  • Mudah disusun. Bagian komponen dapat digabungkan sesuai kebutuhan.

  • Cocok untuk sistem desain. Token dan variant dapat distandardisasi.

  • Mendukung aksesibilitas. Primitive dasarnya menangani banyak detail interaksi.

  • Ramah terhadap AI. Kode lokal lebih mudah dianalisis dan disunting.

  • Ekosistem berkembang. Tersedia registry, blok, template, serta contoh komunitas.

  • Risiko perubahan visual mendadak lebih rendah. Kode lokal tidak otomatis berubah hanya karena paket sumber diperbarui.

Kekurangan

  • Tim bertanggung jawab memelihara kode lokal.

  • Pembaruan tidak selalu otomatis masuk.

  • Penyesuaian tanpa aturan dapat menciptakan kekacauan baru.

  • Tidak semua kebutuhan bisnis memiliki komponen siap pakai.

  • Tailwind CSS tetap perlu dipahami.

  • Kualitas registry pihak ketiga tidak seragam.

  • Kepemilikan kode dapat menambah beban peninjauan keamanan.

  • Proyek kecil sekali mungkin tidak membutuhkan seluruh pola sistem desain.

Siapa yang Paling Cocok Memakainya?

shadcn/ui cocok untuk:

  • Tim React dan Next.js.

  • Produk SaaS.

  • Dasbor internal.

  • Situs bisnis dengan identitas visual khusus.

  • Agensi yang membutuhkan fondasi antarmuka fleksibel.

  • Produk yang akan berkembang dalam jangka panjang.

  • Tim yang terbiasa meninjau dan memelihara kode.

  • Proyek dengan alur kerja AI-assisted development.

Siapa yang Sebaiknya Memilih Pilihan Lain?

Pertimbangkan pustaka lain jika:

  • Tim ingin semua pembaruan komponen datang melalui versi paket.

  • Produk harus mengikuti Material Design secara ketat.

  • Tim tidak ingin menyentuh implementasi komponen.

  • Prototipe hanya dipakai sebentar dan kecepatan mutlak menjadi prioritas.

  • Proyek tidak memakai React.

  • Kapasitas pemeliharaan kode sangat terbatas.

Perbandingan Sumber Komponen dan Template shadcn

Ekosistem shadcn/ui mencakup sumber resmi dan penyedia komunitas. Fungsinya tidak selalu sama.

Sumber

Fokus

Cocok untuk

Hal yang Perlu Diperiksa

shadcn/ui resmi

Primitive dan sistem distribusi

Fondasi proyek

Versi dokumentasi

Registry komunitas

Komponen tambahan

Kebutuhan khusus

Kualitas kode dan lisensi

Template terbuka

Proyek siap dikloning

Validasi ide dan percepatan awal

Arsitektur dan pemeliharaan

Blok premium

Bagian halaman siap pakai

Agensi dan tim dengan deadline ketat

Hak penggunaan komersial

Koleksi animasi

Efek dan elemen pemasaran

Landing page

Performa dan aksesibilitas

Generator tema

Token warna dan radius

Sistem desain

Kontras dan konsistensi merek

Sumber resmi sebaiknya menjadi acuan utama untuk konsep dan implementasi inti. Contoh komunitas dapat mempercepat pekerjaan, tetapi jangan menyalin kode tanpa peninjauan.

Pelajaran Utama dari Implementasi

Open Code Bukan Berarti Bebas Tanpa Aturan

Kebebasan mengubah kode harus disertai disiplin. Tanpa token, dokumentasi, dan peninjauan, komponen lokal dapat bercabang menjadi banyak variasi yang sulit dirawat.

ROI Datang dari Standardisasi

Penghematan waktu bukan berasal dari satu tombol yang selesai dalam lima menit. Nilainya muncul ketika puluhan halaman memakai pola yang sama dan perubahan desain dapat diterapkan dengan risiko lebih rendah.

Primitive dan Domain Harus Dipisahkan

Button tidak perlu tahu soal pesanan. OrdersTable boleh memahami status bisnis, format rupiah, dan area layanan.

Pemisahan ini membuat komponen lebih mudah diuji, digunakan ulang, dan dipahami anggota tim.

Aksesibilitas Tetap Perlu Diuji

Primitive yang baik memberi titik awal kuat, tetapi keputusan aplikasi dapat merusaknya. Label yang hilang, warna yang lemah, atau tombol ikon tanpa nama tetap menjadi tanggung jawab tim.

Template Bukan Produk Jadi

Template dapat mempercepat tahap awal, tetapi masih perlu:

  • Pemeriksaan keamanan.

  • Penyesuaian identitas.

  • Penghapusan kode yang tidak dipakai.

  • Validasi lisensi.

  • Pengujian performa.

  • Pengujian aksesibilitas.

  • Integrasi data nyata.

Metrik Harus Dibaca Sesuai Konteks

Pengurangan waktu pengembangan sebesar 30% pada satu tim tidak menjamin hasil yang sama pada tim lain. Dampak dipengaruhi pengalaman React, tingkat penyesuaian, kualitas desain, dan kompleksitas produk.

Gunakan metrik internal seperti:

  • Waktu pembuatan fitur.

  • Jumlah variasi tak resmi.

  • Jumlah masalah aksesibilitas.

  • Waktu peninjauan desain.

  • Frekuensi regresi visual.

  • Ukuran JavaScript klien.

  • Tingkat penggunaan ulang komponen.

Langkah Lanjutan Setelah Dasbor Berfungsi

Setelah fondasi berjalan, pengembangan dapat diteruskan secara bertahap:

  1. Hubungkan server action dengan basis data.

  2. Tambahkan autentikasi dan otorisasi.

  3. Buat paginasi untuk tabel pesanan.

  4. Tambahkan penyortiran dan pencarian.

  5. Buat filter berdasarkan area Jakarta.

  6. Tambahkan format tanggal dan rupiah yang konsisten.

  7. Bangun halaman detail pesanan.

  8. Tambahkan konfirmasi sebelum tindakan destruktif.

  9. Tulis tes untuk validasi dan fungsi normalisasi.

  10. Jalankan audit aksesibilitas.

  11. Ukur performa halaman produksi.

  12. Dokumentasikan token dan variant yang disetujui.

Untuk formatter rupiah, gunakan API bawaan JavaScript:

TS
export function formatRupiah(value: number) {
  return new Intl. NumberFormat("id-ID", {
    style: "currency",
    currency: "IDR",
    maximumFractionDigits: 0,
  }).format(value)
}

Contoh pemakaian:

TSX
<p>{formatRupiah(8450000)}</p>

Hasil yang diharapkan:

TEXT
Rp8.450.000

Untuk tanggal Indonesia:

TS
export function formatIndonesianDate(value: Date) {
  return new Intl. DateTimeFormat("id-ID", {
    day: "numeric",
    month: "long",
    year: "numeric",
  }).format(value)
}

Contoh hasil:

TEXT
11 Juli 2026

Tambahkan pengujian sederhana agar format tidak berubah tanpa sengaja:

TS
import { describe, expect, it } from "vitest"

import { formatRupiah } from "@/lib/formatters"

describe("formatRupiah", () => {
  it("memformat angka sebagai mata uang rupiah", () => {
    expect(formatRupiah(8450000)).toContain("8.450.000")
  })
})

Dengan fondasi tersebut, proyek sudah memiliki komponen lokal, token tema, dialog, formulir tervalidasi, notifikasi, tema gelap, tabel operasional, serta struktur yang siap dikembangkan menjadi aplikasi produksi.

Kesimpulan

shadcn menawarkan pendekatan praktis untuk membangun antarmuka React yang konsisten tanpa mengorbankan kendali atas kode. Komponen lokal, token tema, validasi formulir, dialog, notifikasi, dan dukungan tema gelap dapat mempercepat pembuatan dasbor sekaligus menjaga arsitektur tetap mudah dipahami dan dikembangkan.

Namun, hasil akhirnya tetap bergantung pada kualitas implementasi. Template perlu disesuaikan dengan kebutuhan bisnis, termasuk format rupiah, tanggal Indonesia, alur pesanan, dan area layanan Jakarta. Aksesibilitas, keamanan, performa, pengujian, serta pemisahan komponen generik dari logika domain juga harus menjadi bagian dari proses, bukan pemeriksaan tambahan menjelang peluncuran.

Gunakan dokumentasi resmi shadcn sebagai rujukan teknis, lalu ukur dampaknya berdasarkan metrik proyek Anda sendiri. Mulailah dari fondasi yang sederhana, uji setiap keputusan, dan kembangkan sistem komponen secara disiplin hingga siap mendukung produk nyata.


Referensi

Shadcn UI. (2026). The foundation for your design system.

Shadcn UI. (2026). Introduction.

GitHub. (2026). Shadcn UI: Beautifully designed, accessible components.

Shadcn. (2026). Free React and shadcn/ui templates.

Shadcn. (2026). The AI-native shadcn/ui component library for React.

GitHub. (2026). Shadcn.

Shadcn Studio. (2026). Shadcn UI components, blocks, and templates.

Cult UI. (2026). Shadcn UI components, blocks, and templates.

GeeksforGeeks. (2026). What is ShadCN and why is it used?

Shadcn. (2026). Shadcn.

Komentar (0)

Belum ada komentar. Jadilah yang pertama berbagi pendapat!

Tinggalkan komentar