# Shadcn Pricing Page: Jahres-Toggle, Tabelle, Mobile-Layout

> So baust du eine shadcn pricing page in React: Monatlich / Jährlich mit rollenden Preisen, Vergleichstabelle mit Sticky Header und Pläne fürs Smartphone.

- Autor: [Serban Rusu](https://wingo-ui.com/blog/authors/serban), Founder of Wingo UI
- Veröffentlicht: 16. Okt. 2026
- Kategorie: [Praxis: Screens und Apps](https://wingo-ui.com/de/blog/category/build)
- Lesezeit: 10 Min.
- Kanonische URL: https://wingo-ui.com/de/blog/shadcn-pricing-page
- Englische Fassung: https://wingo-ui.com/blog/shadcn-pricing-page

## Kurz gesagt

Eine shadcn pricing page braucht einen Umschalter Monatlich / Jährlich, Plan-Karten mit einem hervorgehobenen Plan, eine Vergleichstabelle, Buttons, die zum Checkout führen, und ein FAQ. shadcn/ui liefert Stand Oktober 2026 keine Pricing-Komponente und keinen Pricing-Block, also setzt du sie aus Card, Tabs und Table zusammen oder installierst eine fertige Preistabelle. Mit der Pricing Table von Wingo UI speicherst du pro Plan den Jahrespreis, und die Komponente zeigt den Monatswert, das Ersparnis-Badge und auf dem Smartphone wischbare Karten mit einem einspaltigen Vergleich.

Eine shadcn pricing page wirkt einfach, bis du eine baust: ein Umschalter Monatlich / Jährlich, drei Plan-Karten, eine Vergleichstabelle, Buttons, die einen Checkout starten. shadcn/ui selbst liefert Stand Oktober 2026 keinen Pricing-Block, also starten die meisten mit einer Galerie. [shadcnblocks](https://www.shadcnblocks.com/pages/pricing-page) listet fünf Layouts für Pricing Pages, [shadcn/studio](https://shadcnstudio.com/pages/pricing-page) listet zwanzig, davon eins als kostenlos markiert, und 21st.dev hostet Preistabellen aus der Community wie die von [Kokonut UI](https://21st.dev/@kokonutd/components/pricing-table.md), die Preise mit NumberFlow animiert (alles geprüft am 16. Oktober 2026). Was diese Seiten selten erklären, ist der Teil, der kaputtgeht: die Rechnung beim Jahrespreis, was die Vergleichstabelle auf einem 390px breiten Bildschirm macht und was ein Screenreader hört, wenn sich die Preise ändern.

In diesem Tutorial baust du eine React Pricing Page mit der [Pricing Table](https://wingo-ui.com/components/pricing-table) von Wingo UI. Wir entwickeln diese Bibliothek selbst, sind also nicht neutral. Jedes Snippet nutzt die echten Props der Komponente, und jede Angabe von außen ist mit ihrer Quelle verlinkt.

## Was braucht eine shadcn pricing page?

Eine shadcn pricing page braucht fünf Teile, in dieser Reihenfolge:

1. **Einen Umschalter für den Zeitraum.** Monatlich und Jährlich, mit der Ersparnis an der Option Jährlich. Er ändert Preise, die schon zu sehen sind, also ist er eine Radio Group; Tabs wechseln dagegen ganze Panels.
2. **Plan-Karten.** Zwei bis vier Pläne, einer hervorgehoben. Jede Karte hat einen Namen, eine Zeile „für wen“, einen Preis, eine kurze Feature-Liste und einen Button.
3. **Eine Vergleichstabelle.** Jedes Feature in einer Zeile, die Pläne in Spalten. Die [Richtlinien für Vergleichstabellen](https://www.nngroup.com/articles/comparison-tables/) der Nielsen Norman Group empfehlen höchstens etwa fünf Elemente, für jedes Element dieselben Merkmale und einen Header, der beim Scrollen langer Tabellen sichtbar bleibt.
4. **Buttons, die zum Checkout führen.** Ein Button, der zeigt, dass er arbeitet, während dein Server die Checkout-Session anlegt.
5. **Antworten auf Einwände.** Ein FAQ und ein abschließender Call to Action. Diese gehören zur Seite rund um die Tabelle.

Baust du das aus Teilen von shadcn/ui, greifst du zu Card, Toggle Group oder Tabs und Table und schreibst dann die Preisformatierung, die Ersparnis-Rechnung, die Ansage für den Screenreader und das Layout fürs Smartphone selbst. Der Rest dieses Artikels nutzt eine React Pricing Table Component, die das schon erledigt und sich wie jede shadcn-Komponente als Quellcode installiert. Sie deckt die ersten vier Teile in einer Datei ab.

## Wie installierst du die Preistabelle?

Ein einziger CLI-Befehl kopiert diese shadcn pricing table und alles, was sie importiert, in dein Projekt:

```bash
npx wingo-ui@latest add pricing-table
```

Das CLI kopiert `components/ui/pricing-table.tsx` als Quellcode in dein Projekt, dazu alles, was die Datei importiert: das [Segmented Control](https://wingo-ui.com/components/segmented-control) für den Umschalter, Price für die rollenden Ziffern, Carousel fürs Smartphone, Table für den Vergleich und die gemeinsamen Helfer für Tones, Motion und Locale. Außerdem installiert es `motion` und `lucide-react`. Die Datei ist eine Client-Komponente (`"use client"`) und funktioniert deshalb auch in einer Seite im App Router.

## Wie modellierst du Pläne und Jahrespreise?

Pläne sind einfache Daten. Ein Preis ist eine Zahl, wenn er sich nie ändert, `{ monthly, yearly }`, wenn er dem Umschalter folgt, `0` für kostenlos und `null` für einen individuellen Plan (sein Button heißt standardmäßig „Contact us“).

Die eine Entscheidung, die sitzen muss: **`yearly` ist das, was die Kundin oder der Kunde pro Jahr zahlt**, der volle Jahresbetrag. Speicher die Zahl, die dein Billing-Anbieter abbucht, und lass die Komponente teilen.

```tsx
// app/pricing/plans.ts
import type { PricingComparisonGroup, PricingPlan } from "@/components/ui/pricing-table";

export const plans: PricingPlan[] = [
  {
    id: "free",
    name: "Free",
    description: "Für Nebenprojekte und zum Ausprobieren.",
    price: 0,
    features: ["1 Projekt", "Community-Support", { label: "Eigene Domain", included: false }],
    cta: { label: "Kostenlos starten", href: "/sign-up" },
  },
  {
    id: "starter",
    name: "Starter",
    description: "Für Freelancer, die für Kunden liefern.",
    price: { monthly: 15, yearly: 144 },
    featured: true,
    features: [
      "10 Projekte",
      "Eigene Domains",
      { label: "E-Mail-Support", hint: "Wir antworten innerhalb eines Werktags." },
    ],
    note: "14 Tage kostenlos, ohne Karte",
  },
  {
    id: "team",
    name: "Team",
    description: "Für Agenturen und Produktteams.",
    price: { monthly: 40, yearly: 384 },
    featuresTitle: "Alles aus Starter, plus:",
    features: ["Unbegrenzte Projekte", "Rollen und Berechtigungen", "Audit-Log"],
  },
];
```

Mit diesen Zahlen zeigt die Tabelle bei Yearly den Plan Starter mit $12/mo mit „Billed $144 yearly“ darunter und Team als $32/mo mit „Billed $384 yearly“. Die Option Yearly bekommt ein Badge „Save 20%“: Die Komponente rechnet für jeden Plan `1 - yearly / (monthly * 12)`, rundet ab und zeigt den besten Wert. Willst du lieber den Jahresbetrag auf der Karte, setz `yearlyDisplay="per-year"`, dann steht darunter „You save $36“.

Ein Feature ist entweder ein String (enthalten) oder `{ label, included?, hint? }`. `included: false` zeigt es als fehlend, und ein `hint` ergänzt einen (i)-Button, der sich bei Hover, Fokus oder Tippen öffnet.

Zum Rendern reicht ein Element:

```tsx
// app/pricing/page.tsx
import { PricingTable } from "@/components/ui/pricing-table";
import { plans } from "./plans";

export default function PricingRoute() {
  return <PricingTable plans={plans} currency="USD" />;
}
```

Diese Seite kann eine Server-Komponente bleiben: Die Pläne sind einfache Daten, und `PricingTable` ist die Client-Grenze.

> Live-Demo (Pricing table): Schalte von Monthly auf Yearly und sieh zu, wie jeder Preis zu seinem neuen Wert rollt, während sich die Zeile mit dem Abrechnungsbetrag ändert. Fahr mit der Maus über ein (i) oder tipp darauf, um den Hinweis zu lesen, und scroll zum Vergleich unter den Plänen. Probier sie unter [Pricing table](https://wingo-ui.com/components/pricing-table) aus und installier sie mit `npx wingo-ui@latest add pricing-table`.

## Wie funktioniert der Umschalter Monatlich / Jährlich?

Der Pricing Toggle ist das kostenlose Segmented Control, gerendert mit `aria-label="Billing period"`. Es ist eine Radio Group mit einem einzigen Tab-Stopp: Die Pfeiltasten Links und Rechts verschieben die Auswahl und wählen dabei aus, und auf Touch-Geräten hältst du den Thumb gedrückt und ziehst ihn hinüber. Wir erklären das Control ausführlich, auch wann Tabs oder eine Toggle Group besser passen, in unserem [Guide zum React Segmented Control](https://wingo-ui.com/de/blog/react-segmented-control).

Drei Details zählen auf einer Pricing Page:

- **Die Preise rollen.** Jede Ziffer rollt zu ihrem neuen Wert, wenn sich der Zeitraum ändert. Ist „Bewegung reduzieren“ aktiv, erscheint der neue Wert sofort. Setz `animatePrice={false}`, um das Rollen für alle abzuschalten.
- **Ein Screenreader hört die Änderung einmal.** Eine Live Region (polite) sagt den Zeitraum und den neuen Preis des hervorgehobenen Plans an, zum Beispiel „Yearly. Starter: $12 per month, Billed $144 yearly“. Beim ersten Rendern bleibt sie still, damit der Seitenaufbau nicht doppelt vorgelesen wird.
- **Der Umschalter blendet sich selbst aus**, wenn kein Plan einen Jahrespreis hat. Mit `showToggle={false}` blendest du ihn bewusst aus.

Die Komponente startet auf Monthly. Soll sie auf Yearly starten, ohne dass du State verwaltest, gib `defaultPeriod="yearly"` mit. Um den Zeitraum von außen zu lesen (für Checkout oder Analytics), steuerst du ihn mit `period` und `onPeriodChange`, typisiert mit dem exportierten `BillingPeriod`. Der Abschnitt zum Checkout weiter unten zeigt die komplette gesteuerte Version.

## Wie fügst du die Vergleichstabelle hinzu?

Gib `comparison` als Gruppen von Zeilen mit. Jede Zeile hat einen Wert pro Plan-ID: `true` ist ein Haken, `false` ein Strich, und alles andere wird als Text ausgegeben. Ergänze sie in derselben `plans.ts`, mit den Plan-IDs von oben als Schlüssel:

```tsx
// app/pricing/plans.ts (Fortsetzung)
export const comparison: PricingComparisonGroup[] = [
  {
    label: "Projekte",
    rows: [
      { label: "Projekte", values: { free: "1", starter: "10", team: "Unbegrenzt" } },
      { label: "Eigene Domains", values: { free: false, starter: true, team: true } },
    ],
  },
  {
    label: "Support",
    rows: [
      { label: "E-Mail-Support", values: { free: false, starter: true, team: true } },
      {
        label: "Antwortzeit",
        hint: "Werktage, ab der ersten Nachricht.",
        values: { free: "Community", starter: "1 Tag", team: "4 Stunden" },
      },
    ],
  },
];
```

Fehlt eine Plan-ID in `values`, erscheint ein Strich, genau wie bei `false`. Ein Tippfehler oder eine umbenannte Plan-ID erscheint also ohne jede Warnung als „nicht enthalten“. Halte die Schlüssel synchron mit `plans`. In einem breiten Layout rendert diese Vergleichstabelle als echte `<table>`, deren Kopfzeile mit Plan-Namen und Buttons beim Scrollen kleben bleibt. Das ist der Sticky Header, den der Artikel von NN/g verlangt. Hat deine Seite eine fixierte Top-Bar, setz die CSS-Variable `--topbar-offset` auf einem Elternelement auf deren Höhe, damit der Sticky Header darunter stoppt statt dahinter.

## Wie ordnest du die Pläne auf dem Smartphone an?

Auf dem Smartphone werden die Pläne zu wischbaren Karten, und der Vergleich zeigt einen Plan nach dem anderen. Nur Stapeln reicht nicht: Drei gestapelte Plan-Karten schieben den Vergleich auf dem Smartphone mehrere Bildschirmhöhen nach unten, und eine Vergleichstabelle mit vier Spalten bei 390px quetscht entweder die Beschriftungen oder scrollt seitlich und verliert sie.

Die Pricing Table wechselt das Layout bei 768px **ihrer eigenen Breite**, gemessen mit Container Queries statt am Viewport. Sie ordnet sich also auch in einer schmalen Doku-Spalte oder neben einer Sidebar richtig an, und weil das reines CSS ist, hat schon das HTML vom Server das richtige Layout, ohne Flackern bei der Hydration.

Unter 768px:

- **Pläne werden zu wischbaren Karten**, die beim hervorgehobenen Plan starten, die nächste Karte schaut am Rand hervor, und Punkte springen zwischen den Karten. Die Pfeiltasten Links und Rechts funktionieren, sobald der Fokus darin liegt. Bei zwei Plänen setzt `mobileLayout="stack"` sie stattdessen untereinander.
- **Der Vergleich wird zur Plan-Auswahl** (ein weiteres Segmented Control) über einer Spalte mit den Zeilen des gewählten Plans, am Ende mit dessen Button. Das ist derselbe Schritt wie bei einer Tabelle, die zu Karten wird; für Datentabellen zeigen wir ihn in [responsive Tabellen als mobile Karten](https://wingo-ui.com/de/blog/responsive-table-mobile-cards).
- **Der Umschalter nimmt die volle Breite ein**, und auf Touchscreens ist er 44px hoch.

Die schmale Spalte auf der Doku-Seite zeigt das Smartphone-Layout, so kannst du beides ausprobieren: Öffne die [Doku der Pricing Table](https://wingo-ui.com/components/pricing-table) am Laptop und auf dem Smartphone.

## Wie verbindest du die Buttons mit dem Checkout?

Das `cta` eines Plans nimmt entweder ein `href` (rendert einen Link) oder ein `onClick`. Gibt `onClick` ein Promise zurück, zeigt der Button dieses Plans einen Spinner, bis es erledigt ist, und weiteres Tippen wird ignoriert. Hier ist die fertige Seite, mit gesteuertem Zeitraum und einem Checkout über deine eigene API-Route für jeden bezahlten Plan:

```tsx
// app/pricing/pricing-plans.tsx
"use client";

import { useState } from "react";
import { PricingTable, type BillingPeriod, type PricingPlan } from "@/components/ui/pricing-table";
import { comparison, plans } from "./plans";

async function startCheckout(planId: string, period: BillingPeriod) {
  // deine Route legt die Checkout-Session auf dem Server an und gibt ihre URL zurück
  const res = await fetch("/api/checkout", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ planId, period }),
  });
  // auch ein abgelehntes Promise stoppt den Spinner, der Button lässt sich also wieder drücken
  if (!res.ok) throw new Error(`Checkout fehlgeschlagen: ${res.status}`);
  const { url } = (await res.json()) as { url: string };
  window.location.assign(url);
}

export function PricingPlans() {
  const [period, setPeriod] = useState<BillingPeriod>("yearly");

  const withCheckout: PricingPlan[] = plans.map((plan) =>
    plan.cta?.href
      ? plan
      : { ...plan, cta: { label: `${plan.name} wählen`, onClick: () => startCheckout(plan.id, period) } },
  );

  return (
    <PricingTable
      plans={withCheckout}
      comparison={comparison}
      period={period}
      onPeriodChange={setPeriod}
      currency="USD"
      tone="brand"
    />
  );
}
```

```tsx
// app/pricing/page.tsx
import type { Metadata } from "next";
import { PricingPlans } from "./pricing-plans";

export const metadata: Metadata = { title: "Preise" };

export default function PricingRoute() {
  return (
    <main className="mx-auto w-full max-w-6xl px-4 py-16">
      <h1 className="mb-10 text-center text-4xl font-semibold tracking-tight">Preise</h1>
      <PricingPlans />
    </main>
  );
}
```

Funktionen können nicht von einer Server-Komponente an eine Client-Komponente übergeben werden, deshalb stehen die Pläne mit `onClick` in `pricing-plans.tsx`. Hat jeder Plan nur ein `href`, kannst du `PricingTable` direkt aus der Server-Seite rendern.

`tone="brand"` färbt Badge und Button des hervorgehobenen Plans, und jeder Tone außer `neutral` macht die Feature-Haken grün. Standard ist `neutral`, dann bleiben die Haken in der Textfarbe, und `color` nimmt jede CSS-Farbe, falls deine Marke keinem der Tones entspricht. `featuredStyle` legt fest, wie der hervorgehobene Plan auffällt: `raised` (Standard), `outline` oder `inverted`.

## Willst du lieber die ganze Pricing Page?

Willst du die komplette Route und nicht nur die Tabelle, installier den [Block Pricing Page](https://wingo-ui.com/components/pricing-page). Er bettet die Tabelle in eine komplette Seite ein: mit Marketing-Header, dessen Menü auf dem Smartphone als Bottom Sheet aufgeht, den Plänen samt Umschalter Monatlich / Jährlich und Umschalter USD / EUR, Kundenlogos, drei Zitaten, einem FAQ-Akkordeon, einem abschließenden Call to Action und einem Footer.

```bash
npx wingo-ui@latest add pricing-page
```

```tsx
// app/(marketing)/pricing/page.tsx
"use client";

import Link from "next/link";
import { useRouter } from "next/navigation";
import { PricingPage } from "@/components/blocks/pricing-page";

export default function PricingRoute() {
  const router = useRouter();
  return (
    <PricingPage
      linkComponent={Link}
      defaultPeriod="yearly"
      onSelectPlan={(planId, period, currency) =>
        router.push(`/sign-up?plan=${planId}&period=${period}&currency=${currency}`)
      }
    />
  );
}
```

Er bringt Beispieldaten mit (`PRICING_PAGE_SAMPLE`) und sieht deshalb ganz ohne Props fertig aus. Gib eigene `plans`, `comparison`, `faqs` und `testimonials` mit, um sie zu ersetzen, und schalte Abschnitte mit `showLogos`, `showTestimonials`, `showFaq` oder `showCta` ab. Euro-Preise werden mit `eurRate` (standardmäßig 1.08) aus deinen Basispreisen umgerechnet und auf ganze Euro gerundet. Die Umrechnung läuft immer, wenn die Währung EUR ist. Gibst du Pläne mit, die schon in Euro bepreist sind, setz also zusätzlich `currency="EUR"`, `currencies={["EUR"]}` (ein einziger Eintrag blendet den Umschalter aus) und `eurRate={1}`, sonst werden deine Euro-Preise ein zweites Mal geteilt. `design="bold"` tauscht den klassischen Look gegen schwere Display-Schrift und einen invertierten hervorgehobenen Plan. Setz `header={false}` und `footer={false}`, wenn die Seite in deinem eigenen Site-Layout sitzt.

> Live-Demo (Pricing Page): Wechsle Zeitraum und Währung und wähl dann einen Plan: Sein Button dreht sich, während onSelectPlan läuft. Öffne auf dem Smartphone das Menü, um das Bottom Sheet zu sehen. Probier sie unter [Pricing Page](https://wingo-ui.com/components/pricing-page) aus und installier sie mit `npx wingo-ui@latest add pricing-page`.

Dieser Block ersetzt die zwei Dateien aus dem vorigen Abschnitt: Nimm für `/pricing` das eine oder das andere. Weil diese `page.tsx` eine Client-Komponente ist, kann sie kein `metadata` exportieren. Brauchst du einen Seitentitel, verschieb das Element `PricingPage` in eine eigene Client-Datei und lass `page.tsx` eine Server-Komponente bleiben, wie im vorigen Abschnitt.

Für einen Preisabschnitt auf einer Landing Page statt einer ganzen Route ist die [Pricing Section](https://wingo-ui.com/components/pricing-section) die kompakte Version mit vier Layouts und einem Link „Compare all features“. Ein Unterschied, auf den du achten musst: Dort ist der Preis `yearly` der Monatswert bei jährlicher Zahlung, während `yearly` in der Pricing Table der Jahresbetrag ist.

## Was ist der nächste Schritt?

Installier die Tabelle mit `npx wingo-ui@latest add pricing-table`, füg deine Pläne ein und prüf sie zuerst bei 390px. Die Pricing Table und die Pricing Page gehören zu [Wingo UI Pro](https://wingo-ui.com/de/pricing); das Segmented Control für den Umschalter ist kostenlos. Für die anderen Screens einer SaaS-App starte mit unserem [Tutorial für ein Next.js Admin Dashboard](https://wingo-ui.com/de/blog/nextjs-admin-dashboard-tutorial) oder stöber in der restlichen Kategorie [Praxis: Screens und Apps](https://wingo-ui.com/de/blog/category/build).

## Komponenten in diesem Artikel

- [Pricing table](https://wingo-ui.com/components/pricing-table) (Pro): A React pricing table: plans side by side with a monthly / yearly switch whose prices roll, a featured plan and a full comparison table. Installation: `npx wingo-ui@latest add pricing-table`
- [Pricing Page](https://wingo-ui.com/components/pricing-page) (Pro): A React SaaS pricing page: plans with monthly or yearly and USD or EUR switches, a comparison table, quotes, an FAQ and a closing call. Installation: `npx wingo-ui@latest add pricing-page`
- [Segmented Control](https://wingo-ui.com/components/segmented-control) (Free): A React iOS-style segmented control with a raised thumb that slides and follows your finger. Installation: `npx wingo-ui@latest add segmented-control`

## Häufige Fragen

### Hat shadcn/ui eine Pricing Page oder eine Preistabelle?

Stand Oktober 2026 nicht. Die offizielle Blocks-Seite zeigt Blöcke für Dashboard, Sidebar, Login und Signup, und die Komponentenliste hat Card, Table, Tabs und Toggle Group, aber keine Pricing-Komponente. Pricing Pages stammen also aus Community-Galerien, oder du baust sie selbst aus diesen Teilen.

### Soll ein Jahresplan den Monatswert oder den Jahresbetrag zeigen?

Zeig den Monatswert und darunter den Jahresbetrag („$12/mo, Billed $144 yearly“), damit sich die Zahl direkt mit dem Monatspreis vergleichen lässt. Die Pricing Table von Wingo UI macht das standardmäßig und zeigt mit yearlyDisplay="per-year" stattdessen den Jahresbetrag und die Ersparnis.

### Wie wird die prozentuale Ersparnis beim Jahresplan berechnet?

Rechne 1 minus Jahrespreis geteilt durch zwölf Monatszahlungen und runde ab. $144 im Jahr gegenüber $15 im Monat ergibt 1 - 144 / 180, also 20 %. Die Pricing Table zeigt im Badge an der Option Yearly die beste Ersparnis über alle Pläne.

### Wie mache ich eine Preistabelle auf dem Smartphone responsive?

Mach aus den Plänen eine wischbare Reihe von Karten, die beim hervorgehobenen Plan startet, oder stapel sie untereinander, wenn es nur zwei sind. Die Vergleichstabelle ersetzt du durch eine Plan-Auswahl über einer einzigen Spalte mit Zeilen. Breite Vergleichstabellen, die auf einem 390px breiten Bildschirm seitlich scrollen, verlieren die Zeilenbeschriftungen.

### Ist die Pricing Table von Wingo UI kostenlos?

Das Segmented Control für den Umschalter ist kostenlos. Die Pricing Table, die Pricing Section und der komplette Block Pricing Page gehören zu Wingo UI Pro, und alle lassen sich mit npx wingo-ui@latest add installieren.

---

Quelle: https://wingo-ui.com/de/blog/shadcn-pricing-page
