Web-components and slotted element interactions

Back in 2010 one of my first websites Employboy featured a parallax effect applied to a floating head. It used JS to explicitly set the position of the inner and outer shapes. Here I'm going to do a run-through of how I modernised this approach using web-components and SVGs.

First of all let's take a peek at what we're working towards, please welcome into the public domain our little friend "Mockey". Moving your mouse cursor over around the image should produce a parallax effect.

Open ↗

We're going to take a top-down look at how this effect has been produced, firstly let's introduce an SVG to the page.

The SVG is included directly in the markdown:

<svg viewBox="0 0 520 390" style="background:#fdfad2">
    <circle id="outer" cx="260" cy="195" r="145" style="fill:#6bb46e;"/>
    <circle id="inner" cx="260" cy="195" r="96.667" style="fill:#ffcc03;"/>
    <circle id="center" cx="260" cy="195" r="48.333" style="fill:#fffdea;"/>
</svg>

The three circles have ids of #outer, #inner and #center. By targeting those ids we can make the svg interactive with the use of a web-component name lib-mouse-parallax.

I see why Nurofen uses this as branding, it's headache inducing! Just take Ibuprofen, it's identical.

So how does our web-component make the SVG behave in this way, let's take a look at the mark-up.

<lib-mouse-parallax
    speed="1"
    foreground="#center"
    middleground="#inner"
    background="#outer">
    <svg viewBox="0 0 520 390" style="background:#fdfad2">
        <circle id="outer" cx="260" cy="195" r="145" style="fill:#C7574D;" />
        <circle id="inner" cx="260" cy="195" r="96.667" style="fill:#ffcc03;" />
        <circle id="center" cx="260" cy="195" r="48.333" style="fill:#fffdea;" />
    </svg>
</lib-mouse-parallax>

As mentioned our three properties "foreground", "middleground" and "background" are just css selectors. The circles could equally be targeted with nth-child(n):

foreground="circle:nth-child(3)"
middleground="circle:nth-child(2)"
background="circle:nth-child(1)"

Lastly, the component accepts a speed setting to determine how fast the foreground (top layer) travels in relation to the mouse. Here we have it set to 1 meaning the centre circle will track the mouse exactly.

The component acts in three stages.

  1. Applies a css transform to svg elements matching a selector
  2. On mousemove update the --mouse-x and --mouse-y variables
  3. On mouseleave reset the position of the image

Similar instruction occurs for touch events.

It's important to know that whilst web-components with a shadow-dom do encapsulate style there are some inherited properties. I believe the complete list is:

As well as these, css variables (--var-name: value;) are also inherited and that's something we'll be harnessing.

To move our shapes around we are using the css translate(x, y) function.

SELECTOR {
    transform: translate(
      calc(1px * var(--mouse-x, 0) * var(--speed)),
      calc(1px * var(--mouse-y, 0) * var(--speed))
    );
}

Multiplying by 1px ensures our translation is applied in pixels. In the CSS we have included the value three times. Multiplying by 0.25px, 0.5px and 1px representing background, middleground and foreground respectively.

As we don't know what the css SELECTOR will be, we instead set the transform as a variable and apply it in JS later on.

:host {
--set-foreground: translate(
    calc(1px * var(--mouse-x, 0) * var(--speed)),
    calc(1px * var(--mouse-y, 0) * var(--speed))
);
/* other properties... */
}
element.style.setProperty("transform", "var(--set-foreground)");

All that's left now is to set --mouse-x and --mouse-y whenever mousemove fires within the component. We will also need to reset the values on mouseleave to restore the image to it's previous state.

svg.addEventListener("mousemove", this._handleMove);

svg.addEventListener("mouseleave", this._resetPosition);

Things get a little more complicated from here but this is really where the walk-through stops. In brief the complicated elements are:

Transform Correction Because we're moving between absolute values (the position of our mouse) and relative values (the viewport of our SVG) we need to correct for how much we adjust our --mouse-x. Without this adjustment the parallax effect would vary when the size of the svg changes. Adjusting for the SVG's viewport corrects this.

Easing To avoid the position of elements snapping as when triggered, we are gradually easing in the effect on each movement.

The web-component itself was rolled in Google's Lit, and developed using Storybook as an environment. Lit is a joy to work with as I imagine are many of the libraries available for developing web-components.

What I like about Lit is it's goal to align the api with that of native web-components. Oftentimes I found the documentation I needed on MDN rather than in the Lit docs. I'm a real advocate of using the platform, developing in Lit doesn't feel like bending to someone else's model (eg "Thinking in React"). I'm yet to try out other libraries, though many seem to share the approach of class based declarations with decorators for added functionality. I imagine porting from one to another would be relatively pain-free.

Summary

In dedication to the Rob of 2010, here's that the floating head of employboy marked up by our web-component.

Open ↗

Web-components are yet to take off in the way that I'd hoped. Whilst big orgs are using them, frameworks and libraries like React and Angular already determine how components are created and creating native web-components in these environments has little value.

It appears libraries like Lit are yet convince devs that it offers a complete solution for developing a site. Whether that's down to a lack of awareness or simply because other frameworks are so firmly ensconced I couldn't say. I hope we see a shift soon.

Links