Tag: Elements

An Approach to Lazy Loading Custom Elements

We’re fans of Custom Elements around here. Their design makes them particularly amenable to lazy loading, which can be a boon for performance.

Inspired by a colleague’s experiments, I recently set about writing a simple auto-loader: Whenever a custom element appears in the DOM, we wanna load the corresponding implementation if it’s not available yet. The browser then takes care of upgrading such elements from there on out.

Chances are you won’t actually need all this; there’s usually a simpler approach. Used deliberately, the techniques shown here might still be a useful addition to your toolset.

For consistency, we want our auto-loader to be a custom element as well — which also means we can easily configure it via HTML. But first, let’s identify those unresolved custom elements, step by step:

class AutoLoader extends HTMLElement {   connectedCallback() {     let scope = this.parentNode;     this.discover(scope);   } } customElements.define("ce-autoloader", AutoLoader);

Assuming we’ve loaded this module up-front (using async is ideal), we can drop a <ce-autoloader> element into the <body> of our document. That will immediately start the discovery process for all child elements of <body>, which now constitutes our root element. We could limit discovery to a subtree of our document by adding <ce-autoloader> to the respective container element instead — indeed, we might even have multiple instances for different subtrees.

Of course, we still have to implement that discover method (as part of the AutoLoader class above):

discover(scope) {   let candidates = [scope, ...scope.querySelectorAll("*")];   for(let el of candidates) {     let tag = el.localName;     if(tag.includes("-") && !customElements.get(tag)) {       this.load(tag);     }   } }

Here we check our root element along with every single descendant (*). If it’s a custom element — as indicated by hyphenated tags — but not yet upgraded, we’ll attempt to load the corresponding definition. Querying the DOM that way might be expensive, so we should be a little careful. We can alleviate load on the main thread by deferring this work:

connectedCallback() {   let scope = this.parentNode;   requestIdleCallback(() => {     this.discover(scope);   }); }

requestIdleCallback is not universally supported yet, but we can use requestAnimationFrame as a fallback:

