Skip to main content

Theme customization

Use the theme object to restyle every embedded Nium form to match your brand, including colors, typography, shape, and accessibility. Nium keeps managing form logic, validation, and compliance behavior, so re-skinning a form never breaks them. Every Nium Embed guide links to this page, so you only need to learn it once.

NiumTheme has 15 properties and covers visual customization only. This page includes four example themes.

Customization areas​

Pass one theme object to createElement. Nium owns flow logic, validation rules, field layout, required fields, and flow sequence. These update automatically when corridor requirements change, so you never need an integration update.

AreaWhat you control
Brand and accentOne brand color drives buttons, links, selected states, radio marks, and focus rings. About a dozen related tones derive from it.
Surfaces and layoutPage background, card and panel color, and borders. Use a white canvas, your dark mode, or anything between.
TypographyBody text, headings, helper and muted text, and the typeface.
Status colorsSuccess, error, and warning colors for status pills, validation messages, and compliance notices such as a required Request for Information (RFI).
Shape and sizingCorner rounding and iFrame sizing, from sharp to soft and compact to spacious.
AccessibilitySet the keyboard focus ring explicitly instead of using an auto-derived default.
Pass the theme on every createElement call

There is no global SDK-level theme registration. Pass the theme option on every createElement call. If you mount multiple forms in one session, pass the same object to each.

RFI and Customer Onboarding style only the loading screen

Respond to RFIs and Onboard customers load their questions from a separate Nium app. For these two components, theme styles only the initial loading screen. The form itself isn't themed. See Theming and compliance forms.

What you can't customize​

NiumTheme changes only the visual layer. You can't configure the following elements. This is a deliberate design choice. Each one serves a compliance or reliability function that Nium manages for you. When Nium adds a corridor or updates banking rail requirements, the SDK handles it and your code stays the same.

ElementWhy it is fixed
Field layout and orderThe sequence is the same across corridors and tuned for completion rate. Reordering fields breaks the progressive disclosure that guides users through complex compliance inputs.
Required fieldsNium's compliance rules and corridor regulations set them. Removing them causes transaction failures or regulatory violations. You can't hide or skip required fields.
Validation rulesNium updates corridor-level validation, such as IBAN formats, sort codes, account number structures, and SWIFT codes, when banking rails change. Your integration doesn't need to change.
FX rate display logicFX shows only at the review stage, not on the input screen. This prevents rate confusion during entry and follows compliance guidelines on rate disclosure timing. The display format and rate lock duration are also fixed.
Flow sequenceThe SDK decides which screen comes next. You set the starting context, such as customer, beneficiary, and amounts, not the navigation between steps.
Error messagesStandard messages keep compliance instructions accurate and legally appropriate. You receive error codes through the error event and can add your own messaging.
Button labels in the formLabels such as Review & Send and Confirm Payout are part of the compliance disclosure sequence. Renaming them could misrepresent the action the user confirms.

To pre-fill fields or scope a form to a customer, wallet, or beneficiary, use the options on each component's page, such as Send payouts. They don't change flow logic or validation.

Theming and compliance forms​

Theming doesn't apply to Respond to RFIs (rfiForm) or Onboard customers (customerOnboardingForm) beyond the loading screen. They use a separate compliance rendering layer instead of the standard Nium Embed pipeline, and that layer doesn't expose theming parameters.

ComponentRendering engineNiumTheme
Manage beneficiaries (beneficiaryForm)Nium EmbedSupported
Send payouts (payoutForm)Nium EmbedSupported
Track payouts (transactionHistoryForm)Nium EmbedSupported
Respond to RFIs (rfiForm)Compliance layerLoading screen only
Onboard customers (customerOnboardingForm)Compliance layerLoading screen only

Every other Nium Embed component supports NiumTheme.

NiumRFIFormOptions and NiumOnboardingFormOptions accept the full theme property without an error. The console shows no warning when these components ignore the keys that they don't style.

Nium generates these compliance forms from a schema that Nium updates independently. The schema sets field types, validation, and document upload requirements. Because the form structure is dynamic, static theme tokens can't be applied reliably. Nium manages the visual layer for these forms.

