Mengenal Base UI: Library Komponen React Unstyled untuk Developer Modern

Pernah nggak sih kamu merasa frustasi waktu pakai library UI kayak Material UI atau Bootstrap, terus mau custom stylenya malah ribet banget? Atau mungkin kamu udah capek-capek bikin design system sendiri, eh ternyata komponennya bentrok sama style bawaan library yang kamu pakai?

Nah, ini problem klasik yang sering banget dialamin sama developer, terutama yang baru mulai belajar bikin aplikasi React. Library UI yang udah ter-styling emang praktis sih, tinggal import langsung jadi. Tapi begitu kamu pengen customisasi lebih dalam atau nyocokin sama design system perusahaan, disitulah masalahnya mulai muncul. Kamu harus override style sana-sini, kadang sampe harus pake !important segala—yang notabene itu bad practice banget.

Makanya, Base UI hadir sebagai solusi alternatif yang lebih flexible. Ini adalah library komponen React yang unstyled, artinya kamu dapet fungsionalitas lengkap tanpa harus berantem sama style bawaan. Kamu bebas styling sesuka hati, mau pake Tailwind CSS, CSS Modules, atau bahkan vanilla CSS sekalipun.

Di artikel ini, kita bakal ngebahas tuntas apa itu Base UI, kenapa library ini cocok banget buat developer modern, dan gimana cara implementasinya di project kamu. Artikel ini ditulis khusus buat pemula yang pengen paham konsep unstyled component tanpa pusing sama istilah-istilah teknis yang bikin pening kepala.

Apa Itu Base UI?

https://base-ui.com/
https://base-ui.com/

Base UI adalah library komponen React yang bersifat headless atau unstyled. Artinya, library ini nyediain komponen-komponen UI yang udah lengkap fungsinya, tapi tanpa style bawaan sama sekali. Jadi kamu bebas mau styling kayak gimana pun tanpa perlu ribet override style yang udah ada.

Library ini dikembangin sama tim yang juga bikin Material UI, dengan kolaborasi dari developer-developer handal yang pernah kerja di Radix dan Floating UI. Jadi dari segi kualitas dan best practice, kamu nggak perlu ragu lagi. Mereka paham banget gimana caranya bikin komponen yang accessible, performant, dan developer-friendly.

Biar lebih gampang dipahamin, gini deh analoginya. Bayangin Base UI itu kayak rangka mobil yang udah lengkap dengan mesin, setir, rem, dan semua fungsi pentingnya. Tapi bodynya masih kosong—kamu yang tentuin mau dicat warna apa, pake velg model apa, atau mau tambahin aksesoris apa aja. Nah, library UI yang udah ter-styling kayak Material UI itu ibarat mobil yang udah jadi lengkap sama cat dan aksesorisnya. Praktis sih, tapi kalau mau ubah warnanya, kamu harus cat ulang dan itu ribet.

Bedanya Base UI sama library lain cukup signifikan. Material UI atau Chakra UI itu dateng dengan design system lengkap, jadi komponennya udah punya tampilan default yang siap pake. Sementara Base UI lebih fokus ke fungsionalitas aja—accessibility, keyboard navigation, state managemnet, dan behavior komponen udah dihandle dengan baik. Stylenya? Terserah kamu sepenuhnya.

Saat ini Base UI masih dalam tahap beta version, tapi udah nyediain lebih dari 25 komponen yang siap dipakai. Mulai dari komponen basic kayak Button dan Input, sampe yang kompleks kayak Dialog, Dropdown, dan Tooltip. Meskipun masih beta, library ini udah cukup stabil buat dipake di project production—asal kamu siap sama kemungkinan ada breaking changes di versi-versi berikutnya.

Mengapa Memilih Base UI?

Ada beberapa alasan kuat kenapa Base UI layak banget buat dipilih, terutama kalau kamu developer yang suka punya kontrol penuh atas project-mu. Yuk kita bahas satu-satu keunggulannya.

Kontrol Penuh Atas CSS

Ini adalah keunggulan utama Base UI. Kamu bebas banget mau pake styling solution apapun—mau Tailwind CSS, CSS Modules, Styled Components, Emotion, atau bahkan vanilla CSS biasa. Nggak ada aturan baku yang mengikat. Misalnya di BuildWithAngga, kamu bisa bikin component Button yang stylenya sepenuhnya custom sesuai panduan brand perusahaan tanpa harus bertarung sama style bawaan library.

