Kenapa Saya Lebih Suka Drawer Daripada Full Page

February 2, 2026⏱️ 10 min read

Tentang penggunaan drawer sebagai transactional UI di web e-commerce, dibahas dari sudut pandang UX, arsitektur state, TypeScript, accessibility, dan pengalaman mobile yang terasa native.

Kenapa Saya Lebih Suka Drawer Daripada Full Page

Catatan tentang Transactional UI di Web E-Commerce (dengan TypeScript)

Sudah cukup lama saya tidak menulis artikel teknis. Bukan karena kehabisan topik, tapi karena semakin ke sini saya merasa banyak hal teknis yang menarik justru tidak cukup dibahas secara jujur—terutama soal keputusan UI yang kelihatannya “kecil”, tapi dampaknya terasa sampai ke pengalaman pengguna dan struktur kode.

Salah satunya adalah pemilihan drawer dibanding full page dalam konteks web e-commerce.

Tulisan ini bukan ajakan untuk meninggalkan full page sepenuhnya. Ini lebih seperti catatan pribadi: kenapa, dalam banyak kasus e-commerce, saya merasa drawer lebih selaras untuk UI transaksi, dan bagaimana keputusan UI ini memengaruhi cara saya menulis TypeScript dan mengelola state aplikasi.


Full Page Bukan Masalah, Tapi Sering Tidak Tepat

Full page adalah pendekatan default di web. Dan itu bukan hal yang buruk.

Untuk:
• Artikel.
• Landing page.
• Halaman informatif.
• Konten panjang yang ingin dibaca pelan-pelan

full page sering kali pilihan terbaik.

Yang mulai terasa “kurang pas” adalah ketika pola full page dibawa mentah-mentah ke e-commerce, khususnya untuk interaksi yang sifatnya cepat dan berulang.

Contoh sederhana:
• User scroll daftar produk.
• Klik satu produk.
• Pindah ke halaman detail.
• Kembali ke halaman sebelumnya.
• Scroll ulang.
• Ulangi proses yang sama.

Secara teknis ini benar.
Secara UX… sering terasa ada friksi yang sebetulnya tidak perlu.


Drawer Bukan Pengganti Page, Tapi Lapisan Interaksi

Kesalahan umum saat membahas drawer adalah menganggapnya sebagai pengganti halaman. Padahal, drawer lebih tepat saya pahami sebagai lapisan interaksi.

Drawer cocok untuk:
• Product detail (quick view).
• Cart.
• Quick checkout.
• Chat / komunikasi.
• Preview informasi.

Full page tetap penting untuk:
• SEO.
• Shareable URL.
• Deep reading.

Jadi ini bukan “drawer vs page” yang saling meniadakan. Lebih ke kapan kita butuh navigasi, dan kapan kita butuh transaksi.


Transactional UI vs Navigational UI

Di titik ini saya merasa membantu untuk membedakan dua “rasa” UI:

Navigational UI.
• Tujuan utama: eksplorasi.
• User “jalan-jalan” dari satu halaman ke halaman lain.
• Contoh: blog, artikel, about page.

Transactional UI.
• Tujuan utama: menyelesaikan niat.
• User sudah tahu mau ngapain.
• Contoh: add to cart, pilih varian, checkout.

Drawer biasanya unggul di transactional UI karena:
• Minim context switching.
• Lebih cepat secara perceived performance.
• Fokus ke aksi, bukan eksplorasi.

Kalau saya sederhanakan: pada e-commerce, user sering tidak sedang ingin “berpindah halaman”, tapi ingin menyelesaikan niat.


Masalah Umum: Drawer yang Tidak “Typed”

Banyak implementasi drawer gagal bukan karena UI-nya, tapi karena state-nya tidak dirancang dengan benar. Drawer sering diperlakukan seperti:

const [isOpen, setIsOpen] = useState(false);

Lalu bertambah menjadi:
• isCartOpen
• isProductOpen
• isCheckoutOpen

Dan pelan-pelan berubah menjadi boolean hell.

Kalau aplikasi masih kecil, mungkin terasa aman. Tapi semakin banyak fitur dan entry point, state yang “sekadar boolean” biasanya mulai bikin perilaku drawer sulit ditebak.


Drawer sebagai State, Bukan Sekadar Komponen UI (TypeScript)

Pendekatan yang menurut saya lebih sehat adalah memperlakukan drawer sebagai state yang eksplisit dan bertipe.

Contoh sederhana dengan TypeScript:

type DrawerState =
  | { key: "PRODUCT_DETAIL"; payload: { productId: string } }
  | { key: "CART" }
  | { key: "CHECKOUT" }
  | { key: null };

Kenapa saya suka model begini:
• Kalau buka PRODUCT_DETAIL, payload wajib ada.
• Kalau buka CART, payload tidak perlu (dan sebaiknya tidak dipaksakan).
• Drawer tidak punya “status menggantung”: terbuka tapi datanya entah dari mana.

Di sini TypeScript membantu bukan sekadar untuk “autocompletion”, tapi untuk menjaga kontrak perilaku aplikasi tetap jelas.


Struktur Drawer yang Saya Anggap “Sehat”

Saya suka struktur seperti ini:

src/
  features/
    drawer/
      drawer.types.ts
      drawer.store.ts
      DrawerRoot.tsx
      drawers/
        ProductDetailDrawer.tsx
        CartDrawer.tsx
        CheckoutDrawer.tsx

• DrawerRoot hanya bertugas merender drawer sesuai state.
• Masing-masing drawer terpisah per fitur.
• drawer.store.ts memegang state + action.
• drawer.types.ts memegang tipe union (inti dari type-safety).

Struktur ini tidak glamor, tapi biasanya yang seperti ini justru bertahan lama.


Store Drawer (Zustand Contoh Sederhana)

Saya pribadi nyaman memakai Zustand untuk state seperti ini karena:
• tidak perlu provider berlapis-lapis.
• logic state lebih gampang diisolasi.
• re-render bisa dibuat lebih granular dibanding Context yang dipakai “global” tanpa seleksi.

Contoh store minimal:

import { create } from "zustand";
import type { DrawerState } from "./drawer.types";

type DrawerActions = {
  openProductDetail: (productId: string) => void;
  openCart: () => void;
  openCheckout: () => void;
  close: () => void;
};

type DrawerStore = DrawerState & DrawerActions;

export const useDrawerStore = create<DrawerStore>((set) => ({
  key: null,

  openProductDetail: (productId) =>
    set({ key: "PRODUCT_DETAIL", payload: { productId } }),

  openCart: () => set({ key: "CART" }),

  openCheckout: () => set({ key: "CHECKOUT" }),

  close: () => set({ key: null }),
}));

Catatan kecil: ini bukan “banyak drawer”, tapi satu drawer dengan banyak mode. Itu membuat kontrol jauh lebih stabil.


DrawerRoot: Satu Komponen untuk Semua Drawer

"use client";

import { useDrawerStore } from "./drawer.store";
import { ProductDetailDrawer } from "./drawers/ProductDetailDrawer";
import { CartDrawer } from "./drawers/CartDrawer";
import { CheckoutDrawer } from "./drawers/CheckoutDrawer";

export function DrawerRoot() {
  const state = useDrawerStore();
  const close = useDrawerStore((s) => s.close);

  if (!state.key) return null;

  return (
    <div>
      {state.key === "PRODUCT_DETAIL" && (
        <ProductDetailDrawer
          productId={state.payload.productId}
          onClose={close}
        />
      )}

      {state.key === "CART" && <CartDrawer onClose={close} />}

      {state.key === "CHECKOUT" && <CheckoutDrawer onClose={close} />}
    </div>
  );
}

Saya suka pola ini karena seluruh aplikasi punya satu “gerbang” drawer. Perilaku jadi konsisten, dan debugging lebih manusiawi.


Cara Buka Drawer dari Card (State-Driven Entry)

Dari item-card, cukup panggil:

const openProductDetail = useDrawerStore((s) => s.openProductDetail);

<button onClick={() => openProductDetail(product.id)}>
  Lihat Detail
</button>

Tidak ubah URL. Cepat. Natural.


Hybrid Drawer: Ketika Route dan State Bertemu

Bagian ini yang buat saya semakin yakin: drawer dan routing tidak harus saling meniadakan.

Konsep sederhananya:
• User klik produk → drawer terbuka (state-driven).
• User buka URL /product/[slug] langsung → drawer yang sama terbuka (route-driven).

UI-nya satu.
Logic-nya satu.
Entry point-nya dua.

Contoh client trigger:

"use client";

import { useEffect } from "react";
import { useDrawerStore } from "@/features/drawer/drawer.store";

export function OpenProductDrawerOnMount({ productId }: { productId: string }) {
  const open = useDrawerStore((s) => s.openProductDetail);

  useEffect(() => {
    open(productId);
  }, [open, productId]);

  return null;
}

Di server page, tinggal render:

// Server Component
import { OpenProductDrawerOnMount } from "./OpenProductDrawerOnMount";

export default async function Page({ params }: { params: { slug: string } }) {
  const product = await getProductBySlug(params.slug);

  return <OpenProductDrawerOnMount productId={product.id} />;
}

Dengan ini:
• share link tetap hidup.
• refresh tetap bisa.
• UX tetap drawer.

Dan buat saya, ini salah satu titik “ketemu” antara web-native (URL, SEO) dan app-like experience (drawer, transaksi cepat).


Kenapa Drawer Terasa Lebih “Cepat”, Walau Backend Sama

Menariknya, drawer sering terasa lebih cepat, meskipun:
• API sama.
• Data sama.
• Network request sama.

Ini karena:
• Tidak ada page unmount besar.
• Context user tetap utuh.
• Transisi visual lebih ringan.

Ini mirip konsep optimistic UI, tapi di level pengalaman, bukan data.


Kapan Full Page Lebih Tepat

Penting untuk tetap jujur: drawer bukan solusi untuk semua hal.

Full page lebih tepat ketika:
• SEO adalah prioritas utama.
• Konten panjang & naratif.
• Share link adalah fitur utama.
• Struktur informasi kompleks.

Memaksakan drawer untuk semua hal justru akan merusak UX.


Dampak ke Codebase dan Maintainability

Dari sisi developer, drawer yang dirancang dengan benar:
• Mengurangi duplikasi halaman.
• Membuat logic lebih reusable.
• Memperjelas alur state.
• Memudahkan testing.

Yang penting bukan drawer-nya, tapi cara berpikir state-driven di baliknya.


Penutup

Di e-commerce, user tidak datang untuk “berpindah halaman”.
Mereka datang dengan niat.

Dan tugas UI yang baik bukan memamerkan navigasi,
tapi membantu niat itu selesai dengan friksi sekecil mungkin.

Itulah kenapa, dalam konteks transactional UI,
saya lebih sering memilih drawer daripada full page.


Catatan Teknis Lanjutan

Bagian ini saya tambah karena beberapa hal sering luput saat kita bicara drawer. Biasanya bukan karena tidak tahu, tapi karena sering fokus ke “bisa jalan dulu”. Padahal kalau drawer dipakai sebagai pusat transaksi, detail-detail ini lama-lama jadi penentu kualitas.


Anti-Pattern: Drawer yang Terlihat Benar, Tapi Membawa Masalah

Tidak semua drawer itu “sehat”. Ada beberapa pola yang terlihat rapi di awal, tapi biasanya menyulitkan saat aplikasi tumbuh.

  1. Banyak Drawer Fisik untuk Banyak Fitur.
    Setiap fitur punya drawer sendiri yang dipasang di banyak tempat. Dampaknya:
    • state tersebar.
    • perilaku tidak konsisten.
    • susah mengatur prioritas (misal: cart vs product detail).

Saya lebih suka: satu drawer root, banyak mode.

  1. Drawer Dibuka Tanpa Payload yang Jelas.
    Misalnya:
openDrawer("PRODUCT_DETAIL");

Tanpa payload, drawer sering bergantung pada state lain “di luar kontrak”, dan itu rawan mismatch data, terutama saat refresh atau switching cepat.

  1. Drawer Menggantikan Semua Halaman.
    Menaruh semua hal ke drawer biasanya bukan optimasi, tapi over-engineering. Konten panjang tetap lebih “jujur” di page.

Accessibility: Drawer yang Baik Harus Bisa Diakses dengan Layak

Drawer itu “panel yang menumpuk di atas UI”. Karena itu, ia punya tanggung jawab accessibility yang sering tidak terlihat.

Beberapa hal yang (menurut saya) minimal harus ada:
• Focus trap: ketika drawer terbuka, fokus keyboard tidak “kabur” ke elemen di belakang.
• ESC untuk close: user harus bisa menutup tanpa mouse.
• Role dan ARIA yang tepat:

role="dialog"
aria-modal="true"

• Restore focus: ketika drawer ditutup, fokus kembali ke elemen pemicu.

Ini bukan soal perfeksionis. Ini soal drawer dipakai sebagai jalur transaksi; artinya ia harus bisa dipakai dengan nyaman oleh lebih banyak kondisi pengguna.


Context vs Zustand untuk Drawer: Pilihan yang Sering Terlihat Sepele

Saya tidak menganggap Context itu buruk. Context cocok untuk banyak hal. Tapi drawer punya karakter yang khas: global, sering berubah, dan diakses banyak komponen.

Context cocok jika:
• drawer sederhana.
• jarang berubah.
• consumer sedikit.
• aplikasi kecil.

Context mulai terasa berat jika:
• drawer sering open/close.
• banyak consumer ikut re-render.
• state makin kompleks (payload, multi-mode, side effect).

Zustand terasa pas untuk drawer karena:
• tanpa provider layering.
• seleksi state granular (mengurangi re-render).
• logic terisolasi (lebih mudah dirawat).

Di sini bukan “mana yang paling benar”, tapi “mana yang paling tenang untuk operasional harian”.


Mobile Gesture dan Native UX: Kenapa Drawer Terasa Alami

Ada alasan drawer terasa nyaman di e-commerce mobile: karena mental model user sudah terbentuk.

User terbiasa dengan:
• swipe dari sisi.
• swipe down untuk menutup.
• panel bertumpuk (bottom sheet, panel, overlay).

Drawer mudah “menumpang” di kebiasaan itu. Full page juga bisa bagus, tapi untuk aksi cepat (lihat detail, tambah cart, pilih varian), drawer sering terasa lebih dekat ke pengalaman native.

Kalau drawer dipoles dengan baik (gesture close, animasi yang halus, fokus yang rapi), e-commerce web bisa terasa seperti aplikasi—tanpa memaksa user meninggalkan pola web yang sudah familiar (URL dan share link).


Refleksi Akhir

Kalau disederhanakan, pilihan drawer vs full page bukan soal UI, tapi soal niat user.
• Jika user ingin membaca → full page.
• Jika user ingin bertindak → drawer.

Drawer yang baik biasanya:
• state-driven.
• type-safe.
• accessible.
• mobile-friendly.
• dan tidak serakah menggantikan semua hal.

Dan ketika semua itu terkumpul, drawer bukan lagi sekadar komponen UI, tapi arsitektur interaksi.