The standard components (Manage beneficiaries, Send payouts, and Track payouts) use a fixed React component tree. NiumTheme properties are injected there as CSS custom properties.

Branding the compliance forms

To make these forms match your branded forms, contact your Nium account team. White-labeling for compliance-rendered forms is a roadmap item and might be available for enterprise accounts on request.

Theme model​

You set up to 15 keys on NiumTheme. Twelve of them hold a color. Nium calls these roots. Every other color on screen blends from them, including hover states, tints, borders, and pill backgrounds. Everything blends against your surfaceColor.

  • Key: a property you set yourself, for example brandColor. The parameter reference lists all 15.
  • Root: any of the 12 keys that hold a color. preset, fontFamily, and borderRadius aren't roots.

Three rules explain almost every surprise:

  • Unset means untouched. A key you omit changes nothing, so the built-in Nium look shows through. Every fallback value matches the look the form had before theming existed. If you pass no theme object, the form renders as it always has.
  • Keys unlock each other. Several visual details need two keys together. If you skip textColor, you silently lose skeletons, muted surfaces, and the strong border, even if you set everything else.
  • surfaceColor is the master switch. Setting it switches on a coordinated set of status pills, badges, and transaction colors. The SDK redraws them to match your surface instead of their default light look.

Parameter reference​

Pass the 15 keys on NiumTheme once to createElement({ theme }). They apply across every supported form.

preset​

Type: 'light' | 'dark'. Category: Base.

  • Changes: nothing directly. It seeds any root you didn't set, then gets out of the way. Your keys always win.
  • Values: light is {}, a deliberate no-op, because the built-in look is already light. dark seeds 7 roots: brand #818cf8, background #12131f, surface #1c1d2b, border #343651, text #e2e3f0, heading #f5f6fa, muted #9799b8.
  • Watch for: the dark preset carries a brand color on purpose. Without one, buttons fall back to the light-theme navy and are unreadable on a near-black surface. If you set preset: 'dark' and override brandColor with a dark color, the problem returns.

brandColor​

Type: string. Category: Brand.

  • Changes: the most influential key. It controls these elements:
    • Filled buttons and their hover state
    • Links, selected cards, and selected rows
    • Focus rings, and radio and checkbox marks
    • Timeline and pagination accents
    • Accent-colored SVG icon fills and strokes
    • Tinted backgrounds and the button glow shadow
  • If unset: the SDK skips the whole accent family and falls back to the Nium navy ladder from the shared preset. This is the same navy as the shared Button, so you never get two competing primaries.
  • Watch for: the SDK computes two luminance thresholds from this value. Above L = 0.179, the button label flips from white to #111827. Below L = 0.08, the hover state lightens instead of darkening, because a near-black brand has no room left to darken. Mid-tone pinks and corals sit on the first boundary, so a small hex change can flip your label color.

brandTextColor​

Type: string. Category: Brand.

  • Changes: the text and icon color on brand-filled surfaces, such as primary button labels and chevrons inside filled controls.
  • If unset: the SDK picks white or #111827 from the Web Content Accessibility Guidelines (WCAG) relative luminance of the brand color. Use this path when you can, because it's right far more often than a hand-picked value.
  • Watch for: setting this key overrides the automatic contrast pick, so you own the contrast result. The auto-pick works only when the brand parses as hex or rgb(). For oklch(), color-mix(), or var(), the SDK can't resolve channels and leaves the label at its default white. On a light brand, that label is invisible. Set this key explicitly whenever your brand isn't a plain hex.

backgroundColor​

Type: string. Category: Surfaces.

  • Changes: the page canvas behind the cards, and the modal scrim.
  • If unset: the page background stays transparent, so the host page shows through. The modal uses its default scrim, rgba(15, 23, 42, .45). The scrim keeps that default unless you set this key, because a theme-driven scrim would change pages that never asked for theming.
  • Watch for: this key only shows in the gutters that the cards don't cover, so it can look broken. If you set it to the same value as surfaceColor, the layout goes flat and cards stop reading as cards.