let defer = window.requestIdleCallback || requestAnimationFrame;  class AutoLoader extends HTMLElement {   connectedCallback() {     let scope = this.parentNode;     defer(() => {       this.discover(scope);     });   }   // ... }

Now we can move on to implementing the missing load method to dynamically inject a <script> element:

load(tag) {   let el = document.createElement("script");   let res = new Promise((resolve, reject) => {     el.addEventListener("load", ev => {       resolve(null);     });     el.addEventListener("error", ev => {       reject(new Error("failed to locate custom-element definition"));     });   });   el.src = this.elementURL(tag);   document.head.appendChild(el);   return res; }  elementURL(tag) {   return `$  {this.rootDir}/$  {tag}.js`; }

Note the hard-coded convention in elementURL. The src attribute’s URL assumes there’s a directory where all custom element definitions reside (e.g. <my-widget>/components/my-widget.js). We could come up with more elaborate strategies, but this is good enough for our purposes. Relegating this URL to a separate method allows for project-specific subclassing when needed:

class FancyLoader extends AutoLoader {   elementURL(tag) {     // fancy logic   } }

Either way, note that we’re relying on this.rootDir. This is where the aforementioned configurability comes in. Let’s add a corresponding getter:

get rootDir() {   let uri = this.getAttribute("root-dir");   if(!uri) {     throw new Error("cannot auto-load custom elements: missing `root-dir`");   }   if(uri.endsWith("/")) { // remove trailing slash     return uri.substring(0, uri.length - 1);   }   return uri; }

You might be thinking of observedAttributes now, but that doesn’t really make things easier. Plus updating root-dir at runtime seems like something we’re never going to need.

Now we can — and must — configure our elements directory: <ce-autoloader root-dir="/components">.

With this, our auto-loader can do its job. Except it only works once, for elements that already exist when the auto-loader is initialized. We’ll probably want to account for dynamically added elements as well. That’s where MutationObserver comes into play:

connectedCallback() {   let scope = this.parentNode;   defer(() => {     this.discover(scope);   });   let observer = this._observer = new MutationObserver(mutations => {     for(let { addedNodes } of mutations) {       for(let node of addedNodes) {         defer(() => {           this.discover(node);         });       }     }   });   observer.observe(scope, { subtree: true, childList: true }); }  disconnectedCallback() {   this._observer.disconnect(); }

This way, the browser notifies us whenever a new element appears in the DOM — or rather, our respective subtree — which we then use to restart the discovery process. (You might argue we’re re-inventing custom elements here, and you’d be kind of correct.)

Our auto-loader is now fully functional. Future enhancements might look into potential race conditions and investigate optimizations. But chances are this is good enough for most scenarios. Let me know in the comments if you have a different approach and we can compare notes!


An Approach to Lazy Loading Custom Elements originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

CSS-Tricks

, , , ,

Adding Box Shadows to WordPress Blocks and Elements

I stumbled across this tweet from Ana Segota looking for a way to add a CSS box-shadow to a button’s hover state in WordPress in the theme.json file.

She’s asking because theme.json is where WordPress wants us to start moving basic styles for block themes. Traditionally, we’d do any and all styling in style.css when working in a “classic” theme. But with the default Twenty Twenty-Three (TT3) theme that recently shipped with WordPress 6.1 moving all of its styles to theme.json, we’re getting closer and closer to being able to do the same with our own themes. I covered this in great detail in a recent article.

I say “closer and closer” because there are still plenty of CSS properties and selectors that are unsupported in theme.json. For example, if you’re hoping to style something with like perspective-origin in theme.json, it just won’t happen — at least as I’m writing this today.

Ana is looking at box-shadow and, luckily for her, that CSS property is supported by theme.json as of WordPress 6.1. Her tweet is dated Nov. 1, the same exact day that 6.1 released. It’s not like support for the property was a headline feature in the release. The bigger headlines were more related to spacing and layout techniques for blocks and block themes.

Here’s how we can apply a box-shadow to a specific block — say the Featured Image block — in theme.json:

{   "version": 2,   "settings": {},   // etc.   "styles": {     "blocks" :{       "core/post-featured-image": {         "shadow": "10px 10px 5px 0px rgba(0, 0, 0, 0.66)"       }     }   } }

Wondering if the new color syntax works? Me too! But when I tried — rgb(0 0 0 / 0.66) — I got nothing. Perhaps that’s already in the works or could use a pull request.

Easy, right? Sure, it’s way different than writing vanilla CSS in style.css and takes some getting used to. But it is indeed possible as of the most recent WordPress release.

And, hey, we can do the same thing to individual “elements”, like a button. A button is a block in and of itself, but it can also be a nested block within another block. So, to apply a box-shadow globally to all buttons, we’d do something like this in theme.json:

{   "version": 2,   "settings": {},   // etc.   "styles": {     "elements": {       "button": {         "shadow": "10px 10px 5px 0px rgba(0,0,0,0.66)"       }     }   } }

But Ana wants to add the shadow to the button’s :hover state. Thankfully, support for styling interactive states for certain elements, like buttons and links, using pseudo-classes — including :hover, :focus, :active, and :visited — also gained theme.json support in WordPress 6.1.

{   "version": 2,   "settings": {},   // etc.   "styles": {     "elements": {       "button": {         ":hover": {           "shadow": "10px 10px 5px 0px rgba(0,0,0,0.66)"         }       }     }   } }

If you’re using a parent theme, you can certainly override a theme’s styles in a child theme. Here, I am completely overriding TT3’s button styles.

View full code
{   "version": 2,   "settings": {},   // etc.   "styles": {     "elements": {       "button": {         "border": {           "radius": "0"         },         "color": {           "background": "var(--wp--preset--color--tertiary)",           "text": "var(--wp--preset--color--contrast)"         },         "outline": {           "offset": "3px",           "width": "3px",           "style": "dashed",           "color": "red"         },         "typography": {           "fontSize": "var(--wp--preset--font-size--medium)"         },         "shadow": "5px 5px 5px 0px rgba(9, 30, 66, 0.25), 5px 5px 5px 1px rgba(9, 30, 66, 0.08)",         ":hover": {           "color": {             "background": "var(--wp--preset--color--contrast)",             "text": "var(--wp--preset--color--base)"           },           "outline": {             "offset": "3px",             "width": "3px",             "style": "solid",             "color": "blue"           }         },         ":focus": {           "color": {             "background": "var(--wp--preset--color--contrast)",             "text": "var(--wp--preset--color--base)"           }         },         ":active": {           "color": {             "background": "var(--wp--preset--color--secondary)",             "text": "var(--wp--preset--color--base)"           }         }       }     }   } }

Here’s how that renders:

Showing two red buttons with box shadows.
The button’s natural state (left) and it’s hovered state (right)

Another way to do it: custom styles

The recently released Pixl block theme provides another example of real-world usage of the box-shadow property in theme.json using an alternative method that defines custom values. In the theme, a custom box-shadow property is defined as .settings.custom.shadow:

{   "version": 2,   "settings": {     // etc.      "custom": {       // etc.       "shadow": "5px 5px 0px -2px var(--wp--preset--color--background), 5px 5px var(--wp--preset--color--foreground)"     },     // etc.   } }

Then, later in the file, the custom shadow property is called on a button element:

{   "version": 2,   "settings": {     // etc.   },   "styles": {     "elements": {       "button": {         // etc.         "shadow": "var(--wp--custom--shadow) !important",         // etc.         ":active": {           // etc.           "shadow": "2px 2px var(--wp--preset--color--primary) !important"         }       },     // etc.   } }

I’m not totally sure about the use of !important in this context. My hunch is that it’s an attempt to prevent overriding those styles using the Global Styles UI in the Site Editor, which has high specificity than styles defined in theme.json. Here’s an anchored link to more information from my previous article on managing block theme styles.

Update: Turns out there was a whole discussion about this in Pull Request #34689, which notes that it was addressed in WordPress 5.9.

And there’s more…

In addition to shadows, the CSS outline property also gained theme.json support in WordPress 6.1 and can be applied to buttons and their interactive states. This GitHub PR shows a good example.

"elements": {   "button": {     "outline": {       "offset": "3px",       "width": "3px",       "style": "dashed",       "color": "red"     },     ":hover": {       "outline": {         "offset": "3px",         "width": "3px",         "style": "solid",         "color": "blue"       }     }   } }

You can also find the real examples of how the outline property works in other themes, including Loudness, Block Canvas, and Blockbase.

Wrapping up

Who knew there was so much to talk about with a single CSS property when it comes to block theming in WordPress 6.1? We saw the officially supported methods for setting a box-shadow on blocks and individual elements, including the interactive states of a button element. We also checked out how we could override shadows in a child theme. And, finally, we cracked open a real-world example that defines and sets shadows in a custom property.

You can find more detailed in-depth discussions about the WordPress and it’s box-shadow implementation in this GitHub PR. There is also a GitHub proposal for adding UI directly in WordPress to set shadow values on blocks — you can jump directly to an animated GIF showing how that would work.

Speaking of which, Justin Tadlock recently developed a block that renders a progress bar and integrated box shadow controls into it. He shows it off in this video:

More information

If you’d like to dig deeper into the box-shadow and other CSS properties that are supported by the theme.json file in a block theme, here are a couple of resources you can use:


Adding Box Shadows to WordPress Blocks and Elements originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

CSS-Tricks

, , , ,
[Top]

Conditionally Styling Selected Elements in a Grid Container

Calendars, shopping carts, galleries, file explorers, and online libraries are some situations where selectable items are shown in grids (i.e. square lattices). You know, even those security checks that ask you to select all images with crosswalks or whatever.

🧐

I found a neat way to display selectable options in a grid. No, not recreating that reCAPTCHA, but simply being able to select multiple items. And when two or more adjoining items are selected, we can use clever :nth-of-type combinators, pseudo elements, and the :checked pseudo-class to style them in a way where they look grouped together.

The whole idea of combinators and pseudos to get the rounded checkboxes came from a previous article I wrote. It was a simple single-column design:

This time, however, the rounding effect is applied to elements along both the vertical and horizontal axes on a grid. You don’t have to have read my last article on checkbox styling for this since I’m going to cover everything you need to know here. But if you’re interested in a slimmed down take on what we’re doing in this article, then that one is worth checking out.

Before we start…

It’ll be useful for you to take note of a few things. For example, I’m using static HTML and CSS in my demo for the sake of simplicity. Depending on your application you might have to generate the grid and the items in it dynamically. I’m leaving out practical checks for accessibility in order to focus on the effect, but you would definitely want to consider that sort of thing in a production environment.

Also, I’m using CSS Grid for the layout. I’d recommend the same but, of course, it’s only a personal preference and your mileage may vary. For me, using grid allows me to easily use sibling-selectors to target an item’s ::before and ::after pseudos.

Hence, whatever layout standard you might want to use in your application, make sure the pseudos can still be targeted in CSS and ensure the layout stays in tact across different browsers and screens.

Let’s get started now

As you may have noticed in the earlier demo, checking and unchecking a checkbox element modifies the design of the boxes, depending on the selection state of the other checkboxes around it. This is possible because I styled each box using the pseudo-elements of its adjacent elements instead of its own element.

The following figure shows how the ::before pseudo-elements of boxes in each column (except the first column) overlap the boxes to their left, and how the ::after pseudo-elements of boxes in each row (except the first row) overlap the boxes above.

Two grids of checkboxes showing the placement of before and after pseudos.

Here’s the base code

The markup is pretty straightforward:

<main>   <input type=checkbox>    <input type=checkbox>    <input type=checkbox>   <!-- more boxes --> </main>

There’s a little more going on in the initial CSS. But, first, the grid itself:

/* The grid */ main {   display: grid;   grid:  repeat(5, 60px) / repeat(4, 85px);   align-items: center;   justify-items: center;   margin: 0; }

That’s a grid of five rows and four columns that contain checkboxes. I decided to wipe out the default appearance of the checkboxes, then give them my own light gray background and super rounded borders:

/* all checkboxes */ input {   -webkit-appearance: none;   appearance: none;   background: #ddd;   border-radius: 20px;   cursor: pointer;   display: grid;   height: 40px;   width: 60px;   margin: 0; }

Notice, too, that the checkboxes themselves are grids. That’s key for placing their ::before and ::after pseudo-elements. Speaking of which, let’s do that now:

/* pseudo-elements except for the first column and first row */ input:not(:nth-of-type(4n+1))::before, input:nth-of-type(n+5)::after {   content: '';           border-radius: 20px;   grid-area: 1 / 1;   pointer-events: none; }

We’re only selecting the pseudo-elements of checkboxes that are not in the first column or the first row of the grid. input:not(:nth-of-type(4n+1)) starts at the first checkbox, then selects the ::before of every fourth item from there. But notice we’re saying :not(), so really what we’re doing is skipping the ::before pseudo-element of every fourth checkbox, starting at the first. Then we’re applying styles to the ::after pseudo of every checkbox from the fifth one.

Now we can style both the ::before and ::after pseudos for each checkbox that is not in the first column or row of the grid, so that they are moved left or up, respectively, hiding them by default.

/* pseudo-elements other than the first column */ input:not(:nth-of-type(4n+1))::before {    transform: translatex(-85px); }  /* pseudo-elements other than the first row */ input:nth-of-type(n+5)::after {  transform: translatey(-60px);  }

Styling the :checked state

Now comes styling the checkboxes when they are in a :checked state. First, let’s give them a color, say a limegreen background:

input:checked { background: limegreen; }

A checked box should be able to re-style all of its adjacent checked boxes. In other words, if we select the eleventh checkbox in the grid, we should also be able to style the boxes surrounding it at the top, bottom, left, and right.

A four-by-five grid of squares numbered one through 20. 11 is selected and 7, 10, 12, and 15 are highlighted.

This is done by targeting the correct pseudo-elements. How do we do that? Well, it depends on the actual number of columns in the grid. Here’s the CSS if two adjacent boxes are checked in a 5⨉4 grid:

/* a checked box's right borders (if the element to its right is checked) */ input:not(:nth-of-type(4n)):checked + input:checked::before {    border-top-right-radius: 0;    border-bottom-right-radius: 0;    background: limegreen; } /* a checked box's bottom borders (if the element below is checked) */ input:nth-last-of-type(n+5):checked + * + * + * + input:checked::after {   border-bottom-right-radius: 0;   border-bottom-left-radius: 0;   background: limegreen; } /* a checked box's adjacent (right side) checked box's left borders */ input:not(:nth-of-type(4n)):checked + input:checked + input::before {            border-top-left-radius: 0;    border-bottom-left-radius: 0;    background: limegreen; } /* a checked box's adjacent (below) checked box's top borders */ input:not(:nth-of-type(4n)):checked + * + * + * +  input:checked + input::before {    border-top-left-radius: 0;    border-top-right-radius: 0;    background: limegreen; }

If you prefer you can generate the above code dynamically. However, a typical grid, say an image gallery, the number of columns will be small and likely a fixed number of items, whereas the rows might keep increasing. Especially if designed for mobile screens. That’s why this approach is still an efficient way to go. If for some reason your application happens to have limited rows and expanding columns, then consider rotating the grid sideways because, with a stream of items, CSS Grid arranges them left-to-right and top-to-bottom (i.e. row by row).

We also need to add styling for the last checkboxes in the grid — they’re not all covered by pseudo-elements as they are the last items in each axis.

/* a checked box's (in last column) left borders */ input:nth-of-type(4n-1):checked + input:checked {   border-top-left-radius: 0;   border-bottom-left-radius: 0; } /* a checked box's (in last column) adjacent (below) checked box's top borders */ input:nth-of-type(4n):checked + * + * + * + input:checked {   border-top-left-radius: 0;   border-top-right-radius: 0; }

Those are some tricky selectors! The first one…

input:nth-of-type(4n-1):checked + input:checked

…is basically saying this:

A checked <input> element next to a checked <input> in the second last column.

And the nth-of-type is calculated like this:

4(0) - 1 = no match 4(1) - 1 = 3rd item 4(2) - 1 = 7th item 4(3) - 1 = 11th item etc.

So, we’re starting at the third checkbox and selecting every fourth one from there. And if a checkbox in that sequence is checked, then we style the checkboxes adjacent, too, if they are also checked.

And this line:

input:nth-of-type(4n):checked + * + * + * + input:checked

Is saying this:

An <input> element provided that is checked, is directly adjacent to an element, which is directly adjacent to another element, which is also directly adjacent to another element, which, in turn, is directly adjacent to an <input> element that is in a checked state.

What that means is we’re selecting every fourth checkbox that is checked. And if a checkbox in that sequence is checked, then we style the next fourth checkbox from that checkbox if it, too, is checked.

Putting it to use

What we just looked at is the general principle and logic behind the design. Again, how useful it is in your application will depend on the grid design.

I used rounded borders, but you can try other shapes or even experiment with background effects (Temani has you covered for ideas). Now that you know how the formula works, the rest is totally up to your imagination.

Here’s an instance of how it might look in a simple calendar:

Again, this is merely a rough prototype using static markup. And, there would be lots and lots of accessibility considerations to consider in a calendar feature.


That’s a wrap! Pretty neat, right? I mean, there’s nothing exactly “new” about what’s happening. But it’s a good example of selecting things in CSS. If we have a handle on more advanced selecting techniques that use combinators and pseudos, then our styling powers can reach far beyond the styling one item — as we saw, we can conditionally style items based on the state of another element.


Conditionally Styling Selected Elements in a Grid Container originally published on CSS-Tricks. You should get the newsletter.

CSS-Tricks

, , , , ,
[Top]

Improving Icons for UI Elements with Typographic Alignment and Scale

Utilizing icons in user interface elements is helpful. In addition to element labeling, icons can help reinforce a user element’s intention to users. But I have to say, I notice a bit of icon misalignment while browsing the web. Even if the icon’s alignment is correct, icons often do not respond well when typographic styles for the element change.

I took note of a couple real-world examples and I’d like to share my thoughts on how I improved them. It’s my hope these techniques can help others build user interface elements that better accommodate typographic changes and while upholding the original goals of the design.

Example 1 — Site messaging

I found this messaging example on a popular media website. The icon’s position doesn’t look so bad. But when changing some of the element’s style properties like font-size and line-height, it begins to unravel.

Identified issues

  • the icon is absolutely positioned from the left edge using a relative unit (rem)
  • because the icon is taken out of the flow, the parent is given a larger padding-left value to help with overall spacing – ideally, our padding-x is uniform, and everything looks good whether or not an icon is present
  • the icon (it’s an SVG) is also sized in rems – this doesn’t allow for respective resizing if its parent’s font-size changes

Recommendations

Screenshot of the site messaging element. It is overlayed with a red-dashed line indicating the icon's top edge and a blue-dashed line indicating the text's topmost point. The red-dashed line is slightly higher than the blue-dashed line.
Indicating the issues with aligning the icon and typography.

We want our icon’s top edge to be at the blue dashed line, but we often find our icon’s top edge at the red dashed line.

Have you ever inserted an icon next to some text and it just won’t align to the top of the text? You may move the icon into place with something like position: relative; top: 0.2em. This works well enough, but if typographic styles change in the future, your icon could look misaligned.

We can position our icon more reliably. Let’s use the element’s baseline distance (the distance from one line’s baseline to the next line’s baseline) to help solve this.

Screenshot of the site messaging element. It is overlayed with arrows indicating the baseline distance from the baseline of one line to the next line's baseline.
Calculating the baseline distance.

Baseline distance is font-size * line-height.

We’ll store that in a CSS custom property:

--baselineDistance: calc(var(--fontSize) * var(--lineHeight));

We can then move our icon down using the result of (baseline distance – font size) / 2.

--iconOffset: calc((var(--baselineDistance) - var(--fontSize)) / 2);

With a font-size of 1rem (16px) and line-height of 1.5, our icon will be moved 4 pixels.

  • baseline distance = 16px * 1.5 = 24px
  • icon offset = (24px16px) / 2 = 4px

Demo: before and after

Example 2 – unordered lists

The second example I found is an unordered list. It uses a web font (Font Awesome) for its icon via a ::before pseudo-element. There have been plenty of great articles on styling both ordered and unordered lists, so I won’t go into details about the relatively new ::marker pseudo-element and such. Web fonts can generally work pretty well with icon alignment depending on the icon used.

Identified issues

  • no absolute positioning used – when using pseudo-elements, we don’t often use flexbox like our first example and absolute positioning shines here
  • the list item uses a combination of padding and negative text-indent to help with layout – I am never able to get this to work well when accounting for multi-line text and icon scalability

Recommendations

Because we’ll also use a pseudo-element in our solution, we’ll leverage absolute positioning. This example’s icon size was a bit larger than its adjacent copy (about 2x). Because of this, we will alter how we calculate the icon’s top position. The center of our icon should align vertically with the center of the first line.

Start with the baseline distance calculation:

--baselineDistance: calc(var(--fontSize) * var(--lineHeight));

Move the icon down using the result of (baseline distance – icon size) / 2.

--iconOffset: calc((var(--baselineDistance) - var(--iconSize)) / 2);

So with a font-size of 1rem (16px), a line-height of 1.6, and an icon sized 2x the copy (32px), our icon will get get a top value of -3.2 pixels.

  • baseline distance = 16px * 1.6 = 25.6px
  • icon offset = (25.6px32px) / 2 = -3.2px

With a larger font-size of 2rem (32px), line-height of 1.2, and 64px icon, our icon will get get a top value of -12.8 pixels.

  • baseline distance = 32px * 1.2 = 38.4px
  • icon offset = (38.4px64px) / 2 = -12.8px

Demo: before and after

Conclusion

For user interface icons, we have a lot of options and techniques. We have SVGs, web fonts, static images, ::marker, and list-style-type. One could even use background-colors and clip-paths to achieve some interesting icon results. Performing some simple calculations can help align and scale icons in a more graceful manner, resulting in implementations that are a bit more bulletproof.

See also: Previous discussion on aligning icon to text.


Improving Icons for UI Elements with Typographic Alignment and Scale originally published on CSS-Tricks. You should get the newsletter.

CSS-Tricks

, , , , ,
[Top]

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

, , , , , , ,
[Top]

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]