A centred heading over two wide plan cards with features in two columns.

Last updated

Open block
Show code
---
/**
 * Simple cards 04: A centred heading over two wide plan cards with features in two columns.
 *
 * @block pricing
 * @order 4
 */
import { Check } from '@lucide/astro'
import Section from '@/components/apogee/Section/Section.astro'
import SectionHeader from '@/components/apogee/SectionHeader/SectionHeader.astro'
import Heading from '@/components/apogee/Heading/Heading.astro'
import Button from '@/components/apogee/Button/Button.astro'
import Badge from '@/components/apogee/Badge/Badge.astro'
import { accentRuns, plain } from '@/components/apogee/sections/_shared/spine.ts'
import type { PricingContent } from '@/components/apogee/sections/_shared/pricing.ts'

/** @composable */
interface Props extends PricingContent {
  /** The small label over each features list. @category content */
  featuresLabel?: string | null
  /** The line under that label, one per plan in order; *asterisks* bold a part. @category content */
  featuresIntros?: string[]
}

const {
  eyebrow = 'Pricing',
  heading = 'Plans that fit your scale',
  description = 'Simple, transparent pricing that grows with you. Try any plan free for 30 days.',
  items = [
    { name: 'Basic plan', price: '$10', period: 'month', description: 'Our most popular plan for small teams.', features: ['Access to basic features', 'Attend events', 'Basic reporting and analytics', 'Automatic updates', 'Up to 10 individual users', 'Back up your account', '20 GB individual data', 'Audit log and notes', 'Basic chat support', 'Feature requests'], action: { label: 'Get started', href: '#' } },
    { name: 'Business plan', price: '$20', period: 'month', description: 'Advanced features and reporting.', features: ['200+ integrations', 'Advanced custom fields', 'Advanced reporting', 'Audit log and data history', 'Up to 20 individual users', 'Back up your account', '40 GB individual data', 'Personalised service', 'Priority chat support', 'And much more'], action: { label: 'Get started', href: '#' }, badge: 'Popular' },
  ],
  featuresLabel = 'Features',
  featuresIntros = ['Everything in our *free* plan, plus:', 'Everything in our *Basic* plan, plus:'],
} = Astro.props
// '$10' → '$' (set small) and '10'.
const split = (price: string) => price.match(/^(\D*)(.*)$/)!.slice(1) as [string, string]
---

<Section spacing="large">
  <div class="flex flex-col gap-12 md:gap-16">
    <SectionHeader eyebrow={eyebrow ?? undefined} heading={plain(heading)} description={description ?? undefined} size="medium" align="center" />
    <div class="mx-auto grid w-full max-w-xl gap-8 xl:max-w-none xl:grid-cols-2">
    {items.map((plan, i) => {
      const intro = featuresIntros[i]
      const [currency, amount] = split(plan.price)
      return (
    <div class="flex flex-col overflow-hidden rounded-2xl bg-elevation-raised shadow-raised ring-1 ring-neutral">
      <div class="flex flex-col-reverse gap-6 p-6 md:flex-row md:justify-between md:p-8">
        <div class="flex flex-col gap-2">
          <div class="flex items-center gap-2"><Heading as="h3" size="h5">{plan.name}</Heading>{(plan.badge || plan.featured) && <Badge color="primary" rounded>{plan.badge ?? 'Popular'}</Badge>}</div>
          <p class="text-neutral-faded">{plan.description}</p>
        </div>
        <p class="flex items-start gap-1">{currency && <span class="mt-2 text-2xl font-semibold">{currency}</span>}<span class="text-6xl font-semibold tracking-tight tabular-nums">{amount}</span>{plan.period && <span class="self-end pb-2 text-neutral-faded">per {plan.period}</span>}</p>
      </div>
      <div class="flex flex-col gap-6 border-t border-neutral p-6 md:p-8">
        {(featuresLabel || intro) && (
        <div class="flex flex-col gap-1">
          {featuresLabel && <p class="font-semibold">{featuresLabel}</p>}
          {intro && <p class="text-neutral-faded">{accentRuns(intro).map((r) => (r.accent ? <strong class="font-semibold text-neutral">{r.text}</strong> : r.text))}</p>}
        </div>
        )}
      <ul class="flex list-none flex-col gap-4 p-0 text-neutral-faded md:grid md:grid-cols-2 md:gap-x-8">
        {plan.features.map((f) => <li class="flex gap-3"><span class="flex size-6 shrink-0 items-center justify-center rounded-full bg-positive-faded text-positive"><Check class="size-4" aria-hidden="true" /></span><span>{f}</span></li>)}
      </ul>
      </div>
      <div class="border-t border-neutral p-6 md:px-8">
        <Button href={plan.action.href} size="large" fullWidth>{plan.action.label}</Button>
      </div>
    </div>
      )
    })}
    </div>
  </div>
</Section>

Add

npx @ava-ui/apogee add pricing/simple-cards-04

This copies the block to src/components/apogee/blocks/pricing/ with everything it uses: Badge, Button, Heading, Section, SectionHeader. Every piece of copy is a prop, with the sample copy as its default. Pass yours, or null to hide a part; see copy is props.

Accessibility

  • The heading is an h2 by default, so the block slots into a page under its h1; item titles are one level down.