A metered-article paywall that clips the preview and fades the last lines into the background beneath a subscribe CTA card.
npx shadcn@latest add @paragon/paywall-fade"use client";
import * as React from "react";
import { Lock } from "lucide-react";
import { cn } from "@/lib/utils";
export interface PaywallFadeProps extends React.ComponentProps<"div"> {
/** Height of visible article before the fade begins, in px. */
previewHeight?: number;
/** CTA heading. */
title?: string;
/** CTA supporting line. */
description?: string;
/** Subscribe button label. */
cta?: string;
/** Called when the CTA is pressed. */
onSubscribe?: () => void;
/** The gated article content. */
children: React.ReactNode;
}
/**
* A metered-article paywall. The article is clipped to a preview height and a
* bottom-anchored gradient mask fades the last lines into the background,
* beneath a subscribe CTA card. The fade is a static gradient (no motion), the
* body is aria-hidden from the fade down so screen readers aren't fed
* half-sentences, and the CTA is a real button. Beautiful in both themes since
* the mask keys off the background token.
*/
export function PaywallFade({
previewHeight = 220,
title = "Continue reading",
description = "Subscribe to unlock the full article and every story in the archive.",
cta = "Subscribe",
onSubscribe,
className,
children,
...props
}: PaywallFadeProps) {
return (
<div
data-slot="paywall-fade"
className={cn("relative w-full max-w-md", className)}
{...props}
>
<div
className="relative overflow-hidden"
style={{ maxHeight: previewHeight }}
>
<div className="prose-sm space-y-3 text-sm leading-relaxed text-foreground/90">
{children}
</div>
{/* Fade to background */}
<div
aria-hidden
className="pointer-events-none absolute inset-x-0 bottom-0 h-32"
style={{
background:
"linear-gradient(to bottom, transparent, var(--background))",
}}
/>
</div>
{/* CTA card, overlapping the fade */}
<div className="-mt-10 relative">
<div className="flex flex-col items-center gap-3 rounded-xl bg-card p-6 text-center shadow-overlay">
<span className="flex size-10 items-center justify-center rounded-full bg-accent text-foreground">
<Lock className="size-4" />
</span>
<div>
<h3 className="text-base font-semibold">{title}</h3>
<p className="mt-1 text-sm text-muted-foreground">{description}</p>
</div>
<button
type="button"
onClick={onSubscribe}
className="pressable inline-flex h-9 items-center justify-center rounded-lg bg-primary px-5 text-sm font-medium text-primary-foreground outline-none transition-[scale,background-color] duration-150 ease-out hover:bg-primary/90 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-card"
>
{cta}
</button>
<p className="text-xs text-muted-foreground">
Already a member?{" "}
<span className="font-medium text-foreground underline-offset-4 hover:underline">
Sign in
</span>
</p>
</div>
</div>
</div>
);
}