Tag: Than

Web Component Pseudo-Classes and Pseudo-Elements are Easier Than You Think

We’ve discussed a lot about the internals of using CSS in this ongoing series on web components, but there are a few special pseudo-elements and pseudo-classes that, like good friends, willingly smell your possibly halitotic breath before you go talk to that potential love interest. You know, they help you out when you need it most. And, like a good friend will hand you a breath mint, these pseudo-elements and pseudo-classes provide you with some solutions both from within the web component and from outside the web component — the website where the web component lives.

I’m specifically referring to the ::part and ::slotted pseudo-elements, and the :defined, :host, and :host-context pseudo-classes. They give us extra ways to interact with web components. Let’s examine them closer.

Article series

The ::part pseudo-element

::part, in short, allows you to pierce the shadow tree, which is just my Lord-of-the-Rings-y way to say it lets you style elements inside the shadow DOM from outside the shadow DOM. In theory, you should encapsulate all of your styles for the shadow DOM within the shadow DOM, i.e. within a <style> element in your <template> element.

So, given something like this from the very first part of this series, where you have an <h2> in your <template>, your styles for that <h2> should all be in the <style> element.

<template id="zprofiletemplate">   <style>     h2 {       font-size: 3em;       margin: 0 0 0.25em 0;       line-height: 0.8;     }     /* other styles */   </style>   <div class="profile-wrapper">     <div class="info">       <h2>         <slot name="zombie-name">Zombie Bob</slot>       </h2>       <!-- other zombie profile info -->     </div> </template>

But sometimes we might need to style an element in the shadow DOM based on information that exists on the page. For instance, let’s say we have a page for each zombie in the undying love system with matches. We could add a class to profiles based on how close of a match they are. We could then, for instance, highlight a match’s name if he/she/it is a good match. The closeness of a match would vary based on whose list of potential matches is being shown and we won’t know that information until we’re on that page, so we can’t bake the functionality into the web component. Since the <h2> is in the shadow DOM, though, we can’t access or style it from outside the shadow DOM meaning a selector of zombie-profile h2 on the matches page won’t work.

But, if we make a slight adjustment to the <template> markup by adding a part attribute to the <h2>:

<template id="zprofiletemplate">   <style>     h2 {       font-size: 3em;       margin: 0 0 0.25em 0;       line-height: 0.8;     }     /* other styles */   </style>   <div class="profile-wrapper">     <div class="info">       <h2 part="zname">         <slot name="zombie-name">Zombie Bob</slot>       </h2>       <!-- other zombie profile info -->     </div> </template>

Like a spray of Bianca in the mouth, we now have the superpowers to break through the shadow DOM barrier and style those elements from outside of the <template>:

/* External stylesheet */ .high-match::part(zname) {   color: blue; } .medium-match::part(zname) {   color: navy; } .low-match::part(zname) {   color: slategray; }

There are lots of things to consider when it comes to using CSS ::part. For example, styling an element inside of a part is a no-go:

/* frowny-face emoji */ .high-match::part(zname) span { ... }

But you can add a part attribute on that element and style it via its own part name.

What happens if we have a web component inside another web component, though? Will ::part still work? If the web component appears in the page’s markup, i.e. you’re slotting it in, ::part works just fine from the main page’s CSS.

<zombie-profile class="high-match">   <img slot="profile-image" src="https://assets.codepen.io/1804713/leroy.png" />   <span slot="zombie-name">Leroy</span>   <zombie-details slot="zdetails">     <!-- Leroy's details -->   </zombie-details> </zombie-profile>

But if the web component is in the template/shadow DOM, then ::part cannot pierce both shadow trees, just the first one. We need to bring the ::part into the light… so to speak. We can do that with an exportparts attribute.

To demonstrate this we’ll add a “watermark” behind the profiles using a web component. (Why? Believe it or not this was the least contrived example I could come up with.) Here are our templates: (1) the template for <zombie-watermark>, and (2) the same template for <zombie-profile> but with added a <zombie-watermark> element on the end.

<template id="zwatermarktemplate">   <style>     div {     text-transform: uppercase;       font-size: 2.1em;       color: rgb(0 0 0 / 0.1);       line-height: 0.75;       letter-spacing: -5px;     }     span {       color: rgb( 255 0 0 / 0.15);     }   </style>   <div part="watermark">     U n d y i n g  L o v e  U n d y i n g  L o v e  U n d y i n g  L o v e  <span part="copyright">©2 0 2 7 U n d y i n g  L o v e  U n L t d .</span>   <!-- Repeat this a bunch of times so we can cover the background of the profile -->   </div>  </template> <template id="zprofiletemplate">   <style>     ::part(watermark) {       color: rgb( 0 0 255 / 0.1);     }     /* More styles */   </style>   <!-- zombie-profile markup -->   <zombie-watermark exportparts="copyright"></zombie-watermark> </template> <style>   /* External styles */   ::part(copyright) {     color: rgb( 0 100 0 / 0.125);   } </style>

Since ::part(watermark) is only one shadow DOM above the <zombie-watermark>, it works fine from within the <zombie-profile>’s template styles. Also, since we’ve used exportparts="copyright" on the <zombie-watermark>, the copyright part has been pushed up into the <zombie-profile>‘s shadow DOM and ::part(copyright) now works even in external styles, but ::part(watermark) will not work outside the <zombie-profile>’s template.

We can also forward and rename parts with that attribute:

<zombie-watermark exportparts="copyright: cpyear"></zombie-watermark>
/* Within zombie-profile's shadow DOM */  /* happy-face emoji */ ::part(cpyear) { ... }  /* frowny-face emoji */ ::part(copyright) { ... }

Structural pseudo-classes (:nth-child, etc.) don’t work on parts either, but you can use pseudo-classes like :hover. Let’s animate the high match names a little and make them shake as they’re lookin’ for some lovin’. Okay, I heard that and agree it’s awkward. Let’s… uh… make them more, shall we say, noticeable, with a little movement.

.high::part(name):hover {   animation: highmatch 1s ease-in-out; }

The ::slotted pseudo-element

The ::slotted CSS pseudo-element actually came up when we covered interactive web components. The basic idea is that ::slotted represents any content in a slot in a web component, i.e. the element that has the slot attribute on it. But, where ::part pierces through the shadow DOM to make a web component’s elements accessible to outside styles, ::slotted remains encapsulated in the <style> element in the component’s <template> and accesses the element that’s technically outside the shadow DOM.

In our <zombie-profile> component, for example, each profile image is inserted into the element through the slot="profile-image".

<zombie-profile>   <img slot="profile-image" src="photo.jpg" />    <!-- rest of the content --> </zombie-profile>

That means we can access that image — as well as any image in any other slot — like this:

::slotted(img) {   width: 100%;   max-width: 300px;   height: auto;   margin: 0 1em 0 0; }

Similarly, we could select all slots with ::slotted(*) regardless of what element it is. Just beware that ::slotted has to select an element — text nodes are immune to ::slotted zombie styles. And children of the element in the slot are inaccessible.

The :defined pseudo-class

:defined matches all defined elements (I know, surprising, right?), both built-in and custom. If your custom element is shuffling along like a zombie avoiding his girlfriend’s dad’s questions about his “living” situation, you may not want the corpses of the content to show while you’re waiting for them to come back to life errr… load.

You can use the :defined pseudo-class to hide a web component before it’s available — or “defined” — like this:

:not(:defined) {   display: none; }

You can see how :defined acts as a sort of mint in the mouth of our component styles, preventing any broken content from showing (or bad breath from leaking) while the page is still loading. Once the element’s defined, it’ll automatically appear because it’s now, you know, defined and not not defined.

I added a setTimeout of five seconds to the web component in the following demo. That way, you can see that <zombie-profile> elements are not shown while they are undefined. The <h1> and the <div> that holds the <zombie-profile> components are still there. It’s just the <zombie-profile> web component that gets display: none since they are not yet defined.

The :host pseudo-class

Let’s say you want to make styling changes to the custom element itself. While you could do this from outside the custom element (like tightening that N95), the result would not be encapsulated, and additional CSS would have to be transferred to wherever this custom element is placed.

It’d be very convenient then to have a pseudo-class that can reach outside the shadow DOM and select the shadow root. That CSS pseudo-class is :host.

In previous examples throughout this series, I set the <zombie-profile> width from the main page’s CSS, like this:

zombie-profile {   width: calc(50% - 1em); }

With :host, however, I can set that width from inside the web component, like this:

:host {   width: calc(50% - 1em); }

In fact, there was a div with a class of .profile-wrapper in my examples that I can now remove because I can use the shadow root as my wrapper with :host. That’s a nice way to slim down the markup.

You can do descendant selectors from the :host, but only descendants inside the shadow DOM can be accessed — nothing that’s been slotted into your web component (without using ::slotted).

Showing the parts of the HTML that are relevant to the :host pseudo-element.

That said, :host isn’t a one trick zombie. It can also take a parameter, e.g. a class selector, and will only apply styling if the class is present.

:host(.high) {   border: 2px solid blue; }

This allows you to make changes should certain classes be added to the custom element.

You can also pass pseudo-classes in there, like :host(:last-child) and :host(:hover).

The :host-context pseudo-class

Now let’s talk about :host-context. It’s like our friend :host(), but on steroids. While :host gets you the shadow root, it won’t tell you anything about the context in which the custom element lives or its parent and ancestor elements.

:host-context, on the other hand, throws the inhibitions to the wind, allowing you to follow the DOM tree up the rainbow to the leprechaun in a leotard. Just note that at the time I’m writing this, :host-context is unsupported in Firefox or Safari. So use it for progressive enhancement.

Here’s how it works. We’ll split our list of zombie profiles into two divs. The first div will have all of the high zombie matches with a .bestmatch class. The second div will hold all the medium and low love matches with a .worstmatch class.

<div class="profiles bestmatch">   <zombie-profile class="high">     <!-- etc. -->   </zombie-profile>   <!-- more profiles --> </div>  <div class="profiles worstmatch">   <zombie-profile class="medium">     <!-- etc. -->   </zombie-profile>   <zombie-profile class="low">     <!-- etc. -->   </zombie-profile>   <!-- more profiles --> </div>

Let’s say we want to apply different background colors to the .bestmatch and .worstmatch classes. We are unable to do this with just :host:

:host(.bestmatch) {   background-color: #eef; } :host(.worstmatch) {   background-color: #ddd; }

That’s because our best and worst match classes are not on our custom elements. What we want is to be able to select the profiles’s parent elements from within the shadow DOM. :host-context pokes past the custom element to match the, er, match classes we want to style.

:host-context(.bestmatch) {   background-color: #eef; } :host-context(.worstmatch) {   background-color: #ddd; }

Well, thanks for hanging out despite all the bad breath. (I know you couldn’t tell, but above when I was talking about your breath, I was secretly talking about my breath.)

How would you use ::part, ::slotted, :defined, :host, and :host-context in your web component? Let me know in the comments. (Or if you have cures to chronic halitosis, my wife would be very interested in to hear more.)


Web Component Pseudo-Classes and Pseudo-Elements are Easier Than You Think originally published on CSS-Tricks. You should get the newsletter.

CSS-Tricks

, , , , ,

Context-Aware Web Components Are Easier Than You Think

Another aspect of web components that we haven’t talked about yet is that a JavaScript function is called whenever a web component is added or removed from a page. These lifecycle callbacks can be used for many things, including making an element aware of its context.

Article series

The four lifecycle callbacks of web components

There are four lifecycle callbacks that can be used with web components:

  • connectedCallback: This callback fires when the custom element is attached to the element.
  • disconnectedCallback: This callback fires when the element is removed from the document.
  • adoptedCallback: This callback fires when the element is added to a new document.
  • attributeChangedCallback: This callback fires when an attribute is changed, added or removed, as long as that attribute is being observed.

Let’s look at each of these in action.

Our post-apocalyptic person component

Two renderings of the web component side-by-side, the left is a human, and the right is a zombie.

We’ll start by creating a web component called <postapocalyptic-person>. Every person after the apocalypse is either a human or a zombie and we’ll know which one based on a class — either .human or .zombie — that’s applied to the parent element of the <postapocalyptic-person> component. We won’t do anything fancy with it (yet), but we’ll add a shadowRoot we can use to attach a corresponding image based on that classification.

customElements.define(   "postapocalyptic-person",   class extends HTMLElement {     constructor() {       super();       const shadowRoot = this.attachShadow({ mode: "open" });     } }

Our HTML looks like this:

<div class="humans">   <postapocalyptic-person></postapocalyptic-person> </div> <div class="zombies">   <postapocalyptic-person></postapocalyptic-person> </div>

Inserting people with connectedCallback

When a <postapocalyptic-person> is loaded on the page, the connectedCallback() function is called.

connectedCallback() {   let image = document.createElement("img");   if (this.parentNode.classList.contains("humans")) {     image.src = "https://assets.codepen.io/1804713/lady.png";     this.shadowRoot.appendChild(image);   } else if (this.parentNode.classList.contains("zombies")) {     image.src = "https://assets.codepen.io/1804713/ladyz.png";     this.shadowRoot.appendChild(image);   } }

This makes sure that an image of a human is output when the <postapocalyptic-person> is a human, and a zombie image when the component is a zombie.

Be careful working with connectedCallback. It runs more often than you might realize, firing any time the element is moved and could (baffling-ly) even run after the node is no longer connected — which can be an expensive performance cost. You can use this.isConnected to know whether the element is connected or not.

Counting people with connectedCallback() when they are added

Let’s get a little more complex by adding a couple of buttons to the mix. One will add a <postapocalyptic-person>, using a “coin flip” approach to decide whether it’s a human or a zombie. The other button will do the opposite, removing a <postapocalyptic-person> at random. We’ll keep track of how many humans and zombies are in view while we’re at it.

<div class="btns">   <button id="addbtn">Add Person</button>   <button id="rmvbtn">Remove Person</button>    <span class="counts">     Humans: <span id="human-count">0</span>      Zombies: <span id="zombie-count">0</span>   </span> </div>

Here’s what our buttons will do:

let zombienest = document.querySelector(".zombies"),   humancamp = document.querySelector(".humans");  document.getElementById("addbtn").addEventListener("click", function () {   // Flips a "coin" and adds either a zombie or a human   if (Math.random() > 0.5) {     zombienest.appendChild(document.createElement("postapocalyptic-person"));   } else {     humancamp.appendChild(document.createElement("postapocalyptic-person"));   } }); document.getElementById("rmvbtn").addEventListener("click", function () {   // Flips a "coin" and removes either a zombie or a human   // A console message is logged if no more are available to remove.   if (Math.random() > 0.5) {     if (zombienest.lastElementChild) {       zombienest.lastElementChild.remove();     } else {       console.log("No more zombies to remove");     }   } else {     if (humancamp.lastElementChild) {       humancamp.lastElementChild.remove();     } else {       console.log("No more humans to remove");     }   } });

Here’s the code in connectedCallback() that counts the humans and zombies as they are added:

connectedCallback() {   let image = document.createElement("img");   if (this.parentNode.classList.contains("humans")) {     image.src = "https://assets.codepen.io/1804713/lady.png";     this.shadowRoot.appendChild(image);     // Get the existing human count.     let humancount = document.getElementById("human-count");     // Increment it     humancount.innerHTML = parseInt(humancount.textContent) + 1;   } else if (this.parentNode.classList.contains("zombies")) {     image.src = "https://assets.codepen.io/1804713/ladyz.png";     this.shadowRoot.appendChild(image);     // Get the existing zombie count.     let zombiecount = document.getElementById("zombie-count");     // Increment it     zombiecount.innerHTML = parseInt(zombiecount.textContent) + 1;   } }

Updating counts with disconnectedCallback

Next, we can use disconnectedCallback() to decrement the number as a humans and zombies are removed. However, we are unable to check the class of the parent element because the parent element with the corresponding class is already gone by the time disconnectedCallback is called. We could set an attribute on the element, or add a property to the object, but since the image’s src attribute is already determined by its parent element, we can use that as a proxy for knowing whether the web component being removed is a human or zombie.

disconnectedCallback() {   let image = this.shadowRoot.querySelector('img');   // Test for the human image   if (image.src == "https://assets.codepen.io/1804713/lady.png") {     let humancount = document.getElementById("human-count");     humancount.innerHTML = parseInt(humancount.textContent) - 1; // Decrement count   // Test for the zombie image   } else if (image.src == "https://assets.codepen.io/1804713/ladyz.png") {     let zombiecount = document.getElementById("zombie-count");     zombiecount.innerHTML = parseInt(zombiecount.textContent) - 1; // Decrement count   } }

Beware of clowns!

Now (and I’m speaking from experience here, of course) the only thing scarier than a horde of zombies bearing down on your position is a clown — all it takes is one! So, even though we’re already dealing with frightening post-apocalyptic zombies, let’s add the possibility of a clown entering the scene for even more horror. In fact, we’ll do it in such a way that there’s a possibility any human or zombie on the screen is secretly a clown in disguise!

I take back what I said earlier: a single zombie clown is scarier than even a group of “normal” clowns. Let’s say that if any sort of clown is found — be it human or zombie — we separate them from the human and zombie populations by sending them to a whole different document — an <iframe> jail, if you will. (I hear that “clowning” may be even more contagious than zombie contagion.)

And when we move a suspected clown from the current document to an <iframe>, it doesn’t destroy and recreate the original node; rather it adopts and connects said node, first calling adoptedCallback then connectedCallback.

We don’t need anything in the <iframe> document except a body with a .clowns class. As long as this document is in the iframe of the main document — not viewed separately — we don’t even need the <postapocalyptic-person> instantiation code. We’ll include one space for humans, another space for zombies, and yes, the clowns’s jail… errr… <iframe> of… fun.

<div class="btns">   <button id="addbtn">Add Person</button>   <button id="jailbtn">Jail Potential Clown</button> </div> <div class="humans">   <postapocalyptic-person></postapocalyptic-person> </div> <div class="zombies">   <postapocalyptic-person></postapocalyptic-person> </div> <iframe class="clowniframeoffun” src="adoptedCallback-iframe.html"> </iframe>

Our “Add Person” button works the same as it did in the last example: it flips a digital coin to randomly insert either a human or a zombie. When we hit the “Jail Potential Clown” button another coin is flipped and takes either a zombie or a human, handing them over to <iframe> jail.

document.getElementById("jailbtn").addEventListener("click", function () {   if (Math.random() > 0.5) {     let human = humancamp.querySelector('postapocalyptic-person');     if (human) {       clowncollege.contentDocument.querySelector('body').appendChild(document.adoptNode(human));     } else {       console.log("No more potential clowns at the human camp");     }   } else {     let zombie = zombienest.querySelector('postapocalyptic-person');     if (zombie) {       clowncollege.contentDocument.querySelector('body').appendChild(document.adoptNode(zombie));     } else {       console.log("No more potential clowns at the zombie nest");     }   } });

Revealing clowns with adoptedCallback

In the adoptedCallback we’ll determine whether the clown is of the zombie human variety based on their corresponding image and then change the image accordingly. connectedCallback will be called after that, but we don’t have anything it needs to do, and what it does won’t interfere with our changes. So we can leave it as is.

adoptedCallback() {   let image = this.shadowRoot.querySelector("img");   if (this.parentNode.dataset.type == "clowns") {     if (image.src.indexOf("lady.png") != -1) {        // Sometimes, the full URL path including the domain is saved in `image.src`.       // Using `indexOf` allows us to skip the unnecessary bits.        image.src = "ladyc.png";       this.shadowRoot.appendChild(image);     } else if (image.src.indexOf("ladyz.png") != -1) {       image.src = "ladyzc.png";       this.shadowRoot.appendChild(image);     }   } }

Detecting hidden clowns with attributeChangedCallback

Finally, we have the attributeChangedCallback. Unlike the the other three lifecycle callbacks, we need to observe the attributes of our web component in order for the the callback to fire. We can do this by adding an observedAttributes() function to the custom element’s class and have that function return an array of attribute names.

static get observedAttributes() {   return [“attribute-name”]; }

Then, if that attribute changes — including being added or removed — the attributeChangedCallback fires.

Now, the thing you have to worry about with clowns is that some of the humans you know and love (or the ones that you knew and loved before they turned into zombies) could secretly be clowns in disguise. I’ve set up a clown detector that looks at a group of humans and zombies and, when you click the “Reveal Clowns” button, the detector will (through completely scientific and totally trustworthy means that are not based on random numbers choosing an index) apply data-clown="true" to the component. And when this attribute is applied, attributeChangedCallback fires and updates the component’s image to uncover their clownish colors.

I should also note that the attributeChangedCallback takes three parameters:

  • the name of the attribute
  • the previous value of the attribute
  • the new value of the attribute

Further, the callback lets you make changes based on how much the attribute has changed, or based on the transition between two states.

Here’s our attributeChangedCallback code:

attributeChangedCallback(name, oldValue, newValue) {   let image = this.shadowRoot.querySelector("img");   // Ensures that `data-clown` was the attribute that changed,   // that its value is true, and that it had an image in its `shadowRoot`   if (name="data-clown" && this.dataset.clown && image) {     // Setting and updating the counts of humans, zombies,     // and clowns on the page     let clowncount = document.getElementById("clown-count"),     humancount = document.getElementById("human-count"),     zombiecount = document.getElementById("zombie-count");     if (image.src.indexOf("lady.png") != -1) {       image.src = "https://assets.codepen.io/1804713/ladyc.png";       this.shadowRoot.appendChild(image);       // Update counts       clowncount.innerHTML = parseInt(clowncount.textContent) + 1;       humancount.innerHTML = parseInt(humancount.textContent) - 1;     } else if (image.src.indexOf("ladyz.png") != -1) {       image.src = "https://assets.codepen.io/1804713/ladyzc.png";       this.shadowRoot.appendChild(image);       // Update counts       clowncount.innerHTML = parseInt(clowncount.textContent) + 1;       zombiecount.innerHTML = parseInt(zombiecount.textContent) - 1;     }   } }

And there you have it! Not only have we found out that web component callbacks and creating context-aware custom elements are easier than you may have thought, but detecting post-apocalyptic clowns, though terrifying, is also easier that you thought. What kind of devious, post-apocalyptic clowns can you detect with these web component callback functions?


Context-Aware Web Components Are Easier Than You Think originally published on CSS-Tricks. You should get the newsletter and become a supporter.

CSS-Tricks

, , , ,
[Top]

Working With Web Feeds: It’s More Than RSS

Between Google Chrome experimenting with “following” sites, along with a growing frustration of how social media platforms limit a creator’s reach to their fans through algorithmic feeds, there’s been renewed interest in RSS feeds and they’re primed for a comeback as we get into 2022.

This research is brought to you by support from Frontend Masters, CSS-Tricks’ official learning partner.

Need front-end development training?

Frontend Masters is the best place to get it. They have courses on all the most important front-end technologies. Interested in going full-stack? Here’s your best bet:

You may have heard whispers that “RSS in dead” around the web, but the truth is that they are still widely used as virtually every podcast uses one. Maybe you used to be an RSS fan and need to get re-acquainted with it. Or maybe you’re like Chris here at CSS-Tricks and still love RSS. Whatever the case, RSS is a web technology like any other, and there are best practices for how to create and curate a feed.

That’s exactly what I’m going to walk you through in this article. Let’s talk about the different kinds of feeds, how to implement them, and what strategies you can use to get the most out of your feed content.

RSS vs. Atom vs. JSON

Believe it or not, RSS is just one format among other types of syndicated web feeds. The most common formats are:

  1. RSS
  2. Atom
  3. JSON Feed

I’ve used RSS to signify these formats since it’s a far more popular search term but I’ll refer to these technologies as web feeds in this article unless I’m referring to a specific format.

On Sept 26–Oct 2, 2021, RSS had 37 points, web 6, atom 2 and jsonfeed 0. S
Google Trends for Atom, JSON, RSS and web feeds. (Source: Google Web Trends)

While Atom, RSS and, JSON feeds accomplish the same thing, there are a few differences between them:

  • Atom and RSS are based on XML while a JSON feed is based on, well, JSON.
  • All of these formats can be extended in some way. In a JSON feed, this is done by adding an object with a key that starts with an underscore anywhere in a feed’s object. With Atom and RSS, you do this by declaring the namespace on the root element. One example of this is on podcast feeds which declare the iTunes podcast namespace, allowing for the use of <itunes:*> tags.
  • JSON feed is a newer feed format meaning that support for it might not be as broad as Atom or RSS. If you have a podcast, however, RSS is a must.
  • While all of the formats require a unique identifier for each entry/item, Atom takes it a step further as it requires a unique identifier for every feed.
  • All allow HTML markup, though they handle it differently. JSON uses the content_html key containing JSON escaped HTML. Atom uses the content tag with type=html containing XML escaped HTML. RSS uses the <description> tag (or the content extension) which either contains XML-escaped HTML or the HTML unescaped in a <![CDATA[]]> tag.

Other than these things, there are only minor differences between them. You might think that the size of the file could be a possible difference, but compression reduces them all to just a few kilobytes apiece. Unless your application has a specific use case that requires a specific format (like podcasts), it doesn’t hurt to provide multiple formats, but RSS and Atom have the most support.

What makes a “good” feed?

Let’s look at some best practices for making feeds. Like most things on the web, there are things we can do to optimize our work to get the most out of it.

1. It’s easy to find

It doesn’t help to have a feed if no one knows about it. And one way to make a feed discoverable is to link it up in the <head> of your site. This way, feed readers are able to crawl and recognize when a site offers a content feed.

Here’s an example showing all three formats linked up in the document head:

<head>   <link rel="alternate" type="application/rss+xml" href="https://codelab.farai.xyz/index.rss.xml" title="Farai's Codelab's RSS Feed" />   <link rel="alternate" type="application/feed+json" href="https://codelab.farai.xyz/index.feed.json" title="Farai's Codelab's JSON Feed" />   <link rel="alternate" type="application/atom+xml" href="https://codelab.farai.xyz/index.atom.xml" title="Farai's Codelab's ATOM Feed" />   <!-- etc. --> </head>

And, yes, it’s OK to use all three! You can specify as many links as you want, though some feed readers might only recognize the first one. The important thing is that one includes rel="alternate" and the feed’s MIME type. There’s also the option to add a title which never hurts.

What else can you do to make your feed easy to find? Advertise it! Place direct links to the feeds somewhere prominent on your site that people can use to copy and paste into their feed reader.

That’s what CSS Tricks does. This is the link to the site’s RSS and it’s available in the footer across the entire site. Some feed readers can pick up on these links, too, even though they are outside of the <head>.

Screenshot of the CSS-Tricks footer showing a column of links with the heading Follow, and links for social networks, including the site's RSS feed. The background is near black, the headings are orange, and the links are light gray.
Oh, you want to subscribe to the CSS-Tricks RSS feed? Please do! Your RSS feed is just as much a thing to follow as any social network.

As for what to name the feed itself, it doesn’t matter as long as it’s discoverable. Here’s a good look into how various sites name their feeds, but I’ve named mine feed.json, feed.rss.xml and feed.atom.xml for JSON feed, Atom, and RSS respectively.

2. It takes advantage of HTTP

There are certain basic features of the web that can be leveraged to make your feeds a little better.

For example, be sure to compress your feeds, as it greatly reduces the overall file size and the time to download it. Most servers can take care of this for you, using gzip, Brotli, or some other compression engine.

Likewise, it’s a good idea to support either ETags or If-Modified-Since as they both allow clients to cache feeds and informs the browser whether a newer version of the feed is ready before it is downloaded. Much like compression, your server may take care of this as well.

Another thing to do: enable permissive CORS. Otherwise, clients could be blocked from fetching the feed. And while you should consider the security implications of letting any old site fetch your feed, it’s highly unlikely that it becomes a major issue for most small sites and feeds. This one-liner is all you need to enable CORS:

Access-Control-Allow-Origin: *

3. It displays full content instead of summaries

This is totally a user experience thing. You may have even experienced this before, where you subscribe to an RSS feed and all you get is the first paragraph or a summary of the post. Why? The traditional thinking is that providing only a summary encourages users to click through to your site, thereby leading to more visits. And more visits equals more eyeballs, which equals more revenue, etc.

I suggest avoiding that and instead allow your feeds to send the entire content for each post/entry/item. Many users prefer reading content in a feed reader because of the emphasis they place on legibility.

If you’re concerned about some dishonest person scraping your content and displaying it on their own site because you’re feeding the full content, let me reassure you: it’s no harder to a web page than a syndicated feed.

And if you’re a publisher who relies on display ads, and are concerned about the impact that sending full content might have on your revenue: you can still add static ads directly into your feed content. Besides, some readers can parse the web page associated with a feed entry so it can be read in the reader as well.

Dark UI with blue links and light gray text showing an article from a blog.
N/N Group article rendered in the NetNewsWire reader

But let’s not be dogmatic and all, because there are situations where summaries make sense. One is when a feed has a bunch of long-form entries. Another is when you have rich content that can only be viewed in a particular way (think show notes for a podcast). In that case, try making a good summary. One example is Nielsen Norman Group’s RSS which has a summary and an excerpt up to the first <h2> tag.

If I ever decide to only show summaries in my feed, I’d make sure to include an image, an outline of the content’s main points, and a link to the canonical version in addition to the summary. It is a bit of work but it gives the reader an idea what to expect, unlike some feeds I’ve seen which awkwardly truncate content to just the first few words.

Showing a post title in white with the published date and time below it in light gray. Below that is an image of a tired looking man with dark slicked back hair holding a 10 of diamonds card. Below that is the first sentence of a post that breaks mid-sentence.
Yes? You want to finish that sentence?

4. It is designed for reading

When crafting content, consider how it might be seen outside the context of a web browser, in places where JavaScript and CSS are limited. Sara Soueidan has a bunch of tips and ideas that are worth checking out. The main idea: provide a good fallback experience for your content.

This is a mostly an issue when it comes to embedded elements. While some embeds contain fallback content in their markup (like Twitter’s embedded tweets and CodePen’s embedded pens), others might not. Certain embeds (including videos posted to Vimeo) can only be embedded on certain domains meaning those won’t show up in a feed reader. So you need provide a way to view it somehow, like an image or a link to a webpage.

Showing the same article rendered in both Microsoft Outlook (left) and NetNewsWire (right). The article includes three embeds, one from Twitter, one from YouTube, and the third from TinkerCad), where each includes some form of fallback content for a better reading experience.

There are plenty of ways to do fallbacks. Twitter’s embed falls back to a <blockquote> — which makes total sense as a tweet is sort of like a quote — and a link to the tweet itself, which allows some clients that do not support embeds, like Outlook, to effectively render the content in a way that is still accessible to the user.

Though NetNewsWire is good with embeds, YouTube sometimes prevents it from playing videos like here. So, instead, the embed falls back to a link that points the user to watch it on YouTube’s site. Outlook doesn’t support YouTube embeds (or any embeds at all), but a descriptive link to the video on YouTube is still available.

The moral of the story: know your readers and how they render content so you can provide the best fallback experience possible.

Beware of relative URLs

One big issue across feeds is resolving relative URLs for images and links. Resolving based off the feed’s canonical link might work, but what happens if that link is in a subdirectory? The XML formats could use the xml:base attribute which defines the base URL to use when resolving relative URLs, but that’s only supported by Atom and is ignored and deprecated by most readers.

The most robust solution is to use absolute URLs for every href and src in an entry’s content. Meaning that the markup looks something like this:

<p>Read <a href="https://css-tricks.com/archives/">all our articles</a>.</p>

…and neither this:

<p>Read <a href="/archives/">all our articles</a>.</p>

…nor this:

<p>Read <a href="archives/">all our articles</a>.</p>

This is hard to do automatically, moreso with statically-generated sites. One approach is to make relative URLs absolute after compiling the feed in a build pipeline. Another approach is to manipulate the way Markdown links and images are rendered by your static site generator so that the URLs are absolute. I hope that more static site generators allow the second option but, for now, Hugo is the only static site generator that supports this through Markdown render hooks.

But wait, there’s an exception to this rule. And it’s footnotes. Some readers can detect footnotes and handle them. Here’s some HTML that should work in any feed reader that supports relative jump links:

<p>They’d managed to place 27.9MB of images onto the Critical Path.  Almost 30MB of previously non-render blocking assets had just been  turned into blocking ones on purpose with no escape hatch. Start  render time was as high as 27.1s over a cable connection<sup id="fnref:1">   <a href="#fn:1" class="footnote">1</a></sup>.</p>  <div class="footnotes">   <ol>     <li id="fn:1">      <p>5Mb up, 1Mb down, 28ms RTT. <a href="#fnref:1" class="reversefootnote">↩</a></p>     </li>   </ol> </div>

How to handle ads in feeds

You’re unlikely to get JavaScript support inside of an RSS reader, and that means no ads connected to an ad server. In other words, ads will need to be part of your content rather than something that is dynamically injected into place.

Showing an advertisement for anima that displays a brightly colored image, text below the image that says our sponsor, followed by the post title in blue, a blurb from the article content, them a blue learn more link.
The Codrops newsletter is an example that does this well. The newsletter includes a sponsored image and text, and clearly indicates the content is sponsored.

PSA: Not all content needs to be included in a feed

I’ve seen feeds in which every piece of content published is packed in and made available all the way back to the very first entry in the feed. I also see plenty of feeds from publishers who post dozens of entries a day. In both cases, I suggest limiting both the amount of content that’s available from past archives and considering multiple feeds instead of one.

Perfect example. Check out MacRumors.com’s feed because it’s extremely active with dozens of new articles published daily. Can you imagine going back to an article from, say, 10 years ago in that feed? Likely not. Unless the feed is for a podcast where storing every episode makes sense, try limiting the number of entries stored in your feed, as users are likely more interested in newer content. This reduces bandwidth and reduces update times which especially counts since users have many feeds to refresh.

I am tempted to say that 10–15 posts is enough to store and display at a time, but like many things, “it depends.” While storing a few makes sense for a site that pushes new content a few times a month, other sites that post way more frequently might eclipse that in a day. So, really, the ideal number of posts is going to depend on the type of content you publish (is it timely or evergreen?) and both the volume and frequency of what you publish (is it a lot throughout the day or a few times a month?).

But what I’m really trying to get at is that you want to avoid overwhelming users by inundating them with a pile of articles to get through. A couple of ways to avoid that include:

  • displaying summaries instead of full content (see, another exception to a previous rule!), and
  • filtering content so that users can choose from multiple feeds for specific types of content. In other words, if you want to provide every single post (or a complete timeline on a particular topic), consider making a dedicated feed for that topic/category/tag/whatever.

The reason I’m so fussy about the size of a feed is that—like images, scripts, and other assets—the number and size of feeds affect the performance of a feed reader. The more feeds a user is subscribed to and the more entries that need to be fetched from those feeds add to the time it takes to refresh and display that content.

Moving feeds

Like websites might change domains, you may need to move a feed from its current address. It’s not terribly difficult to do, but there are important things to consider before making a move.

For instance, it’s a good idea to ensure that your feed’s items have a global unique identifier (GUID). This maps out to feed’s guid in RSS and its id in both Atom and JSON. The GUI prevents feed readers from fetching duplicate entries. This is all the more important (and challenging) if you’re working with a static site.

While it may be tempting to use the entry’s permalink as an identifier, remember, those can change. To make a GUID, I’d recommend looking into using a tag URI. A tag URI consists of:

  • an authority (i.e., the domain of the site)
  • a date (that indicates a point in time that the tagging entity controlled the authority name associated with the feed)
  • the specific URL to fetch
  • a fragment (which might be a sub resource or a timestamp)
tag:<authority>,<YYYY-MM-DD>:<specific>#<fragment>

The <specific> portion could be something like the relative portion of your site’s homepage URL (i.e. /) and the fragment can be the content’s published timestamp. For instance, a post here on CSS Tricks could have a tag URI that looks like this:

tag:css-tricks.com,2021-16-11:/#1637082038781

This way, the authority date ensures that even if the domain changed hands. Plus, it can be managed in a static site generator as you can track domain changes over time.

The biggest reason I suggest the tag URI scheme is that Atom requires a feed’s id to be in a URL format. Even though RSS and JSON don’t have the same constraint, the tag URI scheme works for them as well, meaning we have full support.

And, with a robust id in place, a feed can be safely moved without feed readers pulling in duplicate entries. To move the feed itself, set up a 301 redirect to the new location and you’re done.

You might come across a technique called the XML redirect in which a file containing the feed’s new location is placed at the old location. As great as this would work for times when you can’t manipulate HTTP codes, I couldn’t find any feed readers which implement this.

Validating a feed

Feeds, like HTML, need to be valid in order to properly work. The benefit of a validated feed is that you know your code is free from errors and that entries are properly flowing from your site to feed readers.

W3C’s feed validation service is one option for RSS and Atom feeds. You provide the URL to the feed or paste the feed’s actual code, and you’ll get a full report that shows whether you’re hitting all the best practices. You’re likely to get warnings. It happens. Most warnings are really just a heads up and might not have an impact on the feed.

That said, there are two things that should always be addressed when validating a feed:

  • item should contain a guid element: The unique identifier, as we saw, prevents a feed reader from showing the same entry twice when a feed moves.
  • element should contain absolute URL references—these are hard for readers to resolve, so avoid relative URLs where possible.

What about JSON? To validate a JSON feed, try either using validator.jsonfeed.org or verifying against the JSON Feed schema using any JSON schema validator.

Managing or restricting access to a feed

You know how you can subscribe to a paid podcast and you get access to a special feed URL that contains all the “premium” content you gain access to with your subscription? Well, that’s because we can control who has access to a particular feed while locking others out from receiving the content.

There are two techniques for managing access to a feed:

  • HTTP basic authentication requires a username and password, which are either prompted for the user to provide or inferred in the feed URL itself, e.g. https://username:password@domain.example/path.
  • Providing a token as a query parameter, e.g. http://domain.com/path?token=xyz

As long as the URL is HTTPS, they have the same security, as the URL paths and passwords are encrypted. As for handling authentication it on the server, that’s a whole other topic though there are quite a few articles on it right here on CSS-Tricks.

Join the RSS Club!

OK, so the first rule of the RSS Club is:

Don’t talk about it. Let people find it. Make it worthwhile.

But I’m going to talk about it because feeds that are part of the RSS Club are excellent examples of tailored feeds. That’s because the feed entries are only available in those feeds. In other words, the blog posts are published, but never display on the actual site — they’re only accessible by feed.

Dave Rupert founded the club a number of years ago and it’s a great way to make RSS a first-class citizen for consuming content within a small community.

Joining the club means having a dedicated feed for posts that are only available in that feed. For example, in WordPress, you could create a new “RSS Club” category and filter it out of the main post query. That way, you’re able to either provide a feed just for that category, or the full feed that still includes posts in that category.

(Sorry for spilling the beans, Dave!)

Web feeds beyond content

RSS can be used for more things than blog posts or articles. For instance, GitHub has Atom feeds for issues, commits, pull requests, and releases.

They can also be used to provide updates. Let’s say you wanted a feed that notifies you when there are changes to your website. That’s a great idea, right? Always nice to know what’s happening, especially when there’s more than one cook in the kitchen.

You could build some sort of system that polls your feed periodically for changes then trigger a new feed entry, but that requires a lot of resources. Another idea is to implement webhooks you tell where to look for changes. Then again, managing and sending out notifications can be a hassle, especially if all you want is to monitor content.

I think it’s worth checking out WebSub. You, as a publisher, tell a hub that the site has changed, and the hub notifies whatever system that’s subscribed to the site’s web feeds. You can publish your feed to an existing hub — like Google’s PubSubHubbub Hub — then specify the hub in your feeds. YouTube has implemented this.

WebSub Flow Diagram, Julien Genestoux
Copyright © 2018 World Wide Web Consortium, (MIT, ERCIM, Keio, Beihang).

Examples!

What’s a tutorial like this without a few good examples? Let’s look at three real-world examples.

1. RSS Podcast

Did you know that CSS-Tricks has a podcast for an ongoing series that covers web history? Well, it does. And, yes, you can subscribe to it via RSS.

Podcasts must use RSS with the xmlns:content and xmlns:itunes extensions, which are needed to provide metadata about the podcast and its episodes. The audio file for each episode is specified in an enclosure along with its mime type and size. RSS is limited to one enclosure, but both Atom and JSON support multiple enclosures.

Here’s the feed. Notice the iTunes-specific tags as well as other bits of information that are provided for additional context:

<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd">   <channel>     <atom:link href="https://adactio.s3.amazonaws.com/audio/narration/web_history/podcast.xml" rel="self" type="application/rss+xml" />     <title>Web History</title>     <link>https://css-tricks.com/category/history/</link>     <language>en</language>     <copyright>2020</copyright>     <description>Written by Jay Hoffmann and narrated by Jeremy Keith.</description>     <image>       <url>https://adactio.s3.amazonaws.com/audio/narration/web_history/WebHistoryPodcast.jpg</url>       <title>Web History</title>       <link>https://css-tricks.com/category/history/</link>     </image>     <itunes:author>Jay Hoffman</itunes:author>     <itunes:summary>The history of the web.</itunes:summary>     <itunes:explicit>no</itunes:explicit>     <itunes:type>episodic</itunes:type>     <itunes:owner>       <itunes:name>Jeremy Keith</itunes:name>       <itunes:email>jeremy@adactio.com</itunes:email>     </itunes:owner>     <itunes:image href="https://adactio.s3.amazonaws.com/audio/narration/web_history/WebHistoryPodcast.jpg"/>     <itunes:category text="Technology"></itunes:category>     <item>       <title>Chapter 10: Browser Wars</title>       <description>In June of 1995, representatives from Microsoft arrived at the Netscape offices. The stated goal was to find ways to work together—Netscape as the single dominant force in the browser market and Microsoft as a tech giant just beginning to consider the implications of the Internet. Both groups, however, were suspicious of ulterior motives.</description>       <pubDate>Mon, 8 Nov 2021 12:00:00 -0000</pubDate>       <link>https://css-tricks.com/chapter-10-browser-wars/</link>       <itunes:title>Chapter 10: Browser Wars</itunes:title>       <itunes:episode>10</itunes:episode>       <itunes:episodeType>full</itunes:episodeType>       <itunes:author>Jay Hoffman</itunes:author>       <itunes:summary>In June of 1995, representatives from Microsoft arrived at the Netscape offices. The stated goal was to find ways to work together—Netscape as the single dominant force in the browser market and Microsoft as a tech giant just beginning to consider the implications of the Internet. Both groups, however, were suspicious of ulterior motives.</itunes:summary>       <content:encoded>         <![CDATA[           <p>In June of 1995, representatives from Microsoft arrived at the Netscape offices. The stated goal was to find ways to work together—Netscape as the single dominant force in the browser market and Microsoft as a tech giant just beginning to consider the implications of the Internet. Both groups, however, were suspicious of ulterior motives.</p>         ]]>       </content:encoded>       <itunes:duration>00:40:40</itunes:duration>       <guid>https://adactio.s3.amazonaws.com/audio/narration/web_history/Chapter_10_Browser_Wars.mp3</guid>       <enclosure url="https://adactio.s3.amazonaws.com/audio/narration/web_history/Chapter_10_Browser_Wars.mp3" length="19608877" type="audio/mpeg"/>     </item> </channel> </rss>

2. RSS for posts

Let’s look to CSS-Tricks once again, this time for an example of what a pretty standard RSS feed of blog posts looks like.

The code for this particular RSS feed is a little more verbose than your typical feed, and that’s to do with the multiple extensions added to the <rss> tag. A number of them aren’t reachable but there are some that handle other things, like xmlns:wfw for comments, xmlns:dc for additional metadata, and xmlns:sy for information on how often the feed is refreshed.

<?xml version="1.0" encoding="UTF-8"?> <rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:wfw="http://wellformedweb.org/CommentAPI/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:sy="http://purl.org/rss/1.0/modules/syndication/" xmlns:slash="http://purl.org/rss/1.0/modules/slash/" xmlns:georss="http://www.georss.org/georss" xmlns:geo="http://www.w3.org/2003/01/geo/wgs84_pos#">   <channel>     <title>CSS-Tricks</title>     <atom:link href="https://css-tricks.com/feed/" rel="self" type="application/rss+xml" />     <link>https://css-tricks.com</link>     <description>Tips, Tricks, and Techniques on using Cascading Style Sheets.</description>     <lastBuildDate>Fri, 19 Nov 2021 15:13:49 +0000</lastBuildDate>     <language>en-US</language>     <sy:updatePeriod>   hourly  </sy:updatePeriod>     <sy:updateFrequency>   1 </sy:updateFrequency>     <generator>https://wordpress.org/?v=5.8.2</generator>     <image>       <url>https://i1.wp.com/css-tricks.com/wp-content/uploads/2021/07/star.png?fit=32%2C32&ssl=1</url>       <title>CSS-Tricks</title>       <link>https://css-tricks.com</link>       <width>32</width>       <height>32</height>     </image>     <site xmlns="com-wordpress:feed-additions:1">45537868</site>     <item>       <title>Parallax Powered by CSS Custom Properties</title>       <link>https://css-tricks.com/parallax-powered-by-css-custom-properties/</link>       <comments>https://css-tricks.com/parallax-powered-by-css-custom-properties/#respond</comments>       <dc:creator>         <![CDATA[Jhey Tompkins]]>       </dc:creator>       <pubDate>Fri, 19 Nov 2021 15:13:46 +0000</pubDate>       <category>         <![CDATA[Article]]>       </category>       <category>         <![CDATA[animation]]>       </category>       <category>         <![CDATA[custom properties]]>       </category>       <category>         <![CDATA[GSAP]]>       </category>       <guid isPermaLink="false">https://css-tricks.com/?p=357192</guid>       <description>         <![CDATA[ ]]>       </content:encoded>       <wfw:commentRss>https://css-tricks.com/parallax-powered-by-css-custom-properties/feed/</wfw:commentRss>       <slash:comments>0</slash:comments>        <post-id xmlns="com-wordpress:feed-additions:1">357192</post-id>     </item>   </channel> </rss>

3. JSON feed

This is actually my personal feed and I just so happen to use JSON for it. It’s pretty bare bones and less cluttered than the other examples because, as far as I know, there are no JSON extensions like the RSS ones we saw in Example 2.

I find that JSON is much easier to read and understand all that’s needed is an object with the feed data rather than writing out the entire template.

{   "author": {     "name": "Farai Gandiya"   },   "feed_url": "https://codelab.farai.xyz/feed.json",   "home_page_url": "https://codelab.farai.xyz/",   "icon": "https://codelab.farai.xyz/fcl-logo.png",   "items": [     {       "content_html": "...",       "date_modified": "2021-11-13T05:26:07+02:00",       "date_published": "2021-11-13T05:26:07+02:00",       "id": "https://codelab.farai.xyz/1636773967",       "summary": "...",       "title": "Don't be afraid of the Big Long Page by Amy Hupe, content designer.",       "url": "https://codelab.farai.xyz/links/long-content-ok/"     }   ] }

Web Feed Implementations Across CMSs and Static Site Generators

Many CMSs and static site generators support web feeds, though it’s usually RSS as it has the widest support. Here are some CMSs that support web feeds:

And here’s some resources on adding web feeds (again mostly RSS) to various static site generators

Wrapping up

And there you have it! This is what I believe be nearly everything you need to consider when implementing a web feed. We looked at three different formats (RSS, Atom, JSON), covered best practices for creating a user-friendly feed reading experience, walked through validating a feed, covered the possibility of authenticating feeds, looked at three real-world examples of feeds in the wild, and provided some implementations across various technologies.

(Oh, and there was that thing where the first rule of the thing is not to talk about the thing.)

I hope these guidelines empower you to make resilient web feeds. If you have any questions on implementing a web feed, or you just feel like sharing your RSS feed, please do leave a comment!

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]

My tiny side project has had more impact than my decade in the software industry

That’s a heartwrenching title from Michael Williamson. I believe it though. It’s kinda like a maximized version of the blogging phenomenon where if you work on a post for weeks it’ll flop compared to a post that’s some dumb 20-minute thought. Or how your off-handed remark to some developer at the perfect time might cause some huge pivot in what they are doing, changing the course of a project forever. For Mike, it was a 3,000 line-of-code side project that had more impact on the world than a career of work as a software developer.

I’ve tried to pick companies working on domains that seem useful: developer productivity, treating diseases, education. While my success in those jobs has been variable – in some cases, I’m proud of what I accomplished, in others I’m pretty sure my net effect was, at best, zero – I’d have a tough time saying that the cumulative impact was greater than my little side project.

Impact is fuzzy though, isn’t it? I don’t know Mike, but assuming he is a kind and helpful person, think of all the people he’s likely helped along the way. Not by just saving them minutes of toil, but helped. Helped grow, helped through hard times, helped guide to where they ought to go. Those things are immeasurable and awfully important.

Direct Link to ArticlePermalink


The post My tiny side project has had more impact than my decade in the software industry appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , , , , , ,
[Top]

Using Web Components in WordPress is Easier Than You Think

Now that we’ve seen that web components and interactive web components are both easier than you think, let’s take a look at adding them to a content management system, namely WordPress.

There are three major ways we can add them. First, through manual input into the siteputting them directly into widgets or text blocks, basically anywhere we can place other HTML. Second, we can add them as the output of a theme in a theme file. And, finally, we can add them as the output of a custom block.

Loading the web component files

Now whichever way we end up adding web components, there’s a few things we have to ensure:

  1. our custom element’s template is available when we need it,
  2. any JavaScript we need is properly enqueued, and
  3. any un-encapsulated styles we need are enqueued.

We’ll be adding the <zombie-profile> web component from my previous article on interactive web components. Check out the code over at CodePen.

Let’s hit that first point. Once we have the template it’s easy enough to add that to the WordPress theme’s footer.php file, but rather than adding it directly in the theme, it’d be better to hook into wp_footer so that the component is loaded independent of the footer.php file and independent of the overall theme— assuming that the theme uses wp_footer, which most do. If the template doesn’t appear in your theme when you try it, double check that wp_footer is called in your theme’s footer.php template file.

<?php function diy_ezwebcomp_footer() { ?>   <!-- print/echo Zombie profile template code. -->   <!-- It's available at https://codepen.io/undeadinstitute/pen/KKNLGRg --> <?php }  add_action( 'wp_footer', 'diy_ezwebcomp_footer');

Next is to enqueue our component’s JavaScript. We can add the JavaScript via wp_footer as well, but enqueueing is the recommended way to link JavaScript to WordPress. So let’s put our JavaScript in a file called ezwebcomp.js (that name is totally arbitrary), stick that file in the theme’s JavaScript directory (if there is one), and enqueue it (in the functions.php file).

wp_enqueue_script( 'ezwebcomp_js', get_template_directory_uri() . '/js/ezwebcomp.js', '', '1.0', true ); 

We’ll want to make sure that last parameter is set to true , i.e. it loads the JavaScript before the closing body tag. If we load it in the head instead, it won’t find our HTML template and will get super cranky (throw a bunch of errors.)

If you can fully encapsulate your web component, then you can skip this next step. But if you (like me) are unable to do it, you’ll need to enqueue those un-encapsulated styles so that they’re available wherever the web component is used. (Similar to JavaScript, we could add this directly to the footer, but enqueuing the styles is the recommended way to do it). So we’ll enqueue our CSS file:

wp_enqueue_style( 'ezwebcomp_style', get_template_directory_uri() . '/ezwebcomp.css', '', '1.0', 'screen' ); 

That wasn’t too tough, right? And if you don’t plan to have any users other than Administrators use it, you should be all set for adding these wherever you want them. But that’s not always the case, so we’ll keep moving ahead!

Don’t filter out your web component

WordPress has a few different ways to both help users create valid HTML and prevent your Uncle Eddie from pasting that “hilarious” picture he got from Shady Al directly into the editor (complete with scripts to pwn every one of your visitors).

So when adding web-components directly into blocks or widgets, we’ll need to be careful about WordPress’s built-in code filtering . Disabling it all together would let Uncle Eddie (and, by extension, Shady Al) run wild, but we can modify it to let our awesome web component through the gate that (thankfully) keeps Uncle Eddie out.

First, we can use the wp_kses_allowed filter to add our web component to the list of elements not to filter out. It’s sort of like we’re whitelisting the component, and we do that by adding it to the the allowed tags array that’s passed to the filter function.

function add_diy_ezwebcomp_to_kses_allowed( $ the_allowed_tags ) {   $ the_allowed_tags['zombie-profile'] = array(); } add_filter( 'wp_kses_allowed_html', 'add_diy_ezwebcomp_to_kses_allowed'); 

We’re adding an empty array to the <zombie-profile> component because WordPress filters out attributes in addition to elements—which brings us to another problem: the slot attribute (as well as part and any other web-component-ish attribute you might use) are not allowed by default. So, we have to explitcly allow them on every element on which you anticipate using them, and, by extension, any element your user might decide to add them to. (Wait, those element lists aren’t the same even though you went over it six times with each user… who knew?) Thus, below I have set slot to true on <span>, <img> and <ul>, the three elements I’m putting into slots in the <zombie-profile> component. (I also set part to true on span elements so that I could let that attribute through too.)

function add_diy_ezwebcomp_to_kses_allowed( $ the_allowed_tags ) {   $ the_allowed_tags['zombie-profile'] = array();   $ the_allowed_tags['span']['slot'] = true;   $ the_allowed_tags['span']['part'] = true;   $ the_allowed_tags['ul']['slot'] = true;   $ the_allowed_tags['img']['slot'] = true;   return $ the_allowed_tags; } add_filter( 'wp_kses_allowed_html', 'add_diy_ezwebcomp_to_kses_allowed'); 

We could also enable the slot (and part) attribute in all allowed elements with something like this:

function add_diy_ezwebcomp_to_kses_allowed($ the_allowed_tags) {   $ the_allowed_tags['zombie-profile'] = array();   foreach ($ the_allowed_tags as &$ tag) {     $ tag['slot'] = true;     $ tag['part'] = true;   }   return $ the_allowed_tags; } add_filter('wp_kses_allowed_html', 'add_diy_ezwebcomp_to_kses_allowed'); 

Sadly, there is one more possible wrinkle with this. You may not run into this if all the elements you’re putting in your slots are inline/phrase elements, but if you have a block level element to put into your web component, you’ll probably get into a fistfight with the block parser in the Code Editor. You may be a better fist fighter than I am, but I always lost.

The code editor is an option that allows you to inspect and edit the markup for a block.

For reasons I can’t fully explain, the client-side parser assumes that the web component should only have inline elements within it, and if you put a <ul> or <div>, <h1> or some other block-level element in there, it’ll move the closing web component tag to just after the last inline/phrase element. Worse yet, according to a note in the WordPress Developer Handbook, it’s currently “not possible to replace the client-side parser.”

While this is frustrating and something you’ll have to train your web editors on, there is a workaround. If we put the web component in a Custom HTML block directly in the Block Editor, the client-side parser won’t leave us weeping on the sidewalk, rocking back and forth, and questioning our ability to code… Not that that’s ever happened to anyone… particularly not people who write articles…

Component up the theme

Outputting our fancy web component in our theme file is straightforward as long as it isn’t updated outside the HTML block. We add it the way we would add it in any other context, and, assuming we have the template, scripts and styles in place, things will just work.

But let’s say we want to output the contents of a WordPress post or custom post type in a web component. You know, write a post and that post is the content for the component. This allows us to use the WordPress editor to pump out an archive of <zombie-profile> elements. This is great because the WordPress editor already has most of the UI we need to enter the content for one of the <zombie-profile> components:

  • The post title can be the zombie’s name.
  • A regular paragraph block in the post content can be used for the zombie’s statement.
  • The featured image can be used for the zombie’s profile picture.

That’s most of it! But we’ll still need fields for the zombie’s age, infection date, and interests. We’ll create these with WordPress’s built in Custom Fields feature.

We’ll use the template part that handles printing each post, e.g. content.php, to output the web component. First, we’ll print out the opening <zombie-profile> tag followed by the post thumbnail (if it exists).

<zombie-profile>   <?php      // If the post featured image exists...     if (has_post_thumbnail()) {       $ src = wp_get_attachment_image_url(get_post_thumbnail_id()); ?>       <img src="<?php echo $ src; ?>" slot="profile-image">     <?php     }   ?>

Next we’ll print the title for the name

<?php   // If the post title field exits...   if (get_the_title()) { ?>   <span slot="zombie-name"><?php echo get_the_title(); ?></span>   <?php   } ?>

In my code, I have tested whether these fields exist before printing them for two reasons:

  1. It’s just good programming practice (in most cases) to hide the labels and elements around empty fields.
  2. If we end up outputting an empty <span> for the name (e.g. <span slot="zombie-name"></span>), then the field will show as empty in the final profile rather than use our web component’s built-in default text, image, etc. (If you want, for instance, the text fields to be empty if they have no content, you can either put in a space in the custom field or skip the if statement in the code).

Next, we will grab the custom fields and place them into the slots they belong to. Again, this goes into the theme template that outputs the post content.

<?php   // Zombie age   $ temp = get_post_meta(the_ID(), 'Age', true);   if ($ temp) { ?>     <span slot="z-age"><?php echo $ temp; ?></span>     <?php   }   // Zombie infection date   $ temp = get_post_meta(the_ID(), 'Infection Date', true);   if ($ temp) { ?>     <span slot="idate"><?php echo $ temp; ?></span>     <?php   }   // Zombie interests   $ temp = get_post_meta(the_ID(), 'Interests', true);   if ($ temp) { ?>     <ul slot="z-interests"><?php echo $ temp; ?></ul>     <?php   } ?>

One of the downsides of using the WordPress custom fields is that you can’t do any special formatting, A non-technical web editor who’s filling this out would need to write out the HTML for the list items (<li>) for each and every interest in the list. (You can probably get around this interface limitation by using a more robust custom field plugin, like Advanced Custom Fields, Pods, or similar.)

Lastly. we add the zombie’s statement and the closing <zombie-profile> tag.

<?php   $ temp = get_the_content();   if ($ temp) { ?>     <span slot="statement"><?php echo $ temp; ?></span>   <?php   } ?> </zombie-profile>

Because we’re using the body of the post for our statement, we’ll get a little extra code in the bargain, like paragraph tags around the content. Putting the profile statement in a custom field will mitigate this, but depending on your purposes, it may also be intended/desired behavior.

You can then add as many posts/zombie profiles as you need simply by publishing each one as a post!

Block party: web components in a custom block

Creating a custom block is a great way to add a web component. Your users will be able to fill out the required fields and get that web component magic without needing any code or technical knowledge. Plus, blocks are completely independent of themes, so really, we could use this block on one site and then install it on other WordPress sites—sort of like how we’d expect a web component to work!

There are the two main parts of a custom block: PHP and JavaScript. We’ll also add a little CSS to improve the editing experience.

First, the PHP:

function ez_webcomp_register_block() {   // Enqueues the JavaScript needed to build the custom block   wp_register_script(     'ez-webcomp',     plugins_url('block.js', __FILE__),     array('wp-blocks', 'wp-element', 'wp-editor'),     filemtime(plugin_dir_path(__FILE__) . 'block.js')   );    // Enqueues the component's CSS file   wp_register_style(     'ez-webcomp',     plugins_url('ezwebcomp-style.css', __FILE__),     array(),     filemtime(plugin_dir_path(__FILE__) . 'ezwebcomp-style.css')   );    // Registers the custom block within the ez-webcomp namespace   register_block_type('ez-webcomp/zombie-profile', array(     // We already have the external styles; these are only for when we are in the WordPress editor     'editor_style' =&gt; 'ez-webcomp',     'editor_script' =&gt; 'ez-webcomp',   )); } add_action('init', 'ez_webcomp_register_block'); 

The CSS isn’t necessary, it does help prevent the zombie’s profile image from overlapping the content in the WordPress editor.

/* Sets the width and height of the image.  * Your mileage will likely vary, so adjust as needed.  * "pic" is a class we'll add to the editor in block.js */ #editor .pic img {   width: 300px;   height: 300px; } /* This CSS ensures that the correct space is allocated for the image,  * while also preventing the button from resizing before an image is selected. */ #editor .pic button.components-button {    overflow: visible;   height: auto; } 

The JavaScript we need is a bit more involved. I’ve endeavored to simplify it as much as possible and make it as accessible as possible to everyone, so I’ve written it in ES5 to remove the need to compile anything.

Show code
(function (blocks, editor, element, components) {   // The function that creates elements   var el = element.createElement;   // Handles text input for block fields    var RichText = editor.RichText;   // Handles uploading images/media   var MediaUpload = editor.MediaUpload;        // Harkens back to register_block_type in the PHP   blocks.registerBlockType('ez-webcomp/zombie-profile', {     title: 'Zombie Profile', //User friendly name shown in the block selector     icon: 'id-alt', //the icon to usein the block selector     category: 'layout',     // The attributes are all the different fields we'll use.     // We're defining what they are and how the block editor grabs data from them.     attributes: {       name: {         // The content type         type: 'string',         // Where the info is available to grab         source: 'text',         // Selectors are how the block editor selects and grabs the content.         // These should be unique within an instance of a block.         // If you only have one img or one <ul> etc, you can use element selectors.         selector: '.zname',       },       mediaID: {         type: 'number',       },       mediaURL: {         type: 'string',         source: 'attribute',         selector: 'img',         attribute: 'src',       },       age: {         type: 'string',         source: 'text',         selector: '.age',       },       infectdate: {         type: 'date',         source: 'text',         selector: '.infection-date'       },       interests: {         type: 'array',         source: 'children',         selector: 'ul',       },       statement: {         type: 'array',         source: 'children',         selector: '.statement',       },   },   // The edit function handles how things are displayed in the block editor.   edit: function (props) {     var attributes = props.attributes;     var onSelectImage = function (media) {       return props.setAttributes({         mediaURL: media.url,         mediaID: media.id,       });     };     // The return statement is what will be shown in the editor.     // el() creates an element and sets the different attributes of it.     return el(       // Using a div here instead of the zombie-profile web component for simplicity.       'div', {         className: props.className       },       // The zombie's name       el(RichText, {         tagName: 'h2',         inline: true,         className: 'zname',         placeholder: 'Zombie Name…',         value: attributes.name,         onChange: function (value) {           props.setAttributes({             name: value           });         },       }),       el(         // Zombie profile picture         'div', {           className: 'pic'         },         el(MediaUpload, {           onSelect: onSelectImage,           allowedTypes: 'image',           value: attributes.mediaID,           render: function (obj) {             return el(               components.Button, {                 className: attributes.mediaID ?                   'image-button' : 'button button-large',                 onClick: obj.open,               },               !attributes.mediaID ?               'Upload Image' :               el('img', {                 src: attributes.mediaURL               })             );           },         })       ),       // We'll include a heading for the zombie's age in the block editor       el('h3', {}, 'Age'),       // The age field       el(RichText, {         tagName: 'div',         className: 'age',         placeholder: 'Zombie's Age…',         value: attributes.age,         onChange: function (value) {           props.setAttributes({             age: value           });         },       }),       // Infection date heading       el('h3', {}, 'Infection Date'),       // Infection date field       el(RichText, {         tagName: 'div',         className: 'infection-date',         placeholder: 'Zombie's Infection Date…',         value: attributes.infectdate,         onChange: function (value) {           props.setAttributes({             infectdate: value           });         },       }),       // Interests heading       el('h3', {}, 'Interests'),       // Interests field       el(RichText, {         tagName: 'ul',         // Creates a new <li> every time `Enter` is pressed         multiline: 'li',         placeholder: 'Write a list of interests…',         value: attributes.interests,         onChange: function (value) {           props.setAttributes({             interests: value           });         },         className: 'interests',       }),       // Zombie statement heading       el('h3', {}, 'Statement'),       // Zombie statement field       el(RichText, {         tagName: 'div',         className: "statement",         placeholder: 'Write statement…',         value: attributes.statement,         onChange: function (value) {           props.setAttributes({             statement: value           });         },       })     );   },    // Stores content in the database and what is shown on the front end.   // This is where we have to make sure the web component is used.   save: function (props) {     var attributes = props.attributes;     return el(       // The <zombie-profile web component       'zombie-profile',       // This is empty because the web component does not need any HTML attributes       {},       // Ensure a URL exists before it prints       attributes.mediaURL &&       // Print the image       el('img', {         src: attributes.mediaURL,         slot: 'profile-image'       }),       attributes.name &&       // Print the name       el(RichText.Content, {         tagName: 'span',         slot: 'zombie-name',         className: 'zname',         value: attributes.name,       }),       attributes.age &&       // Print the zombie's age       el(RichText.Content, {         tagName: 'span',         slot: 'z-age',         className: 'age',         value: attributes.age,     }),       attributes.infectdate &&       // Print the infection date       el(RichText.Content, {         tagName: 'span',         slot: 'idate',         className: 'infection-date',         value: attributes.infectdate,     }),       // Need to verify something is in the first element since the interests's type is array       attributes.interests[0] &&       // Pint the interests       el(RichText.Content, {         tagName: 'ul',         slot: 'z-interests',         value: attributes.interests,       }),       attributes.statement[0] &&       // Print the statement       el(RichText.Content, {         tagName: 'span',         slot: 'statement',         className: 'statement',         value: attributes.statement,     })     );     },   }); })(   //import the dependencies   window.wp.blocks,   window.wp.blockEditor,   window.wp.element,   window.wp.components );

Plugging in to web components

Now, wouldn’t it be great if some kind-hearted, article-writing, and totally-awesome person created a template that you could just plug your web component into and use on your site? Well that guy wasn’t available (he was off helping charity or something) so I did it. It’s up on github:

Do It Yourself – Easy Web Components for WordPress

The plugin is a coding template that registers your custom web component, enqueues the scripts and styles the component needs, provides examples of the custom block fields you might need, and even makes sure things are styled nicely in the editor. Put this in a new folder in /wp-content/plugins like you would manually install any other WordPress plugin, make sure to update it with your particular web component, then activate it in WordPress on the “Installed Plugins” screen.

Not that bad, right?

Even though it looks like a lot of code, we’re really doing a few pretty standard WordPress things to register and render a custom web component. And, since we packaged it up as a plugin, we can drop this into any WordPress site and start publishing zombie profiles to our heart’s content.

I’d say that the balancing act is trying to make the component work as nicely in the WordPress block editor as it does on the front end. We would have been able to knock this out with a lot less code without that consideration.

Still, we managed to get the exact same component we made in my previous articles into a CMS, which allows us to plop as many zombie profiles on the site. We combined our knowledge of web components with WordPress blocks to develop a reusable block for our reusable web component.

What sort of components will you build for your WordPress site? I imagine there are lots of possibilities here and I’m interested to see what you wind up making.

Article series

  1. Web Components Are Easier Than You Think
  2. Interactive Web Components Are Easier Than You Think
  3. Using Web Components in WordPress is Easier Than You Think

The post Using Web Components in WordPress is Easier Than You Think appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , , ,
[Top]

Equal Columns With Flexbox: It’s More Complicated Than You Might Think

You get a nice-looking design handed to you and it has this nice big hero section, followed by one of those three-up columns right beneath it. You know, like almost every other website you’ve ever worked on.

You bang through the hero section and get to work on the three-column section. It’s time to pull out our trusty friend flexbox! Except, you write display: flex and you get this mess instead of getting the three equal columns you’d expect.

This happens because of how flexbox calculates the base size of an element. You’ve probably read lots of flexbox tutorials, and many of them (including my own) are an overly simplistic example of what flexbox does without really digging into the complex things that flexbox does for us that we take for granted.

I’m positive you’ve seen examples and tutorials that look at something like this, where three divs shrink down around the content that’s inside them:

In other words, we get some block-level elements shrinking down and slotting next to one another. It feels like flex wants things to be as small as possible. But in reality, flexbox actually wants things to be as big as possible.

Wait, what? Flex shrinks things by default — that can’t be right! Right?

As awesome as flexbox is, what it’s doing under the hood is actually a little strange because, by default, it is doing two things at once. It first looks at the content size which is what we would get if by declaring width: max-content on an element. But on top of that, flex-shrink is also doing some work allowing the items to be smaller, but only if needed.

To really understand what’s going on, let’s break those two down and see how they work together.

Diving into max-content

max-content is a pretty handy property value in certain situations, but we want to understand how it works in this particular situation where we are trying to get three seemingly simple equal columns. So let’s strip away flexbox for a moment and look at what max-content does on its own.

MDN does a good job of explaining it:

The max-content sizing keyword represents the intrinsic maximum width of the content. For text content this means that the content will not wrap at all even if it causes overflows.

Intrinsic might throw you off here, but it basically means we’re letting the content decide on the width, rather than us explicitly setting a set width. Uri Shaked aptly describes the behavior by saying “the browser pretends it has infinite space, and lays all the text in a single line while measuring its width.”

So, bottom line, max-content allows the content itself to define the width of the element. If you have a paragraph, the width of that paragraph will be the text inside it without any line breaks. If it’s a long enough paragraph, then you end up with some horizontal scrolling.

Let’s revisit that overly-simplistic example of three block-level elements that shrink down and slot next to one another. That isn’t happening because of flex-shrink; it’s happening because that’s the size of those elements when their declared width is max-content. That’s literally as wide as they go because that’s as wide as the combined content inside each element.

Here, take a look at those elements without flexbox doing it’s flexbox stuff, but with a width: max-content on there instead:

So, when there’s just a small amount of text, the intrinsic max-content shrinks things down instead of flex-shrink. Of course, flexbox also comes in with it’s default flex-direction: row behavior which turns the flex items into columns, putting them right next to one another. Here’s another look but with the free space highlighted.

Adding flex-shrink to the equation

So we see that declaring display: flex pushes that max-content intrinsic size on flex items. Those items want to be as big as their content. But there is another default that comes in here as well, which is flex-shrink.

flex-shrink is basically looking at all the flex items in a flexible container to make sure they don’t overflow the parent. If the flex items can all fit next to each other without overflowing the flexible container, then flex-shrink won’t do anything at all… it’s job is already done.

But if the flex items do overflow the container (thanks to that max-content intrinsic width thing), the flex items are allowed to shrink to prevent that overflow because flex-shrink is looking out for that.

This is why flex-shrink has a default value of 1. Without it, things would get messy pretty quickly.

Here’s why the columns aren’t equal

Going back to our design scenario where we need three equal columns beneath a hero, we saw that the columns aren’t equal widths. That’s because flexbox starts by looking at the content size of each flex item before even thinking about shrinking them.

Using Firefox’s DevTools (because it’s got some unique flexbox visualizations that others don’t), we can actually see how flexbox is calculating everything.

For simplicity’s sake, as we dive deeper into this, let’s work with some nice round numbers. We can do this by declaring widths on our flex items. When we declare a width on a flex item, we throw that intrinsic size out the window, as we’ve now declared an explicit value instead. This makes figuring out what’s really going on a lot easier.

In the Pen below, we have a parent that’s a 600px wide flexible container (display: flex). I’ve removed anything that might influence the numbers, so no gap or padding. I’ve also switched out the border for an outline so we can still visualize everything easily.

The first and third flex items have a width: 300px and the middle one a width: 600px. If we add that all up, it’s a total of 1200px. That’s bigger than the the 600px available within the parent, so flex-shrink kicks in.

flex-shrink is a ratio. If everything has the same flex-shrink (which is 1 by default), they all shrink at the same rate. That doesn’t mean they all shrink to the same size or by the same amount, but they all shrink at the same rate.

If we jump back into Firefox DevTools, we can see the base size, the flex-shrink and the final size. In this case, the two 300px elements are now 150px, and the 600px one is now 300px.

The two elements that have a base width of 300px become 150px.

The larger element with a base width of 600px becomes 300px.

If we add up all the base sizes of all three flex items (the actual widths we declared on them), the total comes out to 1200px. Our flex container is 600px wide. If we divide everything by 2, it fits! They are all shrinking by the same rate, dividing their own widths by 2.

It’s not often that we have nice round numbers like that in the real world, but I think this does a nice job illustrating how flexbox does what it does when figuring out how big to make things.

Getting the columns to be equal

There are a few different ways to get the three columns we want to be equal in width, but some are better than others. For all the approaches, the basic idea is that we want to get all the columns base size to be the same. If they have an equal base size, then they will shrink (or grow, if we use flex-grow) at an equal rate when flexbox does it’s flex things, and in theory, that should make them the same size.

There are a few common ways to do this, but as I discovered while diving into all of this, I have come to believe those approaches are flawed. Let’s look at two of the most common solutions that I see used in the wild, and I’ll explain why they don’t work.

Method 1: Using flex: 1

One way we can try to get all the flex items to have the same base size is by declaring flex: 1 on all of them:

.flex-parent { display: flex; } .flex-parent > * { flex: 1; }

In a tutorial I made once, I used a different approach, and I must have had 100 people asking why I wasn’t using flex: 1 instead. I replied by saying I didn’t want to dive into the flex shorthand property. But then I used flex: 1 in a new demo, and to my surprise, it didn’t work.

The columns weren’t equal.

The middle column here is larger than the other two. It’s not by a ton, but the whole design pattern I’m creating is just so you have perfectly equal columns every single time, regardless of the content.

So why didn’t it work in this situation? The culprit here is the padding on the component in the middle column.

And maybe you’ll say it’s silly to add padding to one of the items and not the others, or that we can nest things (we’ll get to that). In my opinion, though, I should be able to have a layout that works regardless of the content that we’re putting in it. The web is all about components that we can plug and play these days, after all.

When I first set this up, I was sure it would work, and seeing this issue pop up made me want to learn what was really going on here.

The flex shorthand does more than just set the flex-grow: 1. If you don’t already know, flex is shorthand for flex-grow, flex-shrink, and flex-basis.

The default values for those constituent properties are:

.selector {   flex-grow: 0;   flex-shrink: 1;   flex-basis: auto; }

I’m not going to deep dive flex-basis in this article, as that’s something Robin has already done well. For now, we can think of it like width to keep things simple since we aren’t playing with flex-direction.

We’ve already seen how flex-shrink works. flex-grow is the opposite. If the size of all the flex items along the main axis is smaller than the parent, they can grow to fill that space.

So by default, flex items:

  • don’t grow;
  • if they would otherwise overflow the parent, they are allowed to shrink;
  • their width acts like max-content.

Putting that all together, the flex shorthand defaults to:

.selector {   flex: 0 1 auto; }

The fun thing with the flex shorthand is you can omit values. So, when we declare flex: 1, it’s setting the first value, flex-grow, to 1, which basically turns on flex-grow.

The strange thing here is what happens to the values that you omit. You’d expect them to stay at their defaults, but they don’t. Before we get to what happens, though, let’s first dive into what flex-grow even does.

As we’ve seen, flex-shrink allows elements to shrink if their base sizes add up to a computed value that’s bigger than the available width of the parent container. flex-grow is the opposite. If the grand total of the element base sizes is smaller than the value of the parent container’s width, then they will grow to fill the available space.

If we take that super basic example where we have three small divs next to one another and add flex-grow: 1, they grow to fill that leftover space.

But if we have three divs with unequal widths — like those ones we started with — adding flex-grow to them won’t do anything at all. They won’t grow because they’re already taking up all the available space —so much space, in fact, that flex-shrink needs to kick in and shrink them down to fit!

But, as folks have pointed out to me, setting flex: 1 can work to create equal columns. Well, sort of, as we saw above! In simple situations it does work though, as you can see below.

When we declare flex: 1 it works because, not only does this set the flex-grow to 1, but it also changes the flex-basis!

.selector {   flex: 1;   /* flex-grow: 1; */   /* flex-shrink: 1; */   /* flex-basis: 0%; Wait what? */ }

Yup, setting flex: 1 sets the flex-basis to 0%. This overwrites that intrinsic sizing we had before that behaved like max-content. All of our flex-items now want to have a base size of 0!

Looking at the expanded view of the flex: 1 shorthand in DevTools shows us that the flex-basis has changed to 0

So their base sizes are 0 now, but because of the flex-grow, they can all grow to fill up the empty space. And really, in this case, flex-shrink is no longer doing anything, as all the flex items now have a width of 0, and are growing to fill the available space.

FireFox’s DevTools showing an element with flex: 1 has a content size of 0 and is growing to fill the available space.

Just like the shrink example before, we’re taking the space that’s available, and letting all the flex items grow at an equal rate. Since they are all a base width of 0, growing at an equal rate means the available space is equally divided between them and they all have the same final size!

Except, as we saw, that’s not always the case…

The reason for this is because, when flexbox does all this stuff and distributes the space, whether it’s shrinking or growing a flex item, it’s looking at the content size of the element. If you remember back to the box model, we have the content size itself, then the padding, border, and margin outside of that.

And no, I didn’t forget * { box-sizing: border-box; }.

This is one of those strange quirks of CSS but it does make sense. If the browser looked at the width of those elements and included their padding and borders in the calculations, how could it shrink things down? Either the padding would also have to shrink or things are going to overflow the space. Both of those situations are terrible, so instead of looking at the box-size of elements when calculating the base size of them, it only looks at the content-box!

So, setting flex: 1 causes a problem in cases where you have borders or padding on some of your elements. I now have three elements that have a content-size of 0, but my middle one has padding on it. If we didn’t have that padding, we’d have the same math we did when we looked at how flex-shrink works.

A parent that is 600px and three flex items with a width of 0px. They all have a flex-grow: 1 so they grow at an equal rate, and they each end up 200px wide. But the padding mucks it all up. Instead, I end up with three divs with a content size of 0, but the middle one has padding: 1rem. That means it has a content size of 0, plus 32px padding as well.

We have 600 - 32 = 568px to divide equally, instead of 600px. All the divs want to grow at an equal rate, so 568 / 3 = 189.3333px.

And that’s what happens!

But… remember, that’s their content size, not the total width! That leaves us with two divs with a width of 189.333px, and another with a which of 189.333px + 32 = 221.333px. That’s a pretty substantial difference!

Method 2: flex-basis: 100%

I have always handled this like this:

.flex-parent {   display: flex; }  .flex-parent > * {   flex-basis: 100%; }

I thought this worked for the longest time. Actually, this was supposed to be my final solution after showing that flex: 1 doesn’t work. But while I was writing this article, I realized it also falls into the same problem, but it’s a lot less obvious. Enough so that I didn’t notice it with my eyes.

The elements are all trying to be 100% width, meaning they all want to match the width of the parent, which, again, is 600px (and in normal circumstances, is often much bigger, which further reduces the perceivable difference).

The thing is that 100% includes the padding in the computed values (because of * { box-size: border-box; }, which for the sake of this article, I’m assuming everyone is using). So, the outer divs end up with a content size of 600px, whereas the middle div ends up with a content size of 600 - 32 = 568px.

When the browser is working out how to evenly divide the space, it isn’t looking at how to evenly squish 1800px into a 600px space, but rather it’s looking at how to squish 1768px. Plus, as we saw earlier, flex items don’t shrink by the same amount, but at an equal pace! So, the element with padding shrinks slightly less in total than the others do.

This results in the .card having a final width of 214.483px while the others clock in at 192.75px. Again, this leads to unequal width values, though the difference is smaller than we saw with the flex: 1 solution.

Why CSS Grid is the better choice here

While all this is a little frustrating (and even a little confusing), it all happens for a good reason. If margins, padding, or borders changed sizes when flex gets involved, it would be a nightmare.

And maybe this means that CSS Grid might be a better solution to this really common design pattern.

I’ve long thought that flexbox was easier to pick up and start using than grid, but grid gives you more ultimate control in the long run, but that it’s a lot harder to figure out. I’ve changed my mind on that recently though, and I think not only does grid give us better control in this type of situation, but it’s actually more intuitive as well.

Normally, when we use grid, we explicitly declare our columns using grid-template-columns. We don’t have to do that though. We can make it behave a lot like flexbox does by using grid-auto-flow: column.

.grid-container {   display: grid;   grid-auto-flow: column; }

Just like that, we end up with the same type of behavior as throwing display: flex on there. Like flex, the columns can potentially be unbalanced, but the advantage with grid is that the parent has total control over everything. So, rather than the content of the items having an impact like they would in flexbox, we only need one more line of code and we can solve the problem:

.grid-container {   display: grid;   grid-auto-flow: column;   grid-auto-columns: 1fr; }

I love that this is all on the parent selector, and that we don’t have to select the children to help get the layout that we are after!

The interesting thing here is how fr units work. They are literally called flex units in the spec, and they work just like flexbox does in dividing up space;. The big difference: they’re looking at the other tracks to determine how much space they have, paying no attention to the content inside those tracks.

That’s the real magic here. By declaring grid-auto-columns: 1fr, we are in essence saying, “by default, all my columns should have an equal width,” which is what we’ve been after from the start!

But what about at small screens?

What I love with this approach is we can keep it super simple:

.grid-container {   display: grid;   gap: 1em; }  @media (min-width: 35em) {   grid-auto-flow: column;   grid-auto-columns: 1fr; }

And just like that, it works perfectly. And by declaring display: grid from the start, we can include the gap to maintain equal spacing, whether the children are rows or columns.

I also find this to be a lot more intuitive than changing the flex-direction within a media query to get a similar result. As much as I love flexbox (and I really do still think it has great use cases), the fact that declaring flex-direction: column creates rows, and vice versa, is a little counter-intuitive at first glance.

And of course, if you prefer rolling without media queries, there is nothing stopping you from taking this to the next level with the help of auto-fit, which would be similar to setting something up with flex-wrap (though not exactly the same):

.grid-container {   display: grid;   gap: 1em;   grid-template-columns: repeat(auto-fit, minmax(10em, 25em)); }

Making it work with flexbox

I realize that we can get around this with flexbox by nesting the element with the padding on it. We’ve done this since we started making layouts using floats, and Bootstrap really hammered home this type of design pattern.

<div class="container">   <div class="row">     <div class="col"> <div class="">... </div>     <div class="col"> <div class="element-with-padding">...</div> </div>     <div class="col"> ... </div>   </div> </div>

And there is nothing wrong with that. It works! But floats work too, and we’ve stopped using them for layouts as better solutions have been released in recent years.

One of the reasons that I love grid is because we can simplify our markup quite a bit. Just like we ditched floats for a better solution, I’d at least like to people to keep an open mind that maybe, just maybe, grid could be a better, and more intuitive solution for this type of design pattern.

Flexbox still has it’s place, of course

I still love flexbox, but I think its real power comes from times that we want to rely on the intrinsic width of the flex items, such as with navigations, or groups of elements, such as buttons or other items of varying width that you want to go next to one another.

In those situations, that behavior is an asset that makes things like even spacing between unequal items such a breeze! Flexbox is wonderful, and I have no plans to stop using.

In other situations though, when you find yourself fighting with how flexbox is trying to work, maybe you could turn to grid instead, even if it’s not a typical “2d” grid where you’re told you should be using it for.

People often tell me that they struggle to figure out grid because it’s too complicated, and while it can be, as we saw here, it doesn’t have to be.


The post Equal Columns With Flexbox: It’s More Complicated Than You Might Think appeared first on CSS-Tricks.

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

CSS-Tricks

, , , , , , , ,
[Top]

Interactive Web Components Are Easier Than You Think

In my last article, we saw that web components aren’t as scary as they seem. We looked at a super simple setup and made a zombie dating service profile, complete with a custom <zombie-profile> element. We reused the element for each profile and populated each one with unique info using the <slot> element.

Here’s how it all came together.

That was cool and a lot of fun (well, I had fun anyway…), but what if we take this idea one step further by making it interactive. Our zombie profiles are great, but for this to be a useful, post-apocalyptic dating experience you’d want to, you know, “Like” a zombie or even message them. That’s what we’re going to do in this article. We’ll leave swiping for another article. (Would swiping left be the appropriate thing for zombies?)

This article assumes a base level of knowledge about web components. If you’re new to the concept, that’s totally fine — the previous article should give you everything you need. Go ahead. Read it. I’ll wait. *Twiddles thumbs* Ready? Okay.

First, an update to the original version

Let’s pause for one second (okay, maybe longer) and look at the ::slotted() pseudo element. It was brought to my attention after the last article went out (thanks, Rose!) and it solves some (though not all) of the encapsulation issues I encountered. If you recall, we had some CSS styles outside of the component’s <template> and some inside a <style> element within the <template>. The styles inside the <template> were encapsulated but the ones outside were not.

But that’s where ::slotted comes into play. We declare an element in the selector like so:

::slotted(img) {   width: 100%;   max-width: 300px;   height: auto;   margin: 0 1em 0 0; }

Now, any <img> element placed in any slot will be selected. This helps a lot!

But this doesn’t solve all of our encapsulation woes. While we can select anything directly in a slot, we cannot select any descendant of the element in the slot. So, if we have a slot with children — like the interests section of the zombie profiles — we’re unable to select them from the <style> element. Also, while ::slotted has great browser support, some things (like selecting a pseudo element, e.g., ::slotted(span)::after) will work in some browsers (hello, Chrome), but won’t work in others (hello, Safari).

So, while it’s not perfect, ::slotted does indeed provide more encapsulation than what we had before. Here’s the dating service updated to reflect that:

Back to interactive web components!

First thing I’d like to do is add a little animation to spice things up. Let’s have our zombie profile pics fade in and translate up on load.

When I first attempted this, I used img and ::slotted(img) selectors to directly animate the image. But all I got was Safari support. Chrome and Firefox would not run the animation on the slotted image, but the default image animated just fine. To get it working, I wrapped the slot in a div with a .pic class and applied the animation to the div instead.

.pic {   animation: picfadein 1s 1s ease-in forwards;   transform: translateY(20px);   opacity: 0; }  @keyframes picfadein {   from { opacity: 0; transform: translateY(20px); }   to { opacity: 1; transform: translateY(0); } }

“Liking” zombies

Wouldn’t it be something to “Like” that cute zombie? I mean from the user’s perspective, of course. That seems like something an online dating service ought to have at the very least.

We’ll add a checkbox “button” that initiates a heart animation on click. Let’s add this HTML at the top of the .info div:

<input type="checkbox" id="trigger"><label class="likebtn" for="trigger">Like</label>

Here’s a heart SVG I pulled together. We know that Zombies love things to be terrible, so their heart will be an eye searing shade of chartreuse:

<svg viewBox="0 0 160 135" class="heart" xmlns="http://www.w3.org/2000/svg" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2"><path d="M61 12V0H25v12H12v13H0v36h12v13h13v12h12v12h12v12h12v13h13v12h12v-12h13v-13h11V98h13V86h-1 13V74h12V61h12V25h-12V12h-12V0H98v12H85v13H74V12H61z" fill="#7aff00"/></svg>

Here’s the important bits of the CSS that are added to the template’s <style> element:

#trigger:checked + .likebtn {   /* Checked state of the .likebtn. Flips the foreground/background color of the unchecked state. */   background-color: #960B0B;   color: #fff; }  #trigger {   /* With the label attached to the input with the for attribute, clicking the label checks/unchecks the box, so we can remove the checkbox. */   display: none; }  .heart {   /* Start the heart off so small it's nigh invisible */   transform: scale(0.0001); }  @keyframes heartanim {   /* Heart animation */   0% { transform: scale(0.0001); }   50% { transform: scale(1); }   85%, 100% { transform: scale(0.4); } }  #trigger:checked ~ .heart {   /* Checking the checkbox initiates the animation */   animation: 1s heartanim ease-in-out forwards; }

Pretty much standard HTML and CSS there. Nothing fancy or firmly web-component-ish. But, hey, it works! And since it’s technically a checkbox, it’s just as easy to “unlike” a zombie as it is to “Like” one.

Messaging zombies

If you’re a post-apocalyptic single who’s ready to mingle, and see a zombie whose personality and interests match yours, you might want to message them. (And, remember, zombies aren’t concerned about looks — they’re only interested in your braaains.)

Let’s reveal a message button after a zombie is “Liked.” The fact that the Like button is a checkbox comes in handy once again, because we can use its checked state to conditionally reveal the message option with CSS. Here’s the HTML added just below the heart SVG. It can pretty much go anywhere as long as it’s a sibling of and comes after the #trigger element.

<button type="button" class="messagebtn">Message</button>

Once the #trigger checkbox is checked, we can bring the messaging button into view:

#trigger:checked ~ .messagebtn {   display: block; }

We’ve done a good job avoiding complexity so far, but we’re going to need to reach for a little JavaScript in here. If we click the message button, we’d expect to be able to message that zombie, right? While we could add that HTML to our <template>, for demonstration purposes, lets use some JavaScript to build it on the fly.

My first (naive) assumption was that we could just add a <script> element to the template, create an encapsulated script, and be on our merry way. Yeah, that doesn’t work. Any variables instantiated in the template get instantiated multiple times and well, JavaScript’s cranky about variables that are indistinguishable from each other. *Shakes fist at cranky JavaScript*

You probably would have done something smarter and said, “Hey, we’re already making a JavaScript constructor for this element, so why wouldn’t you put the JavaScript in there?” Well, I was right about you being smarter than me.

Let’s do just that and add JavaScript to the constructor. We’ll add a listener that, once clicked, creates and displays a form to send a message. Here’s what the constructor looks like now, smarty pants:

customElements.define('zombie-profile', class extends HTMLElement {   constructor() {     super();     let profile = document.getElementById('zprofiletemplate');     let myprofile = profile.content;     const shadowRoot = this.attachShadow({       mode: 'open'     }).appendChild(myprofile.cloneNode(true));      // The "new" code     // Grabbing the message button and the div wrapping the profile for later use     let msgbtn = this.shadowRoot.querySelector('.messagebtn'),         profileEl = this.shadowRoot.querySelector('.profile-wrapper');          // Adding the event listener     msgbtn.addEventListener('click', function (e) {        // Creating all the elements we'll need to build our form       let formEl = document.createElement('form'),           subjectEl = document.createElement('input'),           subjectlabel = document.createElement('label'),           contentEl = document.createElement('textarea'),           contentlabel = document.createElement('label'),           submitEl = document.createElement('input'),           closebtn = document.createElement('button');                // Setting up the form element. The action just goes to a page I built that spits what you submitted back at you       formEl.setAttribute('method', 'post');       formEl.setAttribute('action', 'https://johnrhea.com/undead-form-practice.php');       formEl.classList.add('hello');        // Setting up a close button so we can close the message if we get shy       closebtn.innerHTML = "x";       closebtn.addEventListener('click', function () {         formEl.remove();       });        // Setting up form fields and labels       subjectEl.setAttribute('type', 'text');       subjectEl.setAttribute('name', 'subj');       subjectlabel.setAttribute('for', 'subj');       subjectlabel.innerHTML = "Subject:";       contentEl.setAttribute('name', 'cntnt');       contentlabel.setAttribute('for', 'cntnt');       contentlabel.innerHTML = "Message:";       submitEl.setAttribute('type', 'submit');       submitEl.setAttribute('value', 'Send Message');        // Putting all the elments in the Form       formEl.appendChild(closebtn);       formEl.appendChild(subjectlabel);       formEl.appendChild(subjectEl);       formEl.appendChild(contentlabel);       formEl.appendChild(contentEl);       formEl.appendChild(submitEl);        // Putting the form on the page       profileEl.appendChild(formEl);     });   } });

So far, so good!

Before we call it a day, there’s one last thing we need to address. There’s nothing worse than that first awkward introduction, so lets grease those post-apocalyptic dating wheels by adding the zombie’s name to the default message text. That’s a nice little convenience for the user.

Since we know that the first span in the <zombie-profile> element is always the zombie’s name, we can grab it and stick its content in a variable. (If your implementation is different and the elements’s order jumps around, you may want to use a class to ensure you always get the right one.)

let zname = this.getElementsByTagName("span")[0].innerHTML;

And then add this inside the event listener:

contentEl.innerHTML = "Hi " + zname + ",\nI like your braaains...";

That wasn’t so bad, was it? Now we know that interactive web components are just as un-scary as the zombie dating scene… well you know what I mean. Once you get over the initial hurdle of understanding the structure of a web component, it starts to make a lot more sense. Now that you’re armed with interactive web component skills, let’s see what you can come up with! What other sorts of components or interactions would make our zombie dating service even better? Make it and share it in the comments.


The post Interactive Web Components Are Easier Than You Think appeared first on CSS-Tricks.

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

CSS-Tricks

, , , ,
[Top]

Web Components Are Easier Than You Think

When I’d go to a conference (when we were able to do such things) and see someone do a presentation on web components, I always thought it was pretty nifty (yes, apparently, I’m from 1950), but it always seemed complicated and excessive. A thousand lines of JavaScript to save four lines of HTML. The speaker would inevitably either gloss over the oodles of JavaScript to get it working or they’d go into excruciating detail and my eyes would glaze over as I thought about whether my per diem covered snacks.

But in a recent reference project to make learning HTML easier (by adding zombies and silly jokes, of course), the completist in me decided I had to cover every HTML element in the spec. Beyond those conference presentations, this was my first introduction to the <slot> and <template> elements. But as I tried to write something accurate and engaging, I was forced to delve a bit deeper.

And I’ve learned something in the process: web components are a lot easier than I remember.

Either web components have come a long way since the last time I caught myself daydreaming about snacks at a conference, or I let my initial fear of them get in the way of truly knowing them — probably both.

I’m here to tell you that you—yes, you—can create a web component. Let’s leave our distractions, fears, and even our snacks at the door for a moment and do this together.

Let’s start with the <template>

A <template> is an HTML element that allows us to create, well, a template—the HTML structure for the web component. A template doesn’t have to be a huge chunk of code. It can be as simple as:

<template>   <p>The Zombies are coming!</p> </template>

The <template> element is important because it holds things together. It’s like the foundation of building; it’s the base from which everything else is built. Let’s use this small bit of HTML as the template for an <apocalyptic-warning> web component—you know, as a warning when the zombie apocalypse is upon us.

Then there’s the <slot>

<slot> is merely another HTML element just like <template>. But in this case, <slot> customizes what the <template> renders on the page.

<template>   <p>The <slot>Zombies</slot> are coming!</p> </template>

Here, we’ve slotted (is that even a word?) the word “Zombies” in the templated markup. If we don’t do anything with the slot, it defaults to the content between the tags. That would be “Zombies” in this example.

Using <slot> is a lot like having a placeholder. We can use the placeholder as is, or define something else to go in there instead. We do that with the name attribute.

<template>   <p>The <slot name="whats-coming">Zombies</slot> are coming!</p> </template>

The name attribute tells the web component which content goes where in the template. Right now, we’ve got a slot called whats-coming. We’re assuming zombies are coming first in the apocalypse, but the <slot> gives us some flexibility to slot something else in, like if it ends up being a robot, werewolf, or even a web component apocalypse.

Using the component

We’re technically done “writing” the component and can drop it in anywhere we want to use it.

<apocalyptic-warning>   <span slot="whats-coming">Halitosis Laden Undead Minions</span> </apocalyptic-warning>  <template>   <p>The <slot name="whats-coming">Zombies</slot> are coming!</p> </template>

See what we did there? We put the <apocalyptic-warning> component on the page just like any other <div> or whatever. But we also dropped a <span> in there that references the name attribute of our <slot>. And what’s between that <span> is what we want to swap in for “Zombies” when the component renders.

Here’s a little gotcha worth calling out: custom element names must have a hyphen in them. It’s just one of those things you’ve gotta know going into things. The spec (which is still in flux) prescribes that to prevent conflicts in the event that HTML releases a new element with the same name.

Still with me so far? Not too scary, right? Well, minus the zombies. We still have a little work to do to make the <slot> swap possible, and that’s where we start to get into JavaScript.

Registering the component

As I said, you do need some JavaScript to make this all work, but it’s not the super complex, thousand-lined, in-depth code I always thought. Hopefully I can convince you as well.

You need a constructor function that registers the custom element. Otherwise, our component is like the undead: it’s there but not fully alive.

Here’s the constructor we’ll use:

// Defines the custom element with our appropriate name, <apocalyptic-warning> customElements.define("apocalyptic-warning",    // Ensures that we have all the default properties and methods of a built in HTML element   class extends HTMLElement {      // Called anytime a new custom element is created     constructor() {        // Calls the parent constructor, i.e. the constructor for `HTMLElement`, so that everything is set up exactly as we would for creating a built in HTML element       super();        // Grabs the <template> and stores it in `warning`       let warning = document.getElementById("warningtemplate");        // Stores the contents of the template in `mywarning`       let mywarning = warning.content;        const shadowRoot = this.attachShadow({mode: "open"}).appendChild(mywarning.cloneNode(true));     }   });

I left detailed comments in there that explain things line by line. Except the last line:

const shadowRoot = this.attachShadow({mode: "open"}).appendChild(mywarning.cloneNode(true));

We’re doing a lot in here. First, we’re taking our custom element (this) and creating a clandestine operative—I mean, shadow DOM. mode: open simply means that JavaScript from outside the :root can access and manipulate the elements within the shadow DOM, sort of like setting up back door access to the component.

From there, the shadow DOM has been created and we append a node to it. That node will be a deep copy of the template, including all elements and text of the template. With the template attached to the shadow DOM of the custom element, the <slot> and slot attribute take over for matching up content with where it should go.

Check this out. Now we can plop two instances of the same component, rendering different content simply by changing one element.

Styling the component

You may have noticed styling in that demo. As you might expect, we absolutely have the ability to style our component with CSS. In fact, we can include a <style> element right in the <template>.

<template id="warningtemplate">   <style>     p {       background-color: pink;       padding: 0.5em;       border: 1px solid red;     }   </style>      <p>The <slot name="whats-coming">Zombies</slot> are coming!</p> </template>

This way, the styles are scoped directly to the component and nothing leaks out to other elements on the same page, thanks to the shadow DOM.

Now in my head, I assumed that a custom element was taking a copy of the template, inserting the content you’ve added, and then injecting that into the page using the shadow DOM. While that’s what it looks like on the front end, that’s not how it actually works in the DOM. The content in a custom element stays where it is and the shadow DOM is sort of laid on top like an overlay.

Screenshot of the HTML source of the zombie-warning component. The custom element is expanded in the shadow dam, including the style block, the custom element, and the template.

And since the content is technically outside the template, any descendant selectors or classes we use in the template’s <style> element will have no affect on the slotted content. This doesn’t allow full encapsulation the way I had hoped or expected. But since a custom element is an element, we can use it as an element selector in any ol’ CSS file, including the main stylesheet used on a page. And although the inserted material isn’t technically in the template, it is in the custom element and descendant selectors from the CSS will work.

apocalyptic-warning span {   color: blue; }

But beware! Styles in the main CSS file cannot access elements in the <template> or shadow DOM.

Let’s put all of this together

Let’s look at an example, say a profile for a zombie dating service, like one you might need after the apocalypse. In order to style both the default content and any inserted content, we need both a <style> element in the <template> and styling in a CSS file.

The JavaScript code is exactly the same except now we’re working with a different component name, <zombie-profile>.

customElements.define("zombie-profile",   class extends HTMLElement {     constructor() {       super();       let profile = document.getElementById("zprofiletemplate");       let myprofile = profile.content;       const shadowRoot = this.attachShadow({mode: "open"}).appendChild(myprofile.cloneNode(true));     }   } );

Here’s the HTML template, including the encapsulated CSS:

<template id="zprofiletemplate">   <style>     img {       width: 100%;       max-width: 300px;       height: auto;       margin: 0 1em 0 0;     }     h2 {       font-size: 3em;       margin: 0 0 0.25em 0;       line-height: 0.8;     }     h3 {       margin: 0.5em 0 0 0;       font-weight: normal;     }     .age, .infection-date {       display: block;     }     span {       line-height: 1.4;     }     .label {       color: #555;     }     li, ul {       display: inline;       padding: 0;     }     li::after {       content: ', ';     }     li:last-child::after {       content: '';     }     li:last-child::before {       content: ' and ';     }   </style>    <div class="profilepic">     <slot name="profile-image"><img src="https://assets.codepen.io/1804713/default.png" alt=""></slot>   </div>    <div class="info">     <h2><slot name="zombie-name" part="zname">Zombie Bob</slot></h2>      <span class="age"><span class="label">Age:</span> <slot name="z-age">37</slot></span>     <span class="infection-date"><span class="label">Infection Date:</span> <slot name="idate">September 12, 2025</slot></span>      <div class="interests">       <span class="label">Interests: </span>       <slot name="z-interests">         <ul>           <li>Long Walks on Beach</li>           <li>brains</li>           <li>defeating humanity</li>         </ul>       </slot>     </div>      <span class="z-statement"><span class="label">Apocalyptic Statement: </span> <slot name="statement">Moooooooan!</slot></span>    </div> </template>

Here’s the CSS for our <zombie-profile> element and its descendants from our main CSS file. Notice the duplication in there to ensure both the replaced elements and elements from the template are styled the same.

zombie-profile {   width: calc(50% - 1em);   border: 1px solid red;   padding: 1em;   margin-bottom: 2em;   display: grid;   grid-template-columns: 2fr 4fr;   column-gap: 20px; } zombie-profile img {   width: 100%;   max-width: 300px;   height: auto;   margin: 0 1em 0 0; } zombie-profile li, zombie-profile ul {   display: inline;   padding: 0; } zombie-profile li::after {   content: ', '; } zombie-profile li:last-child::after {   content: ''; } zombie-profile li:last-child::before {   content: ' and '; }

All together now!

While there are still a few gotchas and other nuances, I hope you feel more empowered to work with the web components now than you were a few minutes ago. Dip your toes in like we have here. Maybe sprinkle a custom component into your work here and there to get a feel for it and where it makes sense.

That’s really it. Now what are you more scared of, web components or the zombie apocalypse? I might have said web components in the not-so-distant past, but now I’m proud to say that zombies are the only thing that worry me (well, that and whether my per diem will cover snacks…)


The post Web Components Are Easier Than You Think appeared first on CSS-Tricks.

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

CSS-Tricks

, , ,
[Top]

The Web is for More Than Document Viewing

I poked at a tweet from Ken Kocienda over the weekend:

I don’t know Ken, so I feel a little bad for being harsh. But I haven’t changed how I feel. Saying “Web browsers are for viewing documents” is silly to me at this point, and suggesting it’s “the biggest wrong turn in the history of computing” feels like “your career in web development is invalid” and when people dig at what I do, I notoriously don’t take it well.

The point is silly anyway. Ken posted this on Twitter-dot-com, and then followed up with a link to job postings. I hope we all can see that Ken was literally leveraging the not-just-a-document-viewer nature of the web to help spread his message and help himself. It feels like saying “cars are bad” and then getting in your car to go to the grocery store.

Ship: sailed. The web is incredibly feature-rich far beyond viewing documents. I know the argument is that this was a mistake, not that web browsers aren’t currently capable of more. If that’s true though, what would you have the web do? Start stripping away features? Should we strip browsers down to document viewers? Maybe we just hand the keys to Facebook and we’ll just do whatever they say we should (lolz).

A super-capable open web is excellent. It means we can build things on open standards on the open web rather than things on proprietary technologies in walled gardens. It’s the better place to build things. URLs alone are a reason to build on the web.

There is still nuance to getting it right though. I enjoyed Noam Rosenthal’s baby bear porridge “Should The Web Expose Hardware Capabilities?”, which begins by discussing Alex Russell’s “Platform Adjacency Theory”:

I relate with the author’s passion for keeping the open web relevant, and with the concern that going too slow with enhancing the web with new features will make it irrelevant. This is augmented by my dislike of app stores and other walled gardens. But as a user I can relate to the opposite perspective — I get dizzy sometimes when I don’t know what websites I’m browsing are capable or not capable of doing, and I find platform restrictions and auditing to be comforting.

Maybe we just take it slow and do things carefully. Good slow. Slow, like brisket.

We’re doing that now, if by accident. Google forges ahead extremely quickly. Apple says hold on there, there are security issues here. And a smidge of vice-versa. I’d point out the other forces at work, but I guess we’re kinda down to two major browser vendor players. Not to discount Mozilla, but the choices they make with the web platform don’t affect the momentum of the web all that much at the moment.


The post The Web is for More Than Document Viewing appeared first on CSS-Tricks.

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

CSS-Tricks

, , ,
[Top]