Complete guide to installing BlurWrapper, PaywallBanner, FeatureTooltip, UpgradeModal, and UsageProgress in your Next.js project
Add Feature Lock components to your project in seconds
Make sure you have a Next.js project with shadcn/ui set up. If not, initialize it first:
npx shadcn@latest initRun the following command to install the BlurWrapper component and its dependencies:
npx shadcn@latest add https://feature-lock.griffen.codes/r/blur-wrapperProp reference for the standalone upgrade dialog experience
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.Props for the quota tracking component
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").badge, trend ("up", "down","steady"), description. They reinforce messaging without clutter.Get up and running in minutes with BlurWrapper, PaywallBanner, FeatureTooltip, UpgradeModal, and UsageProgress
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>
)
}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:
storageKey to persist dismissals across sessions.onCtaClick and onCtaError for async upgrade flows.variant between "upgrade", "info", "success", and "warning".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:
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:
onPlanSelected to instrument analytics funnels.autoCloseOnSelect to false if you keep workflows inline.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:
Complete prop reference and TypeScript types for BlurWrapper
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>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>
)
}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>
</>
)
}Key props for the announcement banner component
Props for lightweight inline upgrade nudges
Tips for optimal implementation
Common issues and solutions
If you see “Cannot find module” errors, verify your tsconfig.json has correct path aliases:
{
"compilerOptions": {
"paths": {
"@/*": ["./*"]
}
}
}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>By default, the overlay shows automatically when isBlurred is true. If it's not appearing, check that showOverlayOnBlur is not set to false.
Make sure you're using TypeScript 5+ and have proper type definitions installed. The component is fully typed with TypeScript.