Staaarter

Billing Education Discount Application

An application form for student, teacher, and institution pricing. A role picker recalculates the discount and estimated monthly savings live, a school email field flags addresses that don't look like a .edu domain without blocking submission, and a file upload button collects proof of enrollment. Submitting shows a loading state followed by a confirmation screen. Built for SaaS upgrade and onboarding funnels that offer academic pricing.

By Staaarter Team
Billing
Updated August 1, 2026
Docs
Live preview

Docs

Installation

A real shadcn registry command, not a copy-paste stand-in: it fetches this block plus its button, input, label dependencies and any missing npm packages, and installs them straight into your project.

npx shadcn add https://staaarter.com/r/billing-education-discount-application.json

Prefer not to use the CLI? Copy the source from the Code toggle above into src/components/blocks/billing/billing52.tsx instead.

Usage

The file also exports billing52Demo, the exact props behind the preview above. Spread it to get a working section in one line, then replace it with your own data.

import { Billing52, billing52Demo } from "@/components/blocks/billing/billing52";

export default function Page() {
  return <Billing52 {...billing52Demo} />;
}

Props

PropTypeDefaultDescription
headingstring"Apply for the education discount"Form heading.
descriptionstring(see source)Supporting line under the heading.
planPricenumbernoneStandard monthly price in dollars, before any discount.
tiersEducationTier[]noneRoles the applicant can pick, each with its own discount percentage.
defaultRole"student" | "teacher" | "institution"tiers[0].roleWhich role is selected when the form first renders.
submitLabelstring"Submit application"Label on the submit button.
classNamestringnoneExtra classes for the outer form container.

Types

type EducationRole = "student" | "teacher" | "institution";

interface EducationTier {
  role: EducationRole;
  label: string;
  discountPercent: number;
}

Behavior notes

  • Selecting a role is a real radiogroup of buttons, not a native select, so it recalculates the savings preview immediately and works with keyboard navigation and screen readers.
  • The email field doesn't block submission if it's not a .edu address, it just shows an amber note underneath saying additional verification may be needed. Actual verification happens on your backend.
  • The file upload button triggers a hidden native file input via a ref and click(), then shows the selected file's name next to the button. No file is actually uploaded anywhere, wire an onChange handler of your own if you need to send it to storage.
  • Submitting sets a loading state for about 900ms, then replaces the entire form with a confirmation message. There's no callback prop for the submitted data, so lift state up or handle the POST inside your own onSubmit if you fork the component.
  • Estimated savings are computed as planPrice times the selected tier's discountPercent, with the discounted price and original price both shown so the benefit is visible before submitting.

Frequently asked questions