surfaceColor​

Type: string. Category: Surfaces (master switch).

  • Changes: cards, panels, inputs, selects, textareas, comboboxes, and modals. On dark themes, focus rings stop drawing a stray white halo. Nearly everything else on screen mixes into this color, including every tint, loading skeleton, pill background, and muted surface.
  • Also unlocks: compliance and status styling. The compliance callout gets a matching border. The SDK redraws every status badge, pill, and transaction amount color against your surface color, not the default light look.
  • If unset: all mixes blend against #ffffff, and the status styling stays on its light defaults.
  • Watch for: set this key with care. A dark surfaceColor without a matching textColor gives default #374151 body text on a dark card. That's valid but unreadable. Set them together, or use preset: 'dark'.

borderColor​

Type: string. Category: Surfaces.

  • Changes: card borders, dividers, table rules, and input and select outlines. If you also set textColor, emphasized dividers get a stronger variant.
  • If unset: #e5e7eb for borders, inherit for inputs.

textColor​

Type: string. Category: Type.

  • Changes: body copy and the text typed into form fields.
  • Also unlocks: zebra-striped rows and wells, the loading shimmer animation, and a stronger border variant when borderColor is also set. When no headingColor is set, it also unlocks the one non-brand button style in the SDK.
  • Watch for: this is the highest-value key after the two surfaces, because several other visual details depend on it. Omitting it is the most common reason a theme mostly works but shows a wrong loading state.

headingColor​

Type: string. Category: Type.

  • Changes: every h1 to h6 inside the form, plus emphasized values such as wallet balances, transaction amounts, and section totals. It also powers the nickname Save button, the one non-brand button style in the SDK. That button inverts against the surface and doesn't track your brand color.
  • Also drives: whether every status pill's text darkens or lightens against its own fill color.
  • If unset: falls back to textColor, then to #111827.

mutedTextColor​

Type: string. Category: Type.

  • Changes: field labels, helper text, captions, timestamps, and, through derivation, input placeholders and disabled text.
  • Watch for: placeholders and disabled text have no keys of their own by design. They derive from this key. Without it, placeholders render at full strength and read as pre-filled values.

successColor​

Type: string. Category: Semantic.

  • Changes: active status pills, success confirmations, positive balance callouts, and the success acknowledgement screen after a payout. This covers the dot color, pill text, and pill fill.
  • If unset: a default green: #16a34a for the dot, #15803d for the text, and #f0fdf4 for the fill. The default adapts when you theme surfaceColor, so a dark theme still gets a dark-friendly green pill without you naming one.

errorColor​

Type: string. Category: Semantic.

  • Changes: blocked status pills, field validation messages, failed-transaction states, and error banners. This covers the dot color, pill text, and pill fill, the same as successColor.
  • If unset: a default red: #ef4444 for the dot, #dc2626 for the text, and #fef2f2 for the fill.

warningColor​

Type: string. Category: Semantic.

  • Changes: RFI-required pills, compliance notices, the pending-transaction state, and the create-account compliance callout. This covers the pill dot, text, and fill, plus a matching outline used only on the compliance callout box.
  • If unset: a default amber: #b45309 for the dot, #92400e for the text, #fffbeb for the fill, and #fde68a for the outline.
  • Watch for: the compliance callout outline appears only when surfaceColor is also set. Setting warningColor alone doesn't produce it.

fontFamily​

Type: string. Category: Shape.

  • Changes: the typeface for the entire embedded surface, applied once and inherited everywhere.
  • If unset: inherits the font that the embed's own stylesheet uses.
  • Watch for: fontFamily says which font to use. It doesn't load the font. To use a font other than Inter or a system font, also list it in fonts, as the next section describes.

Load a custom font with fonts​

fonts tells the iFrame where to load a font from. Pass fonts and fontFamily together in the theme object that you pass to createElement.

client.ts
theme: {
brandColor: '#7c3aed',
fontFamily: "'Poppins', sans-serif",
fonts: [
{ cssSrc: 'https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700' },
],
},
Fonts must load inside the iFrame