Bayangin kamu lagi kerja sama tim desain yang punya design system super detail. Dengan Base UI, kamu tinggal implementasiin design tokens mereka tanpa harus mikirin "wah ini bentrok nggak ya sama style bawaannya?" Semua dimulai dari kanvas kosong, jadi kamu yang pegang kendali penuh.

Accessibility By Default

Ini poin yang sering dilupain sama developer pemula. Base UI udah include accessibility features dari sananya. Semua komponen udah dilengkapi dengan ARIA attributes yang benar, keyboard navigation yang proper, dan screen reader support. Jadi meskipun komponennya unstyled, dari segi fungsi dan accessibility udah production-ready.

Kamu nggak perlu lagi ribet mikirin "eh ini button-nya udah bisa diakses pake keyboard belum ya?" atau "screen readernya udah kebaca belom nih?" Semua udah dihandle sama Base UI. Ini menghemat waktu banget, apalagi buat project yang memang peduli sama accessibility.

Fully Composable

API Base UI dirancang supaya fleksibel dan mudah dikombinasikan. Kamu bisa compose komponen sesuai kebutuhan tanpa terbatas sama struktur yang kaku. Misalnya kamu mau bikin dropdown menu dengan custom trigger, atau dialog dengan animasi khusus—semua bisa disesuaikan dengan gampang.

Tree-Shakeable dan Bundle Size

Karena Base UI cuma fokus ke fungsionalitas tanpa bawa-bawa CSS, bundle size aplikasi kamu jadi jauh lebih kecil. Plus, library ini tree-shakeable, artinya cuma komponen yang kamu pake aja yang bakal masuk ke bundle. Jadi kalau di project BuildWithAngga kamu cuma butuh Button dan Dialog, ya cuma itu aja yang masuk ke production build.

Tanpa Vendor Lock-in

Ini penting banget buat jangka panjang. Karena Base UI nggak terikat sama styling solution tertentu, kamu bebas ganti-ganti teknologi styling kapanpun. Hari ini pake Tailwind, besok mau migrasi ke CSS Modules? Gampang, tinggal ganti classnya aja. Komponennya tetep jalan karena logic-nya terpisah dari style.

Instalasi dan Setup Awal

Sebelum mulai ngutak-atik Base UI, pastiin dulu kamu udah punya bekal yang cukup ya. Kamu perlu paham dasar-dasar React, familiar sama Vite sebagai build tool, dan minimal ngerti TypeScript—meskipun nggak harus jago banget. Kalau kamu udah pernah ikutin kelas React di BuildWithAngga, harusnya udah lebih dari cukup buat mulai.

Membuat Project Baru dengan Vite

Vite Project
Vite Project

Langkah pertama, kita bikin project baru pake Vite. Buka terminal kamu dan jalanin command ini:

npm create vite@latest bwa-base-ui

Nanti kamu bakal diminta milih framework dan variant. Pilih React, terus pilih TypeScript buat variant-nya. Setelah selesai, masuk ke folder projectnya:

cd bwa-base-ui
npm install

Instalasi Base UI dan Tailwind CSS

Sekarang saatnya install Base UI dan Tailwind CSS sekaligus. Prosesnya gampang banget, tinggal jalanin command ini:

npm install @base-ui-components/react tailwindcss@next @tailwindcss/vite@next

Setup Material Symbols untuk Icon

Buat icon di aplikasi kita, kita bakal pake Material Symbols. Library ini lebih ringan dan punya banyak varian icon yang keren. Install dulu package-nya:

npm install material-symbols

Terus import font-nya di file src/main.tsx:

import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.tsx'
import './index.css'
import 'material-symbols'

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
)

Sekarang kamu udah bisa pake icon Material Symbols dengan class .material-symbols-outlined atau .material-symbols-rounded di komponen kamu nanti.

Setup Tailwind CSS v4

Tailwind CSS v4 udah nggak pake file konfigurasi tailwind.config.js lagi. Semuanya sekarang di-manage lewat CSS. Tambahin Tailwind plugin di vite.config.ts:

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'

export default defineConfig({
  plugins: [react(), tailwindcss()],
})

Terus di file src/index.css, ganti semua isinya dengan:

@import "tailwindcss";

