When the Official Theming Path Was Not Enough: Making Kendo Behave in a Mixed Stack

Written by fedoryshchev | Published 2026/04/07
Tech Story Tags: programming | frontend | react | angular | css | web-development | software-architecture | micro-frontends

TLDRTheming breaks in mixed frontend stacks not because of CSS, but because of lost control over rendering boundaries. To stabilise it, you need explicit style isolation, control over where UI renders (especially modals), and strict limits on composition.via the TL;DR App

When a frontend starts mixing frameworks, embedded apps, global CSS, and multiple active themes on the same page, theming stops being a local styling concern and starts turning into a boundary problem. This pattern shows up in a lot of real systems, especially where legacy pages, gradual migrations, web components, and UI libraries with global selectors all have to coexist longer than anyone originally planned.

A case like that is one my team recently faced, and below you will find how it was approached and handled.

We did not start from a clean theming setup that later became complicated. The base was already off, and over time the patches became the system.

The original setup technically worked, but it used the tool sideways. The theme was not configured in a way that could hold long term, components were not really in place the way they should have been, and a large amount of styling drift had accumulated through overrides. A lot of that code existed simply to force the UI to match mockups and later design changes, even when those changes were working against the original theme model. That kind of setup can survive for quite a while, but it becomes expensive the moment you need to keep evolving it.

That was the position we were in when the direction became React. At that point, the problem stopped being only about old CSS and started becoming a broader technical decision. We could either keep carrying the same base forward and continue layering more fixes on top of it, or we could take the opportunity to fix the model underneath. Looking back, fixing it was the better choice. The alternative would have been to keep multiple teams bending the library toward the desired look, while paying for the same theme problems again and again.

Why the existing setup was a problem

The main issue was not that the UI looked bad on every screen. The bigger issue was that the styling model had become unstable. Some of the code was broad and global by nature, some of it existed to compensate for a theme setup that was wrong from the start, and some of it came from framework-specific escape hatches that are easy to justify in the short term and difficult to live with later.

That is how the styling debt grew. There were broad overrides, framework-level escape routes, loose scoping, and the general habit of fixing the visible result instead of correcting the underlying setup. In isolation, each of those decisions can look reasonable. In combination, they produce a system where changing one area often affects another area in ways that are not obvious at first.

:global(.k-button) {
  border-radius: 10px;
}

::host .k-input {
  padding: 6px 12px;
}

.some-page .k-button.primary,
.other-page .k-button.primary,
.legacy-block .toolbar .k-button.primary {
  background: #3a7;
  color: white;
}

If the stack had stayed exactly the same, this might have remained one more legacy styling problem that everyone tolerated. What changed things was the fact that the broader direction became React. Once that happened, the old setup was no longer just inconvenient. It became something that would keep interfering with every new piece of UI unless the theme model itself was corrected.

What made this harder than a normal theming task

This was not a simple one-app scenario. The page could contain a legacy host, Angular web components, React web components, and dedicated applications in both stacks. Different parts were built and deployed independently, which meant different ownership boundaries and different rendering assumptions could end up on the same page.

repositories/
├── legacy/
├── frontend/
│   ├── angular/
│   │   ├── web-component-foo/
│   │   ├── web-component-bar/
│   │   └── web-app/
│   └── react/
│       ├── web-component-baz/
│       └── web-app/

That alone would already make theming more awkward. The real complication was that two themes needed to coexist at the same time while Kendo was still operating with global selectors in the middle of that setup.

This is where the technical problem became very concrete. Shared classes such as .k-button, .k-input, and similar selectors were not staying isolated. The more the themes visually diverged, the uglier the conflicts became. Part of the behavior was effectively a load-order fight over which styles happened to win in a given render path. That made the results unpredictable. A screen could look acceptable in one case and break in another case simply because a different combination of components loaded in a different order.

/* bundle A */
.k-button {
  border-radius: 2px;
  background: #1976d2;
  color: white;
}

/* bundle B */
.k-button {
  border-radius: 999px;
  background: #2e7d32;
  color: #111;
}

That was the real core of the problem: two live themes, one page, mixed framework boundaries, and global selectors that were never designed for that kind of coexistence.

A practical model for recognising and stabilising this class of problem

Before going into the implementation details, it is useful to make the pattern explicit, because the same type of failure tends to repeat across different stacks.

The problem usually appears when:

  1. a UI library still relies on global selectors or broadly shared styling assumptions
  2. more than one app, framework, or rendering model can appear on the same page
  3. two themes, or two styling systems, need to coexist at the same time
  4. dynamic UI such as modals, popups, or overlays can render outside the boundary where the theme originally looked correct
  5. teams keep solving visible issues locally instead of defining hard rules around ownership and composition

To stabilise that kind of setup, you usually need a combination of the following:

  1. explicit theme isolation rather than hopeful global coexistence
  2. render-path control for dynamic content such as modals and overlays
  3. composition rules that define what kinds of embedding or nesting are acceptable
  4. a small reusable model that can be repeated across codebases without drifting immediately
  5. a willingness to reject combinations that are technically possible but structurally too expensive to support

A specific case of applying this broader model is explored in more detail below.

The first isolation step

The first fix that actually helped was fairly simple in principle. Instead of letting the global Kendo selectors apply everywhere and hoping the page structure would keep them separate, we started isolating theme families explicitly through wrapper classes and pushing that into the CSS through PostCSS.

In practice, that meant taking selectors that were effectively global and rewriting them into scoped selectors. The shape was roughly this:

.wrapper-theme-bootstrap .k-input {
  /* bootstrap-flavored Kendo styling */
}

That part was important because isolating only one side would not have solved much. If one theme is scoped carefully while the other continues acting globally, the problem simply returns from the other direction. Both theme families had to be treated intentionally.

For normal components, this worked well enough. It was not especially fancy, but it was understandable, reproducible, and simple enough to reason about. More importantly, it gave us a model that could be reused instead of one more local patch.

.wrapper-theme-bootstrap .k-button,
.wrapper-theme-bootstrap .k-input {
  /* bootstrap-flavored Kendo styles */
}

.wrapper-theme-material .k-button,
.wrapper-theme-material .k-input {
  /* material-flavored Kendo styles */ 
}

Where the first fix stopped being enough

The first isolation pass handled the straightforward cases, but it did not solve the whole problem. The place where things reopened was modal and popup behavior.

That part was annoying because the base fix could look done until you started testing more realistic UI behavior. A normal component could render correctly inside the expected wrapper, while a modal opened by that same part of the app could still end up outside the boundary the theme relied on. Once that happened, the isolation logic no longer applied in the way it did for normal in-place components.

On the Angular side, part of the solution involved popup container resolution. In simplified form, it looked something like this:

{
  provide: POPUP_CONTAINER,
  useFactory: () => document.querySelector('.wrapper') || document.body
}

That improved things, but it did not cover all cases. There was also centralized popup tracking logic that had originally existed for manual style overrides. That turned out to be a useful place to extend the behavior by identifying the target popup, or its parent container, and tagging it so the correct theme rules could be applied.

Even with that in place, some cases still behaved differently, especially once Angular was bundled as a web component. That is why this did not end up as one neat container-level fix. It was closer to establishing the expected path first and then handling the cases that still escaped it.

On the React side, the dynamic part had to be handled differently. There the relevant tool was MutationObserver. The job was to watch for modal content, catch it when it appeared, and ensure it ended up in the proper themed container instead of wherever the default path wanted to leave it.

useEffect(() => {
  const observer = new MutationObserver(() => {
    const modal = document.querySelector('.k-animation-container');
    const themedRoot = document.querySelector('.wrapper-theme-bootstrap');

    if (modal && themedRoot && !themedRoot.contains(modal)) {
      themedRoot.appendChild(modal);
    }
  });

  observer.observe(document.body, { childList: true, subtree: true });
  return () => observer.disconnect();
}, []);

There was also an extra complication in the selector strategy. The scoping logic that worked for normal components was not always enough for modal behavior, because the relationship between the rendered content and the themed wrapper was different. In practice, part of the isolation had to be approached in the opposite direction for popup cases.

That was the point where the problem stopped being only about CSS scoping and became more about controlling where dynamic content was allowed to live.

The rules that made the setup workable

Once the modal path was under control, the next useful step was to stop assuming every possible combination should be supported. That mattered more than it may sound.

Mixed-stack solutions often become unstable because no one draws a line around what is actually allowed. Then every awkward nesting case gets treated as one more thing to support, and the solution gradually turns back into a collection of exceptions. We tried to avoid that.

The practical rule set was simple. Normal themed content could be isolated. Sibling composition was acceptable. Arbitrary nesting was not. For full application cases, one tech owning the page was usually the cleaner option.

That meant some combinations were explicitly out. The clearest example was React inside the Angular web app. It may have been theoretically possible to keep pushing the mixed rendering model further, but that would have broken the isolation rules faster than it would have helped. Under this setup, it was not worth supporting.

