Skip to main content

Overview

CardCheckbox combines a card layout with selection controls, perfect for creating option pickers, plan selectors, or any interface where users need to choose between multiple options. It supports both single and multiple selection modes.

Import

import { CardCheckbox } from '@invopop/popui'

Basic Usage

<script>
  import { CardCheckbox } from '@invopop/popui'
  
  let selected = $state(false)
</script>

<CardCheckbox
  title="Basic Plan"
  description="Perfect for individuals and small teams"
  bind:checked={selected}
/>

With Icon

Add icons to make options more visually distinctive:
<script>
  import { CardCheckbox } from '@invopop/popui'
  import { CreditCard } from '@steeze-ui/heroicons'
  
  let paymentMethod = $state(false)
</script>

<CardCheckbox
  icon={CreditCard}
  title="Credit Card"
  description="Pay with Visa, Mastercard, or American Express"
  bind:checked={paymentMethod}
/>

Selection Patterns

Single Selection (Radio Group)

Create a mutually exclusive selection group:
<script>
  import { CardCheckbox } from '@invopop/popui'
  import { Server, Cloud, Database } from '@steeze-ui/heroicons'
  
  let selectedPlan = $state('basic')
  
  const plans = [
    { id: 'basic', title: 'Basic', description: 'Up to 10 users', icon: Server },
    { id: 'pro', title: 'Professional', description: 'Up to 50 users', icon: Cloud },
    { id: 'enterprise', title: 'Enterprise', description: 'Unlimited users', icon: Database }
  ]
</script>

<div class="flex flex-col gap-3">
  {#each plans as plan}
    <CardCheckbox
      name="plan"
      icon={plan.icon}
      title={plan.title}
      description={plan.description}
      checked={selectedPlan === plan.id}
      onchange={() => selectedPlan = plan.id}
    />
  {/each}
</div>

Multiple Selection

Allow users to select multiple options:
<script>
  import { CardCheckbox } from '@invopop/popui'
  import { Bell, Mail, Phone } from '@steeze-ui/heroicons'
  
  let notifications = $state({
    email: true,
    sms: false,
    push: true
  })
</script>

<div class="flex flex-col gap-3">
  <CardCheckbox
    icon={Mail}
    title="Email Notifications"
    description="Receive updates via email"
    bind:checked={notifications.email}
  />
  
  <CardCheckbox
    icon={Phone}
    title="SMS Notifications"
    description="Get text message alerts"
    bind:checked={notifications.sms}
  />
  
  <CardCheckbox
    icon={Bell}
    title="Push Notifications"
    description="Browser and mobile push alerts"
    bind:checked={notifications.push}
  />
</div>

With Accent Text

Highlight important information:
<script>
  import { CardCheckbox } from '@invopop/popui'
</script>

<CardCheckbox
  title="Annual Billing"
  accentText="Save 20%"
  description="Billed once per year"
  bind:checked={isAnnual}
/>
Add additional details or actions:
<script>
  import { CardCheckbox, TagStatus } from '@invopop/popui'
  import { Star } from '@steeze-ui/heroicons'
  
  let selected = $state(false)
</script>

<CardCheckbox
  icon={Star}
  title="Premium Plan"
  description="All features included"
  bind:checked={selected}
>
  {#snippet footer()}
    <div class="flex gap-2 items-center">
      <span class="text-2xl font-bold">$49</span>
      <span class="text-sm text-gray-600">/month</span>
      <TagStatus label="Popular" status="blue" />
    </div>
  {/snippet}
</CardCheckbox>

Hide Radio Button

For cleaner designs, hide the radio button while maintaining functionality:
<CardCheckbox
  title="Option without visible radio"
  description="Selection is indicated by border color"
  hideRadio={true}
  bind:checked={selected}
/>

Disabled State

<CardCheckbox
  title="Disabled Option"
  description="This option is not available"
  disabled={true}
/>

Props

id
any
Unique identifier for the input element. Auto-generated if not provided.
name
string
default:"''"
Name attribute for radio button grouping. Cards with the same name form a radio group.
title
string
default:"''"
Main heading text for the card
description
string
default:"''"
Secondary descriptive text displayed below the title
accentText
string
default:"''"
Highlighted text displayed before the description (e.g., “Save 20%”, “Popular”)
checked
boolean
default:"false"
Selected state of the card. Use bind:checked for two-way binding.
disabled
boolean
default:"false"
Disables the card and prevents interaction. Card appears with reduced opacity.
icon
IconSource | undefined
Icon to display at the start of the card content (from @steeze-ui/svelte-icon)
hideRadio
boolean
default:"false"
Hides the radio button while maintaining selection functionality. Selected state is shown via border color.
Svelte snippet for custom footer content below the main card text
onchange
(checked: boolean) => void
Callback function called when the selection state changes

TypeScript Interface

export interface CardCheckboxProps {
  id?: any
  name?: string
  title?: string
  description?: string
  accentText?: string
  checked?: boolean
  disabled?: boolean
  icon?: IconSource | undefined
  hideRadio?: boolean
  footer?: Snippet
  onchange?: (checked: boolean) => void
}

Visual States

Border Styling

  • Selected: Shows accent border color (border-foreground-selected)
  • Unselected: Shows default border color (border-border)
  • Disabled: Gray background overlay (bg-background-default-secondary)

Layout

  • Icon appears on the left (optional)
  • Title and description stack vertically
  • Radio button or checkmark on the right
  • Footer content at the bottom with proper spacing

Accessibility

  • Uses semantic HTML with <label> wrapping the card
  • Radio button properly associated with label via id and for attributes
  • Disabled state prevents interaction and updates visual appearance
  • Keyboard navigable and supports Enter/Space key activation
  • Screen reader compatible with proper ARIA attributes

Examples

Pricing Plan Selector

<script>
  import { CardCheckbox, TagStatus } from '@invopop/popui'
  import { Zap, Star, Rocket } from '@steeze-ui/heroicons'
  
  let selectedPlan = $state('pro')
  
  const plans = [
    {
      id: 'starter',
      icon: Zap,
      title: 'Starter',
      description: 'Essential features for getting started',
      price: '$9',
      features: ['5 projects', '10 GB storage', 'Email support']
    },
    {
      id: 'pro',
      icon: Star,
      title: 'Professional',
      description: 'Advanced features for growing teams',
      price: '$29',
      popular: true,
      features: ['Unlimited projects', '100 GB storage', 'Priority support']
    },
    {
      id: 'enterprise',
      icon: Rocket,
      title: 'Enterprise',
      description: 'Custom solutions for large organizations',
      price: 'Custom',
      features: ['Custom projects', 'Unlimited storage', 'Dedicated support']
    }
  ]
</script>

<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
  {#each plans as plan}
    <CardCheckbox
      name="pricing"
      icon={plan.icon}
      title={plan.title}
      description={plan.description}
      checked={selectedPlan === plan.id}
      onchange={() => selectedPlan = plan.id}
    >
      {#snippet footer()}
        <div class="space-y-3">
          <div class="flex items-baseline gap-1">
            <span class="text-3xl font-bold">{plan.price}</span>
            {#if plan.price !== 'Custom'}
              <span class="text-sm text-gray-600">/month</span>
            {/if}
            {#if plan.popular}
              <TagStatus label="Popular" status="blue" />
            {/if}
          </div>
          <ul class="space-y-2 text-sm">
            {#each plan.features as feature}
              <li>{feature}</li>
            {/each}
          </ul>
        </div>
      {/snippet}
    </CardCheckbox>
  {/each}
</div>

Build docs developers (and LLMs) love