@theme {
  --font-display-large: 3.5625rem;
  --font-display-medium: 2.8125rem;
  --font-display-small: 2.25rem;
  --font-headline-large: 2rem;
  --font-headline-medium: 1.75rem;
  --font-headline-small: 1.5rem;
  --font-title-large: 1.375rem;
  --font-title-medium: 1rem;
  --font-title-small: 0.875rem;
  --font-body-large: 1rem;
  --font-body-medium: 0.875rem;
  --font-body-small: 0.75rem;
  --font-label-large: 0.875rem;
  --font-label-medium: 0.75rem;
  --font-label-small: 0.6875rem;

  --animate-popover-in: popover-in 0.5s cubic-bezier(0.05, 0.7, 0.1, 1);
  --animate-popover-out: popover-out 0.2s cubic-bezier(0.3, 0, 0.8, 0.15);
}

@keyframes popover-in {
  0% {
    opacity: 0;
    transform: scale(0.85) translateY(-16px);
  }
  50% {
    opacity: 0.8;
    transform: scale(1.02) translateY(-2px);
  }
  100% {
    opacity: 1;
    transform: scale(1) translateY(0);
  }
}

@keyframes popover-out {
  0% {
    opacity: 1;
    transform: scale(1);
  }
  100% {
    opacity: 0;
    transform: scale(0.92);
  }
}

Dengan konfigurasi ini, kita udah bikin custom animations yang mengikuti prinsip Material Design 3 Expressive dengan efek spring-like bounce yang subtle. Animasinya punya overshoot di tengah (scale 1.02) yang bikin gerakan terasa lebih hidup dan natural—ini adalah karakteristik khas dari spring physics animation. Typography scale juga udah disesuaikan dengan Material 3 type system yang punya 15 token berbeda untuk berbagai kebutuhan teks.

Kalau masih error, coba hapus dulu folder node_modules dan file package-lock.json, terus install ulang:

rm -rf node_modules package-lock.json
npm install

Konfigurasi Portal untuk Popups

Base UI butuh konfigurasi portal buat handle komponen kayak Dialog, Tooltip, atau Dropdown yang perlu muncul di atas elemen lain. Tambahin div khusus di index.html:

<body>
  <div id="root"></div>
  <div id="portal-root"></div>
  <script type="module" src="/src/main.tsx"></script>
</body>

Ini penting buat memastiin z-index handling berjalan dengan baik, jadi komponen popup nggak ketutupan sama elemen lain.

Konfigurasi iOS Safari Visual Viewport

Terakhir, buat user iOS Safari, kita perlu tambahin meta tag khusus di index.html supaya visual viewport bekerja dengan benar:

<meta name="viewport" content="width=device-width, initial-scale=1.0, interactive-widget=resizes-visual">

Tag ini memastiin keyboard di iOS nggak mengacaukan layout aplikasi kamu, terutama pas ada form atau input field yang aktif. Konfigurasi ini sering dilupain sama developer, padahal penting banget buat user experience di perangkat mobile.

Komponen Pertama: Tutorial Praktis

Oke, sekarang waktunya praktik langsung bikin komponen pertama kita pake Base UI. Kita bakal bikin Popover component yang sering banget dipake di aplikasi modern—misalnya buat nampilin menu pengaturan user, notifikasi, atau informasi tambahan tanpa harus pindah halaman. Di BuildWithAngga, komponen kayak gini bisa dipake buat nampilin detail kursus atau pilihan menu di dashboard.

Struktur Dasar Popover

Pertama-tama, kita perlu paham dulu struktur komponen Popover di Base UI. Nggak kayak library lain yang cuma satu komponen besar, Base UI ngasih kita kontrol penuh dengan pecahin jadi beberapa bagian kecil. Ini yang namanya composable components.

Buat file baru src/components/UserMenu.tsx dan mulai dengan import yang dibutuhin:

import { Popover } from '@base-ui-components/react/popover';

export default function UserMenu() {
  return (
    <Popover.Root>
      <Popover.Trigger>
        <button>Menu Saya</button>
      </Popover.Trigger>

      <Popover.Portal>
        <Popover.Positioner>
          <Popover.Popup>
            <div>Konten menu disini</div>
          </Popover.Popup>
        </Popover.Positioner>
      </Popover.Portal>
    </Popover.Root>
  );
}

Mari kita breakdown satu-satu bagiannya biar nggak bingung:

Popover.Root adalah wrapper utama yang nge-handle semua state dan logic komponen. Dia yang ngatur kapan popover dibuka atau ditutup, plus handle keyboard navigation kayak tombol Escape buat nutup popover.

Popover.Trigger adalah elemen yang bakal di-klik user buat buka popover. Bisa button, link, atau elemen apapun yang clickable. Komponen ini otomatis dapet event handler dan accessibility attributes yang benar.

