Card Element is the secure card entry component in RinneJS. It renders sensitive fields in an isolated context and returns encrypted values and validation state for your checkout UI.
Use Card Element for all card transactions. Rinne expects encrypted values from mountedCard.values and does not accept plain PAN/CVC.
Create a Card Element
const cardElement = await rinne.elements.card({
theme: 'material',
locale: 'pt',
fields: ['number', 'expiry', 'cvc'],
icons: true
})
const mountedCard = await cardElement.mount('#card-element')
To process real card transactions, the merchant or organization must have an active Rinne affiliation for card processing.
Capture Payload Contract
cardElement.mount() returns a mountedCard object. In production checkout, send mountedCard.values.card to your backend.
interface MountedCard {
isValid: boolean
isComplete: boolean
values: {
card: {
number: string // encrypted
cvv: string // encrypted
expiry_month: string // zero-padded, e.g. '01'
expiry_year: string // four digits, e.g. '2026'
name?: string
brand?: string
last_digits?: string
}
}
unmount(): void
}
Before the user finishes input, fields in mountedCard.values.card can still be empty strings.
number and cvv are encrypted in mountedCard.values.card. expiry_month and expiry_year are plain strings.
Options
interface CardElementOptions {
theme?: 'clean' | 'minimal' | 'material' | ThemeConfig | ThemeFunction
colorScheme?: 'light' | 'dark' // iframe root color-scheme; match your page theme
locale?: 'pt' | 'en' | 'es' // default: 'pt'
icons?: boolean
fields?: ('name' | 'number' | 'expiry' | 'cvc')[]
onReady?: () => void
onChange?: (state: CardState) => void
onComplete?: (state: CardState) => void
onFocus?: (event: CardFieldEvent) => void
onBlur?: (event: CardFieldEvent) => void
}
Event Lifecycle
| Event | When it fires | Recommended use |
|---|
onReady() | Card iframe is mounted and interactive | Hide loading state |
onChange(state) | Any input/value/validation change | Keep submit button and UI state in sync |
onComplete(state) | All configured fields are filled | Trigger pre-submit checks |
onFocus(event) | A field receives focus | Clear stale field-level errors |
onBlur(event) | A field loses focus | Show field-level validation message |
onComplete means all fields are filled. It does not guarantee the values are valid.
colorScheme
Set colorScheme to the same value as your page theme so the card iframe blends correctly with light or dark surfaces.
const colorScheme =
document.documentElement.getAttribute('data-theme') === 'dark' ? 'dark' : 'light'
const cardElement = await rinne.elements.card({
theme: 'material',
colorScheme,
locale: 'pt',
fields: ['number', 'expiry', 'cvc']
})
Field Control
// Default visible fields
await rinne.elements.card({ fields: ['number', 'expiry', 'cvc'] })
// Include cardholder name
await rinne.elements.card({ fields: ['name', 'number', 'expiry', 'cvc'] })
Validation State
interface CardState {
isValid: boolean
isComplete: boolean
errors: {
number?: string
expiry?: string
cvc?: string
name?: string
}
}
Use onChange to enable/disable your submit button.
const cardElement = await rinne.elements.card({
onChange: (state) => {
submitButton.disabled = !state.isComplete || !state.isValid
}
})
Read Mounted Card Values
After mount, you can read encrypted values from mountedCard.values.
if (mountedCard.isComplete && mountedCard.isValid) {
const payload = mountedCard.values
// Encrypted values
console.log(payload.card.number)
console.log(payload.card.cvv)
// Additional metadata
console.log(payload.card.brand)
console.log(payload.card.last_digits)
}
Card number and CVV are encrypted before exposure in mountedCard.values.
Submit Pattern
const mountedCard = await cardElement.mount('#card-element')
async function submitCardCheckout() {
if (!mountedCard.isComplete || !mountedCard.isValid) {
return
}
await fetch('/api/checkout/card', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ cardData: mountedCard.values.card })
})
}
Never collect raw PAN/CVC in your own inputs. Use only encrypted values from mountedCard.values.
Live Demo
This sandbox demo mounts the card element directly in Mintlify, applies colorScheme based on the current docs theme, and shows real-time state plus encrypted values.
Theming
theme accepts:
- A preset:
'clean' | 'minimal' | 'material'
- A config object:
{ preset?, styles?, fonts? }
- A function theme:
(utils) => ({ styles, fonts })
Preset Preview
This live sandbox preview uses tabs to switch between built-in presets while keeping the same
card configuration. The iframe window stays on a light surface for easier visual comparison.
Custom Styling
Theme styles are CSS-in-JS rules injected into the secure iframe. You can style CSS selectors, but you cannot change iframe HTML structure.
Full Theme Example
export const checkoutTheme = {
preset: 'minimal',
fonts: ['https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap'],
styles: {
':root': {
colorScheme: 'light'
},
fieldset: {
border: '1px solid #dbe2ea',
borderRadius: '14px',
backgroundColor: '#ffffff',
padding: '14px'
},
label: {
color: '#475569',
fontSize: 12,
fontWeight: 600,
textTransform: 'uppercase',
letterSpacing: '0.04em'
},
'.field input': {
marginTop: 6,
height: '48px',
padding: '10px 12px',
border: '1px solid #cbd5e1',
borderRadius: '10px',
backgroundColor: '#ffffff',
color: '#0f172a',
fontSize: 16
},
'.field[ev-name="number"] input': {
letterSpacing: '0.08em',
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace'
},
'.field:focus-within input': {
borderColor: '#2563eb',
boxShadow: '0 0 0 3px rgba(37, 99, 235, 0.15)'
},
'.field[ev-valid="false"] input': {
borderColor: '#dc2626'
},
'.field[ev-has-value="true"] input': {
backgroundColor: '#f8fafc'
},
'.error': {
marginTop: 6,
fontSize: 12,
color: '#dc2626'
}
}
}
const cardElement = await rinne.elements.card({
colorScheme: 'light',
theme: checkoutTheme,
locale: 'pt',
fields: ['number', 'expiry', 'cvc']
})
State Attributes for Selectors
Use these iframe attributes for state-aware styling:
| Scope | Attribute | Values |
|---|
Root fieldset | ev-component | Card |
Root fieldset | ev-valid | true or false |
Field wrapper .field | ev-name | name, number, expiry, cvc |
Field wrapper .field | ev-valid | true or false |
Field wrapper .field | ev-has-value | true or false |
Responsive Styling
Use a function theme and utils.media() so breakpoints match the parent document viewport.
const responsiveTheme = (utils) => ({
fonts: ['https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap'],
styles: {
label: {
fontSize: 12
},
'.field input': {
height: '48px',
padding: '10px 12px',
fontSize: 16
},
'.field[ev-name="number"] input': {
letterSpacing: '0.08em'
},
...utils.media('(max-width: 768px)', {
'.field input': {
height: '44px',
fontSize: 15
}
}),
...utils.media('(max-width: 480px)', {
label: {
fontSize: 11
},
'.field input': {
height: '40px',
padding: '8px 10px',
fontSize: 14
},
'.field[ev-name="number"] input': {
letterSpacing: '0.05em'
}
})
}
})
const cardElement = await rinne.elements.card({
colorScheme: document.documentElement.classList.contains('dark') ? 'dark' : 'light',
theme: responsiveTheme
})
Set both colorScheme and theme using the same page theme source so iframe and host container stay visually consistent.
Mount Errors
mount() throws when the target selector is invalid or the provider cannot render the component.
try {
const card = await rinne.elements.card()
await card.mount('#missing-card-slot')
} catch (error) {
console.error('Card mount failed', error)
}
Unmount