Back to home

Documentation

Complete guide to installing BlurWrapper, PaywallBanner, FeatureTooltip, UpgradeModal, and UsageProgress in your Next.js project

React 19Next.js 14+TypeScriptshadcn/ui

Installation

Add Feature Lock components to your project in seconds

Prerequisites

Make sure you have a Next.js project with shadcn/ui set up. If not, initialize it first:

npx shadcn@latest init

Run the following command to install the BlurWrapper component and its dependencies:

npx shadcn@latest add https://feature-lock.griffen.codes/r/blur-wrapper

✨ What gets installed?

  • • BlurWrapper component at @/components/blurWrapper/blur-wrapper
  • • Required shadcn/ui components (Button, Dialog)
  • • All necessary peer dependencies (@radix-ui/react-dialog, lucide-react)

UpgradeModal API

Prop reference for the standalone upgrade dialog experience

Core Props

plans
UpgradePlan[]
Plan definitions rendered inside the modal
trigger
React.ReactNode
Optional trigger rendered as a dialog trigger via asChild
open
boolean
Controlled open state; pair with onOpenChange for external toggles
defaultOpen
boolean
Initial open state when the modal manages its own visibility
onOpenChange
(open: boolean) => void
Fired whenever the dialog open state changes
onClose
() => void
Invoked after the modal closes (user action or programmatic)
onPlanSelected
(planId: string) => void
Receive the plan id after a CTA resolves successfully

Content & Styling

title
"Unlock more with Feature Lock"
string
Modal heading shown at the top of the dialog
subtitle
string
Optional subheading styled in the primary color
description
string
Supporting paragraph that explains the upgrade value
badge
"Upgrade"
string | null
Small badge rendered above the title; set to null to hide
highlightLabel
"Everything in Free, plus"
string
Label displayed before each plan's feature list
finePrint
string
Fine print text displayed underneath the plan grid
supportEmail
string
Adds a support callout with a clickable mailto link
supportLabel
"Need help? Reach out:"
string
Label shown before the support email link
footer
React.ReactNode
Custom footer content (secondary actions, guarantees, etc.)
className
string
Extra classes applied to the dialog content wrapper
contentClassName
string
Extra classes for the inner content layout
planCardClassName
string
Extra classes appended to every plan card
showCloseButton
true
boolean
Toggle the default top-right close button

Behavior

autoCloseOnSelect
true
boolean
Automatically closes the modal when a plan CTA succeeds
resetErrorsOnOpen
true
boolean
Clears pending/error state whenever the modal reopens

UpgradePlan fields

Each entry in plans accepts the following properties:

  • id: unique identifier used for analytics callbacks and pending/error state tracking.
  • name, description, price, period, badge, highlight, footnote: text content for the plan card.
  • features: array of strings or { label, included?, footnote? } objects to render capability lists.
  • ctaLabel, ctaHref, ctaPendingLabel: customize primary CTA text and link behavior.
  • onSelect, onSelectSuccess, onSelectError: async handler and hooks for custom upgrade flows.

UsageProgress API

Props for the quota tracking component

Core Props

tracks
UsageTrack[]
Usage rows displayed with progress bars
variant
"card"
"card" | "inline"
Switch between card layout and inline section
title
"Usage overview"
string
Heading shown above the usage metrics
subtitle
"Stay on top of quota limits and see when to upgrade."
string
Supporting text displayed under the heading
showSummary
true
boolean
Toggle visibility of the summary/info banner
summaryLabel
"Upgrade unlocks"
string
Label displayed in the summary banner
summaryValue
string
Highlighted value shown on the right side of the summary banner
summaryMessage
string
Additional text under the summary label
note
string
Small note displayed alongside CTAs
className
string
Custom classes applied to the UsageProgress wrapper
trackClassName
string
Classes appended to the track list container
summaryClassName
string
Classes appended to the summary/info banner
footerClassName
string
Classes appended to the optional footer region

Actions

ctaLabel
"Upgrade plan"
string
Primary CTA button label
ctaHref
string
Link target if no async handler is provided
ctaPendingLabel
"Working..."
string
Text shown while the CTA handler resolves
onCtaClick
() => Promise<void> | void
Async handler for the primary CTA
onCtaSuccess
() => void
Callback fired after a successful CTA
onCtaError
(error: unknown) => void
Callback fired when the CTA handler throws
secondaryLabel
string
Label for the optional secondary button
onSecondaryClick
() => void
Handler for the secondary action
pending
false
boolean
Force the CTA into a loading state externally