Popover.Portal fungsinya render konten popover ke tempat terpisah di DOM tree—biasanya di akhir body. Ini penting banget buat memastiin popover nggak ke-crop sama parent element yang punya overflow: hidden.

Popover.Positioner adalah komponen yang nge-handle positioning popover relatif terhadap trigger-nya. Dia pake algoritma smart positioning, jadi kalau space-nya nggak cukup di bawah, otomatis pindah ke atas atau samping.

Popover.Popup adalah konten actual dari popover. Semua yang mau kamu tampilin masuk kesini—bisa text, form, list menu, atau apapun.

Styling dengan Tailwind CSS

Sekarang kita tambahin styling biar komponennya keliatan profesional. Ini contoh implementasi lengkap dengan Tailwind:

import { Popover } from '@base-ui-components/react/popover';

export default function UserMenu() {
  return (
    <Popover.Root>
      <Popover.Trigger className="inline-flex items-center gap-2 px-6 py-3 bg-blue-600 text-white rounded-full hover:bg-blue-700 transition-all hover:shadow-lg hover:scale-105 active:scale-95">
        <span className="material-symbols-rounded text-2xl">account_circle</span>
        <span className="font-medium text-base">Akun Saya</span>
      </Popover.Trigger>

      <Popover.Portal>
        <Popover.Positioner
          side="bottom"
          align="end"
          sideOffset={12}
        >
          <Popover.Popup className="bg-white rounded-3xl shadow-2xl border border-gray-100 p-3 min-w-[220px] data-[starting-style]:animate-popover-in data-[ending-style]:animate-popover-out">
            <div className="flex flex-col gap-1.5">
              <a href="/profile" className="flex items-center gap-3 px-4 py-3 hover:bg-gray-50 rounded-2xl text-gray-800 transition-all hover:scale-[1.02] active:scale-[0.98]">
                <span className="material-symbols-outlined text-2xl">person</span>
                <span className="font-medium text-sm">Profil Saya</span>
              </a>
              <a href="/courses" className="flex items-center gap-3 px-4 py-3 hover:bg-gray-50 rounded-2xl text-gray-800 transition-all hover:scale-[1.02] active:scale-[0.98]">
                <span className="material-symbols-outlined text-2xl">school</span>
                <span className="font-medium text-sm">Kursus Saya</span>
              </a>
              <a href="/settings" className="flex items-center gap-3 px-4 py-3 hover:bg-gray-50 rounded-2xl text-gray-800 transition-all hover:scale-[1.02] active:scale-[0.98]">
                <span className="material-symbols-outlined text-2xl">settings</span>
                <span className="font-medium text-sm">Pengaturan</span>
              </a>
              <hr className="my-1.5 border-gray-100" />
              <button className="flex items-center gap-3 px-4 py-3 hover:bg-red-50 rounded-2xl text-red-600 transition-all text-left w-full hover:scale-[1.02] active:scale-[0.98]">
                <span className="material-symbols-outlined text-2xl">logout</span>
                <span className="font-medium text-sm">Keluar</span>
              </button>
            </div>
          </Popover.Popup>
        </Popover.Positioner>
      </Popover.Portal>
    </Popover.Root>
  );
}

Perhatiin beberapa hal penting disini. Kita pake props side, align, dan sideOffset di Positioner buat ngatur posisi popover. Property side="bottom" artinya popover muncul di bawah trigger, align="end" artinya sejajar ke kanan, dan sideOffset={12} kasih jarak 12px dari trigger.

Buat animasinya, kita pake custom keyframes yang udah kita define di index.css tadi dengan spring-like effect yang punya overshoot di tengah animasi. Base UI otomatis ngasih data attribute data-[starting-style] pas popover mulai dibuka dan data-[ending-style] pas mulai ditutup. Animasi masuknya (popover-in) dikombinasikan antara fade, scale dengan bounce effect, dan sedikit translateY—ini adalah karakteristik Material Design 3 Expressive yang bikin animasi terasa lebih ekspresif dan punya personality.

Untuk styling, kita pake rounded corner yang lebih besar (rounded-3xl dan rounded-2xl) sesuai prinsip Material 3 Expressive yang embrace bold shapes. Button trigger juga pake rounded-full dengan interactive states kayak hover:scale-105 dan active:scale-95 yang bikin interaction terasa lebih responsive dan playful. Shadow yang lebih dramatis (shadow-2xl) juga nambah depth dan emphasis pada komponen.

Menggunakan Komponen

