1 estrella en GitHub = 1 componente nuevo.

Dar una estrella
E-commerceecommerceshopcheckoutformpayment

Checkout Form

Checkout en una página (contacto, entrega, envío y pago) con autocompletado, validación y confirmación animada.

Vista Previa en Vivo

Contact
Delivery
Shipping method
Payment

All transactions are secure and encrypted.

Demo checkout — no data leaves your browser. Try code ASTRO10.

Uso

Copia y pega este código directamente en tu componente React:

tsx
import { CheckoutForm } from 'react-component-kit-ui/ecommerce';

<div className="checkout-layout">
  <CheckoutForm continueHref="/" />
  <OrderSummary />
</div>

Instalación Manual

Si no estás usando el paquete npm, crea un archivo nuevo y pega el siguiente código: src/components/CheckoutForm.tsx + CheckoutForm.css:

CheckoutForm.tsx
'use client';

import { useEffect, useRef } from 'react';
import AnimatedInput from '../forms/AnimatedInput';
import ModernSelect from '../forms/ModernSelect';
import CustomRadioGroup from '../forms/CustomRadioGroup';
import AnimatedCheckmark from '../feedback/AnimatedCheckmark';
import { formatMoney } from './cart-store';
import type { ShippingRate } from './types';
import { createCart } from './cart-store';
import './CheckoutForm.css';

/** class:list → className */
function cx(...args: unknown[]): string {
  const out: string[] = [];
  const walk = (v: unknown) => {
    if (!v) return;
    if (typeof v === 'string' || typeof v === 'number') out.push(String(v));
    else if (Array.isArray(v)) v.forEach(walk);
    else if (typeof v === 'object') for (const [k, on] of Object.entries(v as Record<string, unknown>)) if (on) out.push(k);
  };
  args.forEach(walk);
  return out.join(' ');
}

/**
 * CheckoutForm — One-page checkout: contact, delivery address, shipping method and payment,
 * built from AnimatedInput, ModernSelect and CustomRadioGroup. Shows an order confirmation
 * (AnimatedCheckmark) on submit and clears the cart.
 *
 * Pair it with OrderSummary: the shipping radios drive its totals and the pay button shows
 * the live total. Replace the demo submit handler with your payment provider.
 *
 * @param {string} id - Form id. Default 'checkout'.
 * @param {ShippingRate[]} shippingRates - Shipping options (must match OrderSummary).
 * @param {number} freeShippingThreshold - Shows "Free" on the first rate above this subtotal. Default 75.
 * @param {Array<{label: string, value: string}>} countries - Country select options.
 * @param {string} cartKey - Cart storage key. Default 'ack-cart'.
 * @param {string} currency - ISO currency. Default 'USD'.
 * @param {string} locale - Number format locale. Default 'en-US'.
 * @param {string} continueHref - "Continue shopping" link on the confirmation. Default '#'.
 * @param {boolean} demo - Simulates payment and never sends data anywhere. Default true.
 * @param {string} variant - Section style (cards|plain). Default 'cards'.
 * @param {string} className - Extra class.
 *
 * Accessibility: grouped fieldsets with legends, autocomplete hints on every field, native
 * validation with focus moved to the first invalid field, confirmation heading receives focus.
 * Emits `ack:order-placed` with `{ orderId, total }`.
 */
export interface CheckoutFormProps {
  id?: string;
  shippingRates?: ShippingRate[];
  freeShippingThreshold?: number;
  countries?: Array<{ label: string; value: string }>;
  cartKey?: string;
  currency?: string;
  locale?: string;
  continueHref?: string;
  demo?: boolean;
  variant?: 'cards' | 'plain';
  className?: string;
}

