New

Feature Gate

Locks premium content behind a glass overlay and provides a path to upgrade. Reuses surface and accent tokens; no new tokens required.

Inline gate

The inline variant blurs the locked content in place and overlays a glass CTA. Use this for feature sections within a larger screen — it preserves layout while signalling premium content.

Team Insights

12
Members
87%
Utilization
34 pts
Velocity

Team Insights

See how your team is performing across all projects.

Sheet paywall

The sheet variant opens a focused modal with feature highlights and a pricing CTA. Use this when the user explicitly triggers a locked action, such as tapping a locked nav item.

Anatomy

LayerDescriptionToken
Blur contentChildren rendered at blur(4px) with pointer-events disabled
Glass surfacebackdrop-filter blur(20px) saturate(160%)--surface-glass
Specular edgeinset 0 1px 0 rgba(255,255,255,0.08) — hairline on top
Icon well44×44 rounded rect, accent-tinted background--accent / 0.15
Upgrade buttonSolid accent fill, white label--accent

Usage guidelines

  • Always show enough of the locked content to communicate value — don't hide it entirely.
  • Keep the feature name concise (2–4 words). Put extra context in the description line.
  • The upgrade CTA must always be visible and tappable, even on small screens.
  • Apply prefers-reduced-transparency fallback: replace backdrop-filter with a solid --surface background.
  • Do not gate core navigation or destructive/dangerous actions — only feature upsells.

Implementation

FeatureGate.tsx
tsx
"use client";

import { useState, ReactNode } from "react";
import { Lock, Sparkles } from "lucide-react";

interface FeatureGateProps {
  feature: string;
  isLocked?: boolean;
  description?: string;
  onUpgrade?: () => void;
  children: ReactNode;
}

export function FeatureGate({
  feature,
  isLocked = true,
  description,
  onUpgrade,
  children,
}: FeatureGateProps) {
  if (!isLocked) return <>{children}</>;

  return (
    <div style={{ position: "relative", borderRadius: 12, overflow: "hidden" }}>
      {/* Blurred content */}
      <div style={{ filter: "blur(4px)", pointerEvents: "none", userSelect: "none" }}>
        {children}
      </div>

      {/* Glass overlay */}
      <div
        style={{
          position: "absolute",
          inset: 0,
          display: "flex",
          flexDirection: "column",
          alignItems: "center",
          justifyContent: "center",
          gap: 12,
          backdropFilter: "blur(20px) saturate(160%)",
          WebkitBackdropFilter: "blur(20px) saturate(160%)",
          background: "rgb(var(--surface) / 0.7)",
          // Specular top edge
          boxShadow: "inset 0 1px 0 rgba(255,255,255,0.08)",
          borderRadius: 12,
        }}
      >
        <div
          style={{
            width: 44,
            height: 44,
            borderRadius: 12,
            background: "rgb(var(--accent) / 0.15)",
            display: "flex",
            alignItems: "center",
            justifyContent: "center",
            color: "rgb(var(--accent))",
          }}
        >
          <Lock size={20} />
        </div>

        <div style={{ textAlign: "center", maxWidth: 240 }}>
          <p style={{ fontWeight: 600, fontSize: 14, marginBottom: 4 }}>
            {feature}
          </p>
          {description && (
            <p style={{ fontSize: 12, color: "rgb(var(--text-secondary))", lineHeight: 1.5 }}>
              {description}
            </p>
          )}
        </div>

        <button
          onClick={onUpgrade}
          style={{
            display: "flex",
            alignItems: "center",
            gap: 6,
            padding: "8px 16px",
            borderRadius: 8,
            background: "rgb(var(--accent))",
            color: "#fff",
            fontSize: 13,
            fontWeight: 600,
            border: "none",
            cursor: "pointer",
          }}
        >
          <Sparkles size={14} />
          Upgrade to Pro
        </button>
      </div>
    </div>
  );
}
PropTypeDefaultDescription
featurestringHuman-readable name of the gated feature, shown in the upgrade prompt.
isLockedbooleantrueWhen false, renders children without any overlay.
variant"inline" | "sheet""inline"Inline overlays the locked content; sheet opens a full paywall modal.
onUpgrade() => voidCallback fired when the user taps the upgrade CTA.
descriptionstringOptional supporting text shown below the feature name in the overlay.