Sekarang tinggal pake komponen ini di halaman utama. Buka file src/App.tsx:

import UserMenu from './components/UserMenu'

function App() {
  return (
    <div className="min-h-screen bg-gray-50 p-8">
      <nav className="flex justify-between items-center mb-8">
        <h1 className="text-2xl font-bold text-gray-900">BuildWithAngga</h1>
        <UserMenu />
      </nav>

      <main>
        <h2 className="text-xl text-gray-700">Dashboard Kursus</h2>
      </main>
    </div>
  )
}

export default App

Popover Component
Popover Component

Tips Debugging untuk Pemula

Saat develop pake Base UI, ada beberapa hal yang sering bikin bingung pemula. Pertama, kalau popover nggak muncul sama sekali, cek dulu apakah kamu udah setup Portal dengan benar. Pastiin ada element dengan id portal-root di index.html kamu.

Kedua, kalau styling nggak ngaruh, kemungkinan ada konflik z-index. Base UI pake z-index cukup tinggi buat popup components. Kalau ada elemen lain yang lebih tinggi z-indexnya, popover bisa ketutupan. Solusinya, atur z-index di parent element atau tambahin utility class z-50 di Popup.

Ketiga, kalau animasi nggak jalan, pastiin kamu udah setup keyframes dengan benar di index.css. Cek juga di browser DevTools apakah data attributes data-starting-style dan data-ending-style muncul pas popover dibuka atau ditutup. Base UI otomatis nge-set attribute ini buat trigger animasi.

Terakhir, selalu cek console browser buat error messages. Base UI cukup helpful dalam ngasih warning kalau ada yang salah di setup kamu—misalnya kalau lupa wrap dengan Root component atau ada props yang invalid.

Komponen Kedua: Tutorial Praktis

Setelah berhasil bikin Popover, sekarang kita lanjut ke komponen yang lebih kompleks tapi tetep asik buat dipelajarin. Kali ini kita bakal bikin profile card yang lengkap dengan avatar, nama user, verified icon, button edit profile, dan list kursus. Komponen kayak gini sering banget dipake di aplikasi modern, termasuk di platform BuildWithAngga buat nampilin informasi instruktur atau student.

Struktur Profile Card

Kita mulai dengan bikin file baru src/components/ProfileCard.tsx. Kali ini kita bakal kombinasiin styling Material Expressive dengan komponen Dialog, Avatar, Form, dan Field dari Base UI buat fitur edit profile yang lengkap.

import { Dialog } from '@base-ui-components/react/dialog';
import { Field } from '@base-ui-components/react/field';
import { Form } from '@base-ui-components/react/form';
import { Avatar } from '@base-ui-components/react/avatar';
import { useState } from 'react';

