Connect: tips for a modern frontend

Original author: Benjamin De Cock
  • Transfer
We recently released a new and improved version of Connect , our toolkit for platforms and stores. The Stripe design team worked hard to create unique landing pages that tell a story for our core products. We have prepared the Connect landing page for the release to reflect these intricate, advanced features, but at the same time without losing clarity and simplicity of presentation.

In this article, we will describe how we used several next-generation web technologies to launch Connect, and walk through some of the small technical details of our front-end journey.

CSS Grid Layout


Earlier this year, three major browsers (Firefox, Chrome, and Safari) rolled out their implementations of the new CSS Grid Layout module almost simultaneously . These specifications give developers a two-dimensional breadboard system that is easy to use and incredibly powerful. Connect's landing page relies on CSS grids almost everywhere, which makes some seemingly tricky design decisions trivial to implement. As an example, let's hide the contents of the header and focus on the background:



Historically, we created such background strips using absolute positioning to accurately place each stripe on the page. This method works, but fragile positioning often leads to small problems: for example, due to rounding errors, a gap of 1 px may form between the bands. Style sheets also swell quickly and are harder to maintain because display queries are more complex to account for differences in the background across different screen sizes.

CSS Grid fixes almost all of these issues. We simply define a flexible grid and place the stripes in the appropriate cells. Firefox has a handy grid inspector that visualizes the structure of your layout. Let's see what it looks like:



We highlighted three stripe and removed the tilt effect for clarity. Here's what the appropriate CSS code will look like:

header .stripes {
  display: grid;
  grid: repeat(5, 200px) / repeat(10, 1fr);
}
header .stripes :nth-child(1) {
  grid-column: span 3;
}
header .stripes :nth-child(2) {
  grid-area: 3 / span 3 / auto / -1;
}
header .stripes :nth-child(3) {
  grid-row: 4;
  grid-column: span 5;
}

Then we can simply transform the entire container .stripesto get the tilt effect:



header .stripes {
  transform: skewY(-12deg);
  transform-origin: 0;
}

And voila! CSS Grid can be frightening at first sight, because it is accompanied by unusual syntax and many new properties and values, but the mental model is actually very simple. And if you are familiar with Flexbox, then you already know the Box Alignment module , which means you can also use familiar properties that you love so much, such as justify-contentand align-items.

CSS 3D


The landing page header shows several cubes as a visual meta-form of the building blocks that make up Connect. These flying cubes rotate in 3D at random speeds (in a certain range) and are illuminated by a single light source that dynamically highlights the corresponding surfaces, video: cubes.mp4 These cubes are simple DOM elements that are generated and animated by JavaScript. Each of them is confirmed by one HTML :



template


// JavaScript
const createCube = () => {
  const template = document.getElementById("cube-template");
  const fragment = document.importNode(template.content, true);
  return fragment;
};

Nothing complicated. Now we can quite easily turn these clean and empty elements into a three-dimensional shape. Thanks to 3D transformations, the addition of perspective and the movement of sides along the z-axes is carried out quite naturally:

.cube, .cube * {
  position: absolute;
  width: 100px;
  height: 100px
}
.sides {
  transform-style: preserve-3d;
  perspective: 600px
}
.front  { transform: rotateY(0deg)    translateZ(50px) }
.back   { transform: rotateY(-180deg) translateZ(50px) }
.left   { transform: rotateY(-90deg)  translateZ(50px) }
.right  { transform: rotateY(90deg)   translateZ(50px) }
.top    { transform: rotateX(90deg)   translateZ(50px) }
.bottom { transform: rotateX(-90deg)  translateZ(50px) }

Although CSS makes tiling cube trivial, it does not provide advanced animation features such as dynamic shading. Instead, the cube animation relies on requestAnimationFrameto compute and update each side at any pivot point. In each frame, three things need to be defined:

  • Visibility . At each moment in time, no more than three sides are visible, so that unnecessary calculations and resource-intensive repainting on hidden sides can be avoided.
  • Transformation . Each side of the cube must be transformed depending on its initial rotation, the current state of the animation, and speed along each axis.
  • Shading . Although CSS allows you to position elements in three-dimensional space, there are no traditional concepts from a 3D environment (such as light sources). To simulate a 3D environment, we can render the light source, progressively darkening the sides of the cube as it moves away from a certain point.

There are other considerations that need to be taken into account (for example, improving performance with requestIdleCallbackJavaScript and backface-visibilityCSS), but these are the main foundations for animation logic.

We can calculate the visibility and transformation for each side, continuously monitoring their status and updating them using simple mathematical operations. Using pure ES2015 features and features such as template literals makes things even easier. Here are two short snippets of JavaScript code to calculate and determine the current transformation:

const getDistance = (state, rotate) =>
  ["x", "y"].reduce((object, axis) => {
    object[axis] = Math.abs(state[axis] + rotate[axis]);
    return object;
  }, {});
const getRotation = (state, size, rotate) => {
  const axis = rotate.x ? "Z" : "Y";
  const direction = rotate.x > 0 ? -1 : 1;
  return `
    rotateX(${state.x + rotate.x}deg)
    rotate${axis}(${direction * (state.y + rotate.y)}deg)
    translateZ(${size / 2}px)
  `;
};