What did work was more controlled composition. Separate pieces could live as siblings on the same host page. Cross-cutting components could stay within that model if they were separable enough. Dedicated applications remained dedicated where that was the more stable choice.

<body>
  <navbar-web-component class="wrapper-theme-material"></navbar-web-component>
  <main class="wrapper-theme-bootstrap">
    <react-app-root></react-app-root>
  </main>
</body>
<body>
  <angular-app class="wrapper-theme-material">
    <navbar-web-component></navbar-web-component>
    <section>
      <react-app-root class="wrapper-theme-bootstrap"></react-app-root>
    </section>
  </angular-app>
</body>

This was not a one-size-fits-all solution, and it was not meant to be. The goal was to make the system stable enough that future work stopped reopening the same class of problem.

What stayed limited

This was never a perfectly clean result, and there is no point pretending otherwise. Kendo still had global-style behavior at the center of the setup. The stack was still mixed. Web components still introduced extra complications. Modal rendering still needed special handling.

The useful part was not elegance. The useful part was having real constraints and real rules.

Not every nesting pattern was allowed. Not every rendering path was treated as equal. Not every clever composition idea was worth supporting. A lot of the eventual stability came from being honest about that early enough.

There was also some balancing on the React side around dynamic content. React needed to support both web component and web app cases, while not accidentally pulling Angular-originated dynamic content into the wrong handling path. By that point, wrapper markers in the hierarchy were already helping, so it was manageable, but it still took some care to keep the behavior predictable.

So no, this was not one of those cases where the final answer became perfectly reusable in every direction. It was more practical than that. It solved the problem we actually had, and it did so in a way that people could keep building on without rediscovering the same issues every time a new screen or popup appeared.

Why the result was reusable

A purely local fix would not have been enough because part of the setup was spread across two repositories. The useful version had to be repeatable. Otherwise the same logic would have started drifting again almost immediately.

The shared part was deliberately small. There was a shared PostCSS configuration, shared file structure, naming conventions, and a couple of small helpers or adapters around the isolation flow. The rest remained specific to the applications that needed it.

Those few shared files were copied rather than packaged. Some people dislike that approach on principle, but in this case it was the right tradeoff. The common surface area was tiny, the files were not supposed to change often, and creating more infrastructure around them would have added more ceremony than value. A simple copy in a consistent shape was enough.

That mattered because the whole point was to reduce drift, not to build another system to manage the fix.

What was actually useful about the final result

The main takeaway for me is not simply that theming gets hard in mixed frontend stacks. That part is already obvious.

The more useful point is that once you have two live themes, shared global selectors, mixed application boundaries, and dynamic UI behavior on the same page, you are no longer dealing with a normal theming task. At that stage the work becomes partly about isolation, partly about render-path control, and partly about deciding which composition patterns are worth supporting at all.

That is also why the official theming path was not enough in this case. Official guidance generally assumes a cleaner environment than the one we had. It assumes clearer ownership, clearer boundaries, and fewer cases where two systems are alive at the same time while both still want to behave as if they are the only one in charge.

The answer, then, was not to force the recommended pattern harder. It was to isolate the theme families deliberately, handle modal escape paths as a separate problem, and draw hard rules around what kinds of composition were actually acceptable.

That did not make the setup elegant. It made it non-random.

And that is probably the clearest takeaway for developers from this case. There is a point where theming stops being mainly about styles. Once mixed boundaries and dynamic render paths enter the picture, the real job is controlling where styling is allowed to apply, where dynamic content is allowed to render, and which composition patterns the system is allowed to support at all.

If those things are left implicit, the setup usually drifts back into local overrides and one-off fixes. If they are made explicit, the system may still be imperfect, but it becomes much easier to keep stable.

That is also why composition limits are not just an implementation detail here. Without them, mixed-stack theming systems tend to reopen the same problem in slightly different forms every time a new embedded piece, popup, or nesting path appears. The technical work is not only about making a theme render correctly once. It is about deciding where coexistence is truly supported and where it should stop.

Instead of solving each new screen, popup, or embedded piece with one more styling workaround, there was finally a model underneath it. Imperfect, pragmatic, and a bit stubborn in places, but real enough to build on.


Written by fedoryshchev | Tech Lead at a Nasdaq-listed SaaS group. Sharing practical engineering lessons and discoveries.
Published by HackerNoon on 2026/04/07