Complete reference for customizing Milkie's visual design and UX.
All available props for customizing the paywall experience:
<PaywallGate
// Content customization
title="Unlock Premium Features"
subtitle="Get full access to all premium tools"
subscribeButtonText="Upgrade to Pro"
signInButtonText="Sign in to continue"
icon={<CustomIcon />}
customUi={<YourCustomComponent />}
// Behavior customization
signInUrl="/signin"
onSignIn={() => router.push("/signin")}
onCheckout={async (email) => ({ url: checkoutUrl })}
onToast={(message, type) => toast[type](message)}
// Visual customization
showBranding={false}
showBlurredChildren={false}
overlayClassName="pt-8"
position="top"
>
<PremiumContent />
</PaywallGate>| Prop | Type | Default | Description |
|---|---|---|---|
title |
string |
"Upgrade to access this feature" | Main heading in paywall card |
subtitle |
string |
"Sign in or subscribe to continue" | Supporting text below title |
subscribeButtonText |
string |
"Subscribe" | CTA button for authenticated users |
signInButtonText |
string |
"Sign in" | CTA button for unauthenticated users |
icon |
ReactNode |
Lock icon | Custom icon at top of card |
customUi |
ReactNode |
null | Complete custom UI replacement |
| Prop | Type | Default | Description |
|---|---|---|---|
signInUrl |
string |
"/signin" | URL to redirect for sign-in |
onSignIn |
() => void |
undefined | Custom sign-in handler (alternative to URL) |
onCheckout |
(email: string) => Promise<{url: string}> |
Default handler | Custom checkout handler |
onToast |
(message: string, type: "success" | "error") => void |
undefined | Toast notification callback |
| Prop | Type | Default | Description |
|---|---|---|---|
showBranding |
boolean |
true | Show "Powered by milkie" footer |
showBlurredChildren |
boolean |
true | Show blurred content preview |
overlayClassName |
string |
"" | Custom className for overlay element |
position |
"center" | "top" |
"center" |
Vertical position of paywall card |
PaywallGate renders protected content behind the overlay with a blur effect:
<PaywallGate>
<PremiumContent /> {/* Shown blurred in background */}
</PaywallGate>How it works:
- Content preview builds desire and provides context
- Uses Tailwind
blur-smclass withopacity-50for darkened effect - Content visible enough to understand what's locked
- Non-interactive (
pointer-events-none select-none)
Disable blur:
<PaywallGate showBlurredChildren={false}>
<PremiumContent /> {/* Paywall card shown inline without blur */}
</PaywallGate>The paywall uses a CSS Grid-based overlay system:
Technical details:
- CSS Grid Stacking: Both overlay and content use
col-start-1 row-start-1to occupy same grid cell - Z-Index Layering: Overlay has
z-10to appear above blurred content - Flexbox Centering: Overlay uses
flex items-center justify-centerfor perfect centering - Dynamic Height: Uses
w-fullinstead of fixed min-height for responsive sizing
Add padding:
<PaywallGate overlayClassName="pt-8">
<PremiumContent />
</PaywallGate>Position card at top:
<PaywallGate position="top">
<LongNewsArticle />
</PaywallGate>Useful for long scrolling content where a centered card might appear off-screen on mobile.
All components are fully theme-aware:
- Automatic adaptation to light/dark themes
- Uses next-themes and shadcn/ui's theming system
- Proper contrast in both modes
- No configuration needed
// Works automatically with your app's theme
<MilkieProvider email={email}>
<PaywallGate /> {/* Adapts to light/dark mode */}
</MilkieProvider>Automatic loading states during subscription checks:
// Shown automatically while checking subscription
<PaywallGate>
<PremiumContent />
</PaywallGate>;
// Or use the hook for custom loading UI
const { loading } = usePaywall();
if (loading) return <CustomSpinner />;Built-in error recovery without jarring alerts:
- Inline error banners with AlertCircle icon
- "Try again" button for immediate retry
- Toast notifications via
onToastcallback - No page refresh needed
<PaywallGate onToast={(msg, type) => toast[type](msg)}>
<PremiumContent />
</PaywallGate>Replace the entire paywall card:
function CustomPaywall() {
return (
<div className="max-w-lg mx-auto p-8 bg-gradient-to-br from-purple-500 to-pink-500 rounded-2xl text-white">
<h2 className="text-3xl font-bold mb-4">Premium Required</h2>
<ul className="space-y-2 mb-6">
<li>✨ Unlimited access</li>
<li>🚀 Priority support</li>
<li>🎯 Advanced features</li>
</ul>
<button className="w-full py-3 bg-white text-purple-600 rounded-lg font-semibold">
Upgrade Now
</button>
</div>
);
}
<PaywallGate customUi={<CustomPaywall />}>
<PremiumContent />
</PaywallGate>;import { Sparkles } from "lucide-react";
<PaywallGate icon={<Sparkles className="w-12 h-12 text-yellow-500" />}>
<PremiumContent />
</PaywallGate>;const handleSignIn = () => {
// Custom sign-in logic
router.push("/signin?redirect=" + window.location.pathname);
};
<PaywallGate onSignIn={handleSignIn}>
<PremiumContent />
</PaywallGate>;PaywallGate uses shadcn/ui components which can be customized via Tailwind:
The paywall card uses the Card component from shadcn/ui. Customize in your tailwind.config.js:
// tailwind.config.js
module.exports = {
theme: {
extend: {
colors: {
border: "hsl(var(--border))",
background: "hsl(var(--background))",
// ... other shadcn/ui colors
},
},
},
};Buttons use the Button component. Customize button variants in components/ui/button.tsx:
// Modify the button variants
const buttonVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium...",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
// ... other variants
},
},
}
);Callback URL handling preserves user context:
// User at /dashboard/analytics tries to access premium feature
<PaywallGate signInUrl="/signin">
<PremiumContent />
</PaywallGate>
// Sign-in URL becomes: /signin?callbackUrl=/dashboard/analytics
// After sign-in, user returns to /dashboard/analyticsHow it works:
- PaywallGate automatically includes
callbackUrlin sign-in redirect - Works with AuthGate too
- Users return to original page after authentication
All components are mobile-optimized:
- Paywall cards adapt to screen size
- Touch-friendly button sizes
- Readable text on small screens
- Stripe checkout is also mobile-optimized
// Works great on mobile automatically
<PaywallGate>
<PremiumContent />
</PaywallGate>For long content on mobile, consider disabling blur to prevent off-screen centering:
"use client";
import { useIsMobile } from "@/hooks/use-is-mobile";
export default function ArticlePage() {
const isMobile = useIsMobile();
return (
<PaywallGate
position="top"
showBlurredChildren={!isMobile}
>
<LongNewsArticle />
</PaywallGate>
);
}Why this helps:
- Long blurred content on mobile can push the paywall card far down the page
- Disabling blur on mobile shows the card inline at the top
- Desktop users still get the blurred preview effect
- Better UX when content height exceeds viewport
<PaywallGate showBranding={false}>
<PremiumContent />
</PaywallGate>The branding is small and unobtrusive by default (appears in card footer).
Use the same tone across all paywalls:
// ✅ Good - consistent, benefit-focused
<PaywallGate
title="Unlock Advanced Analytics"
subtitle="Get insights that drive growth"
/>
// ❌ Avoid - inconsistent, vague
<PaywallGate
title="Pay to see this"
subtitle="You need premium"
/>Explain what users get:
<PaywallGate
title="Premium Feature"
subtitle="Advanced reporting, exports, and priority support"
subscribeButtonText="Get unlimited access"
/>Match icons to your content:
import { BarChart, FileText, Zap } from "lucide-react";
// Analytics feature
<PaywallGate icon={<BarChart />}>
// Document feature
<PaywallGate icon={<FileText />}>
// Speed/performance feature
<PaywallGate icon={<Zap />}>Always test your customizations in light and dark modes:
// Make sure your custom UI works in both themes
function CustomPaywall() {
return (
<div className="bg-background text-foreground">
{" "}
{/* Theme-aware */}
{/* Content */}
</div>
);
}See these patterns in action:
- /mixed - Custom UI with blurred preview
- /dashboard - Standard PaywallGate
- /metered - Custom title/subtitle
- Implementation Guide - 7 paywall patterns (component gating, metered access, custom checkout, etc.)
- Best Practices - UX and design tips