Newsletter Band
A free form input section for newsletter signups, with a one-time reveal and four honest submission states.
Newsletter Band is a free form input section for Next.js and React, styled with Tailwind and animated with Motion: a newsletter signup component react projects drop into a page as a finished band rather than a bare input. The heading, subtitle and form reveal once as it scrolls into view, and the idle, loading, success and error states swap inside a reserved-height grid cell, so nothing on the page moves at any point. Your code owns the request and passes the resulting state back in; the form itself is real, so native validation and Enter submission work with no JavaScript of ours.
Install
Add the @te registry to your components.json once:
{
"registries": {
"@te": "https://templateempire.io/r/{name}.json"
}
}Then install the component:
npx shadcn@latest add @te/input-newsletter-bandUsage
"use client";
import { useState } from "react";
import {
InputNewsletterBand,
type NewsletterBandState,
} from "@/components/input-newsletter-band";
export default function Page() {
const [state, setState] = useState<NewsletterBandState>("idle");
return (
<InputNewsletterBand
title="Design notes, once a month"
subtitle="Motion breakdowns from the templates we ship."
state={state}
onSubmit={async (email) => {
setState("loading");
try {
const response = await fetch("/api/subscribe", {
method: "POST",
body: JSON.stringify({ email }),
});
setState(response.ok ? "success" : "error");
} catch {
setState("error");
}
}}
/>
);
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
| title | string | required | Section heading, rendered as an h2. Blocks never own the h1. |
| subtitle | string | undefined | Optional supporting line beneath the heading. |
| state | "idle" | "loading" | "success" | "error" | required | Controlled submission state. The component performs no request of its own. |
| onSubmit | (email: string) => void | required | Called with the email on valid native submission, in the idle and error states only. |
| emailLabel | string | "Email address" | Visible label for the email field. The placeholder never replaces it. |
| emailPlaceholder | string | "you@example.com" | Supplementary placeholder, never the sole label. |
| submitLabel | string | "Subscribe" | Submit copy in the idle state. |
| loadingLabel | string | "Subscribing" | Shown beside the spinner and announced while loading. |
| successMessage | string | "You're on the list." | Success content and announcement. |
| errorMessage | string | "Something went wrong. Please try again." | Error content and announcement, shown as text as well as in the error colour. |
| name | string | "email" | Native email field name. |
| className | string | undefined | Additional classes merged onto the section. |
Customisation
Newsletter Band exposes one Tier 3 CSS custom property. Set --input-newsletter-error on any ancestor to colour the error line. Its default var(--destructive, var(--foreground)) stays vanilla-safe by falling back to the foreground colour when a theme has no destructive token, and it is read with that default inline, so an override on :root or on a wrapper reaches the component. Every other choice is a real prop: the four state labels and messages, the field name, and the heading copy.
.marketing-footer {
/* Any semantic colour: the default is var(--destructive, var(--foreground)).
--ring exists in a stock shadcn theme, so this works on paste. */
--input-newsletter-error: var(--ring);
}Frequently asked questions
- Is Newsletter Band free for commercial use?
- Yes. Newsletter Band is MIT licensed, so you can use it in personal and commercial projects without attribution.
- Is Newsletter Band accessible and performant?
- Yes. It renders a real form with a visible label, native email validation and Enter submission, and a stable polite status region that announces the loading, success and error copy. Error is expressed in text as well as colour. The form and success panels share one grid cell so the taller state reserves the height and layout shift is zero, and whichever panel is inactive is inert and aria-hidden, so no invisible control stays in the Tab order. Focus transfers to the success message when it replaces the form, because the button it was on has just gone inert. Only transform and opacity animate, and reduced motion removes the travel, the spinner rotation and the check's scale while every behaviour stays intact.
- How do I customise Newsletter Band?
- Set --input-newsletter-error on any ancestor to colour the error line. Its default var(--destructive, var(--foreground)) stays vanilla-safe by falling back to the foreground colour when a theme has no destructive token, and it is read with that default inline, so an override on :root or on any wrapper reaches the component. Everything else is a real prop: title and subtitle for the copy, emailLabel, emailPlaceholder and submitLabel for the field and button, loadingLabel, successMessage and errorMessage for the four states, and name for the submitted field. Wiring it to your own endpoint is the state and onSubmit pair: onSubmit receives the validated email, and you set state to loading, then success or error, from your own request.