Card Input
billui/card-inputFreeComponent
Native formatted card inputs with auto-formatting, brand detection, and masking.
Live preview
live · rendered by the registry
Rendered by billui on their own page — this is the component running, not a screenshot of it.Install it
npx shadcn@latest add https://billui.com/r/card-input.jsonSource
1"use client";23import * as React from "react";4import { useControllableState } from "@/lib/use-controllable-state";5import { cn } from "@/lib/utils";6import { type CardBrand, CardBrandIcons } from "./card-icons";78interface CardInputContextValue {9 brand: CardBrand;10 setBrand: (brand: CardBrand) => void;11}1213const CardInputContext = React.createContext<CardInputContextValue | null>(14 null,15);1617function useCardInputContext() {18 return React.useContext(CardInputContext);19}2021function detectCardBrand(cardNumber: string): CardBrand {22 const number = cardNumber.replace(/\s/g, "");2324 if (!number) return "unknown";2526 // Visa: starts with 427 if (/^4/.test(number)) return "visa";2829 // Mastercard: starts with 51-55 or 2221-272030 if (/^5[1-5]/.test(number) || /^2[2-7]/.test(number)) return "mastercard";3132 // Amex: starts with 34 or 3733 if (/^3[47]/.test(number)) return "amex";3435 // Discover: starts with 6011, 644-649, 6536 if (/^6011|^64[4-9]|^65/.test(number)) return "discover";3738 // Diners Club: starts with 36, 38, or 300-30539 if (/^36|^38|^30[0-5]/.test(number)) return "diners";4041 // JCB: starts with 3542 if (/^35/.test(number)) return "jcb";4344 // UnionPay: starts with 6245 if (/^62/.test(number)) return "unionpay";4647 return "unknown";48}4950function validateLuhn(cardNumber: string): boolean {51 const number = cardNumber.replace(/\s/g, "");5253 if (!number || !/^\d+$/.test(number)) return false;5455 let sum = 0;56 let isEven = false;5758 // Loop through digits from right to left59 for (let i = number.length - 1; i >= 0; i--) {60 let digit = parseInt(number[i], 10);6162 if (isEven) {63 digit *= 2;64 if (digit > 9) {65 digit -= 9;66 }67 }6869 sum += digit;70 isEven = !isEven;71 }7273 return sum % 10 === 0;74}7576function isCardNumberComplete(cardNumber: string, brand: CardBrand): boolean {77 const number = cardNumber.replace(/\s/g, "");78 const maxLength = getCardMaxLength(brand);79 return number.length === maxLength;80}8182function validateCardNumber(cardNumber: string): {83 isValid: boolean;84 isComplete: boolean;85 brand: CardBrand;86} {87 const brand = detectCardBrand(cardNumber);88 const isComplete = isCardNumberComplete(cardNumber, brand);89 const isValid = isComplete && validateLuhn(cardNumber);9091 return { isValid, isComplete, brand };92}9394function getCardMaxLength(brand: CardBrand): number {95 // Amex has 15 digits, others have 1696 return brand === "amex" ? 15 : 16;97}9899function getCvcMaxLength(brand: CardBrand): number {100// … truncated
What it pulls in
Other registry items
Files it writes
More from billui
All 13 items| Component | Registry | Kind | Access | Installs | Command |
|---|---|---|---|---|---|
| Animated Usage Cardanimated-usage-card | billui | Components | Free | 2 deps | |
| Billing Addressbilling-address | billui | Components | Free | no deps | |
| Card Iconscard-icons | billui | Components | Free | 1 dep | |
| Invoice Cardinvoice-card | billui | Components | Free | 2 deps | |
| Payment Methodpayment-method | billui | Components | Free | 2 deps | |
| Plan Cardplan-card | billui | Components | Free | 2 deps | |
| Plan Groupplan-group | billui | Components | Free | 1 dep | |
| Pricing Tablepricing-table | billui | Components | Free | 2 deps |
