Case Study theSkimm
case study · theskimm · FRONTEND engineer, WEB TEAM · react, NEXTJS
Using someone else’s components
theSkimm's loyalty program ran on Oracle CrowdTwist, whose widgets render as iframes you can't reach and mutate a DOM React doesn't know about. The two screens that matter most in a loyalty program — what you can earn, and what you can spend it on — were the two we had the least control over.
The page is ours — header, tier, navigation, layout. The two panels are CrowdTwist widgets: iframes we could only reach through a stylesheet loaded from a field in the vendor's admin.
what we were building
Skimm'bassadors earned points for things they already did: opening the Daily Skimm, reading articles, finishing onboarding, watching videos. Points became rewards.
The premise was that the program rewarded existing habits rather than asking for new ones — which meant the interface had to feel like the rest of theSkimm, not like a rewards portal bolted to the side of it. That distinction is the whole design problem, and it's why the vendor's styling mattered more than it looks like it should.
I built the member-facing side: registration and account creation, loyalty account management, password reset, and the activities and rewards dashboard that became the program's main destination.
Two problems, both about ownership
CrowdTwist ships a Widget SDK you load once in the document head. You then declare a widget as a bare div and the SDK finds it and replaces it with an iframe:
You cannot touch that div. Not one extra attribute, not a modified one — the SDK stops finding it.
The widgets didn't render. That markup is fine on a server-rendered page and broken inside React. The SDK mutates the real DOM; React reconciles against a virtual one. Neither knows the other exists, so on a client-side route change the widget simply never appeared.
Nothing about them could be styled. Once loaded, the widget is an iframe. No stylesheet in our application could reach inside it. Specificity doesn't help, wrappers don't help — there is no CSS path from our app into their frame.
driving the sdk from react’s lifecycle
If the SDK won't notice React, React has to notice the SDK. On mount, tear down every widget the SDK is currently tracking and ask it to load them again:
CrowdTwist widgets as envisioned by the Design Team
The design team's intent for the CrowdTwist widgets. Everything inside those cards was the vendor's markup, rendered in an iframe our stylesheets couldn't reach. One CSS file, loaded from a field in their admin, was the only way in — enough to get close to this, not enough to land it.
Called from useEffect in whichever component hosts a widget. Unload, then load — a full teardown on every mount, because a partial one leaves the SDK's internal list and the actual DOM disagreeing about what exists.
styling through the one seam they left open
The only way into an iframe you don't control is a door the vendor built. CrowdTwist's Control Center has a Custom CSS URL field on each widget's configuration, and that field is the entire styling surface.
So the visual integration for theSkimm's loyalty widgets lived in a stylesheet hosted outside the application, referenced from a text input in someone else's admin panel, configured one widget at a time.
“Making a component you don’t control feel like one you do is most of front-end work, and almost nobody writes about it.”
what it cost?
Decision: Keep the vendor's widgets and drive them imperatively, rather than rebuild the activity and reward surfaces against CrowdTwist's API.
Cost: Two, and both were load-bearing. _widgets is a private property — we were reaching into the vendor's internals, and a rename in any CrowdTwist release would have taken the dashboard down with no warning and no type error to catch it. And the styling lived outside version control, outside code review, and outside our deploys: changing how a widget looked meant editing a stylesheet in one place and a config field in another, with nothing in the repo recording that it had happened.
i wrote the runbook
None of this was documented by the vendor in a way that helps a React team. So, I wrote the internal guide — what the SDK does, why the widgets don't appear, the reload function, where the CSS actually lives — so the next person didn't have to rediscover it from first principles.
what shipped?
The program launched in 2021 and ran until theSkimm retired it.
Loyalty programs have a lifespan. The integration problem doesn't — every team that drops a vendor's components into their own product hits the same wall, and the way through is still the same two seams: whatever the SDK exposes imperatively, and whatever configuration field the vendor happened to leave open.