EmpireUI
Get Pro
← Blog8 min read#glassmorphism#pricing#table

Glassmorphism Pricing Table: Frosted Glass Cards With Highlight Plan

Build a frosted-glass pricing table in React with a highlighted "Pro" card, backdrop-filter blur, and Tailwind — step by step with working code.

frosted glass pricing cards on dark gradient background

Why Glassmorphism Works So Well for Pricing Tables

Pricing tables are high-stakes UI. You're asking someone to hand over money — so the card they stare at before clicking 'Subscribe' needs to feel premium. Glassmorphism does that better than almost any other aesthetic right now, and it's not subtle about it. The frosted panels, the soft light bleed, the transparency that lets the background breathe — it all screams 'this product is worth it' before the user reads a single feature bullet.

Look, I've built pricing tables in flat design, neumorphism, neobrutalism, and everything in between. The one that consistently converts better in A/B tests, at least for SaaS products targeting designers and developers, is the frosted glass variant. You can explore why that is from a design theory angle on Empire UI's glassmorphism section, but in practice it comes down to depth cues. Humans associate physical depth with quality.

That said, the technique has a real failure mode: overdo the blur and you get muddy illegible cards. Keep backdrop-filter: blur() between 8px and 20px in almost every case. Going to 40px looks cool in a screenshot and catastrophic in production.

One more thing — glassmorphism pricing tables need a background worth blurring. A solid #1a1a2e navy doesn't give you much. You want a gradient, an image, or animated blobs. The blur is only meaningful when it's distorting something interesting.

Setting Up the Foundation: Background and Base Styles

Before you touch the cards, nail the background. This is the step most tutorials skip, and then they wonder why their glass effect looks flat. You need a colorful gradient or blob layer behind everything — the backdrop-filter is what gives you the glass; the background is what makes that glass visible.

Here's the base setup I'd use for a React + Tailwind project. This gives you a dark purple-to-blue gradient with two soft blob shapes that the cards can blur into:

// app/pricing/page.tsx
export default function PricingPage() {
  return (
    <div className="relative min-h-screen bg-[#0d0d1a] overflow-hidden flex items-center justify-center px-4 py-20">
      {/* Background blobs */}
      <div className="absolute top-[-100px] left-[-100px] w-[500px] h-[500px] rounded-full bg-purple-600/30 blur-[120px] pointer-events-none" />
      <div className="absolute bottom-[-80px] right-[-60px] w-[400px] h-[400px] rounded-full bg-blue-500/20 blur-[100px] pointer-events-none" />

      <PricingCards />
    </div>
  )
}

Worth noting: those blobs use Tailwind's arbitrary value syntax for blur-[120px] — you need Tailwind v3.2+ for that to work without a plugin. If you're on an older version, drop an inline style or extend your config. Also make sure overflow-hidden is on the wrapper or your blobs will create horizontal scroll.

The pointer-events-none on the blobs is not optional if you ever add hover states to elements inside. I've seen people skip it and spend an hour debugging why their button won't click.

Building the Glass Card Component

The card itself is where all the interesting CSS lives. Glass effect requires three things working together: a semi-transparent background, backdrop-filter: blur(), and a subtle border that catches the light. Miss any one of these and it stops looking like glass and starts looking like a broken opacity slider.

Here's a reusable PricingCard component. The highlighted prop drives the 'Pro' treatment — brighter border, slight scale-up, a colored glow:

interface PricingCardProps {
  tier: string
  price: string
  period?: string
  features: string[]
  cta: string
  highlighted?: boolean
}

export function PricingCard({
  tier,
  price,
  period = '/mo',
  features,
  cta,
  highlighted = false,
}: PricingCardProps) {
  return (
    <div
      className={[
        'relative rounded-2xl p-8 flex flex-col gap-6 transition-transform duration-300',
        // Glass base
        'backdrop-blur-[16px] bg-white/5 border',
        // Conditional highlight
        highlighted
          ? 'border-purple-400/60 shadow-[0_0_40px_rgba(168,85,247,0.25)] scale-105'
          : 'border-white/10 hover:border-white/20',
      ].join(' ')}
    >
      {highlighted && (
        <span className="absolute -top-3 left-1/2 -translate-x-1/2 bg-purple-500 text-white text-xs font-semibold px-3 py-1 rounded-full">
          Most Popular
        </span>
      )}

      <div>
        <p className="text-sm font-medium text-white/50 uppercase tracking-widest">{tier}</p>
        <p className="text-5xl font-bold text-white mt-1">
          {price}
          <span className="text-lg font-normal text-white/40">{period}</span>
        </p>
      </div>

      <ul className="flex flex-col gap-3 flex-1">
        {features.map((f) => (
          <li key={f} className="flex items-center gap-2 text-white/70 text-sm">
            <CheckIcon className="w-4 h-4 text-purple-400 shrink-0" />
            {f}
          </li>
        ))}
      </ul>

      <button
        className={[
          'w-full py-3 rounded-xl text-sm font-semibold transition-all duration-200',
          highlighted
            ? 'bg-purple-500 hover:bg-purple-400 text-white shadow-lg shadow-purple-500/30'
            : 'bg-white/10 hover:bg-white/20 text-white/80',
        ].join(' ')}
      >
        {cta}
      </button>
    </div>
  )
}

