Overview
Triple WLUI (white-label UI) is the consumer-facing side of the Triple offers and rewards platform — the app a bank's customers actually use to browse merchant offers, claim rewards, and manage their preferences. It's deployed per issuer (PNC, Bank of America, and a "triple-demo" reference build), each themed to that bank's branding from a single codebase. It sits alongside Triple CMS, which is where the configurations driving this app's theming and content get built.
My work here spanned the whole app, but accessibility was the recurring thread across a year of tickets — this is the one product in the Triple family where a11y wasn't a one-off cleanup pass, it was ongoing.
The problem
A white-label consumer app has two problems layered on top of each other. First, the ordinary problem of building a good offers-and-rewards experience — search, filtering, a map view, claiming and tracking offers. Second, the constraint that every visual and behavioural detail needs to work correctly across multiple bank brands with different colour palettes, logos, and fonts, some of which weren't designed with accessibility contrast ratios in mind.
That second constraint is where most of the interesting work lived. A theming system that just swaps a CSS variable isn't enough if a bank's brand colour happens to fail contrast against white text — the app needs sensible fallbacks, not a broken screen for one issuer.
What I built
Accessibility, ticket by ticket
This is genuinely the part of the codebase I spent the most sustained time on: a skip-to-content button (SkipToContent), keyboard visibility for the side info panel, ARIA labelling on quicklinks, filter buttons, and breadcrumbs, focus management across modals, screen-reader labels on search inputs, and toast alignment with the header so screen readers announce things in a sensible order. None of these individually are hard engineering, but they add up — accessibility work is rarely one big fix, it's dozens of specific, unglamorous corrections that only show up if someone's actually testing with a keyboard and a screen reader rather than a mouse.
White-label theming that degrades gracefully
Falling back to a sane text colour wherever a bank hasn't set one, converting hard-coded hex values to CSS variables throughout the app, and getting header font and colour inheritance right per issuer. The goal was that a new bank onboarding onto the platform shouldn't require a code change — just configuration — and shouldn't be able to accidentally ship something illegible.
Offers, maps, and preferences
Save-preferred-location on both the map and search flows, page-visit analytics through PostHog, an offer-terminology change from "claimed" to "activated" that rippled through several components and translation strings, responsive spacing fixes for tablet, and capping carousel controls so they didn't become unusable on brands with long offer lists. The map view uses @googlemaps/js-api-loader with marker clustering for merchant locations, which needed its own set of interaction and accessibility considerations — a cluster of pins isn't naturally keyboard-navigable, so that took some extra thought.
Forms and data
react-hook-form with Zod schemas for validation, TanStack Table for tabular data, and Datadog for monitoring. Forms in a rewards app carry real consequences — a validation bug in the preference centre means a customer can't opt out of marketing they don't want, which isn't a cosmetic bug.
Engineering decisions
Treating accessibility as ongoing, not a sprint
A lot of teams run one accessibility audit, fix the findings, and move on. Here it stayed a live category of work across the whole time I was on the project — new tickets kept surfacing because new components kept getting built, and it's much cheaper to catch an ARIA gap in review than to rediscover it in a later audit. That required a bit of a mindset shift on the team: a11y bugs got triaged like any other bug, not batched into a separate "compliance" workstream.
CSS variables over hard-coded brand values
Every hard-coded hex value in a white-label app is a future bug waiting for the next bank to onboard. Migrating those to CSS custom properties, issuer by issuer, was slow going but meant the theming system actually held together as more brands came on. The alternative — a growing pile of if (issuer === 'pnc') conditionals scattered through components — is the thing you end up rewriting from scratch eighteen months later.
Stack
| Layer | Choice |
|---|---|
| Framework | React · TypeScript · Vite |
| Data | TanStack Query · TanStack Table |
| Forms | react-hook-form + Zod |
| Maps | Google Maps JS API + marker clustering |
| i18n | FormatJS |
| Analytics | PostHog · Datadog |
| Testing | Playwright · BrowserStack |
| API client | swagger-typescript-api-generated TypeScript client |
Reflections
The accessibility work is what I'd point to first from this project. It's easy to treat a11y as a checkbox — run an automated scanner, fix what it flags, ship. The automated scanners catch maybe a third of what actually matters; the rest is things like "does focus land somewhere sensible after this modal closes" or "does a screen reader announce this toast before or after the page content it refers to," which only surface from manual testing and from actually using the app the way someone with a screen reader would.
The theming system taught me the same lesson from a different angle: the parts of a white-label product that feel like plumbing — CSS variables, fallback colours, per-issuer config — are the parts that determine whether onboarding a new bank takes a day or a sprint.