UsageTrack fields

  • label (string): Display name for the quota. value (number) and limit (number) calculate percentages automatically.
  • percentage (number): Override percentage when no limit applies. status controls styling ("ok", "warning","critical").
  • Optional fields: badge, trend ("up", "down","steady"), description. They reinforce messaging without clutter.

Quick Start

Get up and running in minutes with BlurWrapper, PaywallBanner, FeatureTooltip, UpgradeModal, and UsageProgress

BlurWrapper

Dialog mode shows the upgrade prompt in a modal overlay—great for critical actions:

"use client"

import { useState } from "react"
import BlurWrapper from "@/components/blurWrapper/blur-wrapper"

export function LockedFeature() {
  const [locked, setLocked] = useState(true)

  async function handleUpgrade() {
    // Your upgrade logic here
    await fetch("/api/upgrade", { method: "POST" })
    setLocked(false)
  }

  return (
    <BlurWrapper
      isBlurred={locked}
      blurIntensity="md"
      dimOpacity={0.6}
      focusInert
      dialogTitle="Upgrade Required"
      dialogDescription="Unlock this feature with a Pro plan."
      confirmLabel="Upgrade Now"
      pendingLabel="Processing..."
      onConfirm={handleUpgrade}
      onUnblur={() => setLocked(false)}
    >
      <div className="p-6 border rounded-lg">
        <h3 className="font-semibold mb-2">Premium Analytics</h3>
        <p className="text-muted-foreground">
          Advanced insights and export capabilities
        </p>
      </div>
    </BlurWrapper>
  )
}

PaywallBanner

Add a dismissible announcement banner that respects user intent and keeps upgrade pathways visible:

"use client"

import { useState } from "react"
import { PaywallBanner } from "@/components/paywallBanner/paywall-banner"

export function LaunchAnnouncement() {
  const [open, setOpen] = useState(true)

  async function handleTrial() {
    const response = await fetch("/api/trials", { method: "POST" })
    if (!response.ok) throw new Error("Unable to start trial")
  }

  return (
    <PaywallBanner
      open={open}
      onOpenChange={setOpen}
      variant="upgrade"
      badge="New feature"
      title="Workflow automation just shipped"
      description="Upgrade to the Scale plan to unlock automated handoffs, SLAs, and export scheduling."
      storageKey="workflow-automation-banner"
      ctaLabel="Start trial"
      ctaPendingLabel="Launching..."
      onCtaClick={handleTrial}
      onCtaSuccess={() => setOpen(false)}
      onCtaError={(error) => console.error(error)}
      secondaryLabel="Read release notes"
      secondaryHref="/changelog#workflow-automation"
    />
  )
}

Pro tips:

  • Use storageKey to persist dismissals across sessions.
  • Leverage onCtaClick and onCtaError for async upgrade flows.
  • Swap variant between "upgrade", "info", "success", and "warning".

FeatureTooltip

Surface inline upsells for disabled actions, icons, or compact UI without forcing a modal:

"use client"

import { FeatureTooltip } from "@/components/featureTooltip/feature-tooltip"
import { Lock } from "lucide-react"

export function InlineUpsell() {
  return (
    <FeatureTooltip
      title="Unlock scheduled exports"
      description="Upgrade to automate CSV delivery to your stakeholders."
      highlights={[
        "Send summaries to unlimited recipients",
        "Choose daily, weekly, or monthly cadence",
        "Attach filtered dashboards",
      ]}
      ctaLabel="Upgrade"
      ctaHref="/pricing"
    >
      <button className="inline-flex items-center gap-2 rounded-lg border border-primary/20 px-3 py-2 text-sm font-medium text-muted-foreground hover:border-primary/40 hover:text-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40">
        <Lock className="size-4" aria-hidden="true" />
        Scheduled exports (Pro)
      </button>
    </FeatureTooltip>
  )
}

Use it for:

  • Disabled buttons that require higher plans
  • Inline icons in tables or charts
  • Feature flags where you still want to tease capabilities

UpgradeModal

Give users a full plan comparison without leaving the current surface:

"use client"

import { Button } from "@/components/ui/button"
import { UpgradeModal } from "@/components/upgradeModal/upgrade-modal"