Honestly, the scale-105 on the highlighted card is the most impactful 10 characters you'll write in this whole component. Without it the three cards all look equal and the eye doesn't know where to land. With it, the Pro card commands attention immediately. You don't need animation libraries for this — pure CSS transform does it.

Quick aside: text-white/70 is the Tailwind shorthand for rgba(255,255,255,0.7). The whole design leans on white-with-opacity text rather than literal grays. This is intentional — it keeps the text color contextually tied to whatever the card is blurring behind it, which makes the glass effect feel coherent instead of pasted-on.

Assembling the Three-Tier Layout

Three tiers is the SaaS standard for good reason. One card is a non-choice. Two forces a binary. Three gives you anchoring — the middle card looks reasonable because it's sandwiched between something cheap and something expensive. Make sure your highlighted plan is always center.

Here's the full PricingCards assembly:

const plans = [
  {
    tier: 'Starter',
    price: '$9',
    features: [
      '3 projects',
      '5GB storage',
      'Email support',
      'Basic analytics',
    ],
    cta: 'Get started',
    highlighted: false,
  },
  {
    tier: 'Pro',
    price: '$29',
    features: [
      'Unlimited projects',
      '50GB storage',
      'Priority support',
      'Advanced analytics',
      'Custom domain',
      'Team seats (up to 5)',
    ],
    cta: 'Start free trial',
    highlighted: true,
  },
  {
    tier: 'Enterprise',
    price: '$99',
    features: [
      'Everything in Pro',
      '500GB storage',
      'Dedicated support',
      'SSO / SAML',
      'SLA guarantee',
      'Unlimited seats',
    ],
    cta: 'Contact sales',
    highlighted: false,
  },
]

export function PricingCards() {
  return (
    <div className="grid grid-cols-1 md:grid-cols-3 gap-6 max-w-5xl w-full items-center">
      {plans.map((plan) => (
        <PricingCard key={plan.tier} {...plan} />
      ))}
    </div>
  )
}

In practice, items-center on the grid is what keeps the taller Pro card (which has more features) centered relative to the shorter flanking cards. If you use items-stretch instead, all cards grow to the same height and the internal layout of each card handles spacing — both approaches work, pick based on whether you want equal heights or natural heights with centered alignment.

The mobile layout collapses to a single column. Worth thinking about which card appears first on mobile since scale-105 doesn't help you there. I'd consider reordering the array on mobile — or at minimum, putting the Pro card first in DOM order so it's above the fold.

Handling the Backdrop Filter Browser Support Gotcha

As of 2026, backdrop-filter is supported in every major browser including Firefox (since v103). But there's still a caveat worth knowing: it requires a stacking context. If a parent element somewhere up the tree has transform, filter, or will-change: transform applied, backdrop-filter on a descendant can silently fail in Safari.

The symptom is your glass card looking completely opaque and dark — the blur isn't rendering and the background is just showing through as a solid fill. Nine times out of ten, search your component tree for a transform or will-change on any ancestor. Remove it, or restructure so the glass card is not a descendant of that element.

/* Global fallback for reduced-motion environments */
@media (prefers-reduced-motion: reduce) {
  .glass-card {
    backdrop-filter: none;
    background-color: rgba(255, 255, 255, 0.08);
  }
}

If you want to test your glassmorphism components without setting up the full React environment, the glassmorphism generator lets you dial in blur, opacity, and border values visually and copy out ready-to-use CSS. Genuinely useful for prototyping before you commit to a design direction.

One more thing — on lower-end Android devices backdrop-filter with a blur value above 20px can tank frame rate. Keep blur at 16px or below for cards that appear on scroll, or wrap the blur in a @media (hover: hover) query so mobile gets the simpler non-blurred version.

Adding a Billing Toggle (Monthly / Annual)

