Tag: Elements

Supercharging Built-In Elements With Web Components “is” Easier Than You Think

We’ve already discussed how creating web components is easier than you think, but there’s another aspect of the specification that we haven’t discussed yet and it’s a way to customize (nay, supercharge) a built-in element. It’s similar to creating fully custom or “autonomous” elements — like the <zombie-profile> element from the previous articles—but requires a few differences.

Customized built-in elements use an is attribute to tell the browser that this built-in element is no mild-mannered, glasses-wearing element from Kansas, but is, in fact, the faster than a speeding bullet, ready to save the world, element from planet Web Component. (No offense intended, Kansans. You’re super too.)

Supercharging a mild-mannered element not only gives us the benefits of the element’s formatting, syntax, and built-in features, but we also get an element that search engines and screen readers already know how to interpret. The screen reader has to guess what’s going on in a <my-func> element, but has some idea of what’s happening in a <nav is="my-func"> element. (If you have func, please, for the love of all that is good, don’t put it in an element. Think of the children.)

It’s important to note here that Safari (and a handful of more niche browsers) only support autonomous elements and not these customized built-in elements. We’ll discuss polyfills for that later.

Until we get the hang of this, let’s start by rewriting the <apocalyptic-warning> element we created back in our first article as a customized built-in element. (The code is also available in the CodePen demo.)

The changes are actually fairly simple. Instead of extending the generic HTMLElement, we’ll extend a specific element, in this case the <div> element which has the class HTMLDivElement. We’ll also add a third argument to the customElements.defines function: {extends: 'div'}.

customElements.define(   "apocalyptic-warning",   class ApocalypseWarning extends HTMLDivElement {     constructor() {       super();       let warning = document.getElementById("warningtemplate");       let mywarning = warning.content;        const shadowRoot = this.attachShadow({ mode: "open" }).appendChild(         mywarning.cloneNode(true)       );     }   },   { extends: "div" } );

Lastly, we’ll update our HTML from <apocalyptic-warning> tags to <div> tags that include an is attribute set to “apocalyptic-warning” like this:

<div is="apocalyptic-warning">   <span slot="whats-coming">Undead</span> </div>

Reminder: If you’re looking at the below in Safari, you won’t see any beautiful web component goodness *shakes fist at Safari*

Only certain elements can have a shadow root attached to them. Some of this is because attaching a shadow root to, say, an <a> element or <form> element could have security implications. The list of available elements is mostly layout elements, such as <article>, <section>, <aside>, <main>, <header>, <div>, <nav>, and <footer>, plus text-related elements like <p>, <span>, <blockquote>, and <h1><h6>. Last but not least, we also get the body element and any valid autonomous custom element.

Adding a shadow root is not the only thing we can do to create a web component. At its base, a web component is a way to bake functionality into an element and we don’t need additional markup in the shadows to do that. Let’s create an image with a built-in light box feature to illustrate the point.

We’ll take a normal <img> element and add two attributes: first, the is attribute that signifies this <img> is a customized built-in element; and a data attribute that holds the path to the larger image that we’ll show in the light box. (Since I’m using an SVG, I just used the same URL, but you could easily have a smaller raster image embedded in the site and a larger version of it in the light box.)

<img is="light-box" src="https://assets.codepen.io/1804713/ninja2.svg" data-lbsrc="https://assets.codepen.io/1804713/ninja2.svg" alt="Silent but Undeadly Zombie Ninja" />

Since we can’t do a shadow DOM for this <img>, there’s no need for a <template> element, <slot> elements, or any of those other things. We also won’t have any encapsulated styles.

So, let’s skip straight to the JavaScript:

customElements.define(   "light-box",   class LightBox extends HTMLImageElement {     constructor() {       super();       // We’re creating a div element to use as the light box. We’ll eventually insert it just before the image in question.       let lb = document.createElement("div");       // Since we can’t use a shadow DOM, we can’t encapsulate our styles there. We could add these styles to the main CSS file, but they could bleed out if we do that, so I’m setting all styles for the light box div right here       lb.style.display = "none";       lb.style.position = "absolute";       lb.style.height = "100vh";       lb.style.width = "100vw";       lb.style.top = 0;       lb.style.left = 0;       lb.style.background =         "rgba(0,0,0, 0.7) url(" + this.dataset.lbsrc + ") no-repeat center";       lb.style.backgroundSize = "contain";        lb.addEventListener("click", function (evt) {         // We’ll close our light box by clicking on it         this.style.display = "none";       });       this.parentNode.insertBefore(lb, this); // This inserts the light box div right before the image       this.addEventListener("click", function (evt) {         // Opens the light box when the image is clicked.         lb.style.display = "block";       });     }   },   { extends: "img" } );

Now that we know how customized built-in elements work, we need to move toward ensuring they’ll work everywhere. Yes, Safari, this stink eye is for you.

WebComponents.org has a generalized polyfill that handles both customized built-in elements and autonomous elements, but because it can handle so much, it may be a lot more than you need, particularly if all you’re looking to do is support customized built-in elements in Safari.

Since Safari supports autonomous custom elements, we can swap out the <img> with an autonomous custom element such as <lightbox-polyfill>. “This will be like two lines of code!” the author naively said to himself. Thirty-seven hours of staring at a code editor, two mental breakdowns, and a serious reevaluation of his career path later, he realized that he’d need to start typing if he wanted to write those two lines of code. It also ended up being more like sixty lines of code (but you’re probably good enough to do it in like ten lines).

The original code for the light box can mostly stand as-is (although we’ll add a new autonomous custom element shortly), but it needs a few small adjustments. Outside the definition of the custom element, we need to set a Boolean.

let customBuiltInElementsSupported = false;

Then within the LightBox constructor, we set the Boolean to true. If customized built-in elements aren’t supported, the constructor won’t run and the Boolean won’t be set to true; thus we have a direct test for whether customized built-in elements are supported.

Before we use that test to replace our customized built-in element, we need to create an autonomous custom element to be used as a polyfill, namely <lightbox-polyfill>.

customElements.define(   "lightbox-polyfill", // We extend the general HTMLElement instead of a specific one   class LightBoxPoly extends HTMLElement {      constructor() {       super();        // This part is the same as the customized built-in element’s constructor       let lb = document.createElement("div");       lb.style.display = "none";       lb.style.position = "absolute";       lb.style.height = "100vh";       lb.style.width = "100vw";       lb.style.top = 0;       lb.style.left = 0;       lb.style.background =         "rgba(0,0,0, 0.7) url(" + this.dataset.lbsrc + ") no-repeat center";       lb.style.backgroundSize = "contain";        // Here’s where things start to diverge. We add a `shadowRoot` to the autonomous custom element because we can’t add child nodes directly to the custom element in the constructor. We could use an HTML template and slots for this, but since we only need two elements, it's easier to just create them in JavaScript.       const shadowRoot = this.attachShadow({ mode: "open" });        // We create an image element to display the image on the page       let lbpimg = document.createElement("img");        // Grab the `src` and `alt` attributes from the autonomous custom element and set them on the image       lbpimg.setAttribute("src", this.getAttribute("src"));       lbpimg.setAttribute("alt", this.getAttribute("alt"));        // Add the div and the image to the `shadowRoot`       shadowRoot.appendChild(lb);       shadowRoot.appendChild(lbpimg);        // Set the event listeners so that you show the div when the image is clicked, and hide the div when the div is clicked.       lb.addEventListener("click", function (evt) {         this.style.display = "none";       });       lbpimg.addEventListener("click", function (evt) {         lb.style.display = "block";       });     }   } );

Now that we have the autonomous element ready, we need some code to replace the customized <img> element when it’s unsupported in the browser.

if (!customBuiltInElementsSupported) {   // Select any image with the `is` attribute set to `light-box`   let lbimgs = document.querySelectorAll('img[is="light-box"]');   for (let i = 0; i < lbimgs.length; i++) { // Go through all light-box images     let replacement = document.createElement("lightbox-polyfill"); // Create an autonomous custom element      // Grab the image and div from the `shadowRoot` of the new lighbox-polyfill element and set the attributes to those originally on the customized image, and set the background on the div.     replacement.shadowRoot.querySelector("img").setAttribute("src", lbimgs[i].getAttribute("src"));     replacement.shadowRoot.querySelector("img").setAttribute("alt", lbimgs[i].getAttribute("alt"));     replacement.shadowRoot.querySelector("div").style.background =       "rgba(0,0,0, 0.7) url(" + lbimgs[i].dataset.lbsrc + ") no-repeat center";      // Stick the new lightbox-polyfill element into the DOM just before the image we’re replacing     lbimgs[i].parentNode.insertBefore(replacement, lbimgs[i]);     // Remove the customized built-in image     lbimgs[i].remove();   } }

So there you have it! We not only built autonomous custom elements, but customized built-in elements as well — including how to make them work in Safari. And we get all the benefits of structured, semantic HTML elements to boot including giving screen readers and search engines an idea of what these custom elements are.

Go forth and customize yon built-in elements with impunity!


The post Supercharging Built-In Elements With Web Components “is” Easier Than You Think appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , , , , ,

Using Custom Elements in Svelte

Svelte fully supports custom elements (e.g. <my-component>) without any custom configuration or wrapper components and has a perfect score on Custom Elements Everywhere. However, there are still a few quirks you need to watch out for, especially around how Svelte sets data on custom elements. At Alaska Airlines, we experienced many of these issues first-hand as we integrated the custom elements from our design system into a Svelte application.

While Svelte supports compiling to custom elements, that is not within the scope of this post. Instead, I will focus on using custom elements built with the Lit custom element library in a Svelte application. These concepts should transfer to custom elements built with or without a supporting library.

Property or attribute?

To fully understand how to use custom elements in Svelte, you need to understand how Svelte passes data to a custom element.

Svelte uses a simple heuristic to determine whether to pass data to a custom element as a property or an attribute. If a corresponding property exists on the custom element at runtime, Svelte will pass the data as a property. Otherwise, it will pass it as an attribute. This seems simple, but has interesting implications.

For instance, let’s say you have a coffee-mug custom element that takes a size property. You can use it in a Svelte component like so:

<coffee-mug class="mug" size="large"></coffee-mug>

You can open this Svelte REPL to follow along. You should see the custom element render the text “This coffee mug’s size is: large ☕️.”

When writing the HTML inside the component, it seems like you’re setting both class and size as attributes. However, this is not the case. Right-click on the “This coffee mug’s size is” text in the REPL’s output and click “Inspect.” This will bring open the DevTools inspector. When you inspect the rendered HTML, you’ll notice that only class was set as an attribute — it’s as if size simply disappeared! However, size is getting set somehow, because “large” still appears in the element’s rendered text.

This is because size is a property on the element, but class is not. Because Svelte detects a size property, it chooses to set that property instead of an attribute. There is no class property, so Svelte sets it as an attribute instead. That’s not a problem or something that changes how we expect the component to behave, but can be very confusing if you’re unaware of it, because there’s a disconnect between the HTML you think you’re writing and what Svelte actually outputs.

Svelte isn’t unique in this behavior — Preact uses a similar method to determine whether to set an attribute or a property on custom elements. Because of that, the use cases I discuss will also occur in Preact, though the workarounds will be different. You will not run into these issues with Angular or Vue because they have a special syntax that lets you choose to set an attribute or a property.

Svelte’s heuristic makes it easy to pass complex data like arrays and objects which need to be set as properties. Consumers of your custom elements shouldn’t need to think about whether they need to set an attribute or a property — it just magically works. However, like any magic in web development, you eventually run into some cases that require you to dig a little deeper and understand what’s going on behind the scenes.

Let’s go through some use cases where custom elements behave strangely. You can find the final examples in this Svelte REPL.

Attributes used as styling hooks

Let’s say you have a custom-text element that displays some text. If the flag attribute is present, it prepends a flag emoji and the word “Flagged:” to the text. The element is coded as follows:

import { html, css, LitElement } from 'lit'; export class CustomText extends LitElement {   static get styles() {     return css`       :host([flag]) p::before {         content: '🚩';       }     `;   }   static get properties() {     return {       flag: {         type: Boolean       }     };   }   constructor() {     super();     this.flag = false;   }   render() {     return html`<p>       $  {this.flag ? html`<strong>Flagged:</strong>` : ''}       <slot></slot>     </p>`;   } } customElements.define('custom-text', CustomText);

You can see the element in action in this CodePen.

However, if you try to use the custom element the same way in Svelte, it doesn’t entirely work. The “Flagged:” text is shown, but the emoji is not. What gives?

<script>   import './custom-elements/custom-text'; </script>  <!-- This shows the "Flagged:" text, but not 🚩 --> <custom-text flag>Just some custom text.</custom-text>

The key here is the :host([flag]) selector. :host selects the element’s shadow root (i.e. the <custom-text> element), so this selector only applies if the flag attribute is present on the element. Since Svelte chooses to set the property instead, this selector doesn’t apply. The “Flagged:” text is added based on the property, which is why that still showed.

So what are our options here? Well, the custom element shouldn’t have assumed that flag would always be set as an attribute. It is a custom element best practice to keep primitive data attributes and properties in sync since you don’t know how the consumer of the element will interact with it. The ideal solution is for the element author to make sure any primitive properties are reflected to attributes, especially if those attributes are used for styling. Lit makes it easy to reflect your properties:

static get properties() {   return {     flag: {       type: Boolean,       reflect: true     }   }; }

With that change, the flag property is reflected back to the attribute, and everything displays as expected.

However, there may be cases where you don’t have control over the custom element definition. In that case, you can force Svelte to set the attribute using a Svelte action.

Using a Svelte action to force setting attributes

Actions are a powerful Svelte feature that run a function when a certain node is added to the DOM. For example, we can write an action that will set the flag attribute on our custom-text element:

<script>   import './custom-elements/custom-text';   function setAttributes(node) {     node.setAttribute('flag', '');   } </script>  <custom-text use:setAttributes>   Just some custom text. </custom-text>

Actions can also take parameters. For instance, we could make this action more generic and accept an object containing the attributes we want to set on a node.

<script>   import './custom-elements/custom-text';   function setAttributes(node, attributes) {     Object.entries(attributes).forEach(([k, v]) => {       if (v !== undefined) {         node.setAttribute(k, v);       } else {         node.removeAttribute(k);       }     });   } </script>  <custom-text use:setAttributes={{ flag: true }}>   Just some custom text. </custom-text>

Finally, if we want the attributes to react to state changes, we can return an object with an update method from the action. Whenever the parameters we pass to the action change, the update function will be called.

<script>   import './custom-elements/custom-text';   function setAttributes(node, attributes) {     const applyAttributes = () => {       Object.entries(attributes).forEach(([k, v]) => {         if (v !== undefined) {           node.setAttribute(k, v);         } else {           node.removeAttribute(k);         }       });     };     applyAttributes();     return {       update(updatedAttributes) {         attributes = updatedAttributes;         applyAttributes();       }     };   }   let flagged = true; </script> <label><input type="checkbox" bind:checked={flagged} /> Flagged</label> <custom-text use:setAttributes={{ flag: flagged ? '' : undefined }}>   Just some custom text. </custom-text>

Using this approach, we don’t have to update the custom element to reflect the property — we can control setting the attribute from inside our Svelte app.

Lazy-loading custom elements

Custom elements are not always defined when the component first renders. For example, you may wait to import your custom elements until after the web component polyfills have loaded. Also, in a server-side rendering context such as Sapper or SvelteKit, the initial server render will take place without loading the custom element definition.

In either case, if the custom element is not defined, Svelte will set everything as attributes. This is because the property does not exist on the element yet. This is confusing if you’ve grown accustomed to Svelte only setting properties on custom elements. This can cause issues with complex data such as objects and arrays.

As an example, let’s look at the following custom element that displays a greeting followed by a list of names.

import { html, css, LitElement } from 'lit'; export class FancyGreeting extends LitElement {   static get styles() {     return css`       p {         border: 5px dashed mediumaquamarine;         padding: 4px;       }     `;   }   static get properties() {     return {       names: { type: Array },       greeting: { type: String }     };   }   constructor() {     super();     this.names = [];   }   render() {     return html`<p>       $  {this.greeting},       $  {this.names && this.names.length > 0 ? this.names.join(', ') : 'no one'}!     </p>`;   } } customElements.define('fancy-greeting', FancyGreeting);

You can see the element in action in this CodePen.

If we statically import the element in a Svelte application, everything works as expected.

<script>   import './custom-elements/fancy-greeting'; </script> <!-- This displays "Howdy, Amy, Bill, Clara!" --> <fancy-greeting greeting="Howdy" names={['Amy', 'Bill', 'Clara']} />

However, if we dynamically import the component, the custom element does not become defined until after the component has first rendered. In this example, I wait to import the element until the Svelte component has been mounted using the onMount lifecycle function. When we delay importing the custom element, the list of names is not set properly and the fallback content is displayed instead.

<script>   import { onMount } from 'svelte';   onMount(async () => {     await import('./custom-elements/fancy-greeting');   }); </script> <!-- This displays "Howdy, no one!"--> <fancy-greeting greeting="Howdy" names={['Amy', 'Bill', 'Clara']} />

Because the custom element definition is not loaded when Svelte adds fancy-greeting to the DOM, fancy-greeting does not have a names property and Svelte sets the names attribute — but as a string, not as a stringified array. If you inspect the element in your browser DevTools, you’ll see the following:

<fancy-greeting greeting="Howdy" names="Amy,Bill,Clara"></fancy-greeting> 

Our custom element tries to parse the names attribute as an array using JSON.parse, which throws an exception. This is handled automatically using Lit’s default array converter, but the same would apply to any element that expects an attribute to contain a valid JSON array.

Interestingly, once you update the data passed to the custom element Svelte will start setting the property again. In the below example, I moved the array of names to the state variable names so that I can update it. I also added an “Add name” button that will append the name “Rory” to the end of the names array when clicked.

Once the button is clicked, the names array is updated, which triggers a re-render of the component. Since the custom element is now defined, Svelte detects the names property on the custom element and sets that instead of the attribute. This causes the custom element to properly display the list of names instead of the fallback content.

<script>   import { onMount } from 'svelte';   onMount(async () => {     await import('./custom-elements/fancy-greeting');   });   let names = ['Amy', 'Bill', 'Clara'];   function addName() {     names = [...names, 'Rory'];   } </script>  <!-- Once the button is clicked, the element displays "Howdy, Amy, Bill, Clara, Rory!" --> <fancy-greeting greeting="Howdy" {names} /> <button on:click={addName}>Add name</button>

As in the previous example, we can force Svelte to set the data how we want using an action. This time, instead of setting everything as an attribute, we want to set everything as a property. We will pass an object as a parameter that contains the properties we want to set on the node. Here’s how our action will be applied to the custom element:

<fancy-greeting   greeting="Howdy"   use:setProperties={{ names: ['Amy', 'Bill', 'Clara'] }} />

Below is the the implementation of the action. We iterate over the properties object and use each entry to set the property on the custom element node. We also return an update function so that the properties are reapplied if the parameters passed to the action change. See the previous section if you want a refresher on how you can react to state changes with an action.

function setProperties(node, properties) {   const applyProperties = () => {     Object.entries(properties).forEach(([k, v]) => {       node[k] = v;     });   };   applyProperties();   return {     update(updatedProperties) {       properties = updatedProperties;       applyProperties();     }   }; }

By using the action, the names are displayed properly on first render. Svelte sets the property when first rendering the component, and the custom element picks that property up once the element has been defined.

Boolean attributes

The final issue we ran into is how Svelte handles boolean attributes on a custom element. This behavior has recently changed with Svelte 3.38.0, but we’ll explore pre- and post-3.38 behavior since not everyone will be on the latest Svelte version.

Suppose we have a <secret-box> custom element with a boolean property open that indicates whether the box is open or not. The implementation looks like this:

import { html, LitElement } from 'lit'; export class SecretBox extends LitElement {   static get properties() {     return {       open: {         type: Boolean       }     };   }   render() {     return html`<div>The box is $  {this.open ? 'open 🔓' : 'closed 🔒'}</div>`;   } } customElements.define('secret-box', SecretBox);

You can see the element in action in this CodePen.

As seen in the CodePen, you can set the open property to true multiple ways. Per the HTML spec, the presence of a boolean attribute represents the true value, and its absence represents false.

<secret-box open></secret-box> <secret-box open=""></secret-box> <secret-box open="open"></secret-box>

Interestingly, only the last of the above options shows “The box is open” when used inside a Svelte component. The first two show “The box is closed” despite setting the open attribute. What’s going on here?

As with the other examples, it all goes back to Svelte choosing properties over attributes. If you inspect the elements in the browser DevTools, no attributes are set — Svelte has set everything as properties. We can console.log the open property inside our render method (or query the element in the console) to discover what Svelte set the open property to.

// <secret-box open> logs '' // <secret-box open=""> logs '' // <secret-box open="open"> logs 'open' render() {   console.log(this.open);   return html`<div>The box is $  {this.open ? 'open 🔓' : 'closed 🔒'}</div>`; }

In the first two cases, open equals an empty string. Since an empty string is falsy in JavaScript, our ternary statement evaluates to the false case and shows that the box is closed. In the final case, the open property is set to the string “open” which is truthy. The ternary statement evaluates to the true case and shows that the box is open.

As a side note, you don’t run into this issue when you lazy load the element. Since the custom element definition is not loaded when Svelte renders the element, Svelte sets the attribute instead of the property. See the above section for a refresher.

There’s an easy way around this issue. If you remember that you’re setting the property, not the attribute, you can explicitly set the open property to true with the following syntax.

<secret-box open={true}></secret-box>

This way you know you’re setting the open property to true. Setting to a non-empty string also works, but this way is the most accurate since you’re setting true instead of something that happens to be truthy.

Until recently, this was the only way to properly set boolean properties on custom elements. However, with Svelte 3.38, I had a change released that updated Svelte’s heuristic to allow setting shorthand boolean properties. Now, if Svelte knows that the underlying property is a boolean, it will treat the open and open="" syntaxes the same as open={true}.

This is especially helpful since this is how you see examples in many custom element component libraries. This change makes it easy to copy-paste out of the docs without having to troubleshoot why a certain attribute isn’t working how you’d expect.

However, there is one requirement on the custom element author side — the boolean property needs a default value so that Svelte knows it’s of boolean type. This is a good practice anyway if you want that property to be a boolean.

In our secret-box element, we can add a constructor and set the default value:

constructor() {   super();   this.open = true; }

With that change, the following will correctly display “The box is open” in a Svelte component.

<secret-box open></secret-box> <secret-box open=""></secret-box>

Wrapping up

Once you understand how Svelte decides to set an attribute or a property, a lot of these seemingly strange issues start to make more sense. Any time you run into issues passing data to a custom element inside a Svelte application, figure out if it’s being set as an attribute or a property and go from there. I’ve given you a few escape hatches in this article to force one or the other when you need to, but they should generally be unnecessary. Most of the time, custom elements in Svelte just work. You just need to know where to look if something does go wrong.


Special thanks to Dale Sande, Gus Naughton, and Nanette Ranes for reviewing an early version of this article.


The post Using Custom Elements in Svelte appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , ,
[Top]

Put a Background on Open Details Elements

One thing that can be just a smidge funky about the <details> element is that, when open, it’s not always 100% clear what is inside that element and what isn’t. I’m not saying that always matters or that it’s a particularly hard problem to solve, I’m just noting it as it came up recently for me.

Here’s a visual example:

A screenshot of some text with details elements present. One of them is open. It's not clear what text is within that details and what isn't.
What text here is inside a <details> and what isn’t?

The solution is… CSS. Style the <details> somewhat uniquely, and that problem goes away. Even if you want the typography to be the same, or you don’t want any exclusive styling until the <details> is opened, it’s still possible. Using an alpha-transparent fill, you can even make sure that deeper-nested <details> remain clear.

Here’s that CSS:

details[open] {   --bg: rgb(0 0 0 / 0.2);   background: var(--bg);   outline: 1rem solid var(--bg);   margin: 0 0 2rem 0; }

And the demo:


The post Put a Background on Open Details Elements appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , ,
[Top]

How to describe element’s natural sizing behavior

PPK:

When introducing width and height I explain that by default width takes as much horizontal space as it can, while height takes as little vertical space as possible. This leads to a discussion of these two opposed models that I excerpt below.

My question is: which names do I give to these models?

The three options:

  • inside-out and outside-in
  • context-based and content-based
  • extrinsic and intrinsic size

There is more context in the post.

I definitely don’t like inside-out and outside-in — they make my head spin. I think I’m gonna vote for extrinsic and intrinsic size. I hear those terms thrown around a lot more lately and the fact that they match the specs is something I like. At the same time, I do feel like context-based and content-based are maybe a smidge more clear, but since they are already abstracted and made up, might as well go with the abstracted and made up term that already has legs.

Direct Link to ArticlePermalink


The post How to describe element’s natural sizing behavior appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , ,
[Top]

How to describe element’s natural sizing behavior

PPK:

When introducing width and height I explain that by default width takes as much horizontal space as it can, while height takes as little vertical space as possible. This leads to a discussion of these two opposed models that I excerpt below.

My question is: which names do I give to these models?

The three options:

  • inside-out and outside-in
  • context-based and content-based
  • extrinsic and intrinsic size

There is more context in the post.

I definitely don’t like inside-out and outside-in — they make my head spin. I think I’m gonna vote for extrinsic and intrinsic size. I hear those terms thrown around a lot more lately and the fact that they match the specs is something I like. At the same time, I do feel like context-based and content-based are maybe a smidge more clear, but since they are already abstracted and made up, might as well go with the abstracted and made up term that already has legs.


The post How to describe element’s natural sizing behavior appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , ,
[Top]

Recreating Game Elements for the Web: The Among Us Card Swipe

As a web developer, I pay close attention to the design of video games. From the HUD in Overwatch to the catch screen in Pokemon Go to hunting in Oregon Trail, games often have interesting mechanics and satisfying interactions, many of which inspire my own coding games at Codepip.

Beyond that, implementing small slices of these game designs on a web stack is a fun, effective way to broaden your skills. By focusing on a specific element, your time is spent working on an interesting part without having to build out a whole game with everything that entails. And even in this limited scope, you often get exposed to new technologies and techniques that push on the boundaries of your dev knowledge.

As a case study for this idea, I’ll walk you through my recreation of the card swipe from Among Us. For anyone in the dark, Among Us is a popular multiplayer game. Aboard a spaceship, crewmates have to deduce who among them is an imposter. All the while, they complete mundane maintenance tasks and avoid being offed by the imposter.

The card swipe is the most infamous of the maintenance tasks. Despite being simple, so many players have struggled with it that it’s become the stuff of streams and memes.

Here’s my demo

This is my rendition of the card swipe task:

Next, I’ll walk you through some of the techniques I used to create this demo.

Swiping with mouse and touch events

After quickly wireframing the major components in code, I had to make the card draggable. In the game, when you start dragging the card, it follows your pointer’s position horizontally, but stays aligned with the card reader vertically. The card has a limit in how far past the reader it can be dragged to its left or right. Lastly, when you lift your mouse or finger, the card returns to its original position.

All of this is accomplished by assigning functions to mouse and touch events. Three functions are all that‘s needed to handle mouse down, mouse move, and mouse up (or touch start, touch move, and touch end if you‘re on a touchscreen device). Here’s the skeleton of that JavaScript code:

const card = document.getElementById('card'); const reader = document.getElementById('reader'); let active = false; let initialX;  // set event handlers document.addEventListener('mousedown', dragStart); document.addEventListener('mousemove', drag); document.addEventListener('mouseup', dragEnd); document.addEventListener('touchstart', dragStart); document.addEventListener('touchmove', drag); document.addEventListener('touchend', dragEnd);  function dragStart(e) {   // continue only if drag started on card   if (e.target !== card) return;    // get initial pointer position   if (e.type === 'touchstart') {     initialX = e.touches[0].clientX;   } else {     initialX = e.clientX;   }    active = true; }  function drag(e) {   // continue only if drag started on card   if (!active) return;    e.preventDefault();      let x;    // get current pointer position   if (e.type === 'touchmove') {     x = e.touches[0].clientX - initialX;   } else {     x = e.clientX - initialX;   }    // update card position   setTranslate(x); }  function dragEnd(e) {   // continue only if drag started on card   if (!active) return;    e.preventDefault();      let x;    // get final pointer position   if (e.type === 'touchend') {     x = e.touches[0].clientX - initialX;   } else {     x = e.clientX - initialX;   }    active = false;      // reset card position   setTranslate(0); }  function setTranslate(x) {   // don't let card move too far left or right   if (x < 0) {     x = 0;   } else if (x > reader.offsetWidth) {     x = reader.offsetWidth;   }    // set card position on center instead of left edge   x -= (card.offsetWidth / 2);      card.style.transform = 'translateX(' + x + 'px)'; }

Setting status with performance.now()

Next, I had to determine whether the card swipe was valid or invalid. For it to be valid, you must drag the card across the reader at just the right speed. Didn’t drag it far enough? Invalid. Too fast? Invalid. Too slow? Invalid.

To find if the card has been swiped far enough, I checked the card’s position relative to the right edge of the card reader in the function dragEnd:

let status;  // check if card wasn't swiped all the way if (x < reader.offsetWidth) {   status = 'invalid'; }  setStatus(status);

To measure the duration of the card swipe, I set start and end timestamps in dragStart and dragEnd respectively, using performance.now().

function setStatus(status) {    // status is only set for incomplete swipes so far   if (typeof status === 'undefined') {      // timestamps taken at drag start and end using performance.now()     let duration = timeEnd - timeStart;      if (duration > 700) {       status = 'slow';     } else if (duration < 400) {       status = 'fast';     } else {       status = 'valid';     }   }    // set [data-status] attribute on reader   reader.dataset.status = status; } 

Based on each condition, a different value is set on the reader’s data-status attribute. CSS is used to display the relevant message and illuminate either a red or green light.

#message:after {   content: "Please swipe card"; }  [data-status="invalid"] #message:after {   content: "Bad read. Try again."; }  [data-status="slow"] #message:after {   content: "Too slow. Try again."; }  [data-status="fast"] #message:after {   content: "Too fast. Try again."; }  [data-status="valid"] #message:after {   content: "Accepted. Thank you."; }  .red {   background-color: #f52818;   filter: saturate(0.6) brightness(0.7); }  .green {   background-color: #3dd022;   filter: saturate(0.6) brightness(0.7); }  [data-status="invalid"] .red, [data-status="slow"] .red, [data-status="fast"] .red, [data-status="valid"] .green {   filter: none; }

Final touches with fonts, animations, and audio

With the core functionality complete, I added a few more touches to get the project looking even more like Among Us.

First, I used a free custom font called DSEG to imitate the segmented type from old LCDs. All it took was hosting the files and declaring the font face in CSS.

@font-face {   font-family: 'DSEG14Classic';   src: url('../fonts/DSEG14-Classic/DSEG14Classic-Regular.woff2') format('woff2'),        url('../fonts/DSEG14-Classic/DSEG14Classic-Regular.woff') format('woff'),        url('../fonts/DSEG14-Classic/DSEG14Classic-Regular.ttf') format('truetype'); }

Next, I copied the jitter animation of the text in the original. Game developers often add subtle animations to breath life into an element, like making a background drift or a character, well, breathe. To achieve the jitter, I defined a CSS animation:

@keyframes jitter {   from {     transform: translateX(0);   }   to {     transform: translateX(5px);   } }

At this point, the text glides smoothly back and forth. Instead, what I want is for it to jump back and forth five pixels at a time. Enter the steps() function:

#message {   animation: jitter 3s infinite steps(2); }

Finally, I added the same audio feedback as used in Among Us.

let soundAccepted = new Audio('./audio/CardAccepted.mp3'); let soundDenied = new Audio('./audio/CardDenied.mp3');  if (status === 'valid') {   soundAccepted.play(); } else {   soundDenied.play(); }

Sound effects are often frowned upon in the web development world. A project like this an opportunity to run wild with audio.

And with that, the we’re done! Here’s that demo again:

Try your own

Given how standardized the web has become in look and feel, this approach of pulling an element from a game and implementing it for the web is a good way to break out of your comfort zone and try something new.

Take this Among Us card swipe. In a small, simple demo, I tinkered with web fonts and animations in CSS. I monkeyed with input events and audio in JavaScript. I dabbled with an unconventional visual style.

Now it’s time for you to survey interesting mechanics from your favorite games and try your hand at replicating them. You might be surprised what you learn.


The post Recreating Game Elements for the Web: The Among Us Card Swipe appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , , ,
[Top]

3 Approaches to Integrate React with Custom Elements

In my role as a web developer who sits at the intersection of design and code, I am drawn to Web Components because of their portability. It makes sense: custom elements are fully-functional HTML elements that work in all modern browsers, and the shadow DOM encapsulates the right styles with a decent surface area for customization. It’s a really nice fit, especially for larger organizations looking to create consistent user experiences across multiple frameworks, like Angular, Svelte and Vue.

In my experience, however, there is an outlier where many developers believe that custom elements don’t work, specifically those who work with React, which is, arguably, the most popular front-end library out there right now. And it’s true, React does have some definite opportunities for increased compatibility with the web components specifications; however, the idea that React cannot integrate deeply with Web Components is a myth.

In this article, I am going to walk through how to integrate a React application with Web Components to create a (nearly) seamless developer experience. We will look at React best practices its and limitations, then create generic wrappers and custom JSX pragmas in order to more tightly couple our custom elements and today’s most popular framework.

Coloring in the lines

If React is a coloring book — forgive the metaphor, I have two small children who love to color — there are definitely ways to stay within the lines to work with custom elements. To start, we’ll write a very simple custom element that attaches a text input to the shadow DOM and emits an event when the value changes. For the sake of simplicity, we’ll be using LitElement as a base, but you can certainly write your own custom element from scratch if you’d like.

Our super-cool-input element is basically a wrapper with some styles for a plain ol’ <input> element that emits a custom event. It has a reportValue method for letting users know the current value in the most obnoxious way possible. While this element might not be the most useful, the techniques we will illustrate while plugging it into React will be helpful for working with other custom elements.

Approach 1: Use ref

According to React’s documentation for Web Components, “[t]o access the imperative APIs of a Web Component, you will need to use a ref to interact with the DOM node directly.”

This is necessary because React currently doesn’t have a way to listen to native DOM events (preferring, instead, to use it’s own proprietary SyntheticEvent system), nor does it have a way to declaratively access the current DOM element without using a ref.

We will make use of React’s useRef hook to create a reference to the native DOM element we have defined. We will also use React’s useEffect and useState hooks to gain access to the input’s value and render it to our app. We will also use the ref to call our super-cool-input’s reportValue method if the value is ever a variant of the word “rad.”

One thing to take note of in the example above is our React component’s useEffect block.

useEffect(() => {   coolInput.current.addEventListener('custom-input', eventListener);      return () => {     coolInput.current.removeEventListener('custom-input', eventListener);   } });

The useEffect block creates a side effect (adding an event listener not managed by React), so we have to be careful to remove the event listener when the component needs a change so that we don’t have any unintentional memory leaks.

While the above example simply binds an event listener, this is also a technique that can be employed to bind to DOM properties (defined as entries on the DOM object, rather than React props or DOM attributes).

This isn’t too bad. We have our custom element working in React, and we’re able to bind to our custom event, access the value from it, and call our custom element’s methods as well. While this does work, it is verbose and doesn’t really look like React.

Approach 2: Use a wrapper

Our next attempt at using our custom element in our React application is to create a wrapper for the element. Our wrapper is simply a React component that passes down props to our element and creates an API for interfacing with the parts of our element that aren’t typically available in React.

Here, we have moved the complexity into a wrapper component for our custom element. The new CoolInput React component manages creating a ref while adding and removing event listeners for us so that any consuming component can pass props in like any other React component.

function CoolInput(props) {   const ref = useRef();   const { children, onCustomInput, ...rest } = props;      function invokeCallback(event) {     if (onCustomInput) {       onCustomInput(event, ref.current);     }   }      useEffect(() => {     const { current } = ref;     current.addEventListener('custom-input', invokeCallback);     return () => {       current.removeEventListener('custom-input', invokeCallback);     }   });      return <super-cool-input ref={ref} {...rest}>{children}</super-cool-input>; }

On this component, we have created a prop, onCustomInput, that, when present, triggers an event callback from the parent component. Unlike a normal event callback, we chose to add a second argument that passes along the current value of the CoolInput’s internal ref.

Using these same techniques, it is possible to create a generic wrapper for a custom element, such as this reactifyLitElement component from Mathieu Puech. This particular component takes on defining the React component and managing the entire lifecycle.

Approach 3: Use a JSX pragma

One other option is to use a JSX pragma, which is sort of like hijacking React’s JSX parser and adding our own features to the language. In the example below, we import the package jsx-native-events from Skypack. This pragma adds an additional prop type to React elements, and any prop that is prefixed with onEvent adds an event listener to the host.

To invoke a pragma, we need to import it into the file we are using and call it using the /** @jsx <PRAGMA_NAME> */ comment at the top of the file. Your JSX compiler will generally know what to do with this comment (and Babel can be configured to make this global). You might have seen this in libraries like Emotion.

An <input> element with the onEventInput={callback} prop will run the callback function whenever an event with the name 'input' is dispatched. Let’s see how that looks for our super-cool-input.

The code for the pragma is available on GitHub. If you want to bind to native properties instead of React props, you can use react-bind-properties. Let’s take a quick look at that:

import React from 'react'  /**  * Convert a string from camelCase to kebab-case  * @param {string} string - The base string (ostensibly camelCase)  * @return {string} - A kebab-case string  */ const toKebabCase = string => string.replace(/([a-z0-9]|(?=[A-Z]))([A-Z])/g, '$  1-$  2').toLowerCase()  /** @type {Symbol} - Used to save reference to active listeners */ const listeners = Symbol('jsx-native-events/event-listeners')  const eventPattern = /^onEvent/  export default function jsx (type, props, ...children) {   // Make a copy of the props object   const newProps = { ...props }   if (typeof type === 'string') {     newProps.ref = (element) => {       // Merge existing ref prop       if (props && props.ref) {         if (typeof props.ref === 'function') {           props.ref(element)         } else if (typeof props.ref === 'object') {           props.ref.current = element         }       }        if (element) {         if (props) {           const keys = Object.keys(props)           /** Get all keys that have the `onEvent` prefix */           keys             .filter(key => key.match(eventPattern))             .map(key => ({               key,               eventName: toKebabCase(                 key.replace('onEvent', '')               ).replace('-', '')             })           )           .map(({ eventName, key }) => {             /** Add the listeners Map if not present */             if (!element[listeners]) {               element[listeners] = new Map()             }              /** If the listener hasn't be attached, attach it */             if (!element[listeners].has(eventName)) {               element.addEventListener(eventName, props[key])               /** Save a reference to avoid listening to the same value twice */               element[listeners].set(eventName, props[key])             }           })         }       }     }   }      return React.createElement.apply(null, [type, newProps, ...children]) }

Essentially, this code converts any existing props with the onEvent prefix and transforms them to an event name, taking the value passed to that prop (ostensibly a function with the signature (e: Event) => void) and adding it as an event listener on the element instance.

Looking forward

As of the time of this writing, React recently released version 17. The React team had initially planned to release improvements for compatibility with custom elements; unfortunately, those plans seem to have been pushed back to version 18.

Until then it will take a little extra work to use all the features custom elements offer with React. Hopefully, the React team will continue to improve support to bridge the gap between React and the web platform.


The post 3 Approaches to Integrate React with Custom Elements appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , ,
[Top]

Proper Tabbing to Interactive Elements in Firefox on macOS

I just had to debug an issue with focusable elements in Firefox. Someone reported to me that when tabbing to a certain element within a CodePen embed, it shot the scroll position to the top of the page (WTF?!). So, I went to go debug the problem by tabbing through an example page in Firefox, and this is what I saw:

I didn’t even know what to make of that. It was like some elements you could tab to but not others? You can tab to <button>s but not <a>s? Uhhhhh, that doesn’t seem right that you can’t tab to links in Firefox?

After searching and asking around, it turns out it’s this preference at the OS level on macOS.

System Preferences > Keyboard > Shortcuts > User keyboard navigation to move focus between controls

If you have to turn that on, you also have to restart Firefox. Once you have, then you can tab to things you’d expect to be able to tab to, like links.

About that bug with the scrolling to the top of the page. See that “Skip Results Iframe” link that shows up when tabbing through the CodePen Embed? It only shows up when :focus-ed (as the point of it is to skip over the <iframe> rather than being forced to tab through it). I “hid” it by doing a position: absolute; top: -9999px; left: -9999px thing (old muscle memory), then removing those values when in focus. For some reason, when tabbed to, Firefox would see those values and instantly jump the page up, even though the focus style moved it back into a normal place. Must have been some kind of race condition thing.

I also found it very silly that Firefox would do that to the parent page when that link was inside an iframe. I fixed it up using a more vetted accessible hiding technique.


The post Proper Tabbing to Interactive Elements in Firefox on macOS appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , , ,
[Top]

A Utility Class for Covering Elements

Big ol’ same to Michelle Barker here:

Here’s something I find myself needing to do again and again in CSS: completely covering one element with another. It’s the same CSS every time: the first element (the one that needs to be covered) has position: relative applied to it. The second has position: absolute and is positioned so that all four sides align to the edges of the first element.

.original-element {   position: relative; }  .covering-element {   position: absolute;   top: 0;   right: 0;   bottom: 0;   left: 0; }

I have it stuck in my head somehow that it’s “not as reliable” to use bottom and right and that it’s safer to set the top and left then do width: 100% and height: 100%. But I can’t remember why anymore—maybe it was an older browser thing?

But speaking of modernizing things, my favorite bit from Michelle’s article is this:

.overlay {   position: absolute;   inset: 0; }

The inset property is a Logical Property and clearly very handy here! Read the article for another trick involving CSS grid.

Direct Link to ArticlePermalink


The post A Utility Class for Covering Elements appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , ,
[Top]

How to Get Sticky and Full-Bleed Elements to Play Well Together

I had a unique requirement the other day: to build a layout with full-bleed elements while one element stays stuck to the top. This ended up being rather tricky to pull off so I’m documenting it here in case anyone needs to re-create this same effect. Part of the trickiness was dealing with logical positioning on small screens as well.

It’s tough to describe the effect, so I recorded my screen to show what I mean. Pay special attention to the main call to action section, the one with the “Try Domino Today” header.

The idea is to display the main call to action on the right side while users scroll past other sections on larger viewports. On smaller viewports, the call to action element has to display after the main hero section with the “Start your trial” header.

There are a two main challenges here:

  • Make full-bleed elements that don’t interfere with the sticky element
  • Avoid duplicating the HTML

Before we dive into a couple of possible solutions (and their limitations), let’s first set up the semantic HTML structure.

The HTML

When building these kinds of layouts, one might be tempted to build duplicate call-to-action sections: one for the desktop version and the other for the mobile version and then toggle the visibility of them when appropriate. This avoids having to find the perfect place in the HTML and needing to apply CSS that handles both layout needs. I must admit, I am guilty of doing that from time to time. But this time, I wanted to avoid duplicating my HTML.

The other thing to consider is that we’re using the sticky positioning on the .box--sticky element, which means it needs to be the sibling of other elements, including full-bleed ones, for it to properly work.

Here’s the markup:

<div class="grid">    <div class="box box--hero">Hero Box</div>    <div class="box box--sticky">Sticky Box</div>    <div class="box box--bleed">Full-bleed Box</div>   <div class="box box--bleed">Full-bleed Box</div>   <!-- a bunch more of these -->  </div>

Let’s get sticky

Making sticky elements in a CSS grid layout is pretty straightforward. We add position: sticky to the .box--sticky element with a top: 0 offset, indicating where it starts to stick. Oh, and notice that we’re only making the element sticky on viewports larger that 768px.

@media screen and (min-width: 768px) {   .box--sticky {     position: sticky;     top: 0;   } }

Beware that there is a known issue with sticky positioning in Safari when it’s used with overflow: auto. It is documented over at caniuse in the known issues section:

A parent with overflow set to auto will prevent position: sticky from working in Safari.

Nice, that was easy. Let’s solve the challenge of full-bleed elements next.

Solution 1: Pseudo-elements

The first solution is something I use often: absolutely positioned pseudo-elements that stretch from one side to side. The trick here is to use a negative offset.

If we are talking about centered content, then the calculation is quite straightforward:

.full-bleed-element {   max-width: 600px;   margin-right: auto;   margin-left: auto;   padding: 20px;   position: relative;  }  .full-bleed-element::before {   content: "";   background-color: dodgerblue;    position: absolute;   top: 0;   bottom: 0;   right: calc((100vw - 100%) / -2);   left: calc((100vw - 100%) / -2); }

In short, the negative offset is the width of the viewport, 100vw, minus the width of the element, 100%, and then divided by -2, because we need two negative offsets.

Beware that there is a known bug when using 100vw, that is also documented over at caniuse:

Currently all browsers but Firefox incorrectly consider 100vw to be the entire page width, including vertical scroll bar, which can cause a horizontal scroll bar when overflow: auto is set.

Now let’s make full-bleed elements when the content is not centered. If you watch the video again, notice that there is no content below the sticky element. We don’t want our sticky element to overlap the content and that is the reason why be don’t have centered content in this particular layout.

First, we are going to create the grid:

.grid {   display: grid;   grid-gap: var(--gap);   grid-template-columns: var(--cols);   max-width: var(--max-width);   margin-left: auto;   margin-right: auto; }

We’re using custom properties which allows us to redefine the maximum width, the gap, and grid columns without redeclaring the properties. In other words, instead of redeclaring the grid-gap, grid-template-columns, and max-width properties, we are re-declaring variable values:

:root {   --gap: 20px;   --cols: 1fr;   --max-width: calc(100% - 2 * var(--gap)); }  @media screen and (min-width: 768px) {   :root {     --max-width: 600px;     --aside-width: 200px;     --cols: 1fr var(--aside-width);   } }  @media screen and (min-width: 980px) {   :root {     --max-width: 900px;     --aside-width: 300px;   } }

On viewports that are 768px wide and above, we have defined two columns: one with a fixed width, --aside-width, and one with that fills the remaining space, 1fr, as well as maximum width of the grid container, --max-width.

On viewports smaller than 768px, we have defined a single column and the gap. The maximum width of the grid container is 100% of the viewport, minus gaps on each side.

Now comes the fun part. The content isn’t centered on bigger viewports, so the calculation isn’t as straightforward as you might think. Here’s how it looks:

.box--alpha {   position: relative;   z-index: 0; }  .box--alpha::before {   content: "";   display: block;   position: absolute;   top: 0;   bottom: 0;   left: calc((100vw - (100% + var(--gap) + var(--aside-width))) / -2);   right: calc(((100vw - (100% - var(--gap) + var(--aside-width))) / -2) - (var(--aside-width)));   z-index: -1; }

Instead of using 100% of the parent’s width, we’re taking into account the widths of the gap and the sticky element. That means width of the content in full-bleed elements will not exceed the bounds of the hero element. That way, we ensure the sticky element won’t overlap any important piece of information.

The left offset is simpler because we only need to subtract the width of the element (100%), the gap (--gap), and the sticky element (--aside-width) from the viewport width (100vw).

left: (100vw - (100% + var(--gap) + var(--aside-width))) / -2);

The right offset is more complicated because we have to add the width of the sticky element to the previous calculation, --aside-width, as well as the gap, --gap:

right: ((100vw - (100% + var(--gap) + var(--aside-width))) / -2) - (var(--aside-width) + var(--gap));

Now we are sure the sticky element doesn’t overlap any content in full-bleed elements.

Here’s the solution with a horizontal bug:

And here’s the solution with a horizontal bugfix:

The fix is to hide overflow on the x-axis of the body, which might be a good idea in general anyway:

body {   max-width: 100%;   overflow-x: hidden; }

This is a perfectly viable solution and we could end here. But where’s the fun in that? There’s usually more than one way to accomplish something, so let’s look at another approach.

Solution 2: Padding calculations

Instead of using a centered grid container and pseudo elements, we could achieve the same effect by configuring our grid. Let’s start by defining the grid just as we did last time:

.grid {   display: grid;   grid-gap: var(--gap);   grid-template-columns: var(--cols); }

Again, we are using custom properties to define the gap and the template columns:

:root {   --gap: 20px;   --gutter: 1px;   --cols: var(--gutter) 1fr var(--gutter); }

We’re showing three columns on viewports smaller than 768px. The center column takes as much space as possible, while the other two are used only to force the horizontal gap.

@media screen and (max-width: 767px) {   .box {     grid-column: 2 / -2;   } }

Note that all grid elements are placed in the center column.

On viewports bigger than 768px, we are defining a --max-width variable that limits the width of the inner columns. We’re also defining --aside-width, the width of our sticky element. Again, this way we ensure the sticky element won’t be positioned over any content inside the full-bleed elements.

:root {   --gap: 20px; }  @media screen and (min-width: 768px) {   :root {     --max-width: 600px;     --aside-width: 200px;     --gutter: calc((100% - (var(--max-width))) / 2 - var(--gap));     --cols: var(--gutter) 1fr var(--aside-width) var(--gutter);   } }  @media screen and (min-width: 980px) {   :root {     --max-width: 900px;     --aside-width: 300px;   } }

Next, we are calculating the gutter width. The calculation is:

--gutter: calc((100% - (var(--max-width))) / 2 - var(--gap));

…where 100% is the viewport width. First, we are subtracting the maximum width of the inner columns from the width of the viewport. Then, we are dividing that result by 2 to create the gutters. Finally, we are subtracting the grid’s gap to get the correct width of the gutter columns.

Now let’s push the .box--hero element over so it starts at the first inner column of the grid:

@media screen and (min-width: 768px) {   .box--hero {     grid-column-start: 2;   } }

This automatically pushes the sticky box so it starts right after the hero element. We could also explicitly define the placement of the sticky box, like this:

.box--sticky {   grid-column: 3 / span 1; }

Finally, let’s make the full-bleed elements by setting grid-column to 1 / -1. That tells the elements to start the content at the first grid item and span through to the last one.

@media screen and (min-width: 768px) {     .box--alpha {     grid-column: 1 / -1;   } }

To center the content, we are going to calculate left and right padding. The left padding is equal to the size of the gutter column, plus the grid gap. The right padding is equal to the size of the left padding, plus another grid gap as well as the width of the sticky element.

@media screen and (min-width: 768px) {   .box--alpha {       padding-left: calc(var(--gutter) + var(--gap));     padding-right: calc(var(--gutter) + var(--gap) + var(--gap) + var(--aside-width));   } }

Here’s the final solution:

I prefer this solution to the first one because it isn’t using buggy viewport units.


I love CSS calculations. Using mathematical operations is not always straightforward, especially when combining different units, like 100%. Figuring out what 100% means is half of the effort.

I also love solving simple, yet complicated layouts, like this one, using only CSS. Modern CSS has native solutions — like grid, sticky positioning and calculations — that remove complicated and somewhat heavy JavaScript solutions. Let’s leave the dirty work for the browser!

Do you have a better solution or different approach for this? I would be happy to hear about it.


The post How to Get Sticky and Full-Bleed Elements to Play Well Together appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , , ,
[Top]