# SaaS landing (lite)

> A complete SaaS landing page from free GetUrUI blocks: header, hero, logos, features, numbers, pricing, FAQ, call to action and footer.

A shadcn/ui block from the [@geturui](https://geturui.com/?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/geturui/recipe-saas-landing-lite).

- Page, with a live preview: https://shadcn.exchange/r/geturui/recipe-saas-landing-lite
- Screenshot: https://shadcn.exchange/shot/geturui/recipe-saas-landing-lite/light.png
- Registry item: https://geturui.com/r/recipe-saas-landing-lite.json
- Categories: recipes
- Listed: 2026-10-27, source last changed 2026-10-27

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/geturui/recipe-saas-landing-lite.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `lucide-react`
- Registry items: `@geturui/hero-section`, `@geturui/logo-cloud`, `@geturui/feature-grid`, `@geturui/stats-section`, `@geturui/pricing-section`, `@geturui/testimonials-wall`, `@geturui/faq-section`, `@geturui/cta-banner`, `@geturui/footer-block`

## Files

### `registry/recipes/saas-landing-lite/page.tsx`

```tsx
"use client";

import * as React from "react";
import { CalendarClock, ChartColumn, Check, Keyboard, ListChecks, Menu, Route, X } from "lucide-react";
import { HeroSection } from "@/components/blocks/hero-section";
import { LogoCloud, type Logo } from "@/components/blocks/logo-cloud";
import { FeatureGrid, type Feature } from "@/components/blocks/feature-grid";
import { StatsSection, type Stat } from "@/components/blocks/stats-section";
import { PricingSection, type PricingPlan, type SeatTier } from "@/components/blocks/pricing-section";
import { TestimonialsWall, type Testimonial } from "@/components/blocks/testimonials-wall";
import { FaqSection, type FaqContact } from "@/components/blocks/faq-section";
import { CtaBanner } from "@/components/blocks/cta-banner";
import { FooterBlock, type FooterColumn, type FooterLink } from "@/components/blocks/footer-block";

/*
 * SaaS landing (lite) — a GetUrUI recipe.
 * Rewrite the constants below for your product; the JSX underneath only wires them into the sections.
 * Section ids (see NAV.links) are the anchors the header links to.
 */

// ─── Content ────────────────────────────────────────────────────────────────

const BRAND = {
  name: "Taskwell",
  /** One line used in the footer. */
  tagline: "One calm queue for every request your team gets.",
};

const NAV = {
  links: [
    { label: "Features", href: "#features" },
    { label: "Pricing", href: "#pricing" },
    { label: "FAQ", href: "#faq" },
  ],
  secondary: { label: "Sign in", href: "/sign-in" },
  cta: { label: "Start free", href: "#pricing" },
};

const HERO = {
  eyebrow: "New: rules that assign work for you",
  eyebrowBadge: "New",
  eyebrowHref: "#features",
  headline: "Every request, routed to the right person",
  mark: "routed",
  subcopy:
    "Taskwell collects requests from forms, email and chat into one shared queue, assigns an owner and keeps everyone posted until the work is done.",
  primaryAction: { label: "Start free", href: "#pricing" },
  secondaryAction: { label: "See how it works", href: "#features" },
};

const LOGOS = {
  heading: "Teams that keep their requests in Taskwell",
  logos: [
    { name: "Northpeak", style: "geometric", glyph: "triangle" },
    { name: "OSTRA", style: "heavy", glyph: "square" },
    { name: "fernbrook", style: "light", glyph: "bars" },
    { name: "Halden", style: "serif", glyph: "dot" },
    { name: "kestrel", style: "rounded", glyph: "ring" },
    { name: "Lumen & Co", style: "mono", glyph: "slash" },
  ] satisfies Logo[],
};

const FEATURES_COPY = {
  eyebrow: "Why Taskwell",
  heading: "Less chasing, more finishing",
  subheading: "The parts of team work that usually live in someone's head, written down and running on their own.",
};

/** Small strings shown inside the custom feature illustrations. */
const FEATURE_ART = {
  queue: [
    { title: "New laptop for Sam", meta: "IT · today", done: true },
    { title: "Update the pricing page", meta: "Web · Fri", done: false },
    { title: "Renew the office lease", meta: "Ops · in 3 days", done: false },
  ],
  rule: { when: "Form is “Access request”", then: "Assign to IT on call" },
  due: { label: "Due in 2 hours", detail: "Reminder sent to the owner" },
};

const FEATURES: Feature[] = [
  {
    title: "One queue for every request",
    description: "Forms, email and chat land in the same list, already tagged by team and urgency.",
    icon: <ListChecks className="size-4" />,
    media: <QueueArt items={FEATURE_ART.queue} />,
  },
  {
    title: "Keyboard first",
    description: "Triage, assign and close without touching the mouse.",
    icon: <Keyboard className="size-4" />,
    visual: "focus",
  },
  {
    title: "Rules that route work",
    description: "Write a rule once and new requests find their owner on their own.",
    icon: <Route className="size-4" />,
    media: <RuleArt when={FEATURE_ART.rule.when} then={FEATURE_ART.rule.then} />,
  },
  {
    title: "Nothing slips",
    description: "Due dates and gentle reminders keep work moving without a status meeting.",
    icon: <CalendarClock className="size-4" />,
    media: <DueArt label={FEATURE_ART.due.label} detail={FEATURE_ART.due.detail} />,
  },
  {
    title: "Workload at a glance",
    description: "See who is overloaded before it turns into a missed deadline.",
    icon: <ChartColumn className="size-4" />,
    visual: "bars",
  },
];

const STATS = {
  eyebrow: "By the numbers",
  heading: "What teams see in the first month",
  subheading: "Example figures. Replace them with numbers you can stand behind.",
  stats: [
    { label: "Requests routed", value: 12480, trend: [3100, 5200, 7900, 10200, 12480], delta: "+22% MoM" },
    { label: "Median minutes to owner", value: 4, trend: [38, 21, 12, 7, 4], delta: "89% faster", deltaDirection: "up" },
    { label: "On-time completion", value: 96.4, suffix: "%", decimals: 1, trend: [81, 87, 91, 94, 96.4], delta: "+15 pts", deltaDirection: "up" },
    { label: "Teams onboarded", value: 340, trend: [80, 150, 220, 290, 340], delta: "+50 this month", deltaDirection: "up" },
  ] satisfies Stat[],
};

const PRICING = {
  eyebrow: "Pricing",
  heading: "Simple per-seat pricing",
  subheading: "Start free with your team. Upgrade when the queue grows.",
  yearlyNote: "Save 20%",
  currency: "$",
  defaultBilling: "yearly" as const,
  minSeats: 1,
  maxSeats: 100,
  defaultSeats: 10,
  tiers: [
    { from: 1, discount: 0 },
    { from: 25, discount: 0.1 },
    { from: 60, discount: 0.2 },
  ] satisfies SeatTier[],
  plans: [
    {
      id: "starter",
      name: "Starter",
      description: "For small teams getting requests out of chat.",
      monthly: 0,
      yearly: 0,
      cta: "Start free",
      features: [
        { label: "Shared request queue", included: true },
        { label: "Request forms", included: "2 forms" },
        { label: "Routing rules", included: false },
        { label: "Workload reports", included: false },
      ],
    },
    {
      id: "team",
      name: "Team",
      description: "For teams that want work to route itself.",
      monthly: 12,
      yearly: 10,
      cta: "Start a 14-day trial",
      featured: true,
      badge: "Most popular",
      features: [
        { label: "Shared request queue", included: true },
        { label: "Request forms", included: "Unlimited" },
        { label: "Routing rules", included: true },
        { label: "Workload reports", included: true },
      ],
    },
    {
      id: "business",
      name: "Business",
      description: "For several teams with approvals and SSO.",
      monthly: 24,
      yearly: 20,
      cta: "Talk to sales",
      features: [
        { label: "Everything in Team", included: true },
        { label: "Approval steps", included: true },
        { label: "SAML single sign-on", included: true },
        { label: "Audit log", included: "1 year" },
      ],
    },
  ] satisfies PricingPlan[],
};

/** Placeholders on purpose: never ship invented quotes. Replace with real ones, or delete the section. */
const TESTIMONIALS = {
  eyebrow: "Customer stories",
  heading: "Your customers, in their words",
  subheading: "Placeholder cards. Add real quotes with permission, or remove this section.",
  testimonials: [
    { quote: "Replace with a real quote from a customer.", name: "Customer name", handle: "@handle", role: "Role, Company" },
    { quote: "Replace with a real quote about the problem your product solved.", name: "Customer name 2", handle: "@handle", role: "Role, Company" },
    { quote: "Replace with a real quote about a result, ideally with a number in it.", name: "Customer name 3", handle: "@handle", role: "Role, Company" },
    { quote: "Replace with a real quote about what changed for their team.", name: "Customer name 4", handle: "@handle", role: "Role, Company" },
    { quote: "Replace with a real quote about what onboarding felt like.", name: "Customer name 5", handle: "@handle", role: "Role, Company" },
    { quote: "Replace with a real quote about why they would recommend it.", name: "Customer name 6", handle: "@handle", role: "Role, Company" },
  ] satisfies Testimonial[],
};

const FAQ = {
  eyebrow: "FAQ",
  heading: "Questions, answered",
  subheading: "Everything people ask before they move their team's requests over.",
  items: [
    { q: "How long does setup take?", a: "Most teams create their first form and routing rule in under fifteen minutes. You can import open requests from a CSV." },
    { q: "Can people outside my workspace send requests?", a: "Yes. Forms have a public link and email requests work from any address, so requesters never need an account." },
    { q: "What happens when the free plan runs out?", a: "Nothing is deleted. You can keep working in the queue and upgrade when you need more forms or routing rules." },
    { q: "Can I cancel any time?", a: "Yes. Paid plans stay active until the end of the billing period and you can export everything first." },
    { q: "Do you offer discounts for non-profits?", a: "Yes. Write to us from your organisation's address and we will set it up." },
  ],
  contact: {
    title: "Still have questions?",
    description: "Talk to a person on our team. We usually reply within a day.",
    actionLabel: "Contact us",
    href: "mailto:hello@example.com",
    team: ["Alex Kim", "Sam Ortiz", "Mira Lund"],
    responseTime: "Replies in about 4 hours",
  } satisfies FaqContact,
};

const CTA = {
  eyebrow: "Free for small teams",
  headline: "Give every request an owner today",
  subcopy: "Set up your first form in minutes. No card needed.",
  primaryAction: { label: "Start free", href: "#pricing" },
  secondaryAction: { label: "Book a demo", href: "mailto:hello@example.com" },
};

const FOOTER = {
  columns: [
    { title: "Product", links: [{ label: "Features", href: "#features" }, { label: "Pricing", href: "#pricing" }, { label: "Changelog", href: "/changelog", badge: "New" }] },
    { title: "Company", links: [{ label: "About", href: "/about" }, { label: "Careers", href: "/careers" }, { label: "Contact", href: "mailto:hello@example.com" }] },
    { title: "Resources", links: [{ label: "Help center", href: "/help" }, { label: "Templates", href: "/templates" }, { label: "FAQ", href: "#faq" }] },
  ] satisfies FooterColumn[],
  newsletter: { title: "Product updates", description: "One short email a month. No spam.", placeholder: "you@company.com" },
  copyright: "© 2026 Taskwell, Inc.",
  legal: [{ label: "Privacy", href: "/privacy" }, { label: "Terms", href: "/terms" }] satisfies FooterLink[],
  status: "All systems normal" as string | null,
};

/** Wire these to your app: checkout, newsletter API. */
function onSelectPlan({ planId, billing, seats }: { planId: string; billing: string; seats: number }) {
  window.location.assign(new URL(`/sign-up?plan=${planId}&billing=${billing}&seats=${seats}`, window.location.origin));
}
async function onSubscribe(email: string) {
  void email; // e.g. await fetch("/api/subscribe", { method: "POST", body: JSON.stringify({ email }) })
}

// ─── Page ───────────────────────────────────────────────────────────────────

const SECTION_Y = "py-16 sm:py-20";

/** A complete SaaS landing page built from free GetUrUI blocks. Render it from app/page.tsx. */
export interface SaasLandingLiteProps {
  /** Replaces the default plan handler below (the site preview passes a no-op). */
  onSelectPlan?: typeof onSelectPlan;
}

export function SaasLandingLite({ onSelectPlan: selectPlan = onSelectPlan }: SaasLandingLiteProps = {}) {
  return (
    <div className="min-h-dvh overflow-x-clip bg-bg text-ink">
      <SiteHeader />
      <main>
        <HeroSection
          variant="centered"
          eyebrow={HERO.eyebrow}
          eyebrowBadge={HERO.eyebrowBadge}
          eyebrowHref={HERO.eyebrowHref}
          headline={HERO.headline}
          mark={HERO.mark}
          subcopy={HERO.subcopy}
          primaryAction={HERO.primaryAction}
          secondaryAction={HERO.secondaryAction}
          installCommand={null}
          showProductShot={false}
          className="pb-6 pt-20 sm:pb-8 sm:pt-28"
        />
        <LogoCloud heading={LOGOS.heading} logos={LOGOS.logos} variant="marquee" className="py-10 sm:py-12" />
        <div id="features" className="scroll-mt-16">
          <FeatureGrid {...FEATURES_COPY} features={FEATURES} className={SECTION_Y} />
        </div>
        <StatsSection {...STATS} className={SECTION_Y} />
        <div id="pricing" className="scroll-mt-16">
          <PricingSection {...PRICING} onSelectPlan={selectPlan} className={SECTION_Y} />
        </div>
        <TestimonialsWall {...TESTIMONIALS} variant="marquee" className={SECTION_Y} />
        <div id="faq" className="scroll-mt-16">
          <FaqSection {...FAQ} className={SECTION_Y} />
        </div>
        <CtaBanner {...CTA} align="center" className="pb-20 pt-4 sm:pb-24 sm:pt-4" />
      </main>
      <FooterBlock
        brand={{ name: BRAND.name, description: BRAND.tagline, href: "#top" }}
        columns={FOOTER.columns}
        newsletter={{ ...FOOTER.newsletter, onSubscribe }}
        copyright={FOOTER.copyright}
        legal={FOOTER.legal}
        status={FOOTER.status}
      />
    </div>
  );
}

// ─── Header ─────────────────────────────────────────────────────────────────

const FOCUS = "outline-none focus-visible:shadow-[var(--focus-ring)]";

function SiteHeader() {
  const [open, setOpen] = React.useState(false);
  const menuId = React.useId();
  const toggleRef = React.useRef<HTMLButtonElement>(null);

  React.useEffect(() => {
    if (!open) return;
    const onKey = (e: KeyboardEvent) => {
      if (e.key === "Escape") {
        setOpen(false);
        toggleRef.current?.focus();
      }
    };
    const onResize = () => window.innerWidth >= 768 && setOpen(false);
    document.addEventListener("keydown", onKey);
    window.addEventListener("resize", onResize);
    return () => {
      document.removeEventListener("keydown", onKey);
      window.removeEventListener("resize", onResize);
    };
  }, [open]);

  return (
    <header id="top" className="sticky top-0 z-50 border-b border-border bg-bg/80 backdrop-blur-md supports-[backdrop-filter]:bg-bg/70">
      <div className="mx-auto flex h-16 max-w-6xl items-center gap-3 px-4 sm:px-6">
        <a href="#top" className={`-ml-1 inline-flex min-w-0 items-center gap-2 rounded-lg px-1 py-1 ${FOCUS}`}>
          <span aria-hidden className="grid size-7 shrink-0 place-items-center rounded-lg bg-volt font-display text-sm font-bold text-on-volt">
            {BRAND.name.charAt(0)}
          </span>
          <span className="truncate font-display text-[17px] font-semibold tracking-[-0.3px] text-ink">{BRAND.name}</span>
        </a>

        <nav aria-label="Main" className="ml-6 hidden md:block">
          <ul className="flex items-center gap-1">
            {NAV.links.map((l) => (
              <li key={l.href}>
                <a href={l.href} className={`inline-flex h-9 items-center rounded-lg px-3 text-sm font-medium text-ink-muted transition-colors hover:bg-surface hover:text-ink ${FOCUS}`}>
                  {l.label}
                </a>
              </li>
            ))}
          </ul>
        </nav>

        <div className="ml-auto flex shrink-0 items-center gap-2">
          <a
            href={NAV.secondary.href}
            className={`hidden h-9 items-center rounded-lg px-3 text-sm font-medium text-ink-muted transition-colors hover:text-ink sm:inline-flex ${FOCUS}`}
          >
            {NAV.secondary.label}
          </a>
          <a
            href={NAV.cta.href}
            className={`inline-flex h-9 items-center rounded-lg bg-volt px-4 text-sm font-semibold text-on-volt transition-[filter] hover:brightness-105 ${FOCUS}`}
          >
            {NAV.cta.label}
          </a>
          <button
            ref={toggleRef}
            type="button"
            aria-expanded={open}
            aria-controls={menuId}
            aria-label={open ? "Close menu" : "Open menu"}
            onClick={() => setOpen((o) => !o)}
            className={`grid size-9 place-items-center rounded-lg border border-border text-ink transition-colors hover:bg-surface md:hidden ${FOCUS}`}
          >
            {open ? <X aria-hidden className="size-4" /> : <Menu aria-hidden className="size-4" />}
          </button>
        </div>
      </div>

      <nav id={menuId} aria-label="Main menu" hidden={!open} className="border-t border-border bg-bg md:hidden">
        <ul className="mx-auto grid max-w-6xl gap-1 px-4 py-3 sm:px-6">
          {[...NAV.links, NAV.secondary].map((l) => (
            <li key={l.href}>
              <a
                href={l.href}
                onClick={() => setOpen(false)}
                className={`flex h-11 items-center rounded-lg px-3 text-[15px] font-medium text-ink transition-colors hover:bg-surface ${FOCUS}`}
              >
                {l.label}
              </a>
            </li>
          ))}
        </ul>
      </nav>
    </header>
  );
}

// ─── Feature illustrations (decorative; text comes from FEATURE_ART) ───────

function QueueArt({ items }: { items: { title: string; meta: string; done: boolean }[] }) {
  return (
    <div aria-hidden className="grid w-full max-w-[360px] gap-1.5">
      {items.map((t, i) => (
        <div
          key={t.title}
          className={`flex h-9 items-center gap-2.5 rounded-md border px-3 ${i === 1 ? "border-border-strong bg-surface-raised shadow-sm" : "border-border bg-surface"}`}
        >
          <span className={`grid size-4 shrink-0 place-items-center rounded-full ${t.done ? "bg-volt text-on-volt" : "ring-1 ring-inset ring-border-strong"}`}>
            {t.done && <Check className="size-2.5" strokeWidth={3} />}
          </span>
          <span className={`min-w-0 flex-1 truncate text-[12.5px] font-medium ${t.done ? "text-ink-subtle line-through" : "text-ink"}`}>{t.title}</span>
          <span className="shrink-0 text-[11px] text-ink-subtle">{t.meta}</span>
        </div>
      ))}
    </div>
  );
}

function RuleArt({ when, then }: { when: string; then: string }) {
  return (
    <div aria-hidden className="grid w-full max-w-[250px] gap-1.5 text-[12px]">
      <span className="rounded-md border border-border bg-surface-raised px-3 py-2 text-ink-muted">
        <span className="font-semibold text-ink">When</span> {when}
      </span>
      <span className="ml-4 h-3 w-px bg-border-strong" />
      <span className="rounded-md border border-volt-ink/40 bg-volt-soft px-3 py-2 text-ink">
        <span className="font-semibold text-volt-ink">Then</span> {then}
      </span>
    </div>
  );
}

function DueArt({ label, detail }: { label: string; detail: string }) {
  return (
    <div aria-hidden className="grid w-full max-w-[220px] gap-2 rounded-md border border-border bg-surface-raised p-3">
      <span className="inline-flex w-fit items-center gap-1.5 rounded-full bg-ember-soft px-2 py-0.5 text-[11px] font-semibold text-ember-ink">
        <CalendarClock className="size-3" /> {label}
      </span>
      <span className="h-1.5 overflow-hidden rounded-full bg-surface-sunken">
        <span className="block h-full w-3/4 rounded-full bg-volt" />
      </span>
      <span className="text-[11px] text-ink-subtle">{detail}</span>
    </div>
  );
}
```

### `LICENSE-geturui.md`

```md
MIT License

Copyright (c) 2026 geturui.com

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
```
