Why I Prefer Drawer Over Full Page

February 10, 2026⏱️ 11 min read

On using drawers as transactional UI in web e-commerce, discussed through the lens of UX, state architecture, TypeScript, accessibility, and mobile-native experience.

Why I Prefer Drawer Over Full Page

Notes on Transactional UI in Web E-Commerce (with TypeScript)

It has been quite a while since I last wrote a technical article. Not because I ran out of topics, but because over time I’ve felt that many interesting technical decisions are rarely discussed honestly—especially UI decisions that look “small” on the surface, yet have long-term impact on user experience and code structure.

One of those decisions is choosing drawers over full pages in the context of web e-commerce.

This article is not an invitation to abandon full pages entirely. It is more of a personal note: why, in many e-commerce cases, drawers feel more aligned with transactional UI, and how this UI choice gradually influences the way I write TypeScript and manage application state.


Full Pages Are Not the Problem, but Often Not the Right Fit

Full pages are the default approach on the web—and there is nothing wrong with that.

For:
• Articles.
• Landing pages.
• Informational pages.
• Long-form content meant to be read slowly.

full pages are often the best choice.

What starts to feel “off” is when the full-page pattern is applied directly to e-commerce, especially for interactions that are fast and repetitive.

A simple example:
• Users scroll through a product list.
• Click a product.
• Navigate to a detail page.
• Go back to the previous page.
• Scroll again.
• Repeat the same process.

Technically, this works.
From a UX perspective, it often introduces friction that could be avoided.


Drawers Are Not Page Replacements, but Interaction Layers

A common mistake when discussing drawers is treating them as page replacements. I see drawers more accurately as interaction layers.

Drawers work well for:
• Product details (quick view).
• Cart.
• Quick checkout.
• Chat / communication.
• Information previews.

Full pages remain important for:
• SEO.
• Shareable URLs.
• Deep reading.

So this is not “drawer versus page” as a competition, but rather about knowing when we need navigation and when we need transactions.


Transactional UI vs Navigational UI

At this point, I find it helpful to distinguish between two different “feelings” of UI:

Navigational UI.
• Primary goal: exploration.
• Users move from one page to another.
• Examples: blogs, articles, about pages.

Transactional UI.
• Primary goal: completing an intention.
• Users already know what they want to do.
• Examples: add to cart, select variants, checkout.

Drawers often excel in transactional UI because they:
• Minimize context switching.
• Feel faster in perceived performance.
• Focus on action rather than exploration.

In short, in e-commerce users are often not trying to “navigate pages”, but to complete an intention.


Common Issue: Drawers Without Proper Typing

Many drawer implementations fail not because of UI, but because the state is poorly designed. Drawers are often treated like this:

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

Then they grow into:
• isCartOpen
• isProductOpen
• isCheckoutOpen

Slowly, this turns into boolean hell.

In small applications this may feel safe. As features and entry points grow, state that is “just boolean” tends to make drawer behavior unpredictable.


Drawer as State, Not Just a UI Component (TypeScript)

An approach that feels more stable to me is treating the drawer as explicit, typed state.

A simple TypeScript example:

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

Why I like this model:
• When opening PRODUCT_DETAIL, the payload is mandatory.
• When opening CART, no payload is required (and should not be forced).
• The drawer never enters a “half-open” state with unclear data.

Here, TypeScript is not just about autocompletion—it helps enforce a clear behavioral contract for the application.


A Drawer Structure I Consider “Healthy”

I usually prefer a structure like this:

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

• DrawerRoot is responsible only for rendering based on state.
• Each drawer is separated by feature.
• drawer.store.ts holds state and actions.
• drawer.types.ts defines the union types (the core of type safety).

This structure is not flashy, but in my experience it tends to age well.


Drawer Store (A Simple Zustand Example)

I personally feel comfortable using Zustand for this kind of state because:
• it avoids provider nesting.
• state logic is easier to isolate.
• re-renders can be more granular compared to a broadly used Context.

A minimal store example:

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 }),
}));

A small note: this is not “many drawers”, but one drawer with multiple modes—which tends to make control much more predictable.


DrawerRoot: One Component for All Drawers

"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>
  );
}

I like this pattern because the entire application has a single “drawer gate”. Behavior becomes consistent, and debugging feels more humane.


Opening a Drawer from a Card (State-Driven Entry)

From an item card, simply call:

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

<button onClick={() => openProductDetail(product.id)}>
  View Details
</button>

No URL change. Fast. Natural.


Hybrid Drawer: When Routes and State Meet

This is the part that really convinced me: drawers and routing do not have to cancel each other out.

The basic idea:
• User clicks a product → drawer opens (state-driven).
• User opens /product/[slug] directly → the same drawer opens (route-driven).

One UI.
One logic.
Two entry points.

Client trigger example:

"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;
}

Server page rendering:

// 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} />;
}

With this approach:
• Share links still work.
• Refresh is safe.
• The UX remains drawer-based.

For me, this is where web-native concepts (URL, SEO) meet app-like experience (drawers, fast transactions).


Why Drawers Feel Faster, Even with the Same Backend

Interestingly, drawers often feel faster even when:
• The API is the same.
• The data is the same.
• The network requests are the same.

This happens because:
• There is no large page unmount.
• User context remains intact.
• Visual transitions are lighter.

This resembles optimistic UI—but at the experience level, not the data level.


When Full Pages Make More Sense

It’s important to be honest: drawers are not a solution for everything.

Full pages make more sense when:
• SEO is the primary goal.
• Content is long and narrative.
• Sharing links is central.
• Information structure is complex.

Forcing everything into drawers usually harms UX rather than improving it.


Impact on Codebase and Maintainability

From a developer’s perspective, a well-designed drawer:
• Reduces page duplication.
• Makes logic more reusable.
• Clarifies state flow.
• Simplifies testing.

What matters most is not the drawer itself, but the state-driven mindset behind it.


Closing

In e-commerce, users do not come to “navigate pages”.
They come with an intention.

The role of UI is not to showcase navigation,
but to help that intention complete with as little friction as possible.

That is why, in transactional contexts,
I often prefer drawers over full pages.


Additional Technical Notes

I added this section because some important details are often overlooked—not due to lack of knowledge, but because the focus is usually on “making it work first”. When drawers become central to transactions, these details gradually define quality.


Anti-Patterns: Drawers That Look Right but Cause Problems

Not all drawers are healthy. Some patterns look clean at first but tend to cause issues as the application grows.

  1. Multiple Physical Drawers for Multiple Features.
    Each feature owns its own drawer, mounted in different places. This leads to:
    • scattered state.
    • inconsistent behavior.
    • difficulty managing priority (e.g. cart vs product detail).

I prefer one drawer root with multiple modes.

  1. Opening Drawers Without Clear Payloads.
    For example:
openDrawer("PRODUCT_DETAIL");

Without payloads, drawers often depend on “out-of-contract” state, which becomes fragile—especially during refreshes or fast switching.

  1. Using Drawers for Everything.
    Putting everything into drawers is rarely optimization; it is often over-engineering. Long-form content remains more honest on full pages.

Accessibility: Drawers Must Be Properly Usable

A drawer is a panel layered on top of UI. Because of that, it carries accessibility responsibilities that are easy to miss.

At minimum, I consider these essential:
• Focus trap: when the drawer opens, keyboard focus must not escape to the background.
• ESC to close: users should be able to close without a mouse.
• Proper ARIA roles:

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

• Restore focus: when the drawer closes, focus should return to the trigger element.

This is not about perfection—it’s about drawers being reliable transaction paths for more usage conditions.


Context vs Zustand for Drawers: A Subtle Choice

I do not consider Context a bad tool. It fits many use cases well. But drawers have a specific character: global, frequently changing, and accessed by many components.

Context works well when:
• The drawer is simple.
• State changes are infrequent.
• Consumers are limited.
• The application is small.

Context starts to feel heavy when:
• Drawers open and close frequently.
• Many consumers re-render together.
• State grows complex (payloads, modes, side effects).

Zustand often feels more suitable for drawers because:
• No provider layering.
• Granular state selection.
• Isolated logic that’s easier to maintain.

This is less about “right vs wrong”, and more about what feels stable in daily work.


Mobile Gestures and Native UX: Why Drawers Feel Natural

There is a reason drawers feel comfortable on mobile e-commerce: the mental model is already there.

Users are familiar with:
• Swiping from the side.
• Swiping down to close.
• Stacked panels (bottom sheets, overlays).

Drawers naturally fit into these habits. Full pages can still work, but for quick actions (view details, add to cart, select variants), drawers often feel closer to native behavior.

When polished well (gesture close, smooth animation, clean focus handling), drawer-based web e-commerce can feel app-like—without abandoning web fundamentals like URLs and shareable links.


Final Reflection

When simplified, the choice between drawers and full pages is not about UI—it’s about user intention.
• If users want to read → full page.
• If users want to act → drawer.

Good drawers tend to be:
• state-driven.
• type-safe.
• accessible.
• mobile-friendly.
• and not greedy in replacing everything.

When all of these come together, a drawer becomes more than a UI component—it becomes interaction architecture.