npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@increasebank/card-elements

v1.2.0

Published

Composable, PCI-safe components for rendering Increase Card details in your web app.

Readme

@increasebank/card-elements

Composable React components for rendering Increase Card details in your web app without taking on PCI scope. Each field renders inside its own iframe hosted by Increase.

Install

pnpm add @increasebank/card-elements

React 18.2 or newer, including React 19.

Usage

On your server, mint a short-lived token for a specific card:

POST /cards/card_oc8ihzxcv6q2htumc15j/create_details_token
{
  "type": "card_details_token",
  "token": "3f8a1c0e…",
  "expires_at": "2026-08-26T22:01:31Z"
}

In your React app, hand the token to CardElementsProvider and drop in the sub-components:

import {
  CardCvc,
  CardElementsProvider,
  CardExpiry,
  CardNumber,
} from '@increasebank/card-elements';

export const CardDetails = ({ token }: { token: string }) => (
  <CardElementsProvider
    token={token}
    appearance={{
      fontFamily: 'Inter',
      fontSize: '16px',
      color: '#0a0a0a',
    }}
  >
    <div className="grid">
      <div className="my-field-box">
        <CardNumber appearance={{ letterSpacing: '0.05em' }} />
      </div>
      <CardExpiry />
      <CardCvc />
    </div>
  </CardElementsProvider>
);

Sandbox

A token is only valid in the environment that minted it, so say when it came from Sandbox:

<CardElementsProvider token={token} sandbox>

Appearance

The appearance prop on the provider sets defaults for every child. Per-component appearance props override on a key-by-key basis. Allowed keys:

fontFamily, fontSize, fontWeight, fontStyle, fontSmoothing, letterSpacing, lineHeight, color.

fontSmoothing takes auto (the default) or antialiased. It exists because each engine spells the same request differently, so one value sets both: antialiased asks WebKit and Blink for -webkit-font-smoothing: antialiased and Gecko for -moz-osx-font-smoothing: grayscale. Only macOS honours either, and only there does it do anything — light digits on a dark card look thinner and closer to the copy around them. Set it to match whatever your own page already does, or the element's text will be smoothed differently from the label beside it.

Digits are always laid out on a fixed pitch, which is not negotiable: masking depends on every digit occupying the same width.

Keys outside that list are dropped — file an issue if you need one we don't support.

Typography is all the iframe controls, because typography is all it can't inherit across the frame boundary. Borders, padding, background, and alignment belong on your own wrapper element, where your CSS already works. Each element sizes itself to its text and reports its baseline, so it aligns with adjacent copy like a normal inline word.

Fonts

fontFamily takes a single family, not a CSS stack — the library appends its own fallbacks. A font only renders if the iframe can resolve it, and the iframe cannot see @font-face rules from your page, so the set of usable families is fixed and typed. Passing anything else is a compile error rather than a silent fallback to the default font.

Increase self-hosts these, subset to the characters these elements render and served from the same origin as the iframe:

Inter, Roboto, Open Sans, Montserrat, Source Sans 3, Figtree, Plus Jakarta Sans, Roboto Mono, JetBrains Mono

Every one of them has tabular figures, so the digits already sit on a fixed pitch.

Import HOSTED_FONT_FAMILIES if you want to build a picker over them. Each is a variable font covering its full weight range, so any fontWeight you ask for is a real face rather than a synthesised one — check the family's range if a weight looks clamped (Open Sans starts at 300, Source Sans 3 at 200).

You can also use CSS generics and the families that ship with every OS: system-ui, ui-sans-serif, ui-monospace, sans-serif, serif, monospace, Arial, Helvetica, Georgia, Courier New, Times New Roman, Verdana. These need nothing from us.

Your own font

Declare it with customFont and hand it to the provider:

import basierCircle from './fonts/BasierCircle-Regular.woff2';
import { CardElementsProvider, customFont } from '@increasebank/card-elements';

const basier = customFont({ family: 'Basier Circle', source: basierCircle });

<CardElementsProvider token={token} fonts={[basier]} appearance={{ fontFamily: basier.family }}>

customFont returns the family name as a type only it can produce, so fontFamily stays closed: a family you forgot to declare is still a compile error rather than a silent fallback.

Pass source through your bundler, as above, rather than hard-coding a built asset URL: a hashed URL you imported can never go stale, whereas a URL you paste in breaks on your next deploy.

Serve the file with an Access-Control-Allow-Origin header. The element loads the font itself, from your URL, so the request is cross-origin and the browser will refuse a font that does not allow it. Font CDNs send this already; your own server may not. If you cannot add the header, inline the file as a data: URL instead — source accepts one.

weight takes an @font-face weight or range ('400 700'), style takes normal, italic or oblique, and generic picks what the element falls back to while the file loads: sans-serif (the default), serif or monospace. Declare one entry per file if you need more than one face.

Name it something we don't already resolve. A family called Inter or Arial would lose to the one Increase serves, so customFont refuses those rather than letting your file quietly never render.

An element only loads the faces for the family it is actually rendering, and the browser's cache covers the rest: one download serves every element on the page, and repeat visits too if you send a long max-age. A hashed asset URL can safely be immutable.

onReady waits for your font, so the element will not swap fonts under you after you have dismissed your own loading state. If the file cannot be loaded the element reports font_unavailable and carries on in the fallback, rather than waiting forever.

Your font does not need tabular figures. If its digits are not all the same width — Basier Circle's 1 is a third narrower than its 0 — the element puts each digit in a cell of equal width instead, so the value keeps its pitch and does not reflow when you toggle masked. That costs a little letterfit in exchange for a payment field that stays still.