No pricing table in 2026 ships without a billing period toggle. It's table stakes for SaaS. The pattern is simple: local state, two computed price sets, a toggle button. Here's a minimal implementation that doesn't reach for a library:

import { useState } from 'react'

const monthlyPrices = { Starter: '$9', Pro: '$29', Enterprise: '$99' }
const annualPrices  = { Starter: '$7', Pro: '$23', Enterprise: '$79' }

export function PricingPage() {
  const [annual, setAnnual] = useState(false)

  return (
    <div className="flex flex-col items-center gap-10">
      {/* Toggle */}
      <div className="flex items-center gap-3 text-sm text-white/60">
        <span className={!annual ? 'text-white' : ''}>Monthly</span>
        <button
          onClick={() => setAnnual(!annual)}
          className={[
            'relative w-12 h-6 rounded-full transition-colors duration-300',
            annual ? 'bg-purple-500' : 'bg-white/20',
          ].join(' ')}
          aria-label="Toggle billing period"
        >
          <span
            className={[
              'absolute top-1 w-4 h-4 rounded-full bg-white transition-transform duration-300',
              annual ? 'translate-x-7' : 'translate-x-1',
            ].join(' ')}
          />
        </button>
        <span className={annual ? 'text-white' : ''}>
          Annual <span className="text-purple-400 font-semibold">–20%</span>
        </span>
      </div>

      <PricingCards prices={annual ? annualPrices : monthlyPrices} />
    </div>
  )
}

That translate-x-7 on the toggle thumb is 28px — the exact value to push a 16px thumb to the right end of a 48px track, with 4px padding on each side. Tweak if your track width is different. These pixel relationships are worth understanding rather than eyeballing.

What you'll notice is the price text changes instantly. If that feels jarring, wrap the price <p> in a Framer Motion AnimatePresence with a quick 0.15s fade. That said, I've shipped toggle components both ways and users don't complain about the no-animation version. Don't over-engineer it.

Polish: Hover States, Accessibility, and the 'Popular' Badge

The component above is functional. Let's make it feel finished. Three quick wins: hover lift on non-highlighted cards, proper focus rings, and a badge that doesn't break at small viewport widths.

For hover lift, add hover:-translate-y-1 to the non-highlighted card's class list. Pair it with transition-transform duration-200 and it'll feel light and reactive. Don't apply hover lift to the already-scaled highlighted card — it's already elevated, adding another transform on hover looks unstable.

Focus rings on the CTA buttons need to be visible against the dark glass background. The default browser outline often disappears on dark surfaces. Add this to your button:

<button
  className="... focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-purple-400 focus-visible:ring-offset-2 focus-visible:ring-offset-transparent"
>
  {cta}
</button>

The 'Most Popular' badge uses absolute -top-3 which places it 12px above the card's top edge. If your three-column grid has a gap smaller than 24px, the badge can clip into the adjacent card's space. Keep your column gap at gap-6 (24px) minimum, or switch to gap-8. It's one of those things that looks fine at 1440px and breaks at 768px exactly.

If you want to see how far you can push this aesthetic — the three-column frosted card layout is just a starting point — check out the glassmorphism dashboard article for a more complex layout that extends these same primitives. And if you're interested in tooling beyond what's covered here, the box shadow generator pairs well with this kind of dark-themed UI for getting the glow values right without trial and error.

FAQ

Does backdrop-filter work on all browsers for pricing tables?

Yes, as of 2026 all major browsers support backdrop-filter including Firefox since v103. Safari can have issues when a parent element has transform or will-change set — remove those from ancestors if the blur stops rendering.

What blur value should I use for glassmorphism pricing cards?

Stay between 8px and 20px. Anything above 20px starts to hurt legibility and can cause frame-rate drops on mid-range mobile hardware. backdrop-blur-[16px] in Tailwind is a solid default.

How do I make one pricing card stand out from the others?

Use scale-105 to visually elevate the highlighted card, give it a colored border and a box-shadow glow, and add a 'Most Popular' badge positioned above the card. The scale transform alone does most of the work.

Can I use this glassmorphism pricing table with Next.js App Router?

Absolutely. The components are pure client-side React — just add 'use client' at the top of any file that uses useState (the billing toggle). The static card layout with no toggle doesn't need that directive at all.

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

Read next

Glassmorphism Pricing Card: Frosted-Glass Tier Design in ReactGlassmorphism SaaS UI: Frosted Dashboard, Cards and ChartsTailwind Pricing Section: 3-Tier Layout with Annual TogglePricing Table React Component: 3-Tier, Annual Toggle, Highlight