export function PlanComparison() {
  async function startGrowthCheckout() {
    const response = await fetch("/api/checkout/growth", { method: "POST" })
    if (!response.ok) throw new Error("Unable to start checkout")
  }

  return (
    <UpgradeModal
      trigger={
        <Button variant="outline">
          Compare plans
        </Button>
      }
      subtitle="Scale with confidence"
      finePrint="Prices listed in USD. Cancel anytime."
      supportEmail="sales@feature-lock.dev"
      onPlanSelected={(planId) => console.log("Selected plan:", planId)}
      plans={[
        {
          id: "growth",
          name: "Growth",
          price: "$79",
          period: "month",
          highlight: "Everything in Free, plus",
          features: [
            "Unlimited dashboards",
            "Team benchmarks",
            { label: "Priority support", footnote: "1 business-day response time" },
          ],
          ctaLabel: "Upgrade to Growth",
          onSelect: startGrowthCheckout,
        },
        {
          id: "scale",
          name: "Scale",
          recommended: true,
          badge: "Most popular",
          price: "$129",
          period: "month",
          features: [
            "AI churn forecasts",
            "Custom roles & permissions",
            "Dedicated onboarding manager",
          ],
          ctaLabel: "Talk to sales",
          ctaHref: "/contact",
        },
      ]}
    />
  )
}

Best practices:

  • Use onPlanSelected to instrument analytics funnels.
  • Set autoCloseOnSelect to false if you keep workflows inline.
  • Pair with support email or footer CTA for enterprise outreach.

UsageProgress

Track quota consumption, warn before limits, and embed upgrade CTAs directly in your dashboards:

"use client"

import { UsageProgress } from "@/components/usageProgress/usage-progress"

export function UsageOverview() {
  return (
    <UsageProgress
      title="Your workspace usage"
      subtitle="Quotas reset on the 1st of each month."
      tracks={[
        {
          label: "API requests",
          value: 92_300,
          limit: 100_000,
          status: "warning",
          badge: "92% used",
          trend: "up",
          description: "Scale plan adds 500k requests / month",
        },
        {
          label: "Seats",
          value: 28,
          limit: 30,
          status: "critical",
          badge: "2 remaining",
        },
      ]}
      summaryValue="Scale plan unlocks 500k requests & 100 seats"
      summaryMessage="Upgrade before May 1 to avoid throttling."
      ctaLabel="Upgrade usage"
      ctaPendingLabel="Launching checkout..."
      onCtaClick={async () => {
        const response = await fetch("/api/checkout/scale", { method: "POST" })
        if (!response.ok) throw new Error("Unable to start checkout")
      }}
      secondaryLabel="Contact sales"
      onSecondaryClick={() => window.open("mailto:sales@feature-lock.dev")}
      note="Need custom limits? Book a call with sales."
    />
  )
}

Use it for:

  • Usage dashboards & billing pages
  • Account alerts near quota limits
  • Contextual upgrade prompts after heavy usage

BlurWrapper API

Complete prop reference and TypeScript types for BlurWrapper

Core Props

isBlurred
false
boolean
When true, applies blur effect and shows overlay
overlayMode
"dialog"
"dialog" | "inline"
Controls whether overlay appears as modal dialog or inline panel
onConfirm
() => Promise<void> | void
Async function called when user clicks confirm button
onUnblur
() => void
Callback fired after successful confirmation (auto-called if autoUnblurOnConfirm is true)

Visual Customization

blurIntensity
"md"
"sm" | "md" | "lg" | "xl" | "2xl" | "3xl"
Tailwind blur class to apply (overridden by blurPx if set)
blurPx
number
Exact blur amount in pixels (overrides blurIntensity)
dimOpacity
1
number
Opacity of blurred content (0 to 1)
disablePointerEvents
true
boolean
Prevents clicking/interacting with blurred content
icon
LucideIcon
Custom Lucide icon to display in overlay (default: Lock)

Accessibility

focusInert
true
boolean
Adds inert and aria-hidden to prevent keyboard focus on blurred content
announcePending
true
boolean
Screen reader announces pending state during async operations
focusErrorOnSet
true
boolean
Automatically moves focus to error message when error occurs
returnFocusTo
HTMLElement | string
Element or selector to restore focus to after overlay closes

Labels & Content

confirmLabel
"Confirm"
string
Text for the confirm button
pendingLabel
"Working..."
string
Text shown during async operation
dialogTitle
"Feature unavailable"
string
Title for dialog mode overlay
dialogDescription
string
Description text for dialog mode
errorMessage
string
Default error message to display on failure
secondaryLabel
string
Text for the secondary action button
secondaryPendingLabel
"Working..."
string
Text shown during secondary action async operation

Secondary Actions

onSecondaryConfirm
() => Promise<void> | void
Async function called when user clicks secondary action button
onSecondaryConfirmError
(error: unknown) => void
Callback fired when secondary action fails
onSecondaryConfirmFinally
(result: "success" | "error") => void
Callback fired after secondary action completes (success or error)

Inline Mode

inlinePosition
"centerCenter"
"leftTop" | "leftCenter" | "leftBottom" | "centerTop" | "centerCenter" | "centerBottom" | "rightTop" | "rightCenter" | "rightBottom"
Position of inline overlay panel
inlineContainerClassName
string
Custom classes for inline overlay container
inlinePanelClassName
string
Custom classes for inline overlay panel
inlineAriaLabel
"Upgrade panel"
string
ARIA label for inline overlay

Advanced

overlay
ReactNode | (args) => ReactNode
Custom overlay content (function receives isPending, error, confirm, etc.)
open
boolean
Controlled open state for overlay (use with onOpenChange)
onOpenChange
(open: boolean) => void
Callback when overlay open state changes
autoUnblurOnConfirm
true
boolean
Automatically call onUnblur after successful confirmation
autoCloseDialogOnConfirm
true
boolean
Close dialog after successful confirmation

Secondary Action Button

Add a secondary button for alternative actions like “Learn More” or “Contact Sales”:

<BlurWrapper
  isBlurred={locked}
  overlayMode="dialog"
  dialogTitle="Upgrade to Pro"
  dialogDescription="Unlock advanced features and premium support"
  confirmLabel="Start Free Trial"
  secondaryLabel="Learn More"
  onConfirm={async () => {
    await startTrial()
    setLocked(false)
  }}
  onSecondaryConfirm={async () => {
    // Open documentation or pricing page
    window.open("/pricing", "_blank")
  }}
  onUnblur={() => setLocked(false)}
>
  <PremiumFeatureContent />
</BlurWrapper>

Multiple Independent Sections

Each section can be unlocked independently with its own state:

export function Dashboard() {
  const [analyticsLocked, setAnalyticsLocked] = useState(true)
  const [reportsLocked, setReportsLocked] = useState(true)
  const [exportsLocked, setExportsLocked] = useState(true)

  async function upgradeFeature(feature: string) {
    await fetch("/api/upgrade", {
      method: "POST",
      body: JSON.stringify({ feature })
    })
  }

  return (
    <div className="grid gap-6 md:grid-cols-3">
      <BlurWrapper
        isBlurred={analyticsLocked}
        onConfirm={() => upgradeFeature("analytics")}
        onUnblur={() => setAnalyticsLocked(false)}
      >
        <AnalyticsCard />
      </BlurWrapper>

      <BlurWrapper
        isBlurred={reportsLocked}
        onConfirm={() => upgradeFeature("reports")}
        onUnblur={() => setReportsLocked(false)}
      >
        <ReportsCard />
      </BlurWrapper>

      <BlurWrapper
        isBlurred={exportsLocked}
        onConfirm={() => upgradeFeature("exports")}
        onUnblur={() => setExportsLocked(false)}
      >
        <ExportsCard />
      </BlurWrapper>
    </div>
  )
}

Controlled Overlay State

Control when the overlay appears for custom flows:

export function ControlledExample() {
  const [locked, setLocked] = useState(true)
  const [overlayOpen, setOverlayOpen] = useState(false)

  return (
    <>
      <Button onClick={() => setOverlayOpen(true)}>
        Unlock Feature
      </Button>

      <BlurWrapper
        isBlurred={locked}
        open={overlayOpen}
        onOpenChange={setOverlayOpen}
        onConfirm={async () => {
          await upgradeUser()
          setLocked(false)
          setOverlayOpen(false)
        }}
      >
        <LockedContent />
      </BlurWrapper>
    </>
  )
}

PaywallBanner API

Key props for the announcement banner component

Core Props

title
string
Primary headline for the announcement banner
description
string
Supporting copy displayed under the title
badge
"New"
string | null
Optional badge label; set to null to hide
variant
"upgrade"
"upgrade" | "info" | "success" | "warning"
Determines accent styling for the banner
dismissible
true
boolean
Controls whether the close button is rendered
storageKey
string
Persist dismissals in localStorage using this key
defaultOpen
true
boolean
Initial visibility when the component is uncontrolled
open
boolean
Controlled visibility state for the banner
onOpenChange
(open: boolean) => void
Callback fired whenever visibility changes

Primary Action