The hardest piece of the puzzle is how to correctly calculate the shading for each side of the cube. To simulate a virtual light source in the center of the stage, we can gradually increase the lighting effect of each side as it approaches the center point - along all axes. Specifically, this means that we need to calculate the brightness and color for each side. We will perform this calculation in each frame, interpolating the base color and the current shading factor.

// Linear interpolation between a and b
// Example: (100, 200, .5) = 150
const interpolate = (a, b, i) => a * (1 - i) + b * i;
const getShading = (tint, rotate, distance) => {
  const darken = ["x", "y"].reduce((object, axis) => {
    const delta = distance[axis];
    const ratio = delta / 180;
    object[axis] = delta > 180 ? Math.abs(2 - ratio) : ratio;
    return object;
  }, {});
  if (rotate.x)
    darken.y = 0;
  else {
    const {x} = distance;
    if (x > 90 && x < 270)
      directions.forEach(axis => darken[axis] = 1 - darken[axis]);
  }
  const alpha = (darken.x + darken.y) / 2;
  const blend = (value, index) =>
    Math.round(interpolate(value, tint.shading[index], alpha));
  const [r, g, b] = tint.color.map(blend);
  return `rgb(${r}, ${g}, ${b})`;
};

Fuh! Fortunately, the rest of the code is much simpler and consists mainly of boilerplate code, DOM helpers and other elementary abstractions. The last detail worth mentioning is the technique that makes the animation less intrusive, depending on the user's settings: video . Click to watch the video On macOS, when the Reduce Motion mode is enabled in the settings , the trigger for a new media request will work (so far only in Safari), and all decorative animations on the page will turn off. Cubes simultaneously use CSS animations for shading and JavaScript animations for rotation. We can disable these animations with a combination of locks and :




prefers-reduced-motion@mediaMediaQueryList Interface

/* CSS */
@media (prefers-reduced-motion) {
  #header-hero * {
    animation: none
  }
}
// JavaScript
const reduceMotion = matchMedia("(prefers-reduced-motion)").matches;
const tick = () => {
  cubes.forEach(updateSides);
  if (reduceMotion) return;
  requestAnimationFrame(tick);
};

More CSS 3D!


Throughout the site, we use custom three-dimensional computer devices as a showcase for Stripe customers and existing applications. In our endless quest to reduce file size and download time, we examined several options for how to achieve a three-dimensional appearance with a small file size and independence from resolution. Drawing devices directly in CSS met our requirements. Here is a CSS laptop:



Defining an object in CSS is definitely less convenient than exporting a bitmap, but it's worth it. The laptop upstairs takes up less than one kilobyte and is easy to configure. We can add hardware acceleration, animate any part, make it interactive without losing image quality, and accurately position DOM elements (for example, other images) on the laptop display. Such flexibility does not mean that you need to refuse clean code - the markup remains clean, concise and clear:


Styling a laptop involves a mixture of gradients, shadows and transformations. In many ways, this is a simple translation of the workflow and concepts that you know and use in your graphics tools. For example, here is the CSS code for the cover:

