Option card

A radio button as a whole card with a title, a line of detail and a price; for delivery and payment choices.

Install

If this is the first component in the project, set up once. This writes ermine.json and copies the theme stylesheet. The installation guide covers Vue, Nuxt and Astro.

npx ermineui init
pnpm dlx ermineui init
yarn dlx ermineui init
bunx ermineui init

Then add the component:

npx ermineui add option-card
pnpm dlx ermineui add option-card
yarn dlx ermineui add option-card
bunx ermineui add option-card

That copies 2 files into src/components/ui/option-card/, and the 1 component it is built from. Files you already have are kept.

Use

import { OptionCard } from '@/components/ui/option-card'
<RadioGroup v-model="delivery" aria-label="Delivery">
  <OptionCard value="standard" title="Standard" description="3 to 5 working days"><template #end>Free</template></OptionCard>
  <OptionCard value="express" title="Express" description="1 to 2 working days"><template #end>$12.00</template></OptionCard>
</RadioGroup>

<CartLineItem v-model:quantity="item.quantity" :title="item.title" :image="item.image" options="Sand / 350 ml" :price="24" :max="6" @remove="remove(item)" />

<OrderSummary :subtotal="144" :discount="10" discount-label="SAVE10" :shipping="0" :tax="11.52">
  <template #footer><Button size="lg">Checkout</Button></template>
</OrderSummary>

In Nuxt, with the auto-import setting, the tags work in templates without the import. In Astro, use it inside a Vue island.

What gets copied

FileWhere it goes
OptionCard.vuesrc/components/ui/option-card/OptionCard.vue
index.tssrc/components/ui/option-card/index.ts

It is built from: Radio group. These are added with it.

The folders are the suggested ones. ermine.json says where yours are.

Source

This is exactly what the command copies. To install by hand, create each file below and the helpers listed above.

OptionCard.vue 103 lines
<script setup lang="ts">
import { RadioGroupItem } from '../radio-group'

defineProps<{
  value: string
  title: string
  /** A line of detail under the title. */
  description?: string
  disabled?: boolean
}>()
</script>

<template>
  <!-- Goes inside a RadioGroup. The whole card is the radio button's label, so pressing anywhere on it chooses it. -->
  <label class="er-option-card" :data-disabled="disabled ? '' : undefined">
    <RadioGroupItem :value="value" :disabled="disabled" />
    <span class="er-option-card-text">
      <span class="er-option-card-title">{{ title }}</span>
      <span v-if="description || $slots.default" class="er-option-card-description"><slot>{{ description }}</slot></span>
    </span>
    <!-- What it costs, or a logo. -->
    <span v-if="$slots.end" class="er-option-card-end"><slot name="end" /></span>
  </label>
</template>

<style>
@layer components {
  .er-option-card {
    display: flex;
    align-items: flex-start;
    gap: 0.75rem;
    padding: 0.875rem 1rem;
    border: 1px solid var(--er-input);
    border-radius: var(--er-radius-md);
    background: var(--er-input-background);
    cursor: pointer;
    transition:
      background-color var(--er-duration),
      border-color var(--er-duration),
      box-shadow var(--er-duration);
  }

  .er-option-card:hover {
    background: color-mix(in oklab, var(--er-accent) 60%, transparent);
  }

  /* The border thickens without moving anything: the extra pixel is drawn as a shadow. */
  .er-option-card:has(.er-radio:checked) {
    border-color: var(--er-primary);
    box-shadow: 0 0 0 1px var(--er-primary);
  }

  .er-option-card:has(.er-radio:focus-visible) {
    outline: 2px solid var(--er-ring);
    outline-offset: 2px;
  }

  .er-option-card .er-radio {
    margin-top: 0.125rem;
  }

  .er-option-card .er-radio:focus-visible {
    outline: none;
  }

  .er-option-card-text {
    display: grid;
    flex: 1;
    gap: 0.125rem;
    min-width: 0;
  }

  .er-option-card-title {
    font-size: 0.875rem;
    font-weight: var(--er-weight-medium);
    line-height: 1.25rem;
  }

  .er-option-card-description {
    color: var(--er-muted-foreground);
    font-size: 0.8125rem;
    line-height: 1.25rem;
  }

  .er-option-card-end {
    flex-shrink: 0;
    font-size: 0.875rem;
    font-weight: var(--er-weight-medium);
    font-variant-numeric: tabular-nums;
    line-height: 1.25rem;
  }

  .er-option-card[data-disabled] {
    opacity: 0.5;
    cursor: not-allowed;
  }

  .er-option-card[data-disabled]:hover {
    background: var(--er-input-background);
  }
}
</style>
index.ts 3 lines
/** Option card — a radio button as a whole card with a title, a line of detail and a price; for delivery and payment choices. */
export { default as OptionCard } from './OptionCard.vue'