EmpireUI
Get Pro
← Blog7 min read#glassmorphism#bottom-sheet#mobile-ui

Glassmorphism Bottom Sheet: Mobile-First Frosted Panel

Build a glassmorphism bottom sheet with frosted blur, backdrop-filter, and Tailwind CSS. A mobile-first frosted panel pattern that actually works on real devices.

Smartphone displaying a frosted glass bottom sheet panel over a blurred background

What Is a Glassmorphism Bottom Sheet

Honestly, bottom sheets are one of the most underrated mobile UI patterns — and when you layer glassmorphism on top, you get something that feels genuinely premium without much effort.

A bottom sheet is a panel that slides up from the bottom edge of the viewport. It's a staple in native mobile apps (Android's Material Design calls it out explicitly), but it translates well to mobile web and PWAs. The glassmorphism treatment — frosted blur, semi-transparent background, subtle border — turns a plain drawer into something that feels like iOS system UI.

If you want the background on the visual style itself, check out what is glassmorphism before going further. The short version: it's backdrop-filter: blur() plus a translucent fill, creating the illusion of frosted glass over layered content.

The combination works especially well on mobile because the content behind the sheet remains partially visible, giving users spatial context. They know they haven't navigated away. They can see where they came from.

Why Mobile-First Matters for This Pattern

Bottom sheets are touch-first by design. Drag handles, swipe-to-dismiss, snap points — all of these interactions are native to fingers, not mouse pointers. Building a glassmorphism bottom sheet without thinking mobile-first produces something that looks fine in Figma and feels terrible on a real phone.

There's a real constraint to keep in mind: backdrop-filter performance on mid-range Android devices is genuinely rough. A blur(20px) that renders smoothly on a MacBook Pro can stutter on a Pixel 6a. You'll want to keep blur radius in the 8–14px range for mobile, and test on actual hardware — not just DevTools emulation.

The safe-area insets matter too. On iOS with a home indicator, you need padding-bottom: env(safe-area-inset-bottom) or your drag handle sits on top of the gesture bar. This is the kind of detail that separates a component that ships from one that just looks good in a screenshot.

The Core CSS: backdrop-filter and rgba Background

Let's look at the actual styles that make this work. The foundation is two declarations: backdrop-filter for the frosted blur, and an rgba background for the translucency. Everything else — border, shadow, border-radius — is polish.

.glass-bottom-sheet {
  /* Frosted glass foundation */
  background: rgba(255, 255, 255, 0.12);
  backdrop-filter: blur(12px) saturate(160%);
  -webkit-backdrop-filter: blur(12px) saturate(160%);

  /* Border that catches the light */
  border: 1px solid rgba(255, 255, 255, 0.22);
  border-bottom: none;

  /* Shape and depth */
  border-radius: 20px 20px 0 0;
  box-shadow: 0 -4px 32px rgba(0, 0, 0, 0.18);

  /* Safe area on iOS */
  padding-bottom: env(safe-area-inset-bottom);
}

/* Dark mode variant */
@media (prefers-color-scheme: dark) {
  .glass-bottom-sheet {
    background: rgba(15, 15, 20, 0.55);
    border-color: rgba(255, 255, 255, 0.08);
  }
}

The saturate(160%) on the backdrop-filter is optional but makes the underlying colors pop through the glass in a way that reads as more polished. Without it, heavily blurred backgrounds can look washed out. The rgba(255,255,255,0.12) fill is intentionally conservative — go too opaque and you lose the glass effect entirely, go too transparent and it reads as a plain dark overlay.

One thing worth noting: backdrop-filter requires the element to have a stacking context. If your bottom sheet isn't rendering the blur, add will-change: transform or isolation: isolate to force it. This trips people up more than anything else with this effect.

Building the React Component with Tailwind CSS

Here's a working React implementation using Tailwind v4.0.2 with the new backdrop-blur utilities. It includes a drag handle, an open/close toggle, and the safe-area padding setup.

import { useState, useRef } from 'react';

interface GlassBottomSheetProps {
  isOpen: boolean;
  onClose: () => void;
  children: React.ReactNode;
  title?: string;
}

