Checkout
Checkout page in one of the most important pages in e-commerce. Usually it contains information about delivery destination, shipping options and payment methods.
Address form
Ease the checkout process for users by providing them with well-structured address form. Form field Street
provides you an example of how error state could be handled.
import { SfSelect, SfInput, SfCheckbox, SfButton } from '@storefront-ui/react';
import { FormEventHandler, ChangeEvent, FocusEvent, useState } from 'react';
// Here you should provide a list of countries you want to support
// or use an up-to-date country list like: https://www.npmjs.com/package/country-list
const countries = ['Germany', 'Great Britain', 'Poland', 'United States of America'] as const;
const states = ['California', 'Florida', 'New York', 'Texas'] as const;
export default function AddressForm() {
const [streetIsValid, setStreetIsValid] = useState(true);
const validateStreet = (e: ChangeEvent<HTMLInputElement> | FocusEvent<HTMLInputElement>) => {
setStreetIsValid(!!e.target.value);
};
const onSubmit: FormEventHandler<HTMLFormElement> = (e) => {
e.preventDefault();
/* your submit handler e.g.: */
const form = e.target as HTMLFormElement;
// data can be accessed in form of FormData
const formData = new FormData(form);
// or JSON object
const formJSON = Object.fromEntries(formData.entries());
console.log(formJSON);
};
return (
<form className="p-4 flex gap-4 flex-wrap text-neutral-900" onSubmit={onSubmit}>
<h2 className="w-full typography-headline-4 md:typography-headline-3 font-bold">Billing address</h2>
<label className="w-full md:w-auto flex-grow flex flex-col gap-0.5 mt-4 md:mt-0">
<span className="typography-text-sm font-medium">First Name</span>
<SfInput name="firstName" autoComplete="given-name" required />
</label>
<label className="w-full md:w-auto flex-grow flex flex-col gap-0.5">
<span className="typography-text-sm font-medium">Last Name</span>
<SfInput name="lastName" autoComplete="family-name" required />
</label>
<label className="w-full flex flex-col gap-0.5">
<span className="typography-text-sm font-medium">Phone</span>
<SfInput name="phone" type="tel" autoComplete="tel" required />
</label>
<label className="w-full flex flex-col gap-0.5 flex flex-col gap-0.5">
<span className="typography-text-sm font-medium">Country</span>
<SfSelect name="country" placeholder="-- Select --" autoComplete="country-name" required>
{countries.map((countryName) => (
<option key={countryName}>{countryName}</option>
))}
</SfSelect>
</label>
<div className="w-full md:w-auto flex-grow flex flex-col gap-0.5">
<label>
<span className="typography-text-sm font-medium">Street</span>
<SfInput
name="street"
autoComplete="address-line1"
className="mt-0.5"
onBlur={validateStreet}
onChange={validateStreet}
required
invalid={!streetIsValid}
/>
</label>
<div className="flex flex-colr mt-0.5">
{!streetIsValid && (
<strong className="typography-error-sm text-negative-700 font-medium">Please provide a street name</strong>
)}
<small className="typography-hint-xs text-neutral-500 mt-0.5">Street address or P.O. Box</small>
</div>
</div>
<div className="w-full flex flex-col gap-0.5 md:w-[120px]">
<label>
<span className="typography-text-sm font-medium">Apt#, Suite, etc</span>
<SfInput name="aptNo" className="mt-0.5" />
</label>
<small className="typography-hint-xs text-neutral-500 mt-0.5">Optional</small>
</div>
<label className="w-full flex flex-col gap-0.5">
<span className="typography-text-sm font-medium">City</span>
<SfInput name="city" autoComplete="address-level2" required />
</label>
<label className="w-full md:w-auto flex flex-col gap-0.5 flex-grow">
<span className="typography-text-sm font-medium">State</span>
<SfSelect name="state" placeholder="-- Select --" autoComplete="address-level1" required>
{states.map((stateName) => (
<option key={stateName}>{stateName}</option>
))}
</SfSelect>
</label>
<label className="w-full flex flex-col gap-0.5 md:w-[120px]">
<span className="typography-text-sm font-medium">ZIP Code</span>
<SfInput name="zipCode" placeholder="eg. 12345" autoComplete="postal-code" required />
</label>
<label className="w-full flex items-center gap-2">
<SfCheckbox name="useAsShippingAddress" />
Use as shipping address
</label>
<div className="w-full flex gap-4 mt-4 md:mt-0 md:justify-end">
<SfButton type="reset" variant="secondary" className="w-full md:w-auto">
Clear all
</SfButton>
<SfButton type="submit" className="w-full md:w-auto">
Save
</SfButton>
</div>
</form>
);
}
Delivery options
Present possible delivery options in a way where your customers can easily see differences and choose the best one for their needs.
import { SfRadio, SfListItem } from '@storefront-ui/react';
import { useState } from 'react';
const deliveryOptions = [
{
name: 'Standard',
cost: 'Free',
date: 'Thursday, September 15',
},
{
name: 'Express',
cost: '$5.00',
date: 'Tomorrow, September 12',
},
];
export default function DeliveryOptions() {
const [checkedState, setCheckedState] = useState('');
return (
<div>
{deliveryOptions.map(({ name, cost, date }) => (
<SfListItem
as="label"
key={name}
slotPrefix={
<SfRadio
name="delivery-options"
value={name}
checked={checkedState === name}
onChange={(event) => {
setCheckedState(event.target.value);
}}
/>
}
slotSuffix={<span className="text-gray-900">{cost}</span>}
className="!items-start max-w-sm border rounded-md border-neutral-200 first-of-type:mr-4 first-of-type:mb-4"
>
{name}
<span className="text-xs text-gray-500 break-words">{date}</span>
</SfListItem>
))}
</div>
);
}
Payment method
Let your users pick a payment method of their choice in a nice and clear way.
// List of payment methods
const paymentMethods = [
{
label: 'Credit card',
value: 'credit-card',
logo: 'https://storage.googleapis.com/sfui_docs_artifacts_bucket_public/production/visa-logo.svg',
active: true,
},
{
label: 'PayPal',
value: 'paypal',
logo: 'https://storage.googleapis.com/sfui_docs_artifacts_bucket_public/production/paypal-logo.svg',
active: true,
},
{
label: 'ApplePay',
value: 'applepay',
logo: 'https://storage.googleapis.com/sfui_docs_artifacts_bucket_public/production/apple-pay-logo.svg',
active: true,
},
{
label: 'GooglePay',
value: 'googlepay',
logo: 'https://storage.googleapis.com/sfui_docs_artifacts_bucket_public/production/google-pay-logo.svg',
active: false,
},
];
export default function PaymentMethods() {
return (
<fieldset>
<legend className="mb-4 typography-headline-5 font-bold text-neutral-900">Payment method</legend>
<div className="grid grid-cols-2 gap-4 items-stretch">
{paymentMethods.map(({ label, value, logo, active }) => (
<label key={value} className="relative">
<input disabled={!active} type="radio" name="payment_method" value={value} className="peer sr-only" />
<div className="h-full flex flex-col items-center justify-center py-7 px-4 cursor-pointer rounded-md border border-neutral-200 -outline-offset-2 hover:border-primary-200 hover:bg-primary-100 peer-focus:border-primary-200 peer-focus:bg-primary-100 active:border-primary-300 active:bg-primary-200 peer-checked:outline peer-checked:outline-2 peer-checked:outline-primary-700 peer-disabled:opacity-50 peer-disabled:bg-neutral-100 peer-disabled:border-neutral-200 peer-disabled:cursor-not-allowed peer-disabled:[&_img]:grayscale">
<img src={logo} alt={label} className="h-6 select-none" />
{!active && (
<p className="absolute bottom-2 select-none text-disabled-900 typography-text-xs">Coming soon</p>
)}
</div>
</label>
))}
</div>
</fieldset>
);
}
Contact form
The contact form for customers provides the way to send email (field with simple validation) and phone number with separate country code.
import { SfInput, SfButton, SfSelect } from '@storefront-ui/react';
import { useState, ChangeEvent, FormEventHandler } from 'react';
export default function ContactForm() {
const [invalid, setInvalid] = useState(true);
const options = [1, 7, 20, 27, 30, 30, 31, 32, 33, 34, 36, 39, 40, 41, 43, 44, 45, 46, 47, 48, 49, 51];
const emailRegExp = /^[\w-.]+@([\w-]+\.)+[\w-]{2,4}$/;
const handleValidation = (event: ChangeEvent<HTMLInputElement>) =>
emailRegExp.test(event?.target.value) ? setInvalid(false) : setInvalid(true);
const onSubmit: FormEventHandler<HTMLFormElement> = (e) => {
e.preventDefault();
/* your submit handler e.g.: */
const form = e.target as HTMLFormElement;
// data can be accessed in form of FormData
const formData = new FormData(form);
// or JSON object
const formJSON = Object.fromEntries(formData.entries());
// eslint-disable-next-line no-console
console.log(formJSON);
};
return (
<form className="flex flex-col gap-y-4 text-neutral-900" onSubmit={onSubmit}>
<h3 className="font-bold typography-headline-4 md:typography-headline-3">Contact information</h3>
<div className="gap-y-0.5">
<label className="gap-y-0.5">
<span className="text-sm font-medium">Email</span>
<SfInput
name="email"
onChange={handleValidation}
placeholder="email address"
invalid={invalid}
autoComplete="email"
/>
</label>
{invalid && (
<div>
<p className="typography-error-sm text-negative-700 font-medium mt-0.5">Please provide a valid email</p>
</div>
)}
</div>
<label className="flex flex-col gap-y-0.5">
<span className="font-medium typography-text-sm">Phone number</span>
<div className="flex">
<SfSelect name="phone-country-code" className="w-16 mr-4" placeholder="--" autoComplete="tel-country-code">
{options.map((option) => (
<option value={option} key={option}>
{option}
</option>
))}
</SfSelect>
<SfInput
name="phone-national"
wrapperClassName="w-full"
type="tel"
inputMode="tel"
autoComplete="tel-national"
/>
</div>
</label>
<div className="flex justify-between gap-4 md:justify-end">
<SfButton className="w-full md:w-auto" type="reset" variant="secondary">
Clear all
</SfButton>
<SfButton className="w-full md:w-auto" type="submit">
Save
</SfButton>
</div>
</form>
);
}