Componentform1.26 KB gzipZero dependencies
Payment Methods
Payment method selector with card, bank, cash, wallet icons.
Preview
Live Preview
Installation
Tokens guide →Add to your project via CLI (zero dependencies, code you own):
npx bigbullui add payment-methodsOr install the full npm package:
npm install bigbulluiUsage
Open in StackBlitzimport { PaymentMethods } from "@/components/ui/payment-methods";
<PaymentMethods />Usage guidance
Use Payment Methods when a ticket stub surface needs Payment method selector with card, bank, cash, wallet icons. Keep props minimal and prefer composition with sibling stubs.
Avoid Payment Methods for long form flows or dense data grids where a dedicated layout block fits better.
Source code
payment-methods.tsxZero dependencies (only React + utils). Copy and paste directly into your project:
"use client";
import * as React from "react";
import { cn } from "./lib/utils";
export interface PaymentMethod {
id: string;
label: string;
description?: string;
icon: "card" | "bank" | "cash" | "wallet";
}
export interface PaymentMethodsProps extends React.HTMLAttributes<HTMLDivElement> {
methods: PaymentMethod[];
value?: string;
defaultValue?: string;
onValueChange?: (id: string) => void;
}
const ICONS: Record<PaymentMethod["icon"], React.ReactNode> = {
card: <><rect x="2" y="5" width="20" height="14" rx="2" /><path d="M2 10h20" /></>,
bank: <><path d="M3 10l9-7 9 7" /><path d="M5 10v8M9.5 10v8M14.5 10v8M19 10v8" /><path d="M3 20h18" /></>,
cash: <><rect x="2" y="6" width="20" height="12" rx="2" /><circle cx="12" cy="12" r="3" /></>,
wallet: <><path d="M21 12V7H5a2 2 0 0 1 0-4h14v4" /><path d="M3 5v14a2 2 0 0 0 2 2h16v-5" /><path d="M18 12a2 2 0 0 0 0 4h4v-4z" /></>,
};
/** Payment method selector: card / bank / on-delivery / wallet tiles. */
export function PaymentMethods({ methods, value, defaultValue, onValueChange, className, ...props }: PaymentMethodsProps) {
const [internal, setInternal] = React.useState(defaultValue ?? methods[0]?.id);
const active = value ?? internal;
return (
<div className={cn("grid grid-cols-2 gap-2", className)} role="radiogroup" aria-label="Payment method" {...props}>
<style>{`@keyframes pmIn { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: translateY(0); } }`}</style>
{methods.map((m, idx) => {
const selected = active === m.id;
return (
<button
key={m.id}
role="radio"
aria-checked={selected}
onClick={() => { setInternal(m.id); onValueChange?.(m.id); }}
className={cn(
"flex items-center gap-2.5 rounded-md border p-3 text-start transition-all duration-200 motion-reduce:transition-none",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
"animate-[pmIn_0.3s_ease-out_both] motion-reduce:animate-none",
selected ? "border-accent bg-accent/5" : "border-border hover:border-foreground/40"
)}
style={{ animationDelay: `${idx * 55}ms` }}
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" className={cn("shrink-0", selected ? "text-accent" : "text-muted-foreground")} aria-hidden="true">
{ICONS[m.icon]}
</svg>
<span className="min-w-0">
<span className="block truncate text-sm font-medium">{m.label}</span>
{m.description && <span className="block truncate text-[11px] text-muted-foreground">{m.description}</span>}
</span>
</button>
);
})}
</div>
);
}
Props
| Prop | Type | Description |
|---|---|---|
| methods | PaymentMethod[] | |
| value | string | |
| defaultValue | string | |
| onValueChange | (id: string) => void |