Styling, accessibility, and RTL
Preview Cipay components with a neutral theme and apply your own design system safely.
Cipay components ship semantic behavior without a required stylesheet. They inherit your typography and colors, accept native HTML props, and expose render callbacks for markup you want to replace.
Styled product card
This preview follows Cipay’s black/white neutral theme. The same component can use your own classes or CSS variables.
import {
CipayProvider,
ProductCard,
useProduct,
} from "@cipay/client-sdk/react";
import { componentPreviewClient } from "../preview/component-data";
export function StyledProductExample() {
// Styling changes presentation without replacing contract-derived product data.
const product = useProduct("coffee");
if (!product.data) return <p>Loading product…</p>;
return (
<ProductCard
product={product.data}
className="custom-product-card"
priceLabel={<strong>Billing plan</strong>}
footer={<button>Continue</button>}
/>
);
}
export default function StyledProductPreview() {
return (
<CipayProvider endpoint="/api/cipay" queryClient={componentPreviewClient}>
<StyledProductExample />
</CipayProvider>
);
}
<ProductCard
product={product}
className="cipay-product"
priceLabel="Billing plan"
footer={
<CheckoutButton
input={{ offerId, buyer: { email: user.email } }}
className="cipay-checkout"
label="Continue to secure checkout"
/>
}
/>.cipay-product {
display: grid;
gap: 1rem;
padding: 1.5rem;
border: 1px solid #e5e5e5;
border-radius: 0.75rem;
background: #fff;
color: #171717;
}
.cipay-product > img {
width: 100%;
aspect-ratio: 5 / 3;
object-fit: cover;
border-radius: 0.5rem;
}
.cipay-product select,
.cipay-checkout {
width: 100%;
min-height: 2.75rem;
border-radius: 0.5rem;
}
.cipay-checkout {
border: 0;
background: #171717;
color: #fff;
font-weight: 600;
}
.cipay-product select:focus-visible,
.cipay-checkout:focus-visible {
outline: 2px solid #171717;
outline-offset: 3px;
}Customization API
Native styling props
Pass className, style, aria-*, and data-* to the root element. CheckoutButton, InvoiceDownloadButton, and PriceSelector also accept their native button or select props.
Product media
ProductCard renders the first product.assets image. Use renderMedia={false} to hide it or renderMedia={(asset) => ...} to replace its markup.
Rows and labels
Use renderProduct, renderSubscription, renderOrder, renderOption, heading, priceLabel, and footer to replace specific sections without rebuilding query behavior.
Request states
List components accept loading, empty, error, and signedOut render callbacks. Preserve aria-live="polite" for loading text and role="alert" for failures.
Replace all card markup
Use a render callback when your design system owns the full card. The product value comes from the current Cipay response.
<ProductsList
className="product-grid"
renderProduct={(product) => (
<article key={product.id} className="my-card">
{product.assets[0] && (
<img src={product.assets[0].url} alt={product.assets[0].altText ?? ""} />
)}
<h3>{product.name}</h3>
<p>{product.description}</p>
<MyOfferPicker offers={product.offers} />
</article>
)}
/>
Keep headings, labels, buttons, lists, and status messages semantic when replacing renderers.
Arabic and RTL
Set the language and direction at the document root. Native controls and Cipay components then follow the browser’s direction. Cipay’s default checkout typography is Cairo; the preview applies Cairo to both Arabic and Latin text with system fallbacks.
import {
CipayProvider,
ProductCard,
type BuyerOffer,
useProduct,
} from "@cipay/client-sdk/react";
import { componentPreviewClient } from "../preview/component-data";
export function RtlProductExample() {
// Product data still comes from Cipay; only display copy is localized here.
const product = useProduct("coffee");
if (!product.data) return <p>جارٍ تحميل المنتج…</p>;
return (
<div
data-preview="rtl-product"
dir="rtl"
lang="ar"
style={{ fontFamily: '"Cairo", Tahoma, Arial, sans-serif' }}
>
<ProductCard
product={{
...product.data,
name: "اشتراك القهوة",
description: "حبوب طازجة تصلك وفق الخطة التي تختارها.",
}}
priceLabel={<strong>خطة الدفع</strong>}
renderOffer={arabicOfferOption}
footer={<button>متابعة الدفع</button>}
/>
</div>
);
}
const sar = new Intl.NumberFormat("ar-SA", {
style: "currency",
currency: "SAR",
});
function arabicOfferOption(offer: BuyerOffer) {
const name = offer.cadence?.intervalUnit === "year" ? "سنوي" : "شهري";
const label = offer.amountHalalas === null
? name
: `${name} — ${sar.format(offer.amountHalalas / 100)}`;
return <option key={offer.id} value={offer.id}>{label}</option>;
}
export default function RtlProductPreview() {
return (
<CipayProvider endpoint="/api/cipay" queryClient={componentPreviewClient}>
<RtlProductExample />
</CipayProvider>
);
}
import { Cairo } from "next/font/google";
const cairo = Cairo({
subsets: ["arabic", "latin"],
variable: "--font-cairo",
});
export default function Layout({ children, params }: LayoutProps) {
const arabic = params.locale === "ar";
return (
<html
className={cairo.variable}
lang={arabic ? "ar" : "en"}
dir={arabic ? "rtl" : "ltr"}
>
<body>{children}</body>
</html>
);
}.cipay-product {
font-family: var(--font-cairo), Tahoma, Arial, sans-serif;
padding-inline: 1.5rem;
text-align: start;
}
.cipay-product__actions {
margin-inline-start: auto;
}Use logical properties such as padding-inline and margin-inline-start so one stylesheet works in both directions.