export default function ProfileCard() {
  const [openDialog, setOpenDialog] = useState(false);
  const [loading, setLoading] = useState(false);

  const courses = [
    { id: 1, title: "Mastering React Hooks", students: 1240, thumbnail: "<https://ui-avatars.com/api/?name=React&size=100&background=61DAFB&color=fff>" },
    { id: 2, title: "Advanced TypeScript", students: 890, thumbnail: "<https://ui-avatars.com/api/?name=TS&size=100&background=3178C6&color=fff>" },
    { id: 3, title: "UI/UX Design Fundamentals", students: 2100, thumbnail: "<https://ui-avatars.com/api/?name=UI&size=100&background=FF6B6B&color=fff>" }
  ];

  const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    setLoading(true);

    const formData = new FormData(event.currentTarget);
    const fullname = formData.get('fullname') as string;
    const role = formData.get('role') as string;
    const bio = formData.get('bio') as string;

    // Simulasi proses submit
    await new Promise((resolve) => setTimeout(resolve, 1500));

    console.log('Form submitted:', { fullname, role, bio });
    setLoading(false);
    setOpenDialog(false);
  };

  return (
    <>
      <div className="bg-white rounded-3xl shadow-2xl p-8 max-w-2xl mx-auto border border-gray-100 hover:shadow-[0_20px_60px_-15px_rgba(0,0,0,0.3)] transition-all duration-500">
        <div className="flex flex-col">
          {/* Header Section with Avatar and Info */}
          <div className="flex items-start gap-6">
            {/* Avatar Section */}
            <div className="relative w-28 h-28 group flex-shrink-0">
              <Avatar.Root className="w-28 h-28 rounded-full hover:scale-105 transition-transform duration-300">
                <Avatar.Image
                  src="<https://buildwithangga.com/themes/front/images/logo_bwa_new.svg>"
                  alt="Angga Risky"
                  className="w-full h-full rounded-full object-cover border-4 border-white"
                />
                <Avatar.Fallback className="w-full h-full rounded-full flex items-center justify-center bg-blue-200 text-blue-700 font-bold text-2xl border-4 border-white">
                  AR
                </Avatar.Fallback>
              </Avatar.Root>
              {/* Online Status Indicator */}
              <div className="absolute bottom-2 right-2 w-6 h-6 bg-green-500 rounded-full border-4 border-white shadow-lg"></div>
            </div>

            {/* Name, Stats, and Button */}
            <div className="flex-1">
              {/* Name and Verified Badge */}
              <div className="flex items-center gap-2">
                <h2 className="font-bold text-2xl text-gray-900">Angga Risky</h2>
                <span className="material-symbols-rounded text-blue-600 text-2xl">verified</span>
              </div>

              {/* Role/Title */}
              <p className="text-gray-600 text-base mt-1 font-medium">Senior Product Designer</p>

              {/* Stats */}
              <div className="flex gap-6 mt-4">
                <div className="text-left">
                  <p className="font-bold text-xl text-gray-900">24</p>
                  <p className="text-sm text-gray-500 font-medium">Kursus</p>
                </div>
                <div className="text-left">
                  <p className="font-bold text-xl text-gray-900">12.5K</p>
                  <p className="text-sm text-gray-500 font-medium">Student</p>
                </div>
                <div className="text-left">
                  <p className="font-bold text-xl text-gray-900">4.9</p>
                  <p className="text-sm text-gray-500 font-medium">Rating</p>
                </div>
              </div>

              {/* Edit Profile Button with Dialog */}
              <Dialog.Root open={openDialog} onOpenChange={setOpenDialog}>
                <Dialog.Trigger className="mt-5 bg-blue-600 text-white font-medium text-sm px-6 py-3 rounded-full hover:bg-blue-700 hover:shadow-xl hover:scale-[1.02] active:scale-[0.98] transition-all duration-300 flex items-center gap-2">
                  <span className="material-symbols-rounded text-lg">edit</span>
                  <span>Edit Profile</span>
                </Dialog.Trigger>

                <Dialog.Portal>
                  <Dialog.Backdrop className="fixed inset-0 bg-black/50 data-[starting-style]:opacity-0 data-[ending-style]:opacity-0 data-[state=open]:opacity-100 transition-opacity duration-300" />
                  <Dialog.Popup className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 bg-white rounded-3xl shadow-2xl p-8 max-w-md w-full data-[starting-style]:animate-popover-in data-[ending-style]:animate-popover-out">
                    <Dialog.Title className="font-bold text-2xl text-gray-900 mb-2">Edit Profile</Dialog.Title>
                    <Dialog.Description className="text-gray-600 text-sm mb-6">Perbarui informasi profil Anda di BuildWithAngga</Dialog.Description>

                    <Form
                      className="space-y-4"
                      onSubmit={handleSubmit}
                    >
                      <Field.Root name="fullname">
                        <Field.Label className="block text-sm font-medium text-gray-700 mb-2">
                          Nama Lengkap
                        </Field.Label>
                        <Field.Control
                          type="text"
                          defaultValue="Angga Risky"
                          className="w-full px-4 py-3 rounded-2xl border border-gray-200 focus:border-blue-600 focus:ring-2 focus:ring-blue-100 outline-none transition-all"
                        />
                      </Field.Root>

                      <Field.Root name="role">
                        <Field.Label className="block text-sm font-medium text-gray-700 mb-2">
                          Role/Posisi
                        </Field.Label>
                        <Field.Control
                          type="text"
                          defaultValue="Senior Product Designer"
                          className="w-full px-4 py-3 rounded-2xl border border-gray-200 focus:border-blue-600 focus:ring-2 focus:ring-blue-100 outline-none transition-all"
                        />
                      </Field.Root>

                      <Field.Root name="bio">
                        <Field.Label className="block text-sm font-medium text-gray-700 mb-2">
                          Bio
                        </Field.Label>
                        <Field.Control
                          render={(props) => (
                            <textarea
                              {...props}
                              rows={4}
                              defaultValue="Passionate about creating beautiful and functional user experiences."
                              className="w-full px-4 py-3 rounded-2xl border border-gray-200 focus:border-blue-600 focus:ring-2 focus:ring-blue-100 outline-none transition-all resize-none"
                            />
                          )}
                        />
                      </Field.Root>

                      <div className="flex gap-3 mt-8">
                        <Dialog.Close className="flex-1 px-6 py-3 rounded-full border-2 border-gray-200 text-gray-700 font-medium hover:bg-gray-50 hover:scale-[1.02] active:scale-[0.98] transition-all duration-300">
                          Batal
                        </Dialog.Close>
                        <button
                          type="submit"
                          disabled={loading}
                          className="flex-1 px-6 py-3 rounded-full bg-blue-600 text-white font-medium hover:bg-blue-700 hover:shadow-xl hover:scale-[1.02] active:scale-[0.98] transition-all duration-300 disabled:opacity-50 disabled:cursor-not-allowed"
                        >
                          {loading ? 'Menyimpan...' : 'Simpan'}
                        </button>
                      </div>
                    </Form>
                  </Dialog.Popup>
                </Dialog.Portal>
              </Dialog.Root>
            </div>
          </div>

          {/* Divider */}
          <div className="border-t border-gray-100 my-6"></div>

          {/* Courses Section */}
          <div>
            <h3 className="font-bold text-lg text-gray-900 mb-4">Kursus Terpopuler</h3>
            <div className="space-y-3">
              {courses.map((course) => (
                <button
                  key={course.id}
                  className="w-full flex items-center gap-4 p-4 rounded-2xl hover:bg-gray-50 transition-all duration-300 cursor-pointer hover:scale-[1.01] active:scale-[0.99] group text-left"
                >
                  <Avatar.Root className="w-16 h-16 rounded-xl flex-shrink-0 group-hover:scale-105 transition-transform duration-300">
                    <Avatar.Image
                      src={course.thumbnail}
                      alt={course.title}
                      className="w-full h-full rounded-xl object-cover"
                    />
                    <Avatar.Fallback className="w-full h-full rounded-xl flex items-center justify-center bg-gray-200 text-gray-500 font-bold">
                      {course.title.substring(0, 2)}
                    </Avatar.Fallback>
                  </Avatar.Root>
                  <div className="flex-1 min-w-0">
                    <h4 className="font-semibold text-base text-gray-900 truncate">{course.title}</h4>
                    <div className="flex items-center gap-2 mt-1">
                      <span className="material-symbols-outlined text-gray-500 text-sm">group</span>
                      <p className="text-sm text-gray-600 font-medium">{course.students.toLocaleString()} siswa</p>
                    </div>
                  </div>
                  <span className="material-symbols-rounded text-gray-400 text-xl group-hover:text-blue-600 group-hover:translate-x-1 transition-all duration-300">arrow_forward</span>
                </button>
              ))}
            </div>
          </div>
        </div>
      </div>
    </>
  );
}
Profile Component
Profile Component

