Driving automotive CSS into the fast lane

An essential component of AutoFi’s platform is the ability to integrate with 3rd-party car dealer websites and make our UI available to our dealer’s customers, the end-user. We provide CTAs (calls to action; buttons, links, and banners) to the end-user to engage with AutoFi’s experience. Reliably integrating dealers’ constantly-changing websites with our platform has been an ongoing challenge.

One of those challenges with UI integration is existing CSS on the “host” website. We do have CSS rules to make our CTAs appear a certain way—and it will look good on some, possibly most, sites, but on a few sites they will look strange in unexpected ways, and sometimes unusable (buttons may be hidden, or have a text/background color combination that renders the text unreadable).

We can try to override these styles using a more specific CSS selector, but some dealers will use an even more specific selector. We can try to override all their styles using !important, but the dealer can use !important on their styles (and indiscriminately using !important is considered bad practice). Trying to override this CSS turns into an endless walls-and-ladders situation.

Our initial proposed solution

We thought about using shadow DOM, which is meant for this kind of thing, but that was back in 2018, when many of our users were using Internet Explorer, which didn’t support shadow DOM.

Iframes

Our first real solution used iframes. We rendered our CTAs in an iframe, which would prevent the host website’s styles from interfering with ours. This worked by directly adding elements to the iframe, not using an iframe to display another site as iframes are usually used. The solution involved a component that would create a div within the iframe and render the content in that div.

But that came with some difficulties. We had to adjust the dimensions of the iframe to fit its content. There is no practical way to do this using CSS alone.

The iframe had to measure the height of its contents, then set its own height. Figuring out how to measure the iframe’s contents involved some tricky use of window.getComputedStyle, but we thought that once we figured that out, all we had to do was set the content of the iframe (whenever the iframe was created or its content changed), have the iframe measure its content’s dimensions, and appropriately set its own dimensions.

Even that turned out to be more complicated than we thought. Firefox had a race condition where we would create an empty iframe, add some content to it, and try to measure the dimensions of the content, and the width and height would turn out to be 0, since it was returning the height and width of the content when it was empty.

We worked around that by adjusting the dimensions immediately after the content was set, then waiting 200ms, then adjusting the dimensions again. That worked most of the time, but it still didn’t always work in Firefox if I was running something CPU-intensive (maybe because it took over 200ms for it to realize that the iframe’s content had changed), and I don’t have a clear idea if it worked for Firefox users with slower computers than mine. We tried increasing the delay, I think to 1000ms, and that worked more reliably, but the performance was pretty bad. On a dealer site with lots of our buttons, things would look funny, and it would take several seconds for everything to adjust and move to the right place. We eventually decided to adjust the dimensions 4 times when the content changed (immediately, then after 200ms, 1000ms, and 2000ms). Even in browsers other than Firefox, it would sometimes have a noticeable performance impact.

Adjusting the dimensions was the biggest problem we had with iframes, but not the only one. Any time something had to access the page enclosing the iframe, that was tricky.

We eventually concluded that using iframes was awkward and fragile. We had to come up with something better.

Other proposed ideas

We thought about using other things, such as “all: unset” or “all: revert” rules in our CSS, but those turned out not to be practical solutions, because they had limited browser support, and didn’t quite do what we wanted.

Shadow DOM

We finally decided to rethink shadow DOM. We changed our CSS isolation component to use shadow DOM, and it worked beautifully in all non-Microsoft browsers. The hard part was handling Internet Explorer and older versions of Edge. We eventually decided on the following:

  • For browsers that could handle it, we used shadow DOM.
  • For older versions of Edge (before version 79, when they switched to Chromium), we used the ShadyDOM and ShadyCSS polyfills, which would pretend to support shadow DOM and allow our UI to render without throwing exceptions. And we used a somewhat hacky workaround involving a lot of CSS properties to imperfectly override most of the properties in dealer CSS that interfere with our own CSS. Our forms would occasionally look a bit funny, but they would be usable.
  • For IE, we ended up doing the bare minimum required to make the form render without throwing exceptions, and not even trying to load ShadyDom or ShadyCSS. We tried all sorts of things to try to make it work better in IE, but everything we tried would not work, and sometimes cause our UI to not render at all, or seemingly unrelated code to throw mysterious exceptions. Eventually, we decided that properly supporting IE was taking a lot of time and driving us all insane, for something that was only of use to a small and dwindling number of users. We eventually reached a point where we did not care very much about aesthetics, and we didn’t even care if the forms were unusable unless it impacted a lot of users.

Despite the problems we had in Edge and IE, we decided that using shadow DOM was worth using, since it was such a big performance and reliability improvement for most of our users.

Eventually, we officially stopped supporting IE. Later, we decided that the number of users using pre-Chromium versions of Edge was small enough to not worry about, and removed the workaround for older Edge versions, leaving us with only the pure shadow-DOM solution.

Other details

Our UI is rendered in a div that acts as a container, and has a shadow root to isolate the CSS inside of it, but how do we ensure that the dealer’s CSS does not interfere with the div itself? This was another walls-and-ladders scenario. We came up with an imperfect but good-enough solution. We would use a selector like:

:not(#masterShifu):not(#masterOogway) .autofi-modal-container

… referring to an element with a class of autofi-modal-container inside any element that does not have an id attribute of masterShifu or masterOogway.

This was not a perfect solution, since the dealer could override it with a more specific selector, and it may not work if they use those id attributes for anything. But it turned out to be good enough for our purposes. So practically, this matches anything with a class of autofi-modal-container, but with enough ID selectors to make the whole selector specific enough to override the CSS found on almost all dealer sites.

Fun facts: on some sites using custom (site-specific) CSS, we use selectors like:

body:not(#masterShifu):not(#masterOogway):not(#dragonWarrior) .autofi-button-container

… to make our selectors even more specific. I picked the names of characters from the movie Kung-Fu Panda because the service that renders our UI on dealer sites is named “panda,” and one of the first things I worked on at AutoFi was a project to improve panda, which had been named “kung fu” by a coworker who was a fan of the Kung-Fu Panda series.


Elias Zamaria is a Senior Software Engineer at AutoFi, where he has worked for almost 4 years. His previous companies include AppCentral and Google.