.laptop .lid {
  position: absolute;
  width: 100%;
  height: 100%;
  border-radius: 20px;
  background: linear-gradient(45deg, #E5EBF2, #F3F8FB);
  box-shadow: inset 1px -4px 6px rgba(145, 161, 181, .3)
}

Choosing the right tool for the job is not always obvious - the choice between CSS, SVG, Canvas, WebGL and images is not as clear as it should be. It’s easy to abandon CSS as an exclusive document presentation format, but it’s just as easy to go beyond and use its visual capabilities unnecessarily. No matter what technology you choose, optimize it for the user! So pay close attention to client-side performance, accessibility, and rollback options for older browsers.

Web Animations API


The Onboarding & Verification section introduces Demo Express , Connect’s new beginner adaptation system. All animation is entirely code-based and relies heavily on the new Web Animations API .

The Web Animations APIs provide @keyframesJavaScript performance and simplicity , making it easy to create a smooth sequence of animation frames. Unlike low-level interfaces requestAnimationFrame, here you get all the charms of CSS animations, such as native support for softening functions cubic-bezier. As an example, take a look at our code for sliding the keyboard:

const toggleKeyboard = (element, callback, action) => {
  const keyframes = {
    transform: [100, 0].map(n => `translateY(${n}%)`)
  };
  const options = {
    duration: 800,
    fill: "forwards",
    easing: "cubic-bezier(.2, 1, .2, 1)",
    direction: action == "hide" ? "reverse" : "normal"
  };
  const animation = element.animate(keyframes, options);
  animation.addEventListener("finish", callback, {once: true});
};

Nice and easy! Web Animations APIs cover the vast majority of typical user interface animations that you might need without third-party dependencies (as a result, the entire Express animation takes about 5 KB, including everything: scripts, images, etc.). I must say that this is not a complete replacement requestAnimationFrame, nevertheless, it provides more subtle control over the animation and effects that are not obtained otherwise, such as Spring Curve and independent transformation functions, are allowed. If you are not sure which technology to choose for your animations, then you can probably place the options in the following priority order:

  1. Transitions of the CSS . This is the fastest, easiest and most effective way of animations. Suitable for simple things like effects hover.
  2. CSS animations . They have the same performance characteristics as transitions: these are declarative animations that can be highly optimized by browsers and run in separate threads. CSS animations are more functional than transitions and allow multiple steps and multiple iterations. They are also more difficult to implement because they require named declarations @keyframes, and often explicit ones animation-fill-mode. (And named things have always been the hardest parts of computer science!)
  3. Web Animations API . This programming interface provides almost the same performance as CSS animations (these animations run the same engine, but the JavaScript code still works in the main thread), and they are almost as easy to use. They should be your first choice for any animation that requires interactivity, random effects, programmable sequences, and anything more functional than purely declarative animation.
  4. requestAnimationFrame . There are no boundaries in the universe, but you need to build a spaceship. Here the possibilities are endless, and the rendering methods are unlimited (HTML, SVG, canvas - whatever), but this technology is much more difficult to use and it may not work as well as the previous options.

Regardless of which technique you use, here are some simple tips you can always use to make your animation look much better:

  • Custom curves . It is unlikely that you will want to use built-in timing-functionlike ease-in, ease-outand linear. You will save a lot of time if you globally define the number of custom cubic-bezier variables .
  • Performance . By all means, avoid slowing down in your animations. In CSS, for this you should exclusively animate cheap properties ( transformand opacity) and reset animations on the GPU whenever possible (applying will-change).
  • Speed . Animations should never interfere. The main task of animations is to make the interface responsive, harmonious, pleasant and complete. There is no strict limit on the duration of the animation, it depends on the effects and the curve, but in most cases you should not exceed 500 seconds.

Intersection Observer


Playback of an Express animation starts automatically as soon as it appears in the field of view (you can experience this by scrolling through the page ). This is usually accompanied by monitoring scrolling, which works as a trigger, but historically it was implemented through resource-intensive event listeners, which led to a verbose and inefficient code.

The Connect landing page uses the new Intersection Observer API , which provides a much, much more reliable and productive way to determine the visibility of an element. Here's how we start playing Express animations:

const observeScroll = (element, callback) => {
  const observer = new IntersectionObserver(([entry]) => {
    if (entry.intersectionRatio < 1) return;
    callback();
    // Stop watching the element
    observer.disconnect();
  },{
    threshold: 1
  });
  // Start watching the element
  observer.observe(element);
};
const element = document.getElementById("express-animation");
observeScroll(element, startAnimation);

The helper observeScrollmakes it easy for us to detect (for example, when the element is fully visible, then the callback is generated only once) without executing any code in the main thread. Thanks to the Intersection Observer API, we are now one step closer to absolutely smooth web pages!

Polyfills and kickbacks


All these brand new and brilliant software interfaces are very wonderful, but unfortunately, they are not yet available everywhere. A typical workaround is the use of a polyfill, which checks for the presence of features for a specific API and is executed only if this API is not available. The obvious drawback of this approach is that it takes resources from everyone and always , forcing everyone to download the polyfill, regardless of whether it will be used or not. We have chosen a different solution.

For JavaScript APIsthe Connect landing page tests whether a polyfill is needed and can dynamically load it onto the page. Scripts are dynamically created and added to the document, they are asynchronous by default, that is, the execution order is not guaranteed. Obviously, this is a problem, as this script may execute earlier than the expected polyfill. Fortunately, this can be fixed by explicitly indicating that our scripts are not asynchronous and therefore lazily loaded only if necessary:

const insert = name => {
  const el = document.createElement("script");
  el.src = `${name}.js`;
  el.async = false; // Keep the execution order
  document.head.appendChild(el);
};
const scripts = ["main"];
if (!Element.prototype.animate)
  scripts.unshift("web-animations-polyfill");
if (!("IntersectionObserver" in window))
  scripts.unshift("intersection-observer-polyfill");
scripts.forEach(insert);

For CSS, the problem and solutions are much the same as those for JavaScript polyfills. A typical way to use modern CSS features is to write a rollback first and then override it if possible:

div { display: flex }
@supports (display: grid) {
  div { display: grid }
}

Requests for CSS functions are simple, reliable, and most likely should be used first. However, they are not suitable for our audience, because about 90% of our visitors already use a Grid-compatible browser. In our case, it makes no sense to fine the vast majority of visitors with hundreds of rollback rules for the sake of a small and decreasing share of browsers. Given these statistics, we chose to dynamically create and insert a style sheet with a rollback, when necessary:

// Some browsers not supporting Grid don’t support CSS.supports
// either, so we need to feature-test it the old-fashioned way:
if (!("grid" in document.body.style)) {
  const fallback = "";
  document.head.insertAdjacentHTML("beforeend", fallback);
}

Finish!


We hope you enjoy (and maybe benefit) from these front-end tips! Modern browsers give us powerful tools for creating rich, fast and attractive interfaces, allowing us to be creative. If you are as fascinated by the opportunities as we are, then we probably should experiment together .

Also popular now: