# BundleCard

> Several apps sold together for less; shows the overlapping artwork stack, what it is for, the computed saving and the price against the separate total.

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

## Install

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

Sells several apps as one purchase. It shows the products' artwork as an overlapping stack, a title, a short pitch, a line naming what is included, a "Save X" badge computed from `compareAt - price`, and the bundle price with the separate total struck through. It stacks vertically in a narrow container and becomes a row from `36rem` of its own width.

## When to use

- A named set of apps sold together at a lower price.

## When not to use

- A single product: use [`ProductCard`](https://docs.nasaqui.com/components/product-card).
- Tiers of one product (Free, Pro, Team): use [`PlanCard`](https://docs.nasaqui.com/components/plan-card).
- A featured product hero: use [`Spotlight`](https://docs.nasaqui.com/components/spotlight).

## Import

```tsx
import { BundleCard } from "@fadymondy/nasaq/web";
// inside this monorepo: "@nasaq/web"
```

## Quick start

```tsx
import { BundleCard, Button, ProductArtwork } from "@fadymondy/nasaq/web";

export function AgencyKit() {
  return (
    <BundleCard
      items={[
        <ProductArtwork key="m" brand="mahaam" markSize={24} />,
        <ProductArtwork key="z" brand="zekra" markSize={24} />,
      ]}
      title="Agency kit"
      description="Run client projects and remember every decision."
      includes="Mahaam · Zekra"
      price={18}
      compareAt={21}
      action={<Button size="sm">Get the kit</Button>}
    />
  );
}
```

## Anatomy

```
BundleCard                 data-slot="bundle-card"   <div class="@container">
└─ <article>               rounded-card surface, column below @xl, row from @xl
   ├─ stack                aria-hidden; items overlap with a ring
   ├─ text
   │  ├─ <h3> title + Badge (saving)
   │  ├─ description       <p>
   │  └─ includes          <p>
   └─ Price (lg) + action
```

## API

### `BundleCard`

`BundleCardProps extends Omit<ComponentProps<"article">, "title">`. Remaining props and `className` go to the inner `<article>`.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `items` | `ReactNode[]` | required | The bundled products' artwork or glyphs, in order. Each is sized 3.5rem and overlaps the previous one. |
| `title` | `ReactNode` | required | Bundle name, an `<h3>`. |
| `description?` | `ReactNode` | none | What the bundle is for. |
| `includes?` | `ReactNode` | none | A line of names. |
| `price` | `number` | required | Bundle price. |
| `compareAt` | `number` | required | The same apps bought separately. The saving is `compareAt - price`; no badge when it is 0 or less. |
| `currency?` | `string` | `"USD"` | ISO currency code. |
| `period?` | `PricePeriod` | `"month"` | `"month" \| "year" \| "seat-month" \| "once"`. |
| `savingsLabel?` | `ReactNode` | "Save $18" / "وفّر 18 US$" | Replaces the saving badge text. |
| `action?` | `ReactNode` | none | Usually one `Button`. |

## Examples

### Arabic

```tsx
import { BundleCard, Button, ProductArtwork } from "@fadymondy/nasaq/web";

export function AgencyKitAr() {
  return (
    <BundleCard
      items={[<ProductArtwork key="m" brand="mahaam" markSize={24} />, <ProductArtwork key="z" brand="zekra" markSize={24} />]}
      title="حزمة الوكالات"
      description="أدِر مشاريع العملاء، وتذكّر كل قرار."
      includes="مهام · ذكرى"
      price={18}
      compareAt={21}
      action={<Button size="sm" variant="secondary">احصل على الحزمة</Button>}
    />
  );
}
```

### Yearly price with a custom saving label

```tsx
import { BundleCard, Button, ProductArtwork } from "@fadymondy/nasaq/web";

export function YearlyKit() {
  return (
    <BundleCard
      items={[<ProductArtwork key="m" brand="mahaam" markSize={24} />]}
      title="Yearly kit"
      price={180}
      compareAt={252}
      period="year"
      savingsLabel="2 months free"
      action={<Button size="sm">Get the kit</Button>}
    />
  );
}
```

## Accessibility

- The artwork stack is `aria-hidden`; name the products in `includes` so the content is available as text.
- The title is an `<h3>`. The card is not interactive; the `action` is.

| Key | Action |
| --- | --- |
| `Tab` | Moves to the action |
| `Enter` / `Space` | Activates the action |

- Localise `title`, `description`, `includes`, `savingsLabel` and the action label.

## RTL & i18n

- The stack overlaps with `-ms-3`, so it mirrors in RTL and the price sits at the opposite end.
- The saving badge and the `Price` follow the provider locale: "Save $18" becomes "وفّر 18 US$". The amount is inside `<bdi>` and uses tabular numerals.
- Built-in strings: only "Save" / "وفّر", chosen when the locale starts with `ar`.

## Styling & tokens

- Uses `bg-nq-surface`, `rounded-card`, `text-h3`, `text-body-sm`, `text-caption` and `text-muted-foreground`. The saving is a `Badge variant="success"`.
- Target with `[data-slot=bundle-card]`.
- Extend with `className` (applied to the `<article>`). Do not override colours with raw hex.

## Do / Don't

- **Do** pass a real `compareAt`, so the saving is true.
- **Do** keep one action; if it is primary, it is the only primary in the view.
- **Don't** wrap the card in another bordered card.
- **Don't** use it for a single app.

## Related

- [ProductCard](https://docs.nasaqui.com/components/product-card) · [Spotlight](https://docs.nasaqui.com/components/spotlight) · [Price](https://docs.nasaqui.com/components/price) · [PlanCard](https://docs.nasaqui.com/components/plan-card)

## Lab

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

## Code

### React

```tsx
import { BundleCard, Button, ProductArtwork } from "@fadymondy/nasaq/web";

export function AgencyKit() {
  return (
    <BundleCard
      items={[
        <ProductArtwork key="m" brand="mahaam" markSize={24} />,
        <ProductArtwork key="z" brand="zekra" markSize={24} />,
      ]}
      title="Agency kit"
      description="Run client projects and remember every decision."
      includes="Mahaam · Zekra"
      price={18}
      compareAt={21}
      action={<Button size="sm">Get the kit</Button>}
    />
  );
}
```

### shadcn

```tsx
import { BundleCard } from "@/components/ui/bundle-card";
import { Button } from "@/components/ui/button";
import { ProductArtwork } from "@/components/ui/product-artwork";

export function AgencyKit() {
  return (
    <BundleCard
      items={[
        <ProductArtwork key="m" brand="mahaam" markSize={24} />,
        <ProductArtwork key="z" brand="zekra" markSize={24} />,
      ]}
      title="Agency kit"
      description="Run client projects and remember every decision."
      includes="Mahaam · Zekra"
      price={18}
      compareAt={21}
      action={<Button size="sm">Get the kit</Button>}
    />
  );
}
```

### Vue

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

<template>
  <NqBundleCard title="Agency kit" description="Run client projects and remember every decision." includes="Mahaam · Zekra" :price="18" :compare-at="21">
    <template #items>
      <NqProductArtwork brand="mahaam" :mark-size="24" />
      <NqProductArtwork brand="zekra" :mark-size="24" />
    </template>
    <template #action><NqButton size="sm">Get the kit</NqButton></template>
  </NqBundleCard>
</template>
```

### Blade

```blade
<x-nq::bundle-card title="Agency kit" description="Run client projects and remember every decision." includes="Mahaam · Zekra" :price="18" :compare-at="21">
    <x-slot:items>
        <x-nq::bundle-card.item><x-nq::product-artwork brand="mahaam" :mark-size="24" /></x-nq::bundle-card.item>
        <x-nq::bundle-card.item><x-nq::product-artwork brand="zekra" :mark-size="24" /></x-nq::bundle-card.item>
    </x-slot:items>
    <x-slot:action><x-nq::button size="sm">Get the kit</x-nq::button></x-slot:action>
</x-nq::bundle-card>
```

### HTML + Alpine

```html
<div data-slot="bundle-card" class="@container">
    <article class="flex flex-col gap-5 rounded-card bg-nq-surface p-5 @xl:flex-row @xl:items-center @xl:p-6">
        <div aria-hidden="true" class="flex shrink-0 [&>*]:size-14 [&>*+*]:-ms-3"><span class="overflow-hidden rounded-card ring-2 ring-nq-surface [&>*]:size-full"><div data-slot="product-artwork" data-brand="mahaam" style="background-image: radial-gradient(110% 130% at var(--art-x) 0%, color-mix(in oklab, var(--nq-brand) 34%, transparent), transparent 65%),radial-gradient(90% 110% at calc(100% - var(--art-x)) 100%, color-mix(in oklab, var(--nq-brand) 14%, transparent), transparent 70%)"
    class="relative isolate flex items-center justify-center overflow-hidden rounded-card bg-nq-surface-raised [--art-x:100%] rtl:[--art-x:0%]">
    <span aria-hidden="true" class="pointer-events-none absolute inset-0 rounded-[inherit] ring-1 ring-inset ring-[color-mix(in_oklab,var(--nq-brand)_16%,transparent)]"></span>
            <svg data-slot="product-mark" viewBox="0 0 100 100" width="24" height="24"  aria-hidden="true"  shape-rendering="crispEdges"  class="shrink-0">
                        <rect x="25" y="25" width="25" height="25" fill="#8C3FB5" />
                    <rect x="75" y="25" width="25" height="25" fill="#C9A227" />
                    <rect x="0" y="50" width="25" height="25" fill="#8C3FB5" />
                    <rect x="50" y="50" width="25" height="25" fill="#8C3FB5" />
            </svg>
    </div></span>
        <span class="overflow-hidden rounded-card ring-2 ring-nq-surface [&>*]:size-full"><div data-slot="product-artwork" data-brand="zekra" style="background-image: radial-gradient(110% 130% at var(--art-x) 0%, color-mix(in oklab, var(--nq-brand) 34%, transparent), transparent 65%),radial-gradient(90% 110% at calc(100% - var(--art-x)) 100%, color-mix(in oklab, var(--nq-brand) 14%, transparent), transparent 70%)"
    class="relative isolate flex items-center justify-center overflow-hidden rounded-card bg-nq-surface-raised [--art-x:100%] rtl:[--art-x:0%]">
    <span aria-hidden="true" class="pointer-events-none absolute inset-0 rounded-[inherit] ring-1 ring-inset ring-[color-mix(in_oklab,var(--nq-brand)_16%,transparent)]"></span>
            <svg data-slot="product-mark" viewBox="0 0 100 100" width="24" height="24"  aria-hidden="true"  shape-rendering="crispEdges"  class="shrink-0">
                        <rect x="40" y="10" width="20" height="20" fill="#C9A227" />
                    <rect x="20" y="30" width="20" height="20" fill="#6D4DE6" />
                    <rect x="60" y="30" width="20" height="20" fill="#6D4DE6" />
                    <rect x="0" y="50" width="20" height="20" fill="#6D4DE6" />
                    <rect x="40" y="50" width="20" height="20" fill="#6D4DE6" />
                    <rect x="80" y="50" width="20" height="20" fill="#6D4DE6" />
                    <rect x="20" y="70" width="20" height="20" fill="#6D4DE6" />
                    <rect x="60" y="70" width="20" height="20" fill="#6D4DE6" />
            </svg>
    </div></span></div>
        <div class="flex min-w-0 flex-1 flex-col gap-1.5">
            <div class="flex flex-wrap items-center gap-2">
                <h3 class="text-h3 text-foreground">Agency kit</h3>
                                    <span data-slot="badge"     class="inline-flex h-5 shrink-0 items-center gap-1 whitespace-nowrap rounded-[4px] border px-1.5 text-caption font-medium [&_svg]:size-3 border-nq-success/40 bg-nq-success-soft text-nq-success-text">Save <bdi class="tabular-nums">$3</bdi></span>
                            </div>
                            <p class="text-pretty text-body-sm text-muted-foreground">Run client projects and remember every decision.</p>
                                        <p class="text-caption text-muted-foreground">Mahaam · Zekra</p>
                    </div>
        <div class="flex shrink-0 items-center justify-between gap-4 @xl:flex-col @xl:items-end">
            <span data-slot="price" class="inline-flex flex-wrap items-baseline gap-x-1.5 text-foreground text-body-sm @xl:justify-end">
        <span>
            <bdi class="tabular-nums text-h2 font-semibold tracking-tight">$18</bdi>
            <span class="text-muted-foreground">/mo</span>        </span>
                    <s class="text-caption text-muted-foreground decoration-muted-foreground/60">
                <span class="sr-only">was </span>
                <bdi class="tabular-nums">$21</bdi>
            </s>
            </span>
            <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-sm px-2.5">
        Get the kit</button>
        </div>
    </article>
</div>
```
