A lightweight, framework-agnostic TypeScript library that provides a secure, premium translucent credit card input form with iOS-style visual design principles, real-time input formatting/validations, CVC 3D card flips, and direct REST tokenization vaulting to Stripe and Omise.
No SDK scripts or CDNs are required. The package uses direct browser-to-vault REST APIs, ensuring absolute control over performance, accessibility, and visual aesthetics.
- Home: https://keeratita.github.io/card/
- Vanilla demo: https://keeratita.github.io/card/examples/vanilla/index.html
- Liquid Glass Visual Style: Grouped input list rows with squircle rounded corners, thin separators, glassmorphic card previews, and dynamic visual state triggers.
- 3D Card Preview: Responsive digital preview card that dynamically mirrors input fields and flips 180 degrees to show the signature strip when the CVC field gains focus.
- Direct REST Vaulting: Perform secure browser-to-vault POST tokenization calls to Stripe (
api.stripe.com/v1/tokens) and Omise (vault.omise.co/tokens) without loading external SDK CDNs. - Dynamic Field Preset Configurations: Load required optional fields automatically (ZIP Code for US cards, full billing addresses, or customer contacts) dynamically.
- Double-Loading submission lifecycle: Automatically manages
"Tokenizing..."and"Processing..."states, allowing host backends to verify transactions before form locks or rollbacks. - Strict Data Isolation: Credit card details are dereferenced immediately from in-memory contexts after vault queries complete to guarantee zero caching leaks.
- Framework Agnostic: Integrates natively with Vanilla JS, React, or Angular Reactive Forms.
npm install @keeratita/cardMake sure to install React (for hook integrations) if compiling framework wrappers.
The package includes two stylesheet versions:
| File | Size | Description |
|---|---|---|
styles.css |
~10KB | Uncompressed development version with source maps |
styles.min.css |
~8.2KB | Minified production version with source maps |
For production deployments, use styles.min.css for better performance (~18% smaller file size).
Add the stylesheet to your HTML page header:
<!-- Development (uncompressed) -->
<link
rel="stylesheet"
href="node_modules/@keeratita/card/dist/vanilla/styles.css"
/>
<!-- Production (minified) - Recommended for production -->
<link
rel="stylesheet"
href="node_modules/@keeratita/card/dist/vanilla/styles.min.css"
/>Attach the form directly to a container element on your page:
<div id="inline-payment-form"></div>import { StripeAdapter } from '@keeratita/card';
import { CardForm } from '@keeratita/card/vanilla';
// Initialize the gateway adapter
const stripeAdapter = new StripeAdapter({
publicKey: 'pk_test_your_stripe_key',
});
// Instantiate the form controller
const form = new CardForm('#inline-payment-form', {
adapter: stripeAdapter,
preset: 'billing', // Loads addressLine1, city, state, postalCode, country
cardLabel: 'Visa Platinum',
submitButtonText: 'Pay Now',
onSubmit: async ({ token }) => {
// Phase 2: Send token.id to your backend checkout API
const response = await fetch('/api/charge', {
method: 'POST',
body: JSON.stringify({ tokenId: token.id }),
});
if (!response.ok) {
throw new Error('Transaction declined from backend API.');
}
},
onError: (error) => {
console.error('Validation or API vault failure: ', error.message);
},
});Instantiate a overlay dialog popup that dynamically slides up:
import { OmiseAdapter } from '@keeratita/card';
import { CardModal } from '@keeratita/card/vanilla';
const omiseAdapter = new OmiseAdapter({
publicKey: 'pkey_test_your_omise_key',
});
const checkoutModal = new CardModal({
adapter: omiseAdapter,
preset: 'us', // ZIP Code preset
onSubmit: async ({ token }) => {
// Direct secure submit to your backend payment processing API
await processPayment(token);
},
});
// Open checkout popup drawer
button.addEventListener('click', () => checkoutModal.open());Use the headless useCardForm hook to build custom card forms with full control over your visual design. The hook manages input state, real-time masking, brand detection, and tokenization:
import React from 'react';
import { StripeAdapter } from '@keeratita/card';
import { useCardForm } from '@keeratita/card/react';
const stripeAdapter = new StripeAdapter({ publicKey: 'pk_test_stripe_key' });
export default function CustomCheckoutForm() {
const {
values,
brand,
errors,
isTokenizing,
isProcessing,
isSuccess,
paymentError,
isFlipped,
handleChange,
handleBlur,
handleCvcFocus,
handleCvcBlur,
handleSubmit,
} = useCardForm({
adapter: stripeAdapter,
onSubmit: async ({ token }) => {
// Backend transaction API verify
await chargeCard(token.id);
},
});
return (
<form onSubmit={handleSubmit}>
{/* Dynamic 3D Card Preview container */}
<div className={`card-inner ${isFlipped ? 'flipped' : ''}`}>
<div className='card-num-preview'>
{values.number || 'β’β’β’β’ β’β’β’β’ β’β’β’β’ β’β’β’β’'}
</div>
<div className='card-brand'>{brand.toUpperCase()}</div>
</div>
{/* Form Fields */}
<input
type='text'
name='number'
value={values.number}
onChange={handleChange}
onBlur={handleBlur}
placeholder='Card Number'
/>
{errors.number && <span>{errors.number}</span>}
<input
type='password'
name='cvc'
value={values.cvc}
onChange={handleChange}
onFocus={handleCvcFocus}
onBlur={(e) => {
handleBlur(e);
handleCvcBlur();
}}
placeholder='CVC'
/>
<button type='submit' disabled={isTokenizing || isProcessing}>
{isTokenizing
? 'Tokenizing...'
: isProcessing
? 'Processing...'
: 'Pay'}
</button>
{paymentError && <div className='error'>{paymentError}</div>}
</form>
);
}Initialize a pre-validated FormGroup automatically using the createCardFormGroup helper:
import { Component } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { createCardFormGroup } from '@keeratita/card/angular';
@Component({
selector: 'app-checkout',
templateUrl: './checkout.component.html',
})
export class CheckoutComponent {
checkoutForm: FormGroup;
constructor() {
// Automatically compiles controls for number, expiry, cvc, name,
// and postalCode (via 'us' preset) with correct validators attached.
this.checkoutForm = createCardFormGroup({ preset: 'us' });
}
}Or configure fields manually using the individual validator helpers:
import { Component } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import {
creditCardValidator,
expiryValidator,
cvcValidator,
} from '@keeratita/card/angular';
@Component({
selector: 'app-checkout',
templateUrl: './checkout.component.html',
})
export class CheckoutComponent {
checkoutForm: FormGroup;
constructor(private fb: FormBuilder) {
this.checkoutForm = this.fb.group({
cardNumber: ['', [Validators.required, creditCardValidator()]],
expiry: ['', [Validators.required, expiryValidator()]],
cvc: ['', [Validators.required, cvcValidator('cardNumber')]], // cross-validates against cardNumber control
});
}
}The package exports pre-built standalone input masking directives out-of-the-box. Import them directly into your standalone components:
import { Component } from '@angular/core';
import {
CardNumberDirective,
CardExpiryDirective,
CardCvcDirective,
} from '@keeratita/card/angular';
@Component({
selector: 'app-checkout',
standalone: true,
imports: [CardNumberDirective, CardExpiryDirective, CardCvcDirective],
template: `
<!-- Card Number formatting -->
<input type="text" kgCardNumber placeholder="β’β’β’β’ β’β’β’β’ β’β’β’β’ β’β’β’β’" />
<!-- Expiry date formatting (MM / YY) -->
<input type="text" kgCardExpiry placeholder="MM/YY" />
<!-- CVC formatting dynamically restricted based on card brand -->
<input
type="password"
kgCardCvc
[cardNumber]="cardNumber"
placeholder="β’β’β’"
/>
`,
})
export class CheckoutComponent {
cardNumber = '';
}| Attribute | Type | Required | Description |
|---|---|---|---|
adapter |
PaymentGateway |
Yes | StripeAdapter or OmiseAdapter |
preset |
CardFormPreset |
No | 'none' | 'us' | 'billing' | 'contact' (default: 'none') |
fields |
OptionalCardField[] |
No | Array of optional elements to render |
cardLabel |
string |
No | Overrides gateway text displayed in preview top-right |
submitButtonText |
string |
No | Custom submit button label (default: 'Pay Now') |
onSubmit |
Function |
No | Callback triggered after token retrieval; supports async Promises |
onError |
Function |
No | Hook fired on validation errors or request failures |
'none': Default core layout (Card Number, Expiry, CVC, Cardholder Name).'us': core fields + Postal Code (ZIP Code visual mapping).'billing': core fields + Address Line 1, City, State, Postal Code, Country.'contact': core fields + Email, Phone.
- Vault CORS Requests Only: Data is sent straight to the secure token vault servers from the client's browser session. Your server context never touches raw PAN details.
- Strict Heap Sanitation: Sensitive data properties (
Carddata structures, raw input buffers) are assigned tonullimmediately after vault POST calls finish to ensure immediate garbage collection. - Zero Logs: The library does not contain loggers tracing card details.
- Accessible Semantics: Supports accessibility keyboard focus tags, overlays ARIA markers, autocomplete attributes, and manages focus traps.
This library includes comprehensive examples for both React and Angular integrations. All examples reference the library from the local dist folder for development, or you can install the package via npm.
For local development, you can reference the library directly from the dist folder:
# Build the library first
npm run build
# The dist folder will be created at the root
# Examples can import from: @keeratita/cardIn your package.json for examples:
{
"dependencies": {
"@keeratita/card": "file:../.."
}
}For production or when not working within the monorepo:
npm install @keeratita/cardMake sure to install React (for hook integrations) if compiling framework wrappers.
Located in examples/react/:
| Example | Description | Key Features |
|---|---|---|
basic-card-form.tsx |
Simple integration using the useCardForm hook |
Basic form with token handling |
custom-form-with-presets.tsx |
Demonstration of different form presets using the hook | Preset switching, adapter selection |
modal-checkout.tsx |
Popover checkout modal implementation | Custom modal, hook-based management |
form-with-custom-validation.tsx |
Custom validation logic combined with library validators | Error handling, validation modes |
multi-step-checkout.tsx |
Complete multi-step checkout flow with step indicators | Cart, shipping, payment steps |
# 1. Build the library first from the root
npm run build
# 2. Navigate to the React examples directory
cd examples/react
# 3. Install dependencies (references local dist folder)
npm install
# 4. Start the development server
npx vite// Using useCardForm hook (headless)
import { useCardForm } from '@keeratita/card/react';
// Using country data
import { COUNTRIES } from '@keeratita/card';
// In vite.config.js for local development:
// The package.json references "file:../.." to use local distNote: Pre-built React components (
CardForm,CreditCardPreview) are not yet available. Use theuseCardFormhook to build custom UIs. Thevanillaentry point provides full pre-built UI components (CardForm,CardModal) that require zero framework setup.
Located in examples/angular/:
| Example | Description | Key Features |
|---|---|---|
basic-card-form.component.ts |
Basic integration using reactive forms | CardFormGroup directive |
form-with-presets.component.ts |
Form with different preset configurations | Dynamic preset switching |
directives-demo.component.ts |
Standalone input masking directives demo | Individual field directives |
custom-validators.component.ts |
Custom validators combined with built-in validation | Angular validators + library |
multi-step-checkout.component.ts |
Multi-step checkout with billing address and order summary | Complete checkout flow |
# 1. Build the library first from the root
npm run build
# 2. Navigate to the Angular examples directory
cd examples/angular
# 3. Install dependencies (references local dist folder)
npm install
# 4. Start the development server
ng serve// Using validators and directives
import {
createCardFormGroup,
CardNumberDirective,
CardExpiryDirective,
CardCvcDirective,
COUNTRIES,
formatCardNumber,
formatExpiry,
creditCardValidator,
expiryValidator,
cvcValidator,
} from '@keeratita/card/angular';
// Using country selection component
import { CountrySelectComponent } from '@keeratita/card/angular';
// In angular.json for styles:
// "styles": ["../../dist/vanilla/styles.css", "src/styles.scss"]card/
βββ dist/ # Built distribution files
β βββ react/ # React builds
β βββ angular/ # Angular builds
β βββ vanilla/ # Vanilla JS builds
βββ examples/
β βββ react/ # React examples
β β βββ package.json # References "@keeratita/card": "file:../.."
β β βββ vite.config.js
β β βββ *.tsx # Example components
β βββ angular/ # Angular examples
β βββ package.json # References "@keeratita/card": "file:../.."
β βββ angular.json
β βββ *.component.ts # Example components
βββ src/ # Source files
βββ react/
βββ angular/
βββ vanilla/
See examples/README.md for more details.
- Author: Keerati Tansawatcharoen
- Email: keerati.tansawatcharoen@gmail.com
- GitHub: keeratita/card
MIT License. SIL Open Font License Inter typography stack. Custom MIT SVG brand icons.
The package includes a built-in country dropdown feature that displays country names with their respective flags using Unicode emoji representations.
- 244 Countries & Territories: Pre-loaded with comprehensive international country data sorted alphabetically
- Flag Emojis: Native Unicode flag rendering (no external image assets required)
- ISO 3166-1 Alpha-2 Codes: Standard 2-letter country codes for form submission
- Dialing Codes: International dialing codes included for reference
import { CardForm } from '@keeratita/card/vanilla';
import { COUNTRIES } from '@keeratita/card';
// The country field is automatically rendered as a dropdown when included in the preset or fields
const form = new CardForm('#payment-form', {
adapter: stripeAdapter,
preset: 'billing', // Includes country field
});
// Access country data directly
const allCountries = COUNTRIES; // Array of Country objects
const thailand = COUNTRIES.find((c) => c.code === 'TH');
// { code: 'TH', name: 'Thailand', emoji: 'πΉπ', dialCode: '+66' }import { COUNTRIES, type Country } from '@keeratita/card';
import { useCardForm } from '@keeratita/card/react';
// Access country data directly for custom implementations
const thailand: Country | undefined = COUNTRIES.find((c) => c.code === 'TH');
// { code: 'TH', name: 'Thailand', emoji: 'πΉπ', dialCode: '+66' }import { Component } from '@angular/core';
import { CountrySelectComponent } from '@keeratita/card/angular';
import { COUNTRIES } from '@keeratita/card';
@Component({
selector: 'app-checkout',
standalone: true,
imports: [CountrySelectComponent],
template: `
<kg-country-select
controlName="country"
[value]="selectedCountry"
(countryChange)="onCountryChange($event)"
>
</kg-country-select>
`,
})
export class CheckoutComponent {
selectedCountry = 'US';
onCountryChange(event: { name: string; value: string }) {
console.log('Selected country:', event.value);
}
}interface Country {
code: string; // 2-letter ISO code (e.g., "TH", "US")
name: string; // Full country name (e.g., "Thailand")
emoji: string; // Flag emoji (e.g., "πΉπ")
dialCode?: string; // International dialing code (e.g., "+66")
}| Function | Description |
|---|---|
COUNTRIES |
Array of all available countries |
getCountryByCode(code) |
Get country object by ISO code |
getAllCountryCodes() |
Get array of all country codes |
isValidCountryCode(code) |
Check if a code is valid |
The package includes 244 countries and territories with ISO codes, flag emojis, and dialing codes. See the source code in src/data/countries.ts for the complete list.
The country data (names, ISO codes, and flag emojis) in this package is based on the REST Countries project.
- Project: REST Countries
- License: Creative Commons Attribution 4.0 International (CC BY 4.0)
If you use this package in your project, please acknowledge the source:
Country data and flag emojis are based on the REST Countries project, licensed under CC BY 4.0. https://restcountries.com/
This package is released under the MIT License. The included country data is shared under the CC BY 4.0 license, which is compatible with MIT licensing for derivative works as long as proper attribution is provided.
The package includes a highly reusable, iOS-styled AutocompleteDropdown component that can be used for any field requiring a searchable, filterable dropdown list with icon/emoji support.
- Searchable/Filterable: Real-time filtering as users type
- iOS Glassmorphism Styling: Matches the premium translucent design of the credit card form
- Keyboard Navigation: Full support for β β Enter Escape keys
- Icon/Emoji Support: Display icons or emojis alongside each option
- Click Outside to Close: Intuitive dismissal behavior
- Extensible Base Class: Custom rendering support via
customRendercallback - Accessibility: ARIA attributes for screen reader support
import {
AutocompleteDropdown,
AutocompleteOption,
} from '@keeratita/card/vanilla';
const options: AutocompleteOption[] = [
{ value: 'apple', label: 'Apple', icon: 'π' },
{ value: 'banana', label: 'Banana', icon: 'π' },
{ value: 'cherry', label: 'Cherry', icon: 'π' },
];
const dropdown = new AutocompleteDropdown({
container: '#my-dropdown',
options,
placeholder: 'Select a fruit...',
searchPlaceholder: 'Search fruits...',
noResultsText: 'No results found',
onSelect: (value, option) => {
console.log('Selected:', value, option);
},
});
// Programmatic control
dropdown.setValue('apple');
const selected = dropdown.getSelectedOption();
dropdown.destroy();const options: AutocompleteOption[] = [
{
value: 'user1',
label: 'John Doe',
customRender: (option) => `
<img src="avatar.jpg" class="avatar" />
<span class="name">${option.label}</span>
<span class="email">john@example.com</span>
`,
},
];For country selection with flag emojis, use the CountryAutocomplete component:
import { CountryAutocomplete } from '@keeratita/card/vanilla';
const countryDropdown = new CountryAutocomplete({
container: '#country-field',
placeholder: 'Select a country...',
searchPlaceholder: 'Search countries...',
onSelect: (countryCode, country) => {
console.log('Selected:', countryCode, country.emoji, country.name);
// country = { code: 'TH', name: 'Thailand', emoji: 'πΉπ', dialCode: '+66' }
},
});
// Programmatic control
countryDropdown.setValue('US');
const selectedCountry = countryDropdown.getSelectedCountry();
countryDropdown.destroy();| Option | Type | Required | Description |
|---|---|---|---|
container |
HTMLElement | string |
Yes | Container element or CSS selector |
options |
AutocompleteOption[] |
Yes | Array of dropdown options |
onSelect |
Function |
Yes | Callback when an option is selected |
placeholder |
string |
No | Input placeholder text (default: 'Select...') |
searchPlaceholder |
string |
No | Search input placeholder (default: 'Search...') |
noResultsText |
string |
No | Text shown when no results match (default: 'No results found') |
maxHeight |
number |
No | Maximum dropdown height in pixels (default: 300) |
showIcons |
boolean |
No | Show/hide icons (default: true) |
| Key | Action |
|---|---|
| β | Move to previous option |
| β | Move to next option |
| Enter | Select highlighted option |
| Escape | Close dropdown |
| Method | Description |
|---|---|
setValue(value) |
Programmatically set the selected value |
getValue() |
Get the current input value |
getSelectedOption() |
Get the currently selected option |
updateOptions(options) |
Replace all options |
refreshOptions() |
Reset to original options |
destroy() |
Remove the component and clean up |