The embed is an iFrame, which is a separate document. It can't reach your parent page's @font-face rules or preloaded fonts. Loading the font on your own page has no effect. You must list it in fonts.

Follow these rules:

  • The family name in fontFamily must match the Google font name, or the family that you give for the self-hosted file.
  • A wrong or failing font never breaks the form. The form falls back to Inter, and the SDK logs a warning in your client's browser console that explains why.

borderRadius​

Type: string. Category: Shape.

  • Changes: corner rounding on inputs, selects, textareas, and the modal shell. One value keeps all of them consistent.
  • If unset: inherit for fields, 12px for the modal.
  • Watch for: this is a CSS length, not a number. '12px', '0.75rem', and '999px' all work. A bare 12 fails the hosted form's theme schema, and the entire theme is discarded, not just this key. The same happens for any non-string value or an unknown preset. The SDK logs a console warning on your page, but the form renders unthemed.

focusRingColor​

Type: string. Category: Accessibility.

  • Changes: the keyboard focus ring on every interactive control.
  • If unset: derived as a blend toward your brand color, or left at a default blue-violet (#A9B4FF) when no brand is set either.
  • Watch for: the derived ring is 45% brand mixed into 55% surface, which is tuned for light themes. On a dark surface it can land around #4A5163. It's present, but too dim to notice while tabbing. On dark themes, set this key explicitly to a color brighter than the brand.

Gating matrix​

Use this matrix to find out why an expected effect is missing. It shows which key unlocks which visual effects.

You setWhat appears
brandColorFull accent family: buttons, hovers, links, focus rings, radio marks.
brandTextColorOverrides the auto-picked label color.
backgroundColorPage canvas and modal scrim.
surfaceColorCards and inputs. Also unlocks the compliance and status styling block, and becomes the blend target for everything else.
borderColorBorders and input outlines. Gets a stronger variant if textColor is also set.
textColorBody and field text. Unlocks zebra rows, both loading-skeleton shades, the stronger border, and the neutral button when no heading color is set.
headingColorHeadings and emphasized values. Decides whether every status pill's text darkens or lightens.
mutedTextColorLabels, placeholders, faint text, and disabled text.
successColor and errorColorPill dot, text, and fill.
warningColorPill dot, text, and fill. The compliance callout outline needs surfaceColor too.
fontFamilyTypeface.
borderRadiusContainer and input rounding.
focusRingColorFocus ring, overriding the brand-derived one.
Debug a missing effect in DevTools

If a key you set doesn't show up, open DevTools on the iFrame's <html> element. Every computed style that the SDK applies is visible there. A missing effect almost always means its source key is unset, as the matrix shows. It isn't a bug.

Height options​

minHeight and maxHeight are fields on each form's customizations object, not on NiumTheme. This page documents them because they control layout, unlike the behavioral and data-scoping options in the rest of customizations.

OptionDefaultWhat it controls
minHeight400Minimum iFrame height in pixels, as a number. The SDK applies it as the iFrame's min-height inline style. Raise it for forms that can render long document-upload sections, for example RFI. Respond to RFIs and Onboard customers can't auto-resize after their hosted app loads, so for these two minHeight sets the visible height and the form scrolls inside the iFrame.
maxHeightUnsetMaximum iFrame height in pixels, as a number. Omit it to let the iFrame grow to fit its content with no cap. Set it only when you embed in a bounded surface and want an inner scrollbar instead of a growing page.

Test themes​

These four complete themes have every key filled. Contrast figures use the same WCAG luminance formula that the SDK uses internally.

Theme A: light, indigo brand on a tinted page​

Theme A
theme: {
brandColor: '#635BFF',
brandTextColor: '#FFFFFF',
backgroundColor: '#F6F9FC',
surfaceColor: '#FFFFFF',
borderColor: '#E3E8EE',
textColor: '#3C4257',
headingColor: '#0A2540',
mutedTextColor: '#697386',
successColor: '#0E9F6E',
errorColor: '#DF1B41',
warningColor: '#B54708',
fontFamily: "'Inter', sans-serif",
borderRadius: '8px',
focusRingColor: '#A5A0FF',
}

#635BFF sits at L ≈ 0.174, below the 0.179 flip, so the label stays white at 4.70:1. This checks that the threshold picks the right side. White cards over a tinted page make backgroundColor clearly visible.

Theme B: dark, every root set explicitly, no preset​

Theme B
theme: {
brandColor: '#7B8CFF',
brandTextColor: '#0B0E1A',
backgroundColor: '#0E1015',
surfaceColor: '#191B22',
borderColor: '#2A2D37',
textColor: '#D6D9E3',
headingColor: '#F2F4F9',
mutedTextColor: '#8E93A3',
successColor: '#3FD68C',
errorColor: '#FF6B6B',
warningColor: '#F5A524',
fontFamily: "'Inter', sans-serif",
borderRadius: '10px',
focusRingColor: '#9AA6FF',
}

This is the best coverage test. It sets all eight surface and type roots explicitly, with no preset. #7B8CFF is at L ≈ 0.30, so the auto-pick would choose dark anyway, and #0B0E1A gives 6.3:1. The explicit focus ring matters here, because the derived one lands near #4A5163, which is too dim on this surface.

Theme C: light, teal brand, warm neutrals​

Theme C
theme: {
brandColor: '#0F766E',
brandTextColor: '#FFFFFF',
backgroundColor: '#F7F6F2',
surfaceColor: '#FFFFFF',
borderColor: '#E4E1D9',
textColor: '#3F3F46',
headingColor: '#18181B',
mutedTextColor: '#78716C',
successColor: '#15803D',
errorColor: '#B91C1C',
warningColor: '#A16207',
fontFamily: "'Inter', sans-serif",
borderRadius: '6px',
focusRingColor: '#5EEAD4',
}

The teal brand is at L ≈ 0.14, with a white label at 5.47:1. Warm grays instead of blue-grays catch any code that assumes a cool neutral ramp.

Theme D: audit theme, every key a different, identifiable hue​

Theme D
theme: {
brandColor: '#7C3AED', // violet -> buttons, links, selected cards
brandTextColor: '#FFFFFF',
backgroundColor: '#FFF7ED', // peach -> page gutters + modal scrim only
surfaceColor: '#FFFFFF', // white -> keeps the peach bg obvious
borderColor: '#0EA5E9', // cyan -> borders, dividers, input outlines
textColor: '#1E293B', // slate -> body copy
headingColor: '#BE123C', // crimson -> headings, balances, amounts
mutedTextColor: '#0D9488', // teal -> labels, hints, placeholders
successColor: '#16A34A',
errorColor: '#DC2626',
warningColor: '#D97706',
fontFamily: "'Inter', sans-serif",
borderRadius: '14px', // exaggerated, so radius plumbing is obvious
focusRingColor: '#F59E0B', // amber -> unmissable when tabbing
}

This theme is loud but still legible. Crimson headings against slate body text show which strings run through headingColor and which through textColor, the split people most often get wrong. Teal muted text reveals every caption and every placeholder at once, because placeholders derive from that key.

Common mistakes​

Integrations commonly make these five mistakes.

MistakeWhat happens, and the fix
Dark surfaceColor, no textColorDefault #374151 body text on a dark card. Valid, but unreadable. Set the type roots together, or start from preset: 'dark'.
Forcing brandTextColor without checkingYou override an auto-pick that was already correct. Omit it unless your brand is a non-hex color function, where the auto-pick can't resolve channels.
Setting fontFamily without listing the font in fontsThe iFrame can't see fonts that you load on your own page, so the form falls back to Inter. List the font in fonts, and make sure the fontFamily name matches the font's family name.
Expecting backgroundColor to recolor the cardsIt paints the canvas behind them. Cards use surfaceColor.
Testing with named colors such as red on greenEvery named color is valid and nothing is dropped, so it renders exactly as requested, illegibly. Use the audit theme in Theme D instead. It's equally loud, still readable, and exercises the full cascade rather than the ~60% a partial theme reaches.
LLM-ready documentation:: llms.txt and llms-full.txt
Was this page helpful?