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.
| Area | What you control |
|---|---|
| Brand and accent | One brand color drives buttons, links, selected states, radio marks, and focus rings. About a dozen related tones derive from it. |
| Surfaces and layout | Page background, card and panel color, and borders. Use a white canvas, your dark mode, or anything between. |
| Typography | Body text, headings, helper and muted text, and the typeface. |
| Status colors | Success, error, and warning colors for status pills, validation messages, and compliance notices such as a required Request for Information (RFI). |
| Shape and sizing | Corner rounding and iFrame sizing, from sharp to soft and compact to spacious. |
| Accessibility | Set the keyboard focus ring explicitly instead of using an auto-derived default. |
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.
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.
| Element | Why it is fixed |
|---|---|
| Field layout and order | The 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 fields | Nium'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 rules | Nium 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 logic | FX 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 sequence | The SDK decides which screen comes next. You set the starting context, such as customer, beneficiary, and amounts, not the navigation between steps. |
| Error messages | Standard 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 form | Labels 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.
| Component | Rendering engine | NiumTheme |
|---|---|---|
Manage beneficiaries (beneficiaryForm) | Nium Embed | Supported |
Send payouts (payoutForm) | Nium Embed | Supported |
Track payouts (transactionHistoryForm) | Nium Embed | Supported |
Respond to RFIs (rfiForm) | Compliance layer | Loading screen only |
Onboard customers (customerOnboardingForm) | Compliance layer | Loading 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.
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, andborderRadiusaren'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
themeobject, 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. surfaceColoris 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:
lightis{}, a deliberate no-op, because the built-in look is already light.darkseeds 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 overridebrandColorwith 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
#111827from 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(). Foroklch(),color-mix(), orvar(), 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
surfaceColorwithout a matchingtextColorgives default#374151body text on a dark card. That's valid but unreadable. Set them together, or usepreset: '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:
#e5e7ebfor borders,inheritfor 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
borderColoris also set. When noheadingColoris 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:
#16a34afor the dot,#15803dfor the text, and#f0fdf4for the fill. The default adapts when you themesurfaceColor, 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:
#ef4444for the dot,#dc2626for the text, and#fef2f2for 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:
#b45309for the dot,#92400efor the text,#fffbebfor the fill, and#fde68afor the outline. - Watch for: the compliance callout outline appears only when
surfaceColoris also set. SettingwarningColoralone 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:
fontFamilysays which font to use. It doesn't load the font. To use a font other than Inter or a system font, also list it infonts, 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.
theme: {
brandColor: '#7c3aed',
fontFamily: "'Poppins', sans-serif",
fonts: [
{ cssSrc: 'https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700' },
],
},
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
fontFamilymust 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:
inheritfor fields,12pxfor the modal. - Watch for: this is a CSS length, not a number.
'12px','0.75rem', and'999px'all work. A bare12fails 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 unknownpreset. 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 set | What appears |
|---|---|
brandColor | Full accent family: buttons, hovers, links, focus rings, radio marks. |
brandTextColor | Overrides the auto-picked label color. |
backgroundColor | Page canvas and modal scrim. |
surfaceColor | Cards and inputs. Also unlocks the compliance and status styling block, and becomes the blend target for everything else. |
borderColor | Borders and input outlines. Gets a stronger variant if textColor is also set. |
textColor | Body and field text. Unlocks zebra rows, both loading-skeleton shades, the stronger border, and the neutral button when no heading color is set. |
headingColor | Headings and emphasized values. Decides whether every status pill's text darkens or lightens. |
mutedTextColor | Labels, placeholders, faint text, and disabled text. |
successColor and errorColor | Pill dot, text, and fill. |
warningColor | Pill dot, text, and fill. The compliance callout outline needs surfaceColor too. |
fontFamily | Typeface. |
borderRadius | Container and input rounding. |
focusRingColor | Focus ring, overriding the brand-derived one. |
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.
| Option | Default | What it controls |
|---|---|---|
minHeight | 400 | Minimum 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. |
maxHeight | Unset | Maximum 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: {
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: {
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: {
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: {
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.
| Mistake | What happens, and the fix |
|---|---|
Dark surfaceColor, no textColor | Default #374151 body text on a dark card. Valid, but unreadable. Set the type roots together, or start from preset: 'dark'. |
Forcing brandTextColor without checking | You 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 fonts | The 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 cards | It paints the canvas behind them. Cards use surfaceColor. |
Testing with named colors such as red on green | Every 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. |