Skip to content

Two-option comparison

A real either-or decision, with each option stating when it is the wrong choice.

SaaSstartercomparisondecisionpathshonesty

Live preview

full widthLive preview — open it in a new tab for the full-height version.
Open the preview in a new tab

Source

This exact file renders the preview above.

import { ArrowRight } from 'lucide-react'
import Link from 'next/link'
import { Container } from '@/components/ui/layout'
import { Badge } from '@/components/ui/badge'

/**
 * Two-option side-by-side
 *
 * For a real either/or decision, where the goal is helping someone choose
 * rather than steering them. Both cards get identical weight, and each states
 * when it is the wrong choice — which is what makes the section trustworthy.
 */
const options = [
  {
    name: 'Copy the source',
    badge: 'Most teams',
    summary: 'Bring components into your repository and own them from day one.',
    good: [
      'You need to change component internals',
      'Your design will diverge from the defaults',
      'You cannot take a new runtime dependency',
    ],
    bad: 'Not right if you want upstream fixes without touching your code.',
    href: '/docs/installation',
  },
  {
    name: 'Start from a starter',
    badge: 'New products',
    summary: 'Take a whole multi-route product and delete what you do not need.',
    good: [
      'You are starting a new product this week',
      'The route structure is close to what you need',
      'You want a working example to learn from',
    ],
    bad: 'Not right if you are adding to an existing, established codebase.',
    href: '/starters',
  },
]

export default function SideBySideComparison() {
  return (
    <section className="border-b border-line bg-surface-sunken py-section">
      <Container>
        <div className="mx-auto max-w-2xl text-center">
          <h2 className="display-type text-2xl font-semibold text-ink-strong sm:text-3xl">
            Two ways in. Neither is the “advanced” one.
          </h2>
        </div>

        <div className="mt-10 grid gap-4 lg:grid-cols-2">
          {options.map((option) => (
            <div
              key={option.name}
              className="flex flex-col rounded-xl border border-line bg-surface p-6 sm:p-8"
            >
              <div className="flex items-center justify-between gap-3">
                <h3 className="display-type text-lg font-semibold text-ink-strong">
                  {option.name}
                </h3>
                <Badge>{option.badge}</Badge>
              </div>
              <p className="mt-2 text-sm text-ink-muted">{option.summary}</p>

              <p className="label-caps mt-6 text-ink-subtle">Choose this when</p>
              <ul className="mt-2 flex flex-1 flex-col gap-2">
                {option.good.map((item) => (
                  <li key={item} className="text-sm text-ink">
                    · {item}
                  </li>
                ))}
              </ul>

              <p className="mt-5 rounded-md border border-warning-line bg-warning-soft px-3 py-2 text-xs text-ink">
                {option.bad}
              </p>

              <Link
                href={option.href}
                className="mt-6 inline-flex items-center gap-1.5 text-sm font-semibold text-accent underline underline-offset-4"
              >
                {option.name}
                <ArrowRight className="size-4" aria-hidden="true" />
              </Link>
            </div>
          ))}
        </div>
      </Container>
    </section>
  )
}

components/blocks/sections/comparison/side-by-side.tsx

import { ArrowRight } from 'lucide-react'
import Link from 'next/link'
import { Container } from '@/components/ui/layout'
import { Badge } from '@/components/ui/badge'

/**
 * Two-option side-by-side
 *
 * For a real either/or decision, where the goal is helping someone choose
 * rather than steering them. Both cards get identical weight, and each states
 * when it is the wrong choice — which is what makes the section trustworthy.
 */
const options = [
  {
    name: 'Copy the source',
    badge: 'Most teams',
    summary: 'Bring components into your repository and own them from day one.',
    good: [
      'You need to change component internals',
      'Your design will diverge from the defaults',
      'You cannot take a new runtime dependency',
    ],
    bad: 'Not right if you want upstream fixes without touching your code.',
    href: '/docs/installation',
  },
  {
    name: 'Start from a starter',
    badge: 'New products',
    summary: 'Take a whole multi-route product and delete what you do not need.',
    good: [
      'You are starting a new product this week',
      'The route structure is close to what you need',
      'You want a working example to learn from',
    ],
    bad: 'Not right if you are adding to an existing, established codebase.',
    href: '/starters',
  },
]

export default function SideBySideComparison() {
  return (
    <section className="border-b border-line bg-surface-sunken py-section">
      <Container>
        <div className="mx-auto max-w-2xl text-center">
          <h2 className="display-type text-2xl font-semibold text-ink-strong sm:text-3xl">
            Two ways in. Neither is the “advanced” one.
          </h2>
        </div>

        <div className="mt-10 grid gap-4 lg:grid-cols-2">
          {options.map((option) => (
            <div
              key={option.name}
              className="flex flex-col rounded-xl border border-line bg-surface p-6 sm:p-8"
            >
              <div className="flex items-center justify-between gap-3">
                <h3 className="display-type text-lg font-semibold text-ink-strong">
                  {option.name}
                </h3>
                <Badge>{option.badge}</Badge>
              </div>
              <p className="mt-2 text-sm text-ink-muted">{option.summary}</p>

              <p className="label-caps mt-6 text-ink-subtle">Choose this when</p>
              <ul className="mt-2 flex flex-1 flex-col gap-2">
                {option.good.map((item) => (
                  <li key={item} className="text-sm text-ink">
                    · {item}
                  </li>
                ))}
              </ul>

              <p className="mt-5 rounded-md border border-warning-line bg-warning-soft px-3 py-2 text-xs text-ink">
                {option.bad}
              </p>

              <Link
                href={option.href}
                className="mt-6 inline-flex items-center gap-1.5 text-sm font-semibold text-accent underline underline-offset-4"
              >
                {option.name}
                <ArrowRight className="size-4" aria-hidden="true" />
              </Link>
            </div>
          ))}
        </div>
      </Container>
    </section>
  )
}

components/ui/badge.tsx

import type { HTMLAttributes, ReactNode } from 'react'
import { variants } from '@/lib/variants'

/**
 * Badge
 *
 * A compact, non-interactive label. Tones map to the status token trios, and
 * because a badge is often the only signal in a dense table, the `dot` option
 * exists to add a second, colour-independent cue alongside the text.
 */
const badgeVariants = variants('inline-flex items-center gap-1.5 whitespace-nowrap font-medium', {
  variants: {
    tone: {
      neutral: 'bg-surface-sunken text-ink-muted border-line',
      accent: 'bg-accent-soft text-accent-soft-ink border-accent-line',
      success: 'bg-success-soft text-success border-success-line',
      warning: 'bg-warning-soft text-warning border-warning-line',
      danger: 'bg-danger-soft text-danger border-danger-line',
      info: 'bg-info-soft text-info border-info-line',
      inverse: 'bg-surface-inverse text-ink-inverse border-transparent',
    },
    appearance: {
      soft: 'border',
      outline: 'border bg-transparent',
      solid: 'border border-transparent',
    },
    size: {
      sm: 'h-4.5 rounded-sm px-1.5 text-2xs',
      md: 'h-5.5 rounded-sm px-2 text-xs',
    },
  },
  defaultVariants: { tone: 'neutral', appearance: 'soft', size: 'md' },
  compound: [
    { appearance: 'solid', tone: 'accent', class: 'bg-accent text-accent-ink' },
    { appearance: 'solid', tone: 'success', class: 'bg-success text-white' },
    { appearance: 'solid', tone: 'warning', class: 'bg-warning text-white' },
    { appearance: 'solid', tone: 'danger', class: 'bg-danger text-white' },
    { appearance: 'solid', tone: 'info', class: 'bg-info text-white' },
    { appearance: 'solid', tone: 'neutral', class: 'bg-ink text-ink-inverse' },
    { appearance: 'outline', tone: 'neutral', class: 'text-ink-muted' },
  ],
})

export type BadgeTone = 'neutral' | 'accent' | 'success' | 'warning' | 'danger' | 'info' | 'inverse'

export interface BadgeProps extends HTMLAttributes<HTMLSpanElement> {
  tone?: BadgeTone
  appearance?: 'soft' | 'outline' | 'solid'
  size?: 'sm' | 'md'
  /** Adds a leading dot so the badge does not rely on hue alone. */
  dot?: boolean
  icon?: ReactNode
}

export function Badge({
  tone = 'neutral',
  appearance = 'soft',
  size = 'md',
  dot = false,
  icon,
  className,
  children,
  ...props
}: BadgeProps) {
  return (
    <span className={badgeVariants({ tone, appearance, size, className })} {...props}>
      {dot ? (
        <span className="size-1.5 shrink-0 rounded-full bg-current" aria-hidden="true" />
      ) : null}
      {icon}
      {children}
    </span>
  )
}

Demo source — adapt to your project. Foundry is not published as a package.

Usage

Both cards get identical weight, and each states when it is the wrong choice — which is what makes the section trustworthy rather than a disguised recommendation.

  • Neither option may be styled as the advanced or premium one.
  • The 'not right if' note is the most-read line in the card. Write it carefully.

Variants and states

Every entry below is a genuine difference in behaviour or layout, and every one of them is visible in the preview above.

  • Two equal cards
  • Choose-this-when list
  • Not-right-if note

Accessibility

Equal weight
Both cards share identical borders, padding and typography.
Named links
Each card's action names the option rather than saying 'Learn more'.

Foundry implements published ARIA patterns and is tested against them. No WCAG certification is claimed — see the accessibility documentation for what is and is not covered.