Payment Card

Payment method
@use 'sprucecss/scss/spruce' as *;

.payment-card-group {
  @include layout-stack('s');
  border: 0;
  margin: 0;
  min-inline-size: 0;
  padding: 0;

  &__legend {
    color: color('heading');
    font-size: font-size('h4');
    font-weight: 700;
    margin-block-end: spacer('xs');
    padding: 0;
  }
}

.payment-card {
  @include transition($property: 'border-color, background-color, box-shadow');
  align-items: center;
  background-color: color('surface');
  border: config('border-width-base', $display) solid color('border');
  border-radius: radius('lg');
  cursor: pointer;
  display: flex;
  gap: spacer('s');
  padding: spacer-clamp('s', 'm');

  &:hover {
    border-color: color('primary');
  }

  &:has(:checked) {
    background-color: color-alpha('primary', 0.05);
    border-color: color('primary');
    box-shadow: inset 0 0 0 1px color('primary');
  }

  &__control {
    flex-shrink: 0;
  }

  &__caption {
    display: flex;
    flex-direction: column;
    flex-grow: 1;
    gap: spacer('xxs');
  }

  &__title {
    color: color('heading');
    font-weight: 700;
  }

  &__description {
    color: color('muted');
    font-size: config('font-size-sm', $typography);
  }

  &__logo {
    block-size: 1.75rem;
    flex-shrink: 0;
    inline-size: auto;
  }
}
<fieldset class="payment-card-group">
    <legend class="payment-card-group__legend">Payment method</legend>
    <label class="payment-card">
        <input class="form-check__control payment-card__control" type="radio" name="payment-method" value="stripe" aria-describedby="payment-stripe-description" checked>
        <span class="payment-card__caption">
            <span class="payment-card__title">Card payment</span>
            <span class="payment-card__description" id="payment-stripe-description">You’ll be redirected to Stripe to pay with your card.</span>
        </span>
            <img class="payment-card__logo" src="/img/ui/stripe-logo.svg" alt="" height="28" width="80">
    </label>
    <label class="payment-card">
        <input class="form-check__control payment-card__control" type="radio" name="payment-method" value="paypal" aria-describedby="payment-paypal-description">
        <span class="payment-card__caption">
            <span class="payment-card__title">PayPal</span>
            <span class="payment-card__description" id="payment-paypal-description">You’ll be redirected to PayPal to complete the payment.</span>
        </span>
            <img class="payment-card__logo" src="/img/ui/paypal-logo.svg" alt="" height="28" width="80">
    </label>
    <label class="payment-card">
        <input class="form-check__control payment-card__control" type="radio" name="payment-method" value="transfer" aria-describedby="payment-transfer-description">
        <span class="payment-card__caption">
            <span class="payment-card__title">Bank transfer</span>
            <span class="payment-card__description" id="payment-transfer-description">We’ll email you the bank details. Your order ships after the payment arrives.</span>
        </span>
    </label>
</fieldset>

Payment Card

A payment method picker for checkouts: every option is a card with a title, a short note and an optional logo, and the whole card selects the native radio button.

Anatomy

  • .payment-card-group is a fieldset with a legend ("Payment method").
  • .payment-card is a label that wraps the radio input (__control), the caption (__title, __description) and the logo (__logo).

Accessibility

  • The card is the label of the radio button, so a click anywhere on it selects the option, and the name comes from the visible title. Earlier versions used an aria-label and a stretched pseudo-element; the native label is simpler and more robust.
  • The note is linked with aria-describedby, so it's read after the option name.
  • The logos are decorative (alt="") next to the text title, so the name isn't repeated.
  • Native radio buttons keep the arrow key navigation inside the group.

Technical Details

  • The selected card is highlighted with :has(:checked), without JavaScript.
  • The radio uses the Spruce form check style (.form-check__control).
  • Add any content to the caption, like fees or delivery times; keep it short, it's read with every option.

Documentation

Learn about Spruce CSS through our extensive documentation.

Components

Explore our UI library of accessible components built with Spruce CSS.

Blocks and Pages

Full-width page blocks and complete page templates, ready to copy.

Find us on GitHub

Did you find a bug? Have an idea or a question? Please open an issue to help us develop the project.