Skip to content
Cipay
Esc
navigateopen⌘Jpreview
On this page

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.