ctaLabel
"Upgrade now"
string
Text for the primary button
ctaHref
string
Link target when no onCtaClick handler is provided
onCtaClick
() => Promise<void> | void
Async handler for the primary action
ctaPendingLabel
"Working..."
string
Text shown while onCtaClick resolves
onCtaSuccess
() => void
Called after onCtaClick resolves without error
onCtaError
(error: unknown) => void
Called when onCtaClick throws or rejects

Secondary & Dismiss

secondaryLabel
string
Text for the optional secondary button
secondaryHref
string
Link target when no onSecondaryClick handler is provided
onSecondaryClick
() => Promise<void> | void
Async handler for the secondary action
onSecondarySuccess
() => void
Called after onSecondaryClick resolves without error
onSecondaryError
(error: unknown) => void
Called when the secondary handler throws or rejects
onDismiss
() => void
Fired when the banner is dismissed

Layout & Content

showDivider
false
boolean
Adds a divider between content and actions on larger screens
className
string
Custom classes for the banner container
contentClassName
string
Custom classes for the text/content column
actionsClassName
string
Custom classes for the action button container
children
React.ReactNode
Optional additional content (e.g. bullet lists or disclaimers)

FeatureTooltip API

Props for lightweight inline upgrade nudges

Core Props

title
string
Headline displayed at the top of the tooltip
description
string
Supporting body copy underneath the title
badge
"Upgrade to unlock"
string | null
Optional badge text shown next to the icon; set to null to hide
icon
Lock
LucideIcon
Icon rendered within the tooltip header
children
React.ReactNode
Trigger element that displays the tooltip on hover or focus
disabled
false
boolean
Render children without tooltip behavior when true

Highlights & Styling

highlights
(string | { icon?: LucideIcon; label: string })[]
List of feature value props shown as bullet items
highlightIcon
CheckCircle2
LucideIcon
Icon used when highlights are provided as strings
className
string
Custom classes applied to the trigger wrapper
contentClassName
string
Custom classes applied to the tooltip content container
badgeClassName
string
Custom classes for styling the badge element

CTA & Events

ctaLabel
"Upgrade"
string
Label for the primary call-to-action button
ctaHref
string
Link URL for the CTA when no async handler is provided
ctaPendingLabel
"Working..."
string
Label shown while onCtaClick is resolving
onCtaClick
() => Promise<void> | void
Async handler invoked when the CTA button is clicked
onCtaSuccess
() => void
Callback fired after the CTA handler resolves successfully
onCtaError
(error: unknown) => void
Callback fired when the CTA handler throws or rejects

Positioning & Control

side
"top"
"top" | "bottom" | "left" | "right"
Placement of the tooltip relative to the trigger
align
"center"
"start" | "center" | "end"
Alignment of the tooltip on the chosen side
sideOffset
12
number
Pixels of offset between tooltip and trigger
delayDuration
200
number
Delay in milliseconds before the tooltip appears
open
boolean
Controlled open state for the tooltip
defaultOpen
boolean
Initial open state when uncontrolled
onOpenChange
(open: boolean) => void
Callback fired whenever the tooltip open state changes

Best Practices

Tips for optimal implementation

✅ Do

  • • Use dialog mode for critical upgrade decisions
  • • Use inline mode for contextual feature teasers
  • • Provide clear value propositions in overlay content
  • • Persist dismissals with PaywallBanner storage keys to respect user intent
  • • Surface UsageProgress before users hit their limits
  • • Handle async errors gracefully
  • • Test with keyboard navigation and screen readers

❌ Don't

  • • Lock too many features at once
  • • Use aggressive blur that makes content unrecognizable
  • • Forget to handle loading and error states
  • • Disable pointer events if user needs to scroll
  • • Mix dialog and inline modes inconsistently

Troubleshooting

Common issues and solutions

Import errors

If you see “Cannot find module” errors, verify your tsconfig.json has correct path aliases:

{
  "compilerOptions": {
    "paths": {
      "@/*": ["./*"]
    }
  }
}

Blur not visible

Ensure your content has a non-transparent background. The blur effect works by filtering the content:

<BlurWrapper isBlurred={locked}>
  <div className="bg-white dark:bg-slate-900 p-6 rounded-lg">
    {/* Your content */}
  </div>
</BlurWrapper>

Overlay not showing

By default, the overlay shows automatically when isBlurred is true. If it's not appearing, check that showOverlayOnBlur is not set to false.

TypeScript errors

Make sure you're using TypeScript 5+ and have proper type definitions installed. The component is fully typed with TypeScript.