Banner Newsletter
ruixen-ui/banner-newsletterFreeComponent
A newsletter banner with Inline Morphing — one pill that reshapes across input, loading, and confirmation.
Live preview
live · rendered by the registry
Rendered by ruixen-ui on their own page — this is the component running, not a screenshot of it.Install it
npx shadcn@latest add https://ruixen.com/r/banner-newsletter.jsonSource
1"use client";23import * as React from "react";4import { cn } from "@/lib/utils";5import { ArrowRight, X } from "lucide-react";67/* ═══════════════════════════════════════════════════════════8 Inline Morphing — a single pill surface that reshapes9 across input → loading → confirmation. No separate button,10 no status message — the pill IS all three states.11 ═══════════════════════════════════════════════════════════ */1213export interface BannerNewsletterProps {14 title?: string;15 onSubscribe?: (email: string) => Promise<void> | void;16 onDismiss?: () => void;17 className?: string;18}1920export function BannerNewsletter({21 title = "Stay in the loop",22 onSubscribe,23 onDismiss,24 className,25}: BannerNewsletterProps) {26 const [mounted, setMounted] = React.useState(false);27 const [dismissed, setDismissed] = React.useState(false);28 const [email, setEmail] = React.useState("");29 const [status, setStatus] = React.useState<"idle" | "loading" | "success">(30 "idle",31 );32 const [error, setError] = React.useState(false);33 const inputRef = React.useRef<HTMLInputElement>(null);34 const onDismissRef = React.useRef(onDismiss);35 onDismissRef.current = onDismiss;3637 /* entrance expand */38 React.useEffect(() => {39 requestAnimationFrame(() => {40 requestAnimationFrame(() => setMounted(true));41 });42 }, []);4344 const handleDismiss = React.useCallback(() => {45 setDismissed(true);46 setTimeout(() => onDismissRef.current?.(), 350);47 }, []);4849 const handleSubmit = async (e: React.FormEvent) => {50 e.preventDefault();51 if (!email || status !== "idle") return;5253 setStatus("loading");54 try {55 await onSubscribe?.(email);56 setStatus("success");57 setTimeout(handleDismiss, 2000);58 } catch {59 setStatus("idle");60 setError(true);61 inputRef.current?.focus();62 setTimeout(() => setError(false), 600);63 }64 };6566 return (67 <div68 className={cn(69 "grid transition-[grid-template-rows] ease-[cubic-bezier(0.16,1,0.3,1)]",70 mounted && !dismissed71 ? "[grid-template-rows:1fr] duration-500"72 : "[grid-template-rows:0fr] duration-300",73 )}74 >75 <div className="overflow-hidden">76 <div77 className={cn(78// … truncated
What it pulls in
npm packages
Files it writes
More from ruixen-ui
All 417 items| Component | Registry | Kind | Access | Installs | Command |
|---|---|---|---|---|---|
| Editorial Accordionaccordion-editorial | ruixen-ui | Components | Free | no deps | |
| Indexed Accordionaccordion-indexed | ruixen-ui | Components | Free | 1 dep | |
| Account Menuaccount-menu | ruixen-ui | Components | Free | 1 dep | |
| Action Toolbaraction-toolbar | ruixen-ui | Components | Free | 1 dep | |
| Add Task Sheetadd-task-sheet | ruixen-ui | Components | Free | 1 dep | |
| Add To Cart Buttonadd-to-cart-button | ruixen-ui | Components | Free | 1 dep | |
| AI Chat Inputai-chat-input | ruixen-ui | Components | Free | 1 dep | |
| Animated Highlight Textanimated-highlight-text | ruixen-ui | Components | Free | 1 dep |