export function GlassBottomSheet({
  isOpen,
  onClose,
  children,
  title,
}: GlassBottomSheetProps) {
  return (
    <>
      {/* Scrim overlay */}
      {isOpen && (
        <div
          className="fixed inset-0 z-40 bg-black/30"
          onClick={onClose}
          aria-hidden="true"
        />
      )}

      {/* The sheet itself */}
      <div
        role="dialog"
        aria-modal="true"
        aria-label={title}
        className={[
          'fixed bottom-0 left-0 right-0 z-50',
          'rounded-t-[20px]',
          // Glassmorphism
          'bg-white/10 backdrop-blur-md border border-white/20 border-b-0',
          'shadow-[0_-4px_32px_rgba(0,0,0,0.18)]',
          // Safe area
          'pb-[env(safe-area-inset-bottom)]',
          // Transition
          'transition-transform duration-300 ease-out will-change-transform',
          isOpen ? 'translate-y-0' : 'translate-y-full',
        ].join(' ')}
      >
        {/* Drag handle */}
        <div className="flex justify-center pt-3 pb-1">
          <div className="w-10 h-1 rounded-full bg-white/40" />
        </div>

        {/* Header */}
        {title && (
          <div className="px-5 pt-2 pb-3">
            <h2 className="text-base font-semibold text-white/90">{title}</h2>
          </div>
        )}

        {/* Content */}
        <div className="px-5 pb-6 overflow-y-auto max-h-[70vh]">
          {children}
        </div>
      </div>
    </>
  );
}

The will-change-transform is there to promote the sheet to its own GPU layer, which gives you smooth 60fps slide animations even on mobile. Avoid will-change: all — it's too broad and wastes memory. Transform-only is fine here since that's all we're animating.

The scrim overlay deserves its own z-index thought. The sheet is z-50, scrim is z-40. Don't flatten them into the same layer or click-outside-to-close breaks in unpredictable ways depending on how your other components stack.

Snap Points and Swipe-to-Dismiss Behavior

A static open/close toggle is fine for simple use cases. But if your sheet contains variable-length content — a list of options, a mini cart, a filter panel — you'll want snap points. The idea is simple: the sheet can rest at multiple heights (say, 40% and 80% of viewport), and dragging it snaps to the nearest point.

Implementing snap points properly requires tracking touch events. Here's the pattern without a library: store the start Y position on touchstart, compute the delta on touchmove, and on touchend decide which snap point to land on based on velocity and position. The velocity check is important — a fast flick down should dismiss even if the finger only moved 30px.

Should you use a library for this? Probably yes, unless drag physics is specifically what you're building. @radix-ui/react-dialog combined with framer-motion's drag constraints gives you snap points, momentum, and accessibility for free. Rolling your own is a good learning exercise but adds maintenance surface. That said, if you're avoiding the bundle weight, a 60-line touch handler is doable.

Whichever route you take, make sure Escape key closes the sheet on desktop. Screen reader users need aria-modal="true" and focus to be trapped inside the open panel. These aren't optional polish — they're required for anyone using a keyboard or assistive technology.

Glassmorphism Bottom Sheet vs Other Mobile Patterns

It's worth thinking about when a bottom sheet is actually the right call versus other patterns. Modals work better when the action is blocking and you need full attention. Sidebars work better for navigation. Bottom sheets work best for contextual actions tied to the current view — share options, quick settings, item detail previews.

The glassmorphism treatment shifts the visual weight compared to a solid-background sheet. Because the content behind remains visible, the pattern communicates "temporary" and "contextual" more strongly than an opaque drawer. That's often exactly what you want. If you want more contrast between the sheet and the background, you could try a glassmorphism vs neumorphism comparison to find the right depth level for your use case.

One pattern that pairs really well here: stacking a glassmorphism bottom sheet with a particles background underneath. The moving particles visible through the frosted glass create a depth effect that static backgrounds can't match. It's a bit extra, but it reads as genuinely premium on mobile.

Dark Mode and Theming the Frosted Panel

Dark mode changes the math on glassmorphism significantly. A light glass effect (rgba(255,255,255,0.12)) reads beautifully on a vibrant dark background, but switch to a light background and it disappears entirely. For a proper theme toggle in React, you'll need two sets of glass values.

The dark-mode bottom sheet should use rgba(20, 20, 30, 0.55) as the base fill rather than white. The border shifts to rgba(255, 255, 255, 0.08) — barely visible, just enough to define the edge. Keep blur at the same 12px — it reads consistently in both modes.

In Tailwind v4.0.2, you can do this with the dark: variant: bg-white/10 dark:bg-slate-900/55 border-white/20 dark:border-white/8. The Tailwind bg-opacity approach from v2 is gone — use the slash syntax. If you're still on backdrop-blur-sm in v3, note that v4 ships backdrop-blur-md as blur(12px) which aligns better with what we want here.

Empire UI Glassmorphism Components You Can Use Today

If you don't want to build this from scratch, Empire UI ships a collection of free glassmorphism components including cards, modals, and navigation elements — all under MIT license, all built with Tailwind and React. The bottom sheet component follows the same patterns described here, with snap points and dark mode baked in.

The component library targets the 40 visual styles natively — glassmorphism is one of them, alongside neobrutalism, claymorphism, neumorphism, and others. Switching styles is a single prop change, which makes it straightforward to prototype different visual directions without rewriting markup. Worth a look if you're in the early design phase and want to compare how the frosted glass bottom sheet looks against a, say, clay-style alternative.

The bottom sheet specifically supports a snapPoints array prop, a defaultOpen flag, and a backdropBlur number that maps to Tailwind's blur scale. You get the env(safe-area-inset-bottom) padding automatically. It's tested on iOS Safari 17 and Chrome for Android 124 — the two environments where backdrop-filter behavior diverges the most.

FAQ

Does backdrop-filter work on all mobile browsers?

iOS Safari has supported it since Safari 9 with the -webkit- prefix. Chrome for Android added support in Chrome 76. Firefox on Android added it in Firefox 103. You'll want both backdrop-filter and -webkit-backdrop-filter declarations. The only real gap is older Android WebView — if you're targeting WebView below Chrome 76, add a fallback solid background via @supports not (backdrop-filter: blur(1px)).

What blur radius should I use for mobile performance?

Keep it at 8–14px for mobile. A blur(20px) can cause frame drops on mid-range Android devices. blur(12px) with saturate(160%) gives a convincing glass effect without the GPU overhead. Always test on a real mid-range device — DevTools emulation won't surface the performance issue.

How do I handle the iOS home indicator safe area?

Add padding-bottom: env(safe-area-inset-bottom) to the sheet element. In Tailwind, there's no built-in utility for this, so add it via arbitrary CSS: pb-[env(safe-area-inset-bottom)] in Tailwind v3+/v4. You also need <meta name='viewport' content='viewport-fit=cover'> in your HTML head or the env() value returns 0.

Why isn't my backdrop-filter blur rendering?

The most common reason: the element doesn't have a stacking context. Add isolation: isolate or will-change: transform to force one. Also check that the element has a background set — backdrop-filter without any background fill renders nothing visible. A third cause: the parent has overflow: hidden which clips the blur before it renders.

Can I have multiple snap heights on the bottom sheet?

Yes, and it's worth the extra code. Track touchstart Y, compute delta on touchmove, and on touchend snap to the nearest breakpoint. Common heights are 40vh for a peek state and 85vh for expanded. Include a velocity threshold so a fast flick downward dismisses the sheet even if the drag distance was short — around 0.5px/ms is a reasonable cutoff.

How do I make the glassmorphism bottom sheet accessible?

Add role='dialog' and aria-modal='true' to the panel. Trap focus inside the open sheet using a focus trap library or manual tab-key interception. Close on Escape keydown. The scrim overlay should have aria-hidden='true' so screen readers don't announce it. When the sheet closes, return focus to the element that triggered it.

Free components in 40 styles
React & Tailwind, copy-paste ready.
Browse →

Read next

Glassmorphism Video Overlay: Controls on Frosted PanelGlassmorphism Carousel: Slider Component with Frosted CardsDrag-and-Drop Sortable Lists in React: No Library RequiredImage Gallery with Lightbox: Accessible Photo Viewer in React