# PlanCard

> One pricing plan on a quiet surface, with the recommended plan brand-tinted; PlanGrid lays plans side by side.

Source: https://docs.nasaqui.com/components/plan-card

## Install

```bash
npx shadcn@latest add https://docs.nasaqui.com/r/plan-card.json
```

One pricing plan: name, description, price, an action and a feature list. Plans are separated by space and a quiet surface, not borders. The recommended plan is tinted with the product's brand colour. `PlanGrid` lays several out.

## When to use

- A pricing page with two to four plans, when you lay out the cards yourself.
- Building plans from data with a Monthly / Yearly switch and working buttons? Use [`PricingTable`](https://docs.nasaqui.com/components/pricing-table); it renders `PlanCard`s for you.

## When not to use

- A single price with no comparison: use [`Price`](https://docs.nasaqui.com/components/price).
- A product listing: use [`ProductCard`](https://docs.nasaqui.com/components/product-card).
- A feature comparison with many rows: use [`PlanComparison`](https://docs.nasaqui.com/components/pricing-table).
- An upgrade popup: use [`UpgradeDialog`](https://docs.nasaqui.com/components/upgrade-prompt).

## Import

```tsx
import { PlanCard, PlanGrid, type PlanCardProps, type PlanFeature } from "@fadymondy/nasaq/web";
// inside this monorepo: "@nasaq/web"
```

## Quick start

```tsx
import { Button, PlanCard, PlanGrid, Price } from "@fadymondy/nasaq/web";

export function Pricing() {
  return (
    <PlanGrid>
      <PlanCard
        name="Solo"
        description="For one person."
        price={<Price amount={0} size="lg" />}
        features={["1 project", "Community support"]}
        action={<Button variant="secondary">Start free</Button>}
      />
      <PlanCard
        highlighted
        name="Team"
        description="For small teams."
        price={<Price amount={12} period="seat-month" size="lg" />}
        priceNote="Billed yearly"
        featuresTitle="Everything in Solo, plus"
        features={["Unlimited projects", { label: "SSO", included: false }]}
        footnote="No card required"
        badge="Most popular"
        action={<Button variant="primary">Start trial</Button>}
      />
    </PlanGrid>
  );
}
```

## Anatomy

```
PlanGrid                      data-slot="plan-grid", @container wrapper + grid
└─ PlanCard                   data-slot="plan-card", data-highlighted, data-current   <article>
   ├─ badge pill              data-slot="plan-card-badge", on the top edge
   ├─ name                    <h3>, labels the article
   ├─ description
   ├─ price + priceNote
   ├─ action + footnote
   └─ featuresTitle + features  <ul>, check (included) or dash (not included, sr-only "Not included")
```

## API

### `PlanCard`

`PlanCardProps extends Omit<ComponentProps<"article">, "title">`. Remaining props go to the `<article>`.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `name` | `ReactNode` | required | Plan name: "Team". |
| `description?` | `ReactNode` | none | Who it is for, one line. |
| `price` | `ReactNode` | required | Usually `<Price size="lg" />`. |
| `priceNote?` | `ReactNode` | none | A line under the price: "Billed yearly", "Up to 3 people". |
| `features?` | `(ReactNode \| PlanFeature)[]` | none | What the plan includes. A bare node is included; a `PlanFeature` can be `included: false` (a muted dash) or carry a `hint` tooltip. |
| `featuresTitle?` | `ReactNode` | none | A heading over the features: "Everything in Solo, plus". |
| `action?` | `ReactNode` | none | The plan's button. Only the highlighted plan's button should be primary. |
| `footnote?` | `ReactNode` | none | Fine print under the button: "No card required". |
| `highlighted?` | `boolean` | `false` | The recommended plan: brand-tinted, lifted surface with a brand ring. Use on at most one plan. Sets `data-highlighted`. |
| `current?` | `boolean` | `false` | The account's plan: a neutral ring and a "Current plan" pill (unless `badge` is set). Sets `data-current`. |
| `badge?` | `ReactNode` | none | "Most popular". A pill on the card's top edge; solid brand on the highlighted plan. |

### `PlanFeature`

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `label` | `ReactNode` | required | The feature. |
| `included?` | `boolean` | `true` | `false` shows a dash and a hidden "Not included", so people see what a bigger plan adds. |
| `hint?` | `string` | none | A short explanation in a tooltip on the label. |
| `className?` | `string` | none | Merged onto the article. |

### `PlanGrid`

`ComponentProps<"div">`. Lays `PlanCard`s side by side from 48rem of container width (one column per card), stacked below it.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `children?` | `ReactNode` | none | The `PlanCard`s. |
| `className?` | `string` | none | Merged onto the inner grid. |

## Examples

### Three plans, Arabic

```tsx
import { Button, PlanCard, PlanGrid, Price } from "@fadymondy/nasaq/web";

export function ArabicPricing() {
  return (
    <div dir="rtl">
      <PlanGrid>
        <PlanCard
          name="فردي"
          description="لشخص واحد."
          price={<Price amount={0} size="lg" />}
          features={["مشروع واحد", "دعم المجتمع"]}
          action={<Button variant="secondary">ابدأ مجانًا</Button>}
        />
        <PlanCard
          highlighted
          name="فريق"
          description="للفرق الصغيرة."
          price={<Price amount={12} period="seat-month" size="lg" />}
          priceNote="تُدفع سنويًا"
          badge="الأكثر شيوعًا"
          features={["كل ما في الفردي، بالإضافة إلى", "مشاريع غير محدودة"]}
          action={<Button variant="primary">ابدأ التجربة</Button>}
        />
        <PlanCard
          name="مؤسسة"
          description="للمؤسسات الكبيرة."
          price="حسب الطلب"
          features={["تسجيل دخول موحد", "اتفاقية مستوى خدمة"]}
          action={<Button variant="secondary">تواصل معنا</Button>}
        />
      </PlanGrid>
    </div>
  );
}
```

### A single card with a struck-through price

```tsx
import { Button, PlanCard, Price } from "@fadymondy/nasaq/web";

export function Launch() {
  return (
    <PlanCard
      highlighted
      name="Pro"
      price={<Price amount={8} compareAt={12} period="month" size="lg" />}
      priceNote="Launch offer"
      action={<Button variant="primary">Upgrade</Button>}
    />
  );
}
```

## Accessibility

- Each card is an `<article>` labelled by its name (`aria-labelledby`, an `<h3>`), so plans are navigable as named articles. Place the grid under an `h2`.
- The check and dash icons are `aria-hidden`; a not-included feature carries a visually hidden "(Not included)" so it is not read as included.
- A feature `hint` label is focusable and opens its tooltip on focus.
- "Highlighted" is visual (a tint and `data-highlighted`). Say it in text too: use `badge` ("Most popular").
- No keyboard interaction of its own; the `action` button is a normal tab stop.

| Key | Action |
| --- | --- |
| `Tab` | Moves to each plan's `action` in DOM order. |
| `Enter` / `Space` | Activates the focused button. |

The caller localises name, description, notes, features and badge. The built-in "Current plan" and "Not included" follow the Nasaq locale.

## RTL & i18n

- The grid flows in the inline direction: in RTL the first plan is on the right.
- Check icons sit on the inline start; the badge pill sits on the inline start of the top edge.
- `Price` formats the amount for the active locale with the Nasaq digit set and isolates it in Arabic text. Prefer it over hand-formatted strings.
- Built-in strings: "Current plan" / "خطتك الحالية", "Not included" / "غير مشمول".

## Styling & tokens

- Tokens: `bg-nq-surface`, `--nq-brand` (highlight: 9% tint, `shadow-lg`, a `ring-nq-brand/50` ring, check icons), `bg-primary` (highlighted pill), `ring-nq-line-strong` (current), `text-h3`, `text-body-sm`, `text-caption`, `text-muted-foreground`, `rounded-card`.
- Target with `[data-slot=plan-card]`, `[data-highlighted]`, `[data-current]`, `[data-slot=plan-card-badge]`, `[data-slot=plan-grid]`.
- The grid switches by container width (`@3xl`), not viewport. Extend with `className`; do not use raw hex.

## Do / Don't

- **Do** highlight at most one plan, and give only that plan a `primary` button.
- **Do** use `secondary` buttons on the other plans.
- **Do** put "Everything in the smaller plan, plus" in `featuresTitle` on larger plans.
- **Do** mark the account's plan with `current` in an in-app plans page.
- **Don't** add borders to separate plans; the surface and spacing do that.
- **Don't** format prices by hand; use `Price`.
- **Don't** put more than four plans in one `PlanGrid`.

## Related

- [PricingTable](https://docs.nasaqui.com/components/pricing-table) · [UpgradeDialog](https://docs.nasaqui.com/components/upgrade-prompt) · [Price](https://docs.nasaqui.com/components/price) · [Button](https://docs.nasaqui.com/components/button) · [Badge](https://docs.nasaqui.com/components/badge) · [FeatureStory](https://docs.nasaqui.com/components/feature-story)

## Lab

https://docs.nasaqui.com/?path=/docs/components-pricing-plan-card--docs

## Code

### React

```tsx
import { Button, PlanCard, PlanGrid, Price } from "@fadymondy/nasaq/web";

export function Pricing() {
  return (
    <PlanGrid>
      <PlanCard
        name="Solo"
        description="For one person."
        price={<Price amount={0} size="lg" />}
        features={["1 project", "Community support"]}
        action={<Button variant="secondary">Start free</Button>}
      />
      <PlanCard
        highlighted
        name="Team"
        description="For small teams."
        price={<Price amount={12} period="seat-month" size="lg" />}
        priceNote="Billed yearly"
        featuresTitle="Everything in Solo, plus"
        features={["Unlimited projects", { label: "SSO", included: false }]}
        footnote="No card required"
        badge="Most popular"
        action={<Button variant="primary">Start trial</Button>}
      />
    </PlanGrid>
  );
}
```

### shadcn

```tsx
import { Button } from "@/components/ui/button";
import { PlanCard, PlanGrid } from "@/components/ui/plan-card";
import { Price } from "@/components/ui/price";

export function Pricing() {
  return (
    <PlanGrid>
      <PlanCard
        name="Solo"
        description="For one person."
        price={<Price amount={0} size="lg" />}
        features={["1 project", "Community support"]}
        action={<Button variant="secondary">Start free</Button>}
      />
      <PlanCard
        highlighted
        name="Team"
        description="For small teams."
        price={<Price amount={12} period="seat-month" size="lg" />}
        priceNote="Billed yearly"
        featuresTitle="Everything in Solo, plus"
        features={["Unlimited projects", { label: "SSO", included: false }]}
        footnote="No card required"
        badge="Most popular"
        action={<Button variant="primary">Start trial</Button>}
      />
    </PlanGrid>
  );
}
```

### Vue

```vue
<script setup lang="ts">
import { NqButton, NqPlanCard, NqPlanGrid, formatMoney } from "@fadymondy/nasaq/vue";
</script>

<template>
  <NqPlanGrid>
    <NqPlanCard name="Solo" description="For one person." :features="['1 project', 'Community support']">
      <template #price><span class="text-h2 font-semibold tracking-tight text-foreground">{{ formatMoney(0, { compact: true }) }}</span></template>
      <template #action><NqButton variant="secondary">Start free</NqButton></template>
    </NqPlanCard>
    <NqPlanCard
      highlighted
      name="Team"
      description="For small teams."
      price-note="Billed yearly"
      features-title="Everything in Solo, plus"
      :features="['Unlimited projects', { label: 'SSO', included: false }]"
      footnote="No card required"
      badge="Most popular"
    >
      <template #price>
        <span class="text-h2 font-semibold tracking-tight text-foreground">{{ formatMoney(12, { compact: true }) }}</span>
      </template>
      <template #action><NqButton variant="primary">Start trial</NqButton></template>
    </NqPlanCard>
  </NqPlanGrid>
</template>
```

### Blade

```blade
<x-nq::plan-card.grid>
    <x-nq::plan-card name="Solo" description="For one person." :features="['1 project', 'Community support']">
        <x-slot:price><span class="text-h2 font-semibold tracking-tight text-foreground">{{ Nasaq\Nasaq::money(0) }}</span></x-slot:price>
        <x-slot:action><x-nq::button variant="secondary">Start free</x-nq::button></x-slot:action>
    </x-nq::plan-card>
    <x-nq::plan-card highlighted name="Team" description="For small teams." price-note="Billed yearly" features-title="Everything in Solo, plus" :features="['Unlimited projects', ['label' => 'SSO', 'included' => false]]" footnote="No card required" badge="Most popular">
        <x-slot:price><span class="text-h2 font-semibold tracking-tight text-foreground">{{ Nasaq\Nasaq::money(12) }}</span></x-slot:price>
        <x-slot:action><x-nq::button variant="primary">Start trial</x-nq::button></x-slot:action>
    </x-nq::plan-card>
</x-nq::plan-card.grid>
```

### HTML + Alpine

```html
<div data-slot="plan-grid" class="@container">
    <div class="grid grid-cols-1 gap-x-4 gap-y-7 pt-3 @3xl:auto-cols-fr @3xl:grid-flow-col"><article data-slot="plan-card"   aria-labelledby="nq-plan-solo"
    class="relative flex min-w-0 flex-col gap-5 rounded-card p-6 bg-nq-surface">
        <div class="flex flex-col gap-1">
        <h3 id="nq-plan-solo" class="text-h3 text-foreground">Solo</h3>
                    <p class="text-body-sm text-muted-foreground">For one person.</p>
            </div>
    <div class="flex flex-col gap-1">
        <span class="text-h2 font-semibold tracking-tight text-foreground">$0.00</span>
            </div>
            <div class="flex flex-col gap-2">
            <button data-slot="button"
     type="button"                         class="inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-control border font-sans text-label transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 border-border bg-card text-foreground hover:bg-nq-hover h-control px-[var(--nq-control-pad)]">
        Start free</button>
                    </div>
                <div class="flex flex-col gap-3 border-t border-border pt-5">
                        <ul class="flex flex-col gap-2.5">
                                    <li  data-included  class="flex items-start gap-2 text-body-sm text-foreground">
                                                    <svg aria-hidden="true" class="mt-0.5 size-4 shrink-0 text-nq-brand" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M20 6 9 17l-5-5"/>
</svg>                                                <span>
                                                            1 project
                                                                                </span>
                    </li>
                                    <li  data-included  class="flex items-start gap-2 text-body-sm text-foreground">
                                                    <svg aria-hidden="true" class="mt-0.5 size-4 shrink-0 text-nq-brand" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M20 6 9 17l-5-5"/>
</svg>                                                <span>
                                                            Community support
                                                                                </span>
                    </li>
                            </ul>
        </div>
    </article>
    <article data-slot="plan-card"  data-highlighted   aria-labelledby="nq-plan-team"
    class="relative flex min-w-0 flex-col gap-5 rounded-card p-6 bg-[color-mix(in_oklab,var(--nq-brand)_9%,var(--nq-surface))] shadow-lg ring-2 ring-nq-brand/50">
            <span data-slot="plan-card-badge"
            class="absolute -top-3 start-6 inline-flex h-6 items-center rounded-full border px-2.5 text-caption font-medium whitespace-nowrap border-transparent bg-primary text-primary-foreground">Most popular</span>
        <div class="flex flex-col gap-1">
        <h3 id="nq-plan-team" class="text-h3 text-foreground">Team</h3>
                    <p class="text-body-sm text-muted-foreground">For small teams.</p>
            </div>
    <div class="flex flex-col gap-1">
        <span class="text-h2 font-semibold tracking-tight text-foreground">$12.00</span>
                    <p class="text-caption text-muted-foreground">Billed yearly</p>
            </div>
            <div class="flex flex-col gap-2">
            <button data-slot="button"
     type="button"                         class="inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-control border border-transparent font-sans text-label transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 bg-primary text-primary-foreground hover:bg-[color-mix(in_oklab,var(--nq-action)_88%,var(--nq-fg))] h-control px-[var(--nq-control-pad)]">
        Start trial</button>
                            <p class="text-center text-caption text-muted-foreground">No card required</p>
                    </div>
                <div class="flex flex-col gap-3 border-t border-border pt-5">
                            <p class="text-label text-foreground">Everything in Solo, plus</p>
                        <ul class="flex flex-col gap-2.5">
                                    <li  data-included  class="flex items-start gap-2 text-body-sm text-foreground">
                                                    <svg aria-hidden="true" class="mt-0.5 size-4 shrink-0 text-nq-brand" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M20 6 9 17l-5-5"/>
</svg>                                                <span>
                                                            Unlimited projects
                                                                                </span>
                    </li>
                                    <li  class="flex items-start gap-2 text-body-sm text-muted-foreground">
                                                    <svg aria-hidden="true" class="mt-0.5 size-4 shrink-0 text-muted-foreground/70" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M5 12h14"/>
</svg>                                                <span>
                                                            SSO
                                                                                        <span class="sr-only"> (Not included)</span>
                                                    </span>
                    </li>
                            </ul>
        </div>
    </article></div>
</div>
```