Increase never stores or serves your font: the element loads it from wherever you already serve it, under your own licence.

PINs

<CardPin /> renders the card's PIN, for programs configured for cash disbursements:

<CardPin masked={hidden} onError={handleError} />

Not every card has one. When there is no PIN the element reports pin_unavailable and renders nothing, rather than sitting there empty — you should be able to tell whether PINs are available for your program rather than guessing from a blank space.

A PIN is never copyable, and <CardPin /> does not take copyable or onCopy. It is typed into a keypad, so there is nowhere to paste it.

Dark pages

The elements sit on whatever background you put behind them, dark included. Each one takes its colour scheme from your page, so a page that declares color-scheme: dark, or one that follows the user's own preference, needs nothing from you.

An element reads that when it mounts. If your app switches theme after that, tell it:

<CardElementsProvider token={token} colorScheme={theme === 'night' ? 'dark' : 'light'}>

colorScheme resolves per element as element ?? provider ?? detected.

Masking

masked replaces the digits with bullets, keeping the separators, so the shape of the value is preserved: •••• •••• •••• 4242. The card number keeps its last four, which you already have from the Card itself; the expiration, verification code and PIN are masked completely. Set it on the provider to mask every element at once, and on any component to override that — masked resolves per element as element ?? provider ?? false, so a <CardCvc masked /> stays hidden while the rest of the card is revealed, and omitting it inherits whatever the provider says.

It is a display affordance — against shoulder-surfing, screen sharing and screenshots — not an access control. The element still fetches the card details while masked, so it hides the value from the person looking at the screen, not from the page.

Copying

copyable turns the value itself into a copy control: clicking or pressing Enter on it writes the value to the clipboard. Set it on the provider for every element, or on any component to override — the same element ?? provider ?? false resolution as masked.

<CardNumber copyable onCopy={() => setNotice('Copied')} />

It copies the real value even while masked, and without the spaces the element renders. onCopy fires once the write succeeds, so the confirmation UI is yours; a refused write reports copy_failed instead.

The clipboard write happens inside the iframe, so the value never passes through your page. That is also why the value is the click target rather than a button of your own: the browser only permits a clipboard write in the document the click landed in, and a click on your page cannot authorise one inside ours.

Formatting

<CardNumber grouping="network" /> (the default) spaces digits the way the card's network prints them — four-six-five for American Express, four-four-four-four for Visa and Mastercard. Pass grouping="none" for unspaced digits.

<CardExpiry format="MM/YY" /> (the default) or format="MM/YYYY".

Lifecycle

onReady() fires once the element has fetched and rendered its card data and reported its size, so it is safe for dismissing your own loading state. onError({ code, message }) fires if anything fails:

token_expired, token_invalid, card_not_found, pin_unavailable, protocol_version_mismatch, initialisation_timeout, element_load_timeout, font_unavailable, copy_failed, network, unknown

font_unavailable and copy_failed are the non-fatal ones — the element carries on displaying its value. Everything else means no card data is shown. element_load_timeout usually means a Content Security Policy is blocking the iframe.

Filling a container

By default an element sizes itself to its value and sits on the baseline of the text around it, like a word in a sentence. For a card layout, where the page should not move while the details load, pass fill: the element takes the size of its container instead, from its very first render, and draws its value vertically centred inside it.

<div className="card-number">
  <CardNumber fill />
</div>
.card-number {
  width: 254px;
  height: 24px;
}

Size the container to fit the value in your typography. The value is laid out on a fixed pitch, so it is the same size masked or not, and for every card; the design studio in the Card Elements documentation measures it for you. A value that does not fit is cut short with an ellipsis. With copyable, the whole container is the click target.

Set fill on the provider to fill every element, and on any component to override it, with the same element ?? provider ?? false resolution as masked. Keep the default for a value inside a sentence, since a filled element no longer aligns with the text around it.

Loading states

Unless it fills its container, an element takes no space until it has rendered its value, so a wrapper you styled around it — a border, a background, a label — shows up empty while it loads. Each element renders with a data-card-element-state attribute of loading, ready or error, from its very first render, so your CSS can hide the wrapper until the value arrives, with no state of your own:

.card-field:has([data-card-element-state='loading']) {
  opacity: 0;
}

.card-field:has([data-card-element-state='error']) {
  display: none;
}

A filled element's container has its size from the first render, so it can show a skeleton instead of hiding:

.card-number:has([data-card-element-state='loading']) {
  background: #f0f3f5;
  animation: pulse 1.2s ease-in-out infinite;
}

error covers every fatal code, including pin_unavailable, so the second rule also hides the PIN's wrapper on a card without one. font_unavailable and copy_failed leave the element ready. To tell the user why a value is missing, use onError, which says which code it was.

To treat the card as a whole, use onStateChange on the provider. It reports ready once none of its elements is still loading and at least one has rendered, so a card without a PIN does not hold it up; error once none is still loading and every one has failed; and loading again after a new token, or while it has no elements at all. The state starts as loading and that first state is not reported, so seed your own state with it:

<CardElementsProvider
  token={token}
  onStateChange={(state) => setRevealed(state === 'ready')}
>

Security

  • Mint tokens server-side. Never expose your Increase API key in the browser.
  • Each token is scoped to a single card and expires in one hour.
  • The iframes load from increase.com — your CSP needs to allow frame-src https://increase.com.
  • Declaring a custom font is the one thing that lets an element load from an origin other than Increase, and only that font file, from the URL you gave it.