Struktur Layout Horizontal

Container utama pake max-w-2xl biar lebih lebar. Layout horizontal dengan flex items-start gap-6 menempatkan avatar di kiri dan info di kanan. Avatar punya flex-shrink-0 supaya ukurannya tetap stabil.

Stats section diubah jadi text-left dengan gap-6 yang compact. Edit profile button ukurannya px-6 py-3, nggak full width lagi.

Avatar Component

Avatar dari Base UI punya tiga bagian utama:

  • Avatar.Root - wrapper yang mengelola loading state
  • Avatar.Image - gambar utama profil atau thumbnail
  • Avatar.Fallback - cadangan berisi inisial kalau image gagal dimuat

Base UI otomatis beralih ke fallback tanpa perlu coding manual. Course thumbnails juga pake pattern yang sama dengan inisial dua huruf pertama judul.

Courses List Interactive

Setiap item kursus punya hover state ekspresif:

  • Card: hover:bg-gray-50 + hover:scale-[1.01] untuk zoom halus
  • Thumbnail: group-hover:scale-105 saat parent dihover
  • Arrow: transisi warna text-gray-400 ke text-blue-600 + translate-x-1
  • Text: pake truncate supaya judul panjang nggak merusak layout

Dialog dengan Form

Dialog button menggunakan Dialog.Trigger yang otomatis handle open/close state. Di dalam dialog ada beberapa komponen Base UI:

Dialog Structure:

  • Dialog.Backdrop - overlay gelap dengan fade animation
  • Dialog.Popup - container form dengan spring animation Material Expressive
  • Dialog.Title dan Dialog.Description - semantic markup dengan ARIA attributes

Form Implementation:

Form hanya perlu props onSubmit untuk handle submission. Data diekstrak pake FormData API native JavaScript:

const formData = new FormData(event.currentTarget);
const fullname = formData.get('fullname') as string;

Submit disimulasikan dengan delay 1.5 detik. Button jadi disabled dan text berubah "Menyimpan..." untuk feedback visual.

Field Component:

Setiap input dibungkus dengan Field.Root yang punya attribute name:

  • Field.Label - otomatis terasosiasi dengan input
  • Field.Control - input dengan accessibility built-in
  • Focus states: ring biru + border color transition

Untuk textarea pake render prop pattern:

<Field.Control
  render={(props) => (
    <textarea {...props} rows={4} />
  )}
/>

Pattern ini kasih flexibility buat custom element sambil tetap dapat behavior dari Base UI.

Menggunakan Komponen

Buat pake komponen ini, tinggal import di src/App.tsx:

import ProfileCard from './components/ProfileCard'

function App() {
  return (
    <div className="min-h-screen bg-blue-50 p-8 flex items-center justify-center">
      <ProfileCard />
    </div>
  )
}

export default App

Background pake solid color bg-blue-50 yang soft biar profile card lebih stand out. Container pake flex items-center justify-center buat center card di tengah layar.

Tips Debugging

Avatar nggak muncul: Cek URL image valid dan ada koneksi internet. Fallback harusnya muncul otomatis kalau image gagal.

Dialog nggak kebuka: Pastikan state openDialog di-manage dengan benar lewat useState. Cek juga Portal sudah di-setup di index.html.

Form nggak submit: Pastikan button punya type="submit" dan form punya onSubmit handler. Cek console untuk error messages.

Styling nggak jalan: Kalau Material Symbols icon nggak muncul, pastikan sudah import di main.tsx. Kalau animasi nggak smooth, cek setup keyframes di index.css sudah benar.

Resources dan Pembelajaran Lanjutan

Setelah kamu berhasil bikin beberapa komponen dengan Base UI, pasti pengen dong belajar lebih dalam lagi? Tenang, ada banyak banget resources yang bisa kamu manfaatin buat ningkatin skill kamu.

Dokumentasi Resmi

Quick start · Base UI
Quick start · Base UI

Website resmi Base UI di https://base-ui.com adalah tempat terbaik buat mulai. Dokumentasinya lengkap banget, dari penjelasan setiap komponen sampe contoh kode yang bisa langsung dicoba. Semua API dijelasin dengan detail, termasuk props apa aja yang bisa dipake dan data attributes yang tersedia buat styling.

GitHub Repository

https://github.com/mui/base-ui
https://github.com/mui/base-ui

Kalo kamu tipe developer yang suka baca source code, langsung aja cek GitHub repository Base UI. Disana kamu bisa liat implementasi internal komponennya, ngereport bug kalau nemu, atau bahkan kontribusi dengan pull request. Isunya juga aktif banget, jadi kamu bisa belajar dari problem yang dialamin developer lain.

Kesimpulan

Base UI adalah solusi yang tepat buat kamu yang pengen punya kontrol penuh atas styling komponen tanpa harus bikin semuanya dari nol. Dengan pendekatan unstyled components, kamu bebas berkreasi sesuai design system yang kamu pengen sambil tetep dapet benefit accessibility dan behavior management yang solid dari library.

Memang di awal mungkin terasa lebih ribet dibanding pake library yang udah jadi kayak Material UI atau Chakra. Tapi percaya deh, pengalaman dan skill yang kamu dapet dari belajar Base UI bakal sangat berguna buat karir jangka panjang. Kamu jadi paham betul gimana cara kerja komponen UI yang baik, gimana nge-handle accessibility dengan benar, dan gimana cara bikin design system yang scalable.

Buat kamu yang masih pemula dan pengen belajar React lebih dalam lagi, jangan lupa eksplorasi kelas-kelas di BuildWithAngga. Disana ada banyak materi dari basic sampe advanced yang bakal bantu kamu jadi developer yang lebih solid. Mulai dari fundamental React, state managment, sampe advanced patterns kayak compound components dan render props—semuanya diajarin dengan step-by-step yang gampang dipahamin.

Jangan takut buat nyoba teknologi baru. Setiap library yang kamu pelajari bakal nambah perspektif kamu tentang gimana cara ngoding yang lebih baik. Base UI mungkin bukan solusi buat semua project, tapi pasti ada use case yang cocok banget sama pendekatan ini. Yang penting tetep terus belajar dan bereksperimen. Good luck dan semangat belajarnya!