export function CheckoutForm({
  id = 'checkout',
  shippingRates = [
    { id: 'standard', label: 'Standard', eta: '3–5 business days', price: 6 },
    { id: 'express', label: 'Express', eta: '1–2 business days', price: 14 },
  ],
  freeShippingThreshold = 75,
  countries = [
    { label: 'United States', value: 'US' },
    { label: 'Canada', value: 'CA' },
    { label: 'United Kingdom', value: 'GB' },
    { label: 'Spain', value: 'ES' },
    { label: 'Germany', value: 'DE' },
    { label: 'France', value: 'FR' },
  ],
  cartKey = 'ack-cart',
  currency = 'USD',
  locale = 'en-US',
  continueHref = '#',
  demo = true,
  variant = 'cards',
  className = '',
}: CheckoutFormProps) {
  const money = { currency, locale };

  // Ported from the Astro <script>: runs once after mount (DOM logic kept as in the Astro kit).
  const mounted = useRef(false);
  useEffect(() => {
    if (mounted.current) return;
    mounted.current = true;
    document.querySelectorAll<HTMLElement>('[data-checkout]:not([data-bound])').forEach((root) => {
        root.dataset.bound = '';
        const form = root.querySelector<HTMLFormElement>('[data-checkout-form]')!;
        const done = root.querySelector<HTMLElement>('[data-done]')!;
        const pay = root.querySelector<HTMLButtonElement>('[data-pay]')!;
        const payLabel = root.querySelector<HTMLElement>('[data-pay-label]')!;
        const error = root.querySelector<HTMLElement>('[data-error]')!;
        const cardFields = root.querySelector<HTMLElement>('[data-card-fields]')!;
        const altPay = root.querySelector<HTMLElement>('[data-alt-pay]')!;
        const firstRatePrice = root.querySelector<HTMLElement>('[data-first] ~ [data-rate-price]');
        const cart = createCart(root.dataset.key);
        let total = { formatted: '', total: 0, count: 0 };
        const firstRateText = firstRatePrice?.textContent || '';
    
        document.addEventListener('ack:totals', (e) => {
          const d = (e as CustomEvent).detail;
          total = d;
          payLabel.textContent = d.count ? `Pay ${d.formatted}` : 'Pay now';
          if (firstRatePrice) firstRatePrice.textContent = d.freeStandard || (d.subtotal - d.discount) >= Number(root.dataset.free || 0) && Number(root.dataset.free) > 0 ? 'Free' : firstRateText;
        });
        document.dispatchEvent(new CustomEvent('ack:totals-request'));
    
        // Payment method toggles the card fields (and their `required`).
        const setMethod = () => {
          const method = (form.elements.namedItem('payment') as RadioNodeList)?.value || 'card';
          const isCard = method === 'card';
          cardFields.hidden = !isCard;
          altPay.hidden = isCard;
          cardFields.querySelectorAll('input').forEach((i) => { i.required = isCard; });
        };
        form.addEventListener('change', (e) => {
          if ((e.target as HTMLInputElement).name === 'payment') setMethod();
        });
        setMethod();
    
        // Light input masks
        form.querySelector<HTMLInputElement>('[data-cc]')?.addEventListener('input', (e) => {
          const el = e.target as HTMLInputElement;
          el.value = el.value.replace(/\D/g, '').slice(0, 19).replace(/(\d{4})(?=\d)/g, '$1 ');
        });
        form.querySelector<HTMLInputElement>('[data-exp]')?.addEventListener('input', (e) => {
          const el = e.target as HTMLInputElement;
          const digits = el.value.replace(/\D/g, '').slice(0, 4);
          el.value = digits.length > 2 ? `${digits.slice(0, 2)}/${digits.slice(2)}` : digits;
        });
    
        form.addEventListener('submit', (e) => {
          e.preventDefault();
          error.hidden = true;
          form.querySelectorAll('[aria-invalid]').forEach((n) => n.removeAttribute('aria-invalid'));
          if (!cart.items().length) {
            error.textContent = 'Your cart is empty — add a product before checking out.';
            error.hidden = false;
            return;
          }
          if (!form.checkValidity()) {
            const invalid = Array.from(form.querySelectorAll<HTMLInputElement>('input, select')).filter((n) => !n.checkValidity());
            invalid.forEach((n) => n.setAttribute('aria-invalid', 'true'));
            error.textContent = `Please check ${invalid.length === 1 ? '1 field' : `${invalid.length} fields`} highlighted in red.`;
            error.hidden = false;
            invalid[0]?.focus();
            return;
          }
          pay.disabled = true;
          pay.classList.add('is-loading');
          payLabel.textContent = 'Processing…';
    
          const finish = () => {
            const orderId = `#${Math.floor(100000 + Math.random() * 900000)}`;
            const email = (form.elements.namedItem('email') as HTMLInputElement).value;
            root.querySelector('[data-order-id]')!.textContent = orderId;
            root.querySelector('[data-order-email]')!.textContent = email;
            root.querySelector('[data-order-total]')!.textContent = total.formatted;
            form.hidden = true;
            done.hidden = false;
            cart.clear();
            done.querySelector<HTMLElement>('h2')?.focus();
            document.dispatchEvent(new CustomEvent('ack:order-placed', { detail: { orderId, total: total.total } }));
            window.scrollTo({ top: 0, behavior: 'smooth' });
          };
          // Demo mode: simulate the payment round-trip. Replace with your provider's SDK.
          window.setTimeout(finish, root.hasAttribute('data-demo') ? 1100 : 0);
        });
      });
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  return (
    <>
      <div className={cx(['checkout', `checkout--${variant}`, className])} data-checkout data-key={cartKey} data-demo={demo ? '' : undefined} data-free={freeShippingThreshold} data-rk-e0a849="">
        <form className="checkout__form" id={id} noValidate data-checkout-form data-rk-e0a849="">
          <fieldset className="checkout__section" data-rk-e0a849="">
            <legend data-rk-e0a849=""><span className="checkout__num" aria-hidden="true" data-rk-e0a849="">1</span> Contact</legend>
            <AnimatedInput label="Email" id={`${id}-email`} name="email" type="email" required autocomplete="email" />
            <label className="checkout__check" data-rk-e0a849="">
              <input type="checkbox" name="newsletter" defaultChecked data-rk-e0a849="" />
              <span className="checkout__box" aria-hidden="true" data-rk-e0a849=""></span>
              Email me with news and offers
            </label>
          </fieldset>
      
          <fieldset className="checkout__section" data-rk-e0a849="">
            <legend data-rk-e0a849=""><span className="checkout__num" aria-hidden="true" data-rk-e0a849="">2</span> Delivery</legend>
            <ModernSelect label="Country / region" id={`${id}-country`} name="country" options={countries} required />
            <div className="checkout__row" data-rk-e0a849="">
              <AnimatedInput label="First name" id={`${id}-first`} name="firstName" required autocomplete="given-name" />
              <AnimatedInput label="Last name" id={`${id}-last`} name="lastName" required autocomplete="family-name" />
            </div>
            <AnimatedInput label="Address" id={`${id}-address`} name="address" required autocomplete="address-line1" />
            <AnimatedInput label="Apartment, suite (optional)" id={`${id}-address2`} name="address2" autocomplete="address-line2" />
            <div className="checkout__row checkout__row--3" data-rk-e0a849="">
              <AnimatedInput label="City" id={`${id}-city`} name="city" required autocomplete="address-level2" />
              <AnimatedInput label="State" id={`${id}-state`} name="state" autocomplete="address-level1" />
              <AnimatedInput label="ZIP / Postal" id={`${id}-zip`} name="zip" required autocomplete="postal-code" />
            </div>
            <AnimatedInput label="Phone (for delivery updates)" id={`${id}-phone`} name="phone" type="tel" autocomplete="tel" />
          </fieldset>
      
          <fieldset className="checkout__section" data-rk-e0a849="">
            <legend data-rk-e0a849=""><span className="checkout__num" aria-hidden="true" data-rk-e0a849="">3</span> Shipping method</legend>
            <div className="checkout__rates" data-rk-e0a849="">
              {shippingRates.map((r, i) => (
                <label key={i} className="checkout__rate" data-rk-e0a849="">
                  <input type="radio" name="shipping" defaultValue={r.id} defaultChecked={i === 0} data-price={r.price} data-first={i === 0 ? '' : undefined} data-rk-e0a849="" />
                  <span className="checkout__rate-dot" aria-hidden="true" data-rk-e0a849=""></span>
                  <span className="checkout__rate-text" data-rk-e0a849="">
                    <strong data-rk-e0a849="">{r.label}</strong>
                    <span data-rk-e0a849="">{r.eta}</span>
                  </span>
                  <span className="checkout__rate-price" data-rate-price data-rk-e0a849="">{r.price === 0 ? 'Free' : formatMoney(r.price, money)}</span>
                </label>
              ))}
            </div>
          </fieldset>
      
          <fieldset className="checkout__section" data-rk-e0a849="">
            <legend data-rk-e0a849=""><span className="checkout__num" aria-hidden="true" data-rk-e0a849="">4</span> Payment</legend>
            <p className="checkout__secure" data-rk-e0a849="">
              <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" aria-hidden="true" data-rk-e0a849=""><rect x="4" y="11" width="16" height="10" rx="2" data-rk-e0a849="" /><path d="M8 11V7a4 4 0 0 1 8 0v4" data-rk-e0a849="" /></svg>
              All transactions are secure and encrypted.
            </p>
            <CustomRadioGroup name="payment" horizontal options={[
                { label: 'Card', value: 'card', checked: true },
                { label: 'PayPal', value: 'paypal' },
                { label: 'Apple Pay', value: 'applepay' },
              ]} />
            <div className="checkout__card" data-card-fields data-rk-e0a849="">
              <div className="checkout__field" data-rk-e0a849="">
                <label htmlFor={`${id}-cc`} data-rk-e0a849="">Card number</label>
                <input id={`${id}-cc`} name="cc" inputMode="numeric" autoComplete="cc-number" placeholder="1234 1234 1234 1234" required pattern="[0-9 ]{15,23}" maxLength={23} data-cc data-rk-e0a849="" />
              </div>
              <div className="checkout__row" data-rk-e0a849="">
                <div className="checkout__field" data-rk-e0a849="">
                  <label htmlFor={`${id}-exp`} data-rk-e0a849="">Expiry (MM/YY)</label>
                  <input id={`${id}-exp`} name="exp" inputMode="numeric" autoComplete="cc-exp" placeholder="MM/YY" required pattern="(0[1-9]|1[0-2])\\/[0-9]{2}" maxLength={5} data-exp data-rk-e0a849="" />
                </div>
                <div className="checkout__field" data-rk-e0a849="">
                  <label htmlFor={`${id}-cvc`} data-rk-e0a849="">Security code</label>
                  <input id={`${id}-cvc`} name="cvc" inputMode="numeric" autoComplete="cc-csc" placeholder="CVC" required pattern="[0-9]{3,4}" maxLength={4} data-rk-e0a849="" />
                </div>
              </div>
              <div className="checkout__field" data-rk-e0a849="">
                <label htmlFor={`${id}-ccname`} data-rk-e0a849="">Name on card</label>
                <input id={`${id}-ccname`} name="ccName" autoComplete="cc-name" required data-rk-e0a849="" />
              </div>
            </div>
            <p className="checkout__alt" data-alt-pay hidden data-rk-e0a849="">You will be redirected to complete the payment securely.</p>
          </fieldset>
      
          <p className="checkout__error" role="alert" data-error hidden data-rk-e0a849=""></p>
      
          <button className="checkout__pay" type="submit" data-pay data-rk-e0a849="">
            <span className="checkout__spinner" aria-hidden="true" data-rk-e0a849=""></span>
            <span data-pay-label data-rk-e0a849="">Pay now</span>
          </button>
          {demo && <p className="checkout__demo" data-rk-e0a849="">Demo checkout — no data leaves your browser. Try code <strong data-rk-e0a849="">ASTRO10</strong>.</p>}
        </form>
      
        <section className="checkout__done" data-done hidden aria-labelledby={`${id}-done-title`} data-rk-e0a849="">
          <AnimatedCheckmark size={72} color="var(--c-success, #34d399)" />
          <h2 id={`${id}-done-title`} tabIndex={-1} data-rk-e0a849="">Thank you for your order!</h2>
          <p data-rk-e0a849="">Order <strong data-order-id data-rk-e0a849="">#—</strong> is confirmed. A receipt is on its way to <strong data-order-email data-rk-e0a849="">your inbox</strong>.</p>
          <p className="checkout__done-total" data-rk-e0a849="">Total paid: <strong data-order-total data-rk-e0a849="">—</strong></p>
          <a className="checkout__pay checkout__pay--link" href={continueHref} data-rk-e0a849="">Continue shopping</a>
        </section>
      </div>
    </>
  );
}

export default CheckoutForm;
CheckoutForm.css
/* CheckoutForm — ported from astro-component-kit (ecommerce/CheckoutForm.astro) */

.checkout:where([data-rk-e0a849]) { container-type: inline-size; min-width: 0; }
.checkout__form:where([data-rk-e0a849]) { display: grid; gap: var(--sp-5, 1.25rem); }
.checkout__form[hidden]:where([data-rk-e0a849]), .checkout__done[hidden]:where([data-rk-e0a849]) { display: none; }

.checkout__section:where([data-rk-e0a849]) {
  display: grid;
  gap: var(--sp-3, 0.75rem);
  min-width: 0;
  margin: 0;
  padding: 0;
  border: 0;
}
.checkout--cards:where([data-rk-e0a849]) .checkout__section:where([data-rk-e0a849]) {
  padding: var(--sp-6, 1.5rem);
  border-radius: var(--r-lg, 20px);
  border: 1px solid var(--c-border, rgb(var(--ink-rgb, 255 255 255) / 0.05));
  background: var(--c-surface, rgb(var(--ink-rgb, 255 255 255) / 0.02));
}
.checkout__section:where([data-rk-e0a849]) legend:where([data-rk-e0a849]) {
  display: flex;
  align-items: center;
  gap: var(--sp-3, 0.75rem);
  float: left;
  width: 100%;
  margin-bottom: var(--sp-2, 0.5rem);
  font-size: 1.05rem;
  font-weight: 800;
  color: var(--c-text-1, #fff);
}
.checkout__section:where([data-rk-e0a849]) legend:where([data-rk-e0a849]) + *:where([data-rk-e0a849]) { clear: both; }
.checkout__num:where([data-rk-e0a849]) {
  display: grid;
  place-items: center;
  width: 28px;
  height: 28px;
  border-radius: 50%;
  background: rgba(212, 160, 23, 0.14);
  color: var(--c-primary-light, #d4a017);
  font-size: 0.82rem;
}

.checkout__row:where([data-rk-e0a849]) { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--sp-3, 0.75rem); }
.checkout__row--3:where([data-rk-e0a849]) { grid-template-columns: repeat(3, minmax(0, 1fr)); }

/* Reused form components, tuned to the store palette */
.checkout:where([data-rk-e0a849]) .anim-input:focus,
.checkout:where([data-rk-e0a849]) .custom-select:focus { border-color: var(--c-primary-light, #d4a017); box-shadow: 0 0 0 3px rgba(212, 160, 23, 0.14); background: rgba(212, 160, 23, 0.04); }
.checkout:where([data-rk-e0a849]) .anim-input:focus ~ .anim-input-label,
.checkout:where([data-rk-e0a849]) .anim-input:not(:placeholder-shown) ~ .anim-input-label { color: var(--c-primary-light, #d4a017); }
.checkout:where([data-rk-e0a849]) .anim-input-label,
.checkout:where([data-rk-e0a849]) .select-label { color: var(--c-text-2, #a3a3a3); }
.checkout:where([data-rk-e0a849]) [aria-invalid='true'] { border-color: var(--c-danger, #f87171) !important; box-shadow: 0 0 0 3px rgba(248, 113, 113, 0.14) !important; }
.checkout:where([data-rk-e0a849]) .radio-circle::after { background: var(--c-primary-light, #d4a017); }
.checkout:where([data-rk-e0a849]) .radio-item input:checked + .radio-circle { border-color: var(--c-primary-light, #d4a017); box-shadow: 0 0 10px rgba(212, 160, 23, 0.25); }
.checkout:where([data-rk-e0a849]) .radio-group-legend { display: none; }

.checkout__check:where([data-rk-e0a849]) { position: relative; display: flex; align-items: center; gap: var(--sp-3, 0.75rem); min-height: 40px; font-size: 0.88rem; cursor: pointer; }
.checkout__check:where([data-rk-e0a849]) input:where([data-rk-e0a849]) { position: absolute; opacity: 0; width: 1px; height: 1px; }
.checkout__box:where([data-rk-e0a849]) { width: 18px; height: 18px; border-radius: 5px; border: 1.5px solid rgb(var(--ink-rgb, 255 255 255) / 0.25); flex-shrink: 0; transition: background 0.15s ease; }
.checkout__check:where([data-rk-e0a849]) input:checked:where([data-rk-e0a849]) + .checkout__box:where([data-rk-e0a849]) { background: var(--c-primary-light, #d4a017); border-color: var(--c-primary-light, #d4a017); box-shadow: inset 0 0 0 3px var(--c-bg-1, #0a0a0a); }
.checkout__check:where([data-rk-e0a849]) input:focus-visible:where([data-rk-e0a849]) + .checkout__box:where([data-rk-e0a849]) { outline: 2px solid var(--c-primary-light, #d4a017); outline-offset: 2px; }

.checkout__rates:where([data-rk-e0a849]) { display: grid; gap: var(--sp-2, 0.5rem); }
.checkout__rate:where([data-rk-e0a849]) {
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--sp-3, 0.75rem);
  min-height: 64px;
  padding: var(--sp-3, 0.75rem) var(--sp-4, 1rem);
  border-radius: var(--r-md, 12px);
  border: 1px solid rgb(var(--ink-rgb, 255 255 255) / 0.1);
  cursor: pointer;
  transition: border-color 0.15s ease, background 0.15s ease;
}
.checkout__rate:hover:where([data-rk-e0a849]) { border-color: rgb(var(--ink-rgb, 255 255 255) / 0.22); }
.checkout__rate:has(input:checked):where([data-rk-e0a849]) { border-color: var(--c-primary-light, #d4a017); background: rgba(212, 160, 23, 0.06); }
.checkout__rate:where([data-rk-e0a849]) input:where([data-rk-e0a849]) { position: absolute; opacity: 0; width: 1px; height: 1px; }
.checkout__rate-dot:where([data-rk-e0a849]) { width: 20px; height: 20px; border-radius: 50%; border: 2px solid rgb(var(--ink-rgb, 255 255 255) / 0.25); flex-shrink: 0; }
.checkout__rate:where([data-rk-e0a849]) input:checked:where([data-rk-e0a849]) + .checkout__rate-dot:where([data-rk-e0a849]) { border-color: var(--c-primary-light, #d4a017); box-shadow: inset 0 0 0 4px var(--c-bg-1, #0a0a0a); background: var(--c-primary-light, #d4a017); }
.checkout__rate:where([data-rk-e0a849]) input:focus-visible:where([data-rk-e0a849]) + .checkout__rate-dot:where([data-rk-e0a849]) { outline: 2px solid var(--c-primary-light, #d4a017); outline-offset: 2px; }
.checkout__rate-text:where([data-rk-e0a849]) { flex: 1; display: grid; gap: 1px; }
.checkout__rate-text:where([data-rk-e0a849]) strong:where([data-rk-e0a849]) { color: var(--c-text-1, #fff); font-size: 0.92rem; }
.checkout__rate-text:where([data-rk-e0a849]) span:where([data-rk-e0a849]) { font-size: 0.8rem; }
.checkout__rate-price:where([data-rk-e0a849]) { font-weight: 800; color: var(--c-text-1, #fff); }

.checkout__secure:where([data-rk-e0a849]) { display: flex; align-items: center; gap: var(--sp-2, 0.5rem); margin: 0; font-size: 0.82rem; color: var(--c-text-2, #a3a3a3); }
.checkout__card:where([data-rk-e0a849]) { display: grid; gap: var(--sp-3, 0.75rem); padding-top: var(--sp-2, 0.5rem); }
.checkout__card[hidden]:where([data-rk-e0a849]), .checkout__alt[hidden]:where([data-rk-e0a849]) { display: none; }
.checkout__field:where([data-rk-e0a849]) { display: grid; gap: var(--sp-2, 0.5rem); }
.checkout__field:where([data-rk-e0a849]) label:where([data-rk-e0a849]) { font-size: 0.82rem; font-weight: 600; color: var(--c-text-2, #a3a3a3); }
.checkout__field:where([data-rk-e0a849]) input:where([data-rk-e0a849]) {
  width: 100%;
  min-height: 50px;
  padding: 0 var(--sp-4, 1rem);
  border-radius: var(--r-md, 12px);
  border: 1px solid var(--c-border, rgb(var(--ink-rgb, 255 255 255) / 0.1));
  background: rgb(var(--ink-rgb, 255 255 255) / 0.04);
  color: var(--c-text-1, #fff);
  font: inherit;
  font-size: 0.95rem;
  letter-spacing: 0.02em;
}
.checkout__field:where([data-rk-e0a849]) input:where([data-rk-e0a849])::placeholder { color: var(--c-text-3, #737373); }
.checkout__field:where([data-rk-e0a849]) input:focus:where([data-rk-e0a849]) { outline: none; border-color: var(--c-primary-light, #d4a017); box-shadow: 0 0 0 3px rgba(212, 160, 23, 0.14); }
.checkout__alt:where([data-rk-e0a849]) { margin: 0; padding: var(--sp-4, 1rem); border-radius: var(--r-md, 12px); background: rgb(var(--ink-rgb, 255 255 255) / 0.03); font-size: 0.88rem; }

.checkout__error:where([data-rk-e0a849]) { margin: 0; padding: var(--sp-3, 0.75rem) var(--sp-4, 1rem); border-radius: var(--r-md, 12px); background: rgba(248, 113, 113, 0.1); border: 1px solid rgba(248, 113, 113, 0.3); color: var(--c-danger, #fca5a5); font-size: 0.88rem; }
.checkout__error[hidden]:where([data-rk-e0a849]) { display: none; }

.checkout__pay:where([data-rk-e0a849]) {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2, 0.5rem);
  width: 100%;
  min-height: 56px;
  border: 0;
  border-radius: var(--r-full, 9999px);
  background: var(--c-primary-light, #d4a017);
  color: var(--c-on-primary, #050505);
  font: inherit;
  font-size: 1rem;
  font-weight: 800;
  text-decoration: none;
  cursor: pointer;
  transition: filter 0.2s ease, transform 0.2s ease;
}
.checkout__pay:hover:where([data-rk-e0a849]) { filter: brightness(1.08); color: var(--c-on-primary, #050505); }
.checkout__pay:disabled:where([data-rk-e0a849]) { cursor: progress; filter: saturate(0.6); }
.checkout__pay:focus-visible:where([data-rk-e0a849]) { outline: 2px solid var(--c-text-1, #fff); outline-offset: 3px; }
.checkout__spinner:where([data-rk-e0a849]) { display: none; width: 18px; height: 18px; border-radius: 50%; border: 2.5px solid rgba(0, 0, 0, 0.25); border-top-color: var(--c-on-primary, #050505); animation: checkout-form-co-spin 0.7s linear infinite; }
.checkout__pay.is-loading:where([data-rk-e0a849]) .checkout__spinner:where([data-rk-e0a849]) { display: block; }
@keyframes checkout-form-co-spin { to { transform: rotate(360deg); } }
.checkout__demo:where([data-rk-e0a849]) { margin: 0; text-align: center; font-size: 0.8rem; color: var(--c-text-2, #a3a3a3); }

.checkout__done:where([data-rk-e0a849]) {
  display: grid;
  justify-items: center;
  gap: var(--sp-3, 0.75rem);
  padding: var(--sp-12, 3rem) var(--sp-6, 1.5rem);
  border-radius: var(--r-xl, 28px);
  border: 1px solid rgba(52, 211, 153, 0.25);
  background: radial-gradient(circle at 50% 0%, rgba(52, 211, 153, 0.1), transparent 60%), var(--c-surface, rgb(var(--ink-rgb, 255 255 255) / 0.02));
  text-align: center;
}
.checkout__done:where([data-rk-e0a849]) h2:where([data-rk-e0a849]) { margin: 0; font-size: 1.8rem; font-weight: 900; letter-spacing: -0.03em; color: var(--c-text-1, #fff); outline: none; }
.checkout__done:where([data-rk-e0a849]) p:where([data-rk-e0a849]) { margin: 0; max-width: 32rem; }
.checkout__done:where([data-rk-e0a849]) strong:where([data-rk-e0a849]) { color: var(--c-text-1, #fff); }
.checkout__pay--link:where([data-rk-e0a849]) { width: auto; padding: 0 var(--sp-8, 2rem); margin-top: var(--sp-4, 1rem); }

@container (max-width: 560px) {
  .checkout--cards:where([data-rk-e0a849]) .checkout__section:where([data-rk-e0a849]) { padding: var(--sp-4, 1rem); }
  .checkout__row:where([data-rk-e0a849]), .checkout__row--3:where([data-rk-e0a849]) { grid-template-columns: 1fr; }
}
@media (prefers-reduced-motion: reduce) { .checkout__spinner:where([data-rk-e0a849]) { animation-duration: 2s; } }

Información Rápida

Categoría
E-commerce
Nombre de archivo
CheckoutForm.tsx
Dependencias
Ninguna — React + CSS puro
Etiquetas
ecommerceshopcheckoutformpayment