Tag: Custom

Can we have custom media queries, please?

Stefan Judis notes that there is a spec for custom media queries, but seemingly no momentum on it at the moment. That lack of movement is unfortunate, as it’s almost guaranteed front-end developers all over would start using it as soon as it’s ready. I know I would, as I liberally use custom properties now, and the DRYness of custom properties is one of the exact same benefits we’d get with custom media queries.

The syntax is:

@custom-media --narrow-window (max-width: 30em);  @media (--narrow-window) {   /* narrow window styles */ } @media (--narrow-window) and (script) {   /* special styles for when script is allowed */ }

..which I lifted from the Chrome Bug (star it to signal interest).

I’ve come around on the idea of postcss-preset-env. It used to think it was too theoretical which made me nervous. — so much CSS isn’t particularly polyfill-able without JavaScript. Plus, if anything changed with the specs, you’ve basically locked yourself into a CSS processor rather than getting future features, as that processing eventually stops processing. But all the CSS transformations in here generally seem pretty chill, deliver a lot of value (like custom media queries!), and you can pick which stage you feel most comfortable with.

Direct Link to ArticlePermalink


The post Can we have custom media queries, please? appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , ,

The Big Gotcha With Custom Properties

I’ve seen this confuse more than a handful of people recently, including myself, so I’m making sure it’s written down.

Let’s chuck a couple of custom properties into CSS:

html {   --color-1: red;   --color-2: blue; }

Let’s use them right away to make a background gradient:

html {   --color-1: red;   --color-2: blue;    --bg: linear-gradient(to right, var(--color-1), var(--color-2)); }

Now say there is a couple of divs sitting on the page:

<div></div> <div class="variation"></div>

Lemme style them up:

div {   background: var(--bg); }

That totally works! Hell yes!

Now lemme style that variation. I don’t want it to go from red to blue, I want it to go from green to blue. Easy cheesy, I’ll update red to green:

html {   --color-1: red;   --color-2: blue;    --bg: linear-gradient(to right, var(--color-1), var(--color-2)); } div {   background: var(--bg); } .variation {   --color-1: green; }

Nope! (Sirens blaring, horns honking, farm animals taking cover).

That doesn’t work, friends.

The problem, as best I understand it, is that --bg was never declared on either of the divs. It can use --bg, because it was declared higher up, but by the time it is being used there, the value of it is locked. Just because you change some other property that --bg happens to use at the time it was declared, it doesn’t mean that property goes out searching for places it was used and updating everything that’s used it as a dependency.

Ugh, that explanation doesn’t feel quite right. But it’s the best I got.

The solution? Well, there are a few.

Solution 1: Scope the variable to where you’re using it.

You could do this:

html {   --color-1: red;   --color-2: blue; }  div {   --bg: linear-gradient(to right, var(--color-1), var(--color-2));   background: var(--bg); } .variant {   --color-1: green; }

Now that --bg is declared on both divs, the change to the --color-1 dependency does work.

Solution 2: Comma-separate the selector where you set most of the variables.

Say you do the common thing where you set a bunch of variables at the :root. Then you run into this problem. You can just add extra selectors to that main declaration to make sure you hit the right scope.

html, div {   --color-1: red;   --color-2: blue;    --bg: linear-gradient(to right, var(--color-1), var(--color-2)); } div {   background: var(--bg); } .variation {   --color-1: green; }

In some other perhaps less-contrived example, it might look something like this:

:root,  .button, .whatever-it-is-a-bandaid {   --padding-inline: 1rem;   --padding-block: 1rem;   --padding: var(--padding-block) var(--padding-inline); }  .button {   padding: var(--padding); } .button.less-wide {   --padding-inline: 0.5rem; }

Solution 3: Blanket Mode

Screw it — put the variables everywhere.

* {   --access: me;   --whereever: you;   --want: to;    --hogwild: var(--access) var(--whereever); }

This is not a good plan. I overheard a chat recently in which a medium-sized site experienced a 500ms page rendering delay because every draw to the page needed to compute all the properties. It “works” but it’s one of the rare cases where you can cause legit performance problems with a selector.

Solution 4: Introduce a new “default” property and fallback

All credit here to Stephen Shaw who’s exploration on all this is one of the places I saw this confusion in the first place.

Let’s go back to our first demonstration of this problem:

html {   --color-1: red;   --color-2: blue;    --bg: linear-gradient(to right, var(--color-1), var(--color-2)); }

What we want to do is give ourselves two things:

  1. A way to override the entire background
  2. A way to overide a part of the gradient background

So we’re gonna do it this way:

html {   --color-1: red;   --color-2: blue; } div {   --bg-default: linear-gradient(to right, var(--color-1), var(--color-2));   background: var(--bg, var(--bg-default)); }

Notice that we haven’t declared --bg at all. It’s just sitting there waiting for a value, and if it ever gets one, that’s the value that “wins.” But without one, it’ll fall back to our --bg-default. Now…

  1. If I set --color-1 or --color-2, it replaces that part of the gradient as expected (so long as I do it on a selector that touches one of the divs).
  2. Or, I can set --bg to reset the entire background to whatever I want.

Feels like a nice way to handle things.


Sometimes there are actual bugs with CSS custom properties. This isn’t one of them. Even though it sort of feels like a bug to me, apparently it’s not. Just one of those things you gotta know about.


The post The Big Gotcha With Custom Properties appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, ,
[Top]

Efficient Infinite Utility Helpers Using Inline CSS Custom Properties and calc()

I recently wrote a very basic Sass loop that outputs several padding and margin utility classes. Nothing fancy, really, just a Sass map with 11 spacing values, looped over to create classes for both padding and margin on each side. As we’ll see, this works, but it ends up a pretty hefty amount of CSS. We’re going to refactor it to use CSS custom properties and make the system much more trim.

Here’s the original Sass implementation:

$  space-stops: (   '0': 0,   '1': 0.25rem,   '2': 0.5rem,   '3': 0.75rem,   '4': 1rem,   '5': 1.25rem,   '6': 1.5rem,   '7': 1.75rem,   '8': 2rem,   '9': 2.25rem,   '10': 2.5rem, );  @each $  key, $  val in $  space-stops {   .p-#{$  key} {     padding: #{$  val} !important;   }   .pt-#{$  key} {     padding-top: #{$  val} !important;   }   .pr-#{$  key} {     padding-right: #{$  val} !important;   }   .pb-#{$  key} {     padding-bottom: #{$  val} !important;   }   .pl-#{$  key} {     padding-left: #{$  val} !important;   }   .px-#{$  key} {     padding-right: #{$  val} !important;     padding-left: #{$  val} !important;   }   .py-#{$  key} {     padding-top: #{$  val} !important;     padding-bottom: #{$  val} !important;   }    .m-#{$  key} {     margin: #{$  val} !important;   }   .mt-#{$  key} {     margin-top: #{$  val} !important;   }   .mr-#{$  key} {     margin-right: #{$  val} !important;   }   .mb-#{$  key} {     margin-bottom: #{$  val} !important;   }   .ml-#{$  key} {     margin-left: #{$  val} !important;   }   .mx-#{$  key} {     margin-right: #{$  val} !important;     margin-left: #{$  val} !important;   }   .my-#{$  key} {     margin-top: #{$  val} !important;     margin-bottom: #{$  val} !important;   } } 

This very much works. It outputs all the utility classes we need. But, it can also get bloated quickly. In my case, they were about 8.6kb uncompressed and under 1kb compressed. (Brotli was 542 bytes, and gzip came in at 925 bytes.)

Since they are extremely repetitive, they compress well, but I still couldn’t shake the feeling that all these classes were overkill. Plus, I hadn’t even done any small/medium/large breakpoints which are fairly typical for these kinds of helper classes.

Here’s a contrived example of what the responsive version might look like with small/medium/large classes added. We’ll re-use the $ space-stops map defined previously and throw our repetitious code into a mixin

@mixin finite-spacing-utils($  bp: '') {     @each $  key, $  val in $  space-stops {         .p-#{$  key}#{$  bp} {             padding: #{$  val} !important;         }         .pt-#{$  key}#{$  bp} {             padding-top: #{$  val} !important;         }         .pr-#{$  key}#{$  bp} {             padding-right: #{$  val} !important;         }         .pb-#{$  key}#{$  bp} {             padding-bottom: #{$  val} !important;         }         .pl-#{$  key}#{$  bp} {             padding-left: #{$  val} !important;         }         .px-#{$  key}#{$  bp} {             padding-right: #{$  val} !important;             padding-left: #{$  val} !important;         }         .py-#{$  key}#{$  bp} {             padding-top: #{$  val} !important;             padding-bottom: #{$  val} !important;         }          .m-#{$  key}#{$  bp} {             margin: #{$  val} !important;         }         .mt-#{$  key}#{$  bp} {             margin-top: #{$  val} !important;         }         .mr-#{$  key}#{$  bp} {             margin-right: #{$  val} !important;         }         .mb-#{$  key}#{$  bp} {             margin-bottom: #{$  val} !important;         }         .ml-#{$  key}#{$  bp} {             margin-left: #{$  val} !important;         }         .mx-#{$  key}#{$  bp} {             margin-right: #{$  val} !important;             margin-left: #{$  val} !important;         }         .my-#{$  key}#{$  bp} {             margin-top: #{$  val} !important;             margin-bottom: #{$  val} !important;         }     } }  @include finite-spacing-utils;  @media (min-width: 544px) {     @include finite-spacing-utils($  bp: '_sm'); }  @media (min-width: 768px) {     @include finite-spacing-utils($  bp: '_md'); }  @media (min-width: 1024px) {     @include finite-spacing-utils($  bp: '_lg'); }  

That clocks in at about 41.7kb uncompressed (and about 1kb with Brotli, and 3kb with gzip). It still compresses well, but it’s a bit ridiculous.

I knew it was possible to reference data-* attributes from within CSS using the [attr() function, so I wondered if it was possible to use calc() and attr() together to create dynamically-calculated spacing utility helpers via data-* attributes — like data-m="1" or data-m="1@md" — then in the CSS to do something like margin: calc(attr(data-m) * 0.25rem) (assuming I’m using a spacing scale incrementing at 0.25rem intervals). That could be very powerful.

But the end of that story is: no, you (currently) can’t use attr() with any property except the content property. Bummer. But in searching for attr() and calc() information, I found this intriguing Stack Overflow comment by Simon Rigét that suggests setting a CSS variable directly within an inline style attribute. Aha!

So it’s possible to do something like <div style="--p: 4;"> then, in CSS:

:root {   --p: 0; }  [style*='--p:'] {   padding: calc(0.25rem * var(--p)) !important; } 

In the case of the style="--p: 4;" example, you’d effectively end up with padding: 1rem !important;.

… and now you have an infinitely scalable spacing utility class monstrosity helper.

Here’s what that might look like in CSS:

:root {   --p: 0;   --pt: 0;   --pr: 0;   --pb: 0;   --pl: 0;   --px: 0;   --py: 0;   --m: 0;   --mt: 0;   --mr: 0;   --mb: 0;   --ml: 0;   --mx: 0;   --my: 0; }  [style*='--p:'] {   padding: calc(0.25rem * var(--p)) !important; } [style*='--pt:'] {   padding-top: calc(0.25rem * var(--pt)) !important; } [style*='--pr:'] {   padding-right: calc(0.25rem * var(--pr)) !important; } [style*='--pb:'] {   padding-bottom: calc(0.25rem * var(--pb)) !important; } [style*='--pl:'] {   padding-left: calc(0.25rem * var(--pl)) !important; } [style*='--px:'] {   padding-right: calc(0.25rem * var(--px)) !important;   padding-left: calc(0.25rem * var(--px)) !important; } [style*='--py:'] {   padding-top: calc(0.25rem * var(--py)) !important;   padding-bottom: calc(0.25rem * var(--py)) !important; }  [style*='--m:'] {   margin: calc(0.25rem * var(--m)) !important; } [style*='--mt:'] {   margin-top: calc(0.25rem * var(--mt)) !important; } [style*='--mr:'] {   margin-right: calc(0.25rem * var(--mr)) !important; } [style*='--mb:'] {   margin-bottom: calc(0.25rem * var(--mb)) !important; } [style*='--ml:'] {   margin-left: calc(0.25rem * var(--ml)) !important; } [style*='--mx:'] {   margin-right: calc(0.25rem * var(--mx)) !important;   margin-left: calc(0.25rem * var(--mx)) !important; } [style*='--my:'] {   margin-top: calc(0.25rem * var(--my)) !important;   margin-bottom: calc(0.25rem * var(--my)) !important; }  

This is a lot like the first Sass loop above, but there’s no loop going 11 times — and yet it’s infinite. It’s about 1.4kb uncompressed, 226 bytes with Brotli, or 284 bytes gzipped.

If you wanted to extend this for breakpoints, the unfortunate news is that you can’t put the “@” character in CSS variable names (although emojis and other UTF-8 characters are strangely permitted). So you could probably set up variable names like p_sm or sm_p. You’d have to add some extra CSS variables and some media queries to handle all this, but it won’t blow up exponentially the way traditional CSS classnames created with a Sass for-loop do.

Here’s the equivalent responsive version. We’ll use a Sass mixin again to cut down the repetition:

:root {   --p: 0;   --pt: 0;   --pr: 0;   --pb: 0;   --pl: 0;   --px: 0;   --py: 0;   --m: 0;   --mt: 0;   --mr: 0;   --mb: 0;   --ml: 0;   --mx: 0;   --my: 0; }  @mixin infinite-spacing-utils($  bp: '') {     [style*='--p#{$  bp}:'] {         padding: calc(0.25rem * var(--p)) !important;     }     [style*='--pt#{$  bp}:'] {         padding-top: calc(0.25rem * var(--pt)) !important;     }     [style*='--pr#{$  bp}:'] {         padding-right: calc(0.25rem * var(--pr)) !important;     }     [style*='--pb#{$  bp}:'] {         padding-bottom: calc(0.25rem * var(--pb)) !important;     }     [style*='--pl#{$  bp}:'] {         padding-left: calc(0.25rem * var(--pl)) !important;     }     [style*='--px#{$  bp}:'] {         padding-right: calc(0.25rem * var(--px)) !important;         padding-left: calc(0.25rem * var(--px)) !important;     }     [style*='--py#{$  bp}:'] {         padding-top: calc(0.25rem * var(--py)) !important;         padding-bottom: calc(0.25rem * var(--py)) !important;     }     [style*='--m#{$  bp}:'] {         margin: calc(0.25rem * var(--m)) !important;     }     [style*='--mt#{$  bp}:'] {         margin-top: calc(0.25rem * var(--mt)) !important;     }     [style*='--mr#{$  bp}:'] {         margin-right: calc(0.25rem * var(--mr)) !important;     }     [style*='--mb#{$  bp}:'] {         margin-bottom: calc(0.25rem * var(--mb)) !important;     }     [style*='--ml#{$  bp}:'] {         margin-left: calc(0.25rem * var(--ml)) !important;     }     [style*='--mx#{$  bp}:'] {         margin-right: calc(0.25rem * var(--mx)) !important;         margin-left: calc(0.25rem * var(--mx)) !important;     }     [style*='--my#{$  bp}:'] {         margin-top: calc(0.25rem * var(--my)) !important;         margin-bottom: calc(0.25rem * var(--my)) !important;     } }  @include infinite-spacing-utils;  @media (min-width: 544px) {     @include infinite-spacing-utils($  bp: '_sm'); }  @media (min-width: 768px) {     @include infinite-spacing-utils($  bp: '_md'); }  @media (min-width: 1024px) {     @include infinite-spacing-utils($  bp: '_lg'); }  

That’s about 6.1kb uncompressed, 428 bytes with Brotli, and 563 with gzip.

Do I think that writing HTML like <div style="--px:2; --my:4;"> is pleasing to the eye, or good developer ergonomics… no, not particularly. But could this approach be viable in situations where you (for some reason) need extremely minimal CSS, or perhaps no external CSS file at all? Yes, I sure do.

It’s worth pointing out here that CSS variables assigned in inline styles do not leak out. They’re scoped only to the current element and don’t change the value of the variable globally. Thank goodness! The one oddity I have found so far is that DevTools (at least in Chrome, Firefox, and Safari) do not report the styles using this technique in the “Computed” styles tab.

Also worth mentioning is that I’ve used good old padding  and margin properties with -top, -right, -bottom, and -left, but you could use the equivalent logical properties like padding-block and padding-inline. It’s even possible to shave off just a few more bytes by selectively mixing and matching logical properties with traditional properties. I managed to get it down to 400 bytes with Brotli and 521 with gzip this way.

Other use cases

This seems most appropriate for things that are on a (linear) incremental scale (which is why padding and margin seems like a good use case) but I could see this potentially working for widths and heights in grid systems (column numbers and/or widths). Maybe for typographic scales (but maybe not).

I’ve focused a lot on file size, but there may be some other uses here I’m not thinking of. Perhaps you wouldn’t write your code in this way, but a critical CSS tool could potentially refactor the code to use this approach.

Digging deeper

As I dug deeper, I found that Ahmad Shadeed blogged in 2019 about mixing calc() with CSS variable assignments within inline styles particularly for avatar sizes. Miriam Suzanne’s article on Smashing Magazine in 2019 didn’t use calc() but shared some amazing things you can do with variable assignments in inline styles.


The post Efficient Infinite Utility Helpers Using Inline CSS Custom Properties and calc() appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , , , , , ,
[Top]

Build Complex CSS Transitions using Custom Properties and cubic-bezier()

I recently illustrated how we can achieve complex CSS animations using cubic-bezier() and how to do the same when it comes to CSS transitions. I was able to create complex hover effect without resorting to keyframes. In this article, I will show you how to create even more complex CSS transitions.

This time, let’s use the @property feature. It’s only supported on Chrome-based browsers for now but we can still play with it and demonstrate how it, too, and can be used to build complex animations.

I highly recommend reading my previous article because I will be referring to a few concepts I explained in detail there. Also, please note that the demos in this article are best viewed in Chromium-based browsers while @property support is still limited.

Let’s start with a demo:

Click on the button (more than once) and see the “magic” curve we get. It may look trivial at first glance because we can achieve such effect using some complex keyframes. But the trick is that there is no keyframe in there! That animation is done using only a transition.

Awesome right? And this is only the beginning, so let’s dig in!

The main idea

The trick in the previous example relies on this code:

@property --d1 {   syntax: '<number>';   inherits: false;   initial-value: 0; } @property --d2 {   syntax: '<number>';   inherits: false;   initial-value: 0; }  .box {   top: calc((var(--d1) + var(--d2)) * 1%);   transition:     --d1 1s cubic-bezier(0.7, 1200, 0.3, -1200),     --d2 1s cubic-bezier(0.5, 1200, 0.5, -1200); } .box:hover {   --d1: 0.2;   --d1: -0.2; }

We’re defining two custom properties, --d1 and --d2. Then, we declare the top property on a .box element using the sum of both those properties. Nothing overly complex yet—just calc() applied to two variables.

The two properties are defined as <number> and I multiply those values by 1% to convert them into a percentage. We could define these as <percentage> right away to avoid the multiplication. But I’ve chosen numbers instead in favor of more flexibility for more complex operations later.

Notice that we apply a different transition to each variable—more precisely, a different timing-function with the same duration. It’s actually a different sinusoidal curve for both variables which is something I get deep into in my previous article.

From there, the property values change when the .box is hovered, triggering the animation. But why do we get the result we see in the demo?

It’s all about math. We are adding two functions to create a third one. For --d1, we have a function (let’s call it F1); for --d2 , we have another one (let’s call it F2). That means the value of top is F1 + F2.

An example to better illustrate:

The first two transitions illustrate each variable individually. The third one is the sum of them. Imagine that at in each step of the animation we take the value of both variables and we add them together to get each point along the final curve.

Let’s try another example:

This time, we combine two parabolic curve to get a… well, I don’t know its name it but it’s another complex curve!

This trick is not only limited to the parabolic and sinusoidal curve. It can work with any kind of timing function even if the result won’t always be a complex curve.

This time:

  • --d1 goes from 0 to 30 with an ease-in timing function
  • --d2 goes from 0 to -20 with an ease-out timing function

The result? The top value goes from 0 to 10 (30-20) with a custom timing function (the sum of ease-in and ease-out).

We are not getting a complex transition in this case—it’s more to illustrate the fact that it’s a generic idea not only limited to cubic-bezier().

I think it’s time for an interactive demo.

All you have to do is to adjust a few variables to build your own complex transition. I know cubic-bezier() may be tricky, so consider using this online curve generator and also refer to my previous article.

Here are some examples I made:

As you can see, we can combine two different timing functions (created using cubic-bezier() ) to create a third one, complex enough to achieve a fancy transition. The combinations (and possibilities) are unlimited!

In that last example, I wanted to demonstrate how adding two opposite functions lead to the logical result of a constant function (no transition). Hence, the flat line.

Let’s add more variables!

You thought we’d stop at only two variables? Certainly not! We can extend the logic to N variables. There is no restriction—we define each one with a timing function and sum them up.

An example with three variables:

In most cases, two variables are plenty to create a fancy curve, but it’s neat to know that the trick can be extended to more variables.

Can we subract, multiply and divide variables?

Of course! We can also extend the same idea to consider more operations. We can add, subtract, multiply, divide—and even perform a complex formula between variables.

Here, we’re multiplying values:

We can also use one variable and multiply it by itself to get a quadratic function!

Let’s add more fun in there by introducing min()/max() to simulate an abs() function:

Notice that in the second box we will never get higher than the center point on the y-axis because top is always a positive value. (I added a margin-top to make the center of box the reference for 0.)

I won’t get into all the math, but you can imagine the possibilities we have to create any kind of timing function. All we have to do is to find the right formula either using one variable or combining multiple variables.

Our initial code can be generalized:

@property --d1 { /* we do the same for d2 .. dn */   syntax: '<number>';   inherits: false;   initial-value: i1; /* the initial value can be different for each variable */ }  .box {   --duration: 1s; /* the same duration for all */   property: calc(f(var(--d1),var(--d2), .. ,var(--dn))*[1UNIT]);   transition:     --d1 var(--duration) cubic-bezier( ... ),     --d2 var(--duration) cubic-bezier( ... ),     /* .. */     --dn var(--duration) cubic-bezier( ... ); } .box:hover {   --d1:f1;   --d2:f2;   /* .. */   --dn:f3; }

This is pseudo-code to illustrate the logic:

  1. We use @property to define numeric custom properties, each with an initial value.
  2. Each variable has its own timing function but the same duration.
  3. We define an f function that is the formula used between the variables. The function provides a number that we use to multiply the relevant unit. All this runs in calc() applied to the property.
  4. We update the value of each variable on hover (or toggle, or whatever).

Given this, the property transitions from f(i1,i2,…,in) to f(f1,f2,..,fn) with a custom timing function.

Chaining timing functions

We’ve reached the point where we were able to create a complex timing function by combining basic ones. Let’s try another idea that allow us to have more complex timing function: chaining timing functions together.

The trick is to run the transitions sequentially using the transition-delay property. Let’s look back at the interactive demo and apply a delay to one of the variables:

We are chaining timing functions instead of adding them together for yet another way to create more complex timing functions! Mathematically, it’s still a sum, but since the transitions do not run at the same time, we will be summing a function with a constant, and that simulates the chaining.

Now imagine the case with N variables that we are incrementally delayed. Not only can we create complex transitions this way, but we have enough flexibility to build complex timelines.

Here is a funny hover effect I built using that technique:

You will find no keyframes there. A small action scene is made entirely using one element and a CSS transition.

Here is a realistic pendulum animation using the same idea:

Or, how about a ball that bounces naturally:

Or maybe a ball rolling along a curve:

See that? We just created complex animations without a single keyframe in the code!

That’s a wrap!

I hope you took three key points away from this article and the previous one:

  1. We can get parabolic and sinusoidal curves using cubic-bezier() that allow us to create complex transitions without keyframes.
  2. We can create more curves by combining different timing functions using custom properties and calc().
  3. We can chain the curves using the transition-delay to build a complex timeline.

Thanks to these three features, we have no limits when it comes to creating complex animations.


The post Build Complex CSS Transitions using Custom Properties and cubic-bezier() appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , , , ,
[Top]

Custom Property Brain Twisters

I am part of that 82% that got it wrong in Lea’s quiz (tweet version).

Here’s the code:

:root {   --accent-color: skyblue; }  div {   --accent-color: revert;    background: var(--accent-color, orange); }

So what background do I expect <div> to have?

My brain goes like this:

  1. Well, --accent-color is declared, so it’s definitely not orange (the fallback).
  2. The value for the background is revert, so it’s essentially background: revert;
  3. The background property doesn’t inherit though, and even if you force it to, it would inherit from the <body>, not the root.
  4. So… transparent.

Nope.

Lea:

[Because the value is revert it] cancels out any author styles, and resets back to whatever value the property would have from the user stylesheet and UA stylesheet. Assuming there is no --accent-color declaration in the user stylesheet, and of course UA stylesheets don’t set custom properties, then that means the property doesn’t have a value.

Since custom properties are inherited properties (unless they are registered with inherits: false, but this one is not), this means the inherited value trickles in, which is — you guessed it — skyblue.

Stephen posted a similar quiz the other day:

Again, my brain does it totally wrong. It goes:

  1. OK, well, --color is declared, so it’s not blue (the fallback).
  2. It’s not red because the second declaration will override that one.
  3. So, it’s essentially like p { color: inherit; }.
  4. The <p> will inherit yellow from the <body>, which it would have done naturally anyway, but whatever, it’s still yellow.

Nope.

Apparently inherit there is actually inheriting from the next place up the tree that sets it, which html does, so green. That actually is now normal inheriting works. It’s just a brain twister because it’s easy to conflate color the property with --color the custom property.

It also might be useful to know that when you actually declare a custom property with @property you can say whether you want it to inherit or not. So that would change the game with these brain twisters!

@property --property-name {   syntax: '<color>';   inherits: false;   initial-value: #c0ffee; }

The post Custom Property Brain Twisters appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , ,
[Top]

Using Custom Elements in Svelte

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

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

Property or attribute?

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

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

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

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

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

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

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

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

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

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

Attributes used as styling hooks

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

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

You can see the element in action in this CodePen.

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

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

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

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

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

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

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

Using a Svelte action to force setting attributes

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

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

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

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

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

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

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

Lazy-loading custom elements

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

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

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

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

You can see the element in action in this CodePen.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Boolean attributes

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

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

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

You can see the element in action in this CodePen.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Wrapping up

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


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


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

CSS-Tricks

, , ,
[Top]

Are Custom Properties a “Menu of What Will Change”?

PPK laid out an interesting situation in “Two options for using custom properties” where he and Stefan Judis had two different approaches for doing the same thing with custom properties. In one approach, hover and focus styles for a link are handled with two different custom properties, one for each state. In the other approach, a single custom property is used.

Two custom properties:

.component1 {   --linkcolor: red;   --hovercolor: blue; }  .component2 {   --linkcolor: purple;   --hovercolor: cyan; }  a {   color: var(--linkcolor); }  a:hover,a:focus {   color: var(--hovercolor) }

One custom property:

.component1 a {   --componentcolor: red; }  .component1 :is(a:hover,a:focus) {   --componentcolor: blue; } 	 .component2 a {   --componentcolor: purple; }  .component2 :is(a:hover,a:focus) {   --componentcolor: cyan; } 	 a {   color: var(--componentcolor)		 }

There is something more natural feeling about using two properties, like it’s very explicit about what a particular custom property is meant to do. But there is a lot of elegance to using one custom property. Not just for the sake of being one-less custom property, but that the custom property is 1-to-1 matched with a single property.

Taking this a bit further, you could set up a single ruleset with one custom property per property, giving it a sort of menu for what things will change. To that PPK says:

Now you essentially found a definition file. Not only do you see the component’s default styles, you also see what might change and what will not.

That is to say, you’d use a custom property for anything you intend to change, and anything you don’t, you wouldn’t. That’s certainly an interesting approach that I wouldn’t blame anyone for trying.

.lil-grid {   /* will change */   --padding: 1rem;   padding: var(--padding);   --grid-template-columns: 1fr 1fr 1fr;   grid-columns: var(--grid-template-columns);    /* won't change */   border: 1px solid #ccc;   gap: 1rem; }

My hesitation with this is that it’s, at best, a hint at what will and won’t change. For example, I can still change things even though they aren’t set in a custom property. Later, I could do:

.lil-grid.two-up {   grid-columns: 1fr 1fr; }

That wipes out the custom property usage. Similarly, I could never change the value of --grid-template-columns, meaning it looks like it changes under different circumstances, but never does.

Likewise, I could do:

.lil-grid.thick {   border-width: 3px; }

…and even though my original component ruleset implies that the border width doesn’t change, it does with a modifier class.

So, in order to make an approach like that work, you treat it like a convention that you stick to, like a generic coding standard. I’d worry it becomes a pain in the butt, though. For any declaration you decide to change, you gotta go back and refactor it to either be or not be a custom property.

This makes me think about the “implicit styling API” that is HTML and CSS. We’ve already got a styling API in browsers. HTML is turned into the DOM in the browser, and we style the DOM with CSS. Select things, style them.

Maybe we don’t need a menu for what you can and cannot style because that’s what the DOM and CSS already are. That’s not to say a well-crafted set of custom properties can’t be a part of that, but they don’t need to represent hardline rules on what changes and what doesn’t.

Speaking of implicit styling APIs, Jim Nielsen writes in “Shadow DOM and Its Effect on the Unofficial Styling API”:

[…] the shadow DOM breaks the self-documenting style API we’ve had on the web for years.

What style API? If you want to style an element on screen, you open the dev tools, look at the DOM, find the element you want, figure out the right selector to target that element, write your selector and styles, and you’re done.

That’s pretty remarkable when you stop and think about it.

I suppose that’s my biggest beef with web components. I don’t dislike the Shadow DOM; in fact, it’s probably my favorite aspect of web components. I just dislike how I have to invent a styling API for them (à la custom properties that wiggle inside, or ::part) rather than use the styling API that has served us well forever: DOM + CSS.


The post Are Custom Properties a “Menu of What Will Change”? appeared first on CSS-Tricks.

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

CSS-Tricks

, , ,
[Top]

Custom State Pseudo-Classes in Chrome

There is an increasing number of “custom” features on the web platform. We have custom properties (--my-property), custom elements (<my-element>), and custom events (new CustomEvent('myEvent')). At one point, we might even get custom media queries (@media (--my-media)).

But that’s not all! You might have missed it because it wasn’t mentioned in Google’s “New in Chrome 90” article (to be fair, declarative shadow DOM stole the show in this release), but Chrome just added support for yet another “custom” feature: custom state pseudo-classes (:--my-state).

Built-in states

Before talking about custom states, let’s take a quick look at the built-in states that are defined for built-in HTML elements. The CSS Selectors module and the “Pseudo-classes” section of the HTML Standard specify a number of pseudo-classes that can be used to match elements in different states. The following pseudo-classes are all widely supported in today’s browsers:

User action
:hover the mouse cursor hovers over the element
:active the element is being activated by the user
:focus the element has the focus
:focus-within the element has or contains the focus
Location
:visited the link has been visited by the user
:target the element is targeted by the page URL’s fragment
Input
:disabled the form element is disabled
:placeholder-shown the input element is showing placeholder text
:checked the checkbox or radio button is selected
:invalid the form element’s value is invalid
:out-of-range the input element’s value is outside the specificed range
:-webkit-autofill the input element has been autofilled by the browser
Other
:defined the custom element has been registered

Note: For brevity, some pseudo-classes have been omitted, and some descriptions don’t mention every possible use-case.

Custom states

Like built-in elements, custom elements can have different states. A web page that uses a custom element may want to style these states. The custom element could expose its states via CSS classes (class attribute) on its host element, but that’s considered an anti-pattern.

Chrome now supports an API for adding internal states to custom elements. These custom states are exposed to the outer page via custom state pseudo-classes. For example, a page that uses a <live-score> element can declare styles for that element’s custom --loading state.

live-score {   /* default styles for this element */ }  live-score:--loading {   /* styles for when new content is loading */ }

Let’s add a --checked state to a <labeled-checkbox> element

The Custom State Pseudo Class specification contains a complete code example, which I will use to explain the API. The JavaScript portion of this feature is located in the custom element‘s class definition. In the constructor, an “element internals” object is created for the custom element. Then, custom states can be set and unset on the internal states object.

Note that the ElementInternals API ensures that the custom states are read-only to the outside. In other words, the outer page cannot modify the custom element’s internal states.

class LabeledCheckbox extends HTMLElement {   constructor() {     super();      // 1. instantiate the element’s “internals”     this._internals = this.attachInternals();      // (other code)   }    // 2. toggle a custom state   set checked(flag) {     if (flag) {       this._internals.states.add("--checked");     } else {       this._internals.states.delete("--checked");     }   }    // (other code) }

The web page can now style the custom element’s internal states via custom pseudo-classes of the same name. In our example, the --checked state is exposed via the :--checked pseudo-class.

labeled-checkbox {   /* styles for the default state */ }  labeled-checkbox:--checked {   /* styles for the --checked state */ }
Try the demo in Chrome

This feature is not (yet) a standard

Browser vendors have been debating for the past three years how to expose the internal states of custom elements via custom pseudo-classes. Google’s Custom State Pseudo Class specification remains an “unofficial draft” hosted by WICG. The feature underwent a design review at the W3C TAG and has been handed over to the CSS Working Group. In Chrome’s ”intent to ship” discussion, Mounir Lamouri wrote this:

It looks like this feature has good support. It sounds that it may be hard for web developers to benefit from it as long as it’s not widely shipped, but hopefully Firefox and Safari will follow and implement it too. Someone has to implement it first, and given that there are no foreseeable backward incompatible changes, it sounds safe to go first.

We now have to wait for the implementations in Firefox and Safari. The browser bugs have been filed (Mozilla #1588763 and WebKit #215911) but have not received much attention yet.


The post Custom State Pseudo-Classes in Chrome appeared first on CSS-Tricks.

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

CSS-Tricks

, , ,
[Top]

A Complete Guide to Custom Properties

A custom property is most commonly thought of as a variable in CSS.

.card {   --spacing: 1.2rem;   padding: var(--spacing);   margin-bottom: var(--spacing); }

Above, --spacing is the custom property with 1.2rem as the value and var(--spacing) is the variable in use.

Perhaps the most valuable reason to use them: not repeating yourself (DRY code). In the example above, I can change the value 1.2rem in one place and have it affect two things. This brings something programming languages do to CSS.

There is a good bit to know about custom properties, so let’s get into it.

Why care about CSS Custom Properties?

  1. They help DRY up your CSS. That is “Don’t Repeat Yourself.” Custom properties can make code easier to maintain because you can update one value and have it reflected in multiple places. Careful though, overdoing abstraction can make have the opposite effect and make code less understandable.
  2. They are particularly helpful for things like creating color themes on a website.
  3. They unlock interesting possibilities in CSS.
  4. The fact that they can be updated in JavaScript opens up even more interesting doors.

Naming custom properties

Custom properties must be within a selector and start with two dashes (--):

/* Nope, not within a selector */ --foo: 1;  body {   /* No, 0 or 1 dash won't work */   foo: 1;   -foo: 1;     /* Yep! */   --foo: 1;    /* OK, but they're different properties */   --FOO: 1;   --Foo: 1;      /* Totally fine */   --mainColor: red;   --main-color: red;    /* Special characters are a no */   --color@home: red;   --black&blue: black;   --black^2: black; }

Best to stick with letters, numbers, and dashes while making sure the custom property is defined inside of a valid selector.

Properties as properties

You can set the value of a custom property with another custom property:

html {   --red: #a24e34;   --green: #01f3e6;   --yellow: #f0e765;    --error: var(--red);   --errorBorder: 1px dashed var(--red);   --ok: var(--green);   --warning: var(--yellow); }

Some people like doing it this way because it allows the name of a custom property to be descriptive and then used in another property with a more functional name, again helping keep things DRY. It can even help make the functional names more readable and understandable.

Valid values for custom properties

Custom properties are surprisingly tolerant when it comes to the values they accept.

Here are some basic examples that you’d expect to work, and do.

body {   --brand-color: #990000;   --transparent-black: rgba(0, 0, 0, 0.5);      --spacing: 0.66rem;   --max-reading-length: 70ch;   --brandAngle: 22deg;    --visibility: hidden;   --my-name: "Chris Coyier"; }

See that? They can be hex values, color functions, units of all kinds, and even strings of text.

But custom properties don’t have to be complete values like that. Let’s look at how useful it can be to break up valid CSS values into parts we can shove into custom properties.

Breaking up values

You can use custom properties to break up multi-part values.

Let’s imagine you’re using a color function, say rgba(). Each color channel value in there can be its own custom property. That opens up a ton of possibilities, like changing the alpha value for a specific use case, or perhaps creating color themes.

Splitting colors

Take HSL color, for example. We can split it up into parts, then very easily adjust the parts where we want. Maybe we’re working with the background color of a button. We can update specific parts of its HSL makeup when the button is hovered, in focus, or disabled, without declaring background on any of those states at all.

button {   --h: 100;   --s: 50%;   --l: 50%;   --a: 1;    background: hsl(var(--h) var(--s) var(--l) / var(--a)); } button:hover { /* Change the lightness on hover */   --l: 75%; } button:focus { /* Change the saturation on focus */   --s: 75%; } button[disabled] {  /* Make look disabled */   --s: 0%;   --a: 0.5; }

By breaking apart values like that, we can control parts of them in a way we never could before. Just look at how we didn’t need to declare all of the HSL arguments to style the hover, focus and disabled state of a button. We simply overrode specific HSL values when we needed to. Pretty cool stuff!

Shadows

box-shadow doesn’t have a shorthand property for controlling the shadow’s spread on its own. But we could break out the box-shadow spread value and control it as a custom property (demo).

button {   --spread: 5px;   box-shadow: 0 0 20px var(--spread) black; } button:hover {   --spread: 10px; }

Gradients

There is no such thing as a background-gradient-angle (or the like) shorthand for gradients. With custom properties, we can change just change that part as if there was such a thing.

body {   --angle: 180deg;   background: linear-gradient(var(--angle), red, blue); } body.sideways {   --angle: 90deg; }

Comma-separated values (like backgrounds)

Any property that supports multiple comma-separated values might be a good candidate for splitting values too, since there is no such thing as targeting just one value of a comma-separated list and changing it alone.

/* Lots of backgrounds! */ background-image:   url(./img/angles-top-left.svg),   url(./img/angles-top-right.svg),   url(./img/angles-bottom-right.svg),   url(./img/angles-bottom-left.svg),   url(./img/bonus-background.svg);

Say you wanted to remove just one of many multiple backgrounds at a media query. You could do that with custom properties like this, making it a trivial task to swap or override backgrounds.

body {   --bg1: url(./img/angles-top-left.svg);   --bg2: url(./img/angles-top-right.svg);   --bg3: url(./img/angles-bottom-right.svg);   --bg4: url(./img/angles-bottom-left.svg);   --bg5: url(./img/bonus-background.svg);      background-image: var(--bg1), var(--bg2), var(--bg3), var(--bg4); } @media (min-width: 1500px) {   body {     background-image: var(--bg1), var(--bg2), var(--bg3), var(--bg4), var(--bg5);   } }

Grids

We’re on a roll here, so we might as well do a few more examples. Like, hey, we can take the grid-template-columns property and abstract its values into custom properties to make a super flexible grid system:

.grid {   display: grid;   --edge: 10px;   grid-template-columns: var(--edge) 1fr var(--edge); } @media (min-width: 1000px) {   .grid {      --edge: 15%;    } }

Transforms

CSS will soon get individual transforms but we can get it sooner with custom properties. The idea is to apply all the transforms an element might get up front, then control them individually as needed:

button {   transform: var(--scale, scale(1)) var(--translate, translate(0)); } button:active {   --translate: translate(0, 2px); } button:hover {   --scale: scale(0.9); }

Concatenation of unit types

There are times when combining parts of values doesn’t work quite how you might hope. For example, you can’t make 24px by smashing 24 and px together. It can be done though, by multiplying the raw number by a number value with a unit.

body {   --value: 24;   --unit: px;      /* Nope */   font-size: var(--value) + var(--unit);      /* Yep */   font-size: calc(var(--value) * 1px);    /* Yep */   --pixel_converter: 1px;   font-size: calc(var(--value) * var(--pixel_converter)); }

Using the cascade

The fact that custom properties use the cascade is one of the most useful things about them.

You’ve already seen it in action in many of the examples we’ve covered, but let’s put a point on it. Say we have a custom property set pretty “high up” (on the body), and then set again on a specific class. We use it on a specific component.

body {   --background: white; } .sidebar {   --background: gray; } .module {   background: var(--background); }

Then say we’ve got practical HTML like this:

<body> <!-- --background: white -->    <main>     <div class="module">       I will have a white background.     </div>   <main>    <aside class="sidebar"> <!-- --background: gray -->     <div class="module">       I will have a gray background.     </div>   </aside>  </body>

Three CSS rulesets, one for a body, sidebar and module. the background custom property is defined as white on body and gray on sidebar. The module calls the custom property and shows an orange arrow pointing to the custom property defined in the sidebar since it is the nearest ancestor.
For the second module, .sidebar is a closer ancestor than body, thus --background resolves to gray there, but white in other places.

The “module” in the sidebar has a gray background because custom properties (like many other CSS properties) inherit through the HTML structure. Each module takes the --background value from the nearest “ancestor” where it’s been defined in CSS.

So, we have one CSS declaration but it’s doing different things in different contexts, thanks to the cascade. That’s just cool.

This plays out in other ways:

button {   --foo: Default; } button:hover {   --foo: I win, when hovered;   /* This is a more specific selector, so re-setting       custom properties here will override those in `button` */ }

Media queries don’t change specificity, but they often come later (or lower) in the CSS file than where the original selector sets a value, which also means a custom property will be overridden inside the media query:

body {   --size: 16px;   font-size: var(--size); } @media (max-width: 600px) {   body {     --size: 14px;   }  }

Media queries aren’t only for screen sizes. They can be used for things like accessibility preferences. For example, dark mode:

body {   --bg-color: white;    --text-color: black;    background-color: var(--bg-color);   color: var(--text-color); }  /* If the user's preferred color scheme is dark */ @media screen and (prefers-color-scheme: dark) {   body {     --bg-color: black;     --text-color: white;   } }

The :root thing

You’ll often see custom properties being set “at the root.” Here’s what that means:

:root {   --color: red; }  /* ...is largely the same as writing: */ html {   --color: red; }  /* ...except :root has higher specificity, so remember that! */

There is no particularly compelling reason to define custom properties like that. It’s just a way of setting custom properties as high up as they can go. If you like that, that’s totally fine. I find it somehow more normal-feeling to apply them to the html or body selectors when setting properties I intend to make available globally, or everywhere.

There is also no reason you need to set variables at this broad of a scope. It can be just as useful, and perhaps more readable and understandable, to set them right at the level you are going to use them (or fairly close in the DOM tree).

.module {   --module-spacing: 1rem;   --module-border-width: 2px;    border: var(--module-border-width) solid black; }  .module + .module {   margin-top: var(--module-spacing); }

Note that setting a custom property on the module itself means that property will no longer inherit from an ancestor (unless we set the value to inherit). Like other inherited properties, there are sometimes reasons to specify them in place (at the global level), and other times we want to inherit them from context (at the component level). Both are useful. What’s cool about custom properties is that we can define them in one place, inherit them behind the scenes and apply them somewhere completely different. We take control of the cascade!

Combining with !important

You can make an !important modifier within or outside of a variable.

.override-red {   /* this works */   --color: red !important;     color: var(--color);    /* this works, too */   --border: red;   border: 1px solid var(--border) !important; }

Applying !important to the --color variable, makes it difficult to override the value of the --color variable, but we can still ignore it by changing the color property. In the second example, our --border variable remains low-specificity (easy to override), but it’s hard to change how that value will be applied to the border itself.

Custom property fallbacks

The var() function is what allows for fallback values in custom properties.

Here we’re setting a scale() transform function to a custom property, but there is a comma-separated second value of 1.2. That 1.2 value will be used if --scale is not set.

.bigger {   transform: scale(var(--scale, 1.2)); }

After the first comma, any additional commas are part of the fallback value. That allows us to create fallbacks with comma-separated values inside them. For example, we can have one variable fall back to an entire stack of fonts:

html {   font-family: var(--fonts, Helvetica, Arial, sans-serif); }

We can also provide a series of variable fallbacks (as many as we want), but we have to nest them for that to work:

.bigger {   transform: scale(var(--scale, var(--second-fallback, 1.2)); }

If --scale is undefined, we try the --second-fallback. If that is also undefined, we finally fall back to 1.2.

Using calc() and custom properties

Even more power of custom properties is unlocked when we combine them with math!

This kind of thing is common:

main {   --spacing: 2rem; }  .module {   padding: var(--spacing); }  .module.tight {   /* divide the amount of spacing in half */   padding: calc(var(--spacing) / 2));  }

We could also use that to calculate the hue of a complementary color:

html {   --brand-hue: 320deg;   --brand-color: hsl(var(--brand-hue), 50%, 50%);   --complement: hsl(calc(var(--brand-hue) + 180deg), 50%, 50%); }

calc() can even be used with multiple custom properties:

.slider {   width: calc(var(--number-of-boxes) * var(--width-of-box)); }

Deferring the calc()

It might look weird to see calculous-like math without a calc():

body {   /* Valid, but the math isn't actually performed just yet ... */   --font-size: var(--base-font-size) * var(--modifier);    /* ... so this isn't going to work */   font-size: var(--font-size); }

The trick is that as long as you eventually put it in a calc() function, it works fine:

body {   --base-font-size: 16px;   --modifier: 2;   --font-size: var(--base-font-size) * var(--modifier);    /* The calc() is "deferred" down to here, which works */   font-size: calc(var(--font-size)); }

This might be useful if you’re doing quite a bit of math on your variables, and the calc() wrapper becomes distracting or noisy in the code.

@property

The @property “at-rule” in CSS allows you to declare the type of a custom property, as well its as initial value and whether it inherits or not.

It’s sort of like you’re creating an actual CSS property and have the ability to define what it’s called, it’s syntax, how it interacts with the cascade, and its initial value.

@property --x {   syntax: '<number>';   inherits: false;   initial-value: 42; }
Valid Types
  • length
  • number
  • percentage
  • length-percentage
  • color
  • image
  • url
  • integer
  • angle
  • time
  • resolution
  • transform-list
  • transform-function
  • custom-ident (a custom identifier string)

This means that the browser knows what kind of value it is dealing with, rather than assuming everything is a string. That means you can animate things in ways you couldn’t otherwise.

For example, say you have a star-shaped icon that you want to spin around with @keyframes and rotate with a transform. So you do this:

.star {   --r: 0deg;   transform: rotate(var(--r));   animation: spin 1s linear infinite; }  @keyframes spin {   100% {     --r: 360deg;   } }

That actually won’t work, as the browser doesn’t know that 0deg and 360deg are valid angle values. You have to define them as an <angle> type with @property for that to work.

@property --angle {   syntax: '<angle>';   initial-value: 0deg;   inherits: false; }  .star {   --r: 0deg;   transform: rotate(var(--r));   animation: spin 1s linear infinite; }  @keyframes spin {   100% {     --r: 360deg;   } }
Demo

Commas in values

This can be a smidge confusing. Maybe not so much this:

html {   --list: 1, 2, 3; }

But below, you’ll need a sharp eye to realize the fallback value is actually 1.2, 2. The first comma separates the fallback, but all the rest is part of the value.

html {   transform: scale(var(--scale, 1.2, 2)); }

Learn more about fallbacks above ⮑

Advanced usage

The Raven is a technique that emulates container queries using math and custom properties. Be prepared, this goes from 0-100 in complexity right out of the gate!

Demo

Resize this demo to see a grid of inline-block elements change number of columns from 4 to 3 to 1.

Here’s a few more favorite examples that show off advanced usage of custom properties:

The initial and whitespace trick

Think of @media queries and how when one thing changes (e.g. the width of the page) you can control multiple things. That’s kind of the idea with this trick. You change one custom property and control multiple things.

The trick is that the value of initial for a custom property will trigger a fallback, while an empty whitespace value will not. For the sake of explanation, it let’s define two globally-scoped custom properties, ON and OFF:

:root {   --ON: initial;   --OFF: ; }

Say we have a “dark” variation class which sets a number of different properties. The default is --OFF, but can be flipped to --ON whenever:

.module {   --dark: var(--OFF); }  .dark { /* could be a media query or whatever */   --dark: var(--ON); }

Now you can use --dark to conditinally set values that apply only when you’ve flipped --dark to --ON. Demo:

Lea Verou has a great writeup that covers all of this.

Inline styles

It’s totally legit to set a custom property in HTML with an inline style.

<div style="--color: red;"></div>

That will, like any inline style, have a very high level of specificity.

This can be super useful for when the HTML might have access to some useful styling information that would be too weird/difficult to put into a static CSS file. A good example of that is maintaining the aspect ratio of an element:

<div style="--aspect-ratio: 16 / 9;"></div>

Now I can set up some CSS to make a box of that exact size wherever I need to. The full writeup on that is here, but here’s CSS that uses trickery like the ol’ padded box applied to a pseudo element which pushes the box to the desired size:

[style*="--aspect-ratio"] > :first-child {   width: 100%; } [style*="--aspect-ratio"] > img {     height: auto; }  @supports (--custom: property) {   [style*="--aspect-ratio"] {     position: relative;   }   [style*="--aspect-ratio"]::before {     content: "";     display: block;     padding-bottom: calc(100% / (var(--aspect-ratio)));   }     [style*="--aspect-ratio"] > :first-child {     position: absolute;     top: 0;     left: 0;     height: 100%;   }   }

But hey, these days, we have a native aspect-ratio property in CSS, so setting that in the inline style might make more sense going forward.

<div style="aspect-ratio: 16 / 9;"></div>

Hovers and pseudos

There is no way to apply a :hover style (or other pseudo classes/elements) with inline styles. That is, unless we get tricky with custom properties. Say we want custom hover colors on some boxes — we can pass that information in as a custom property:

<div style="--hover-color: red;"><div> <div style="--hover-color: blue;"><div> <div style="--hover-color: yellow;"><div>

Then use it in CSS which, of course, can style a link’s hover state:

div:hover {   background-color: var(--hover-color); }  /* And use in other pseudos! */ div:hover::after {   content: "I am " attr(style);   border-color: var(--hover-color); }

Custom properties and JavaScript

JavaScript can set the value of a custom property.

element.style.setProperty('--x', value);

Here’s an example of a red square that is positioned with custom properties, and JavaScript updates those custom property values with the mouse position:

Typically you think of JavaScript passing values to CSS to use, which is probably 99% of usage here, but note that you can pass things from CSS to JavaScript as well. As we’ve seen, the value of a custom property can be fairly permissive. That means you could pass it a logical statement. For example:

html {   --logic: if (x > 5) document.body.style.background = "blue"; }

Then grab that value and execute it in JavaScript:

const x = 10;  const logic = getComputedStyle(document.documentElement).getPropertyValue(   "--logic" );  eval(logic);

Custom properties are different than preprocessor variables

Say you’re already using Sass, Less, or Stylus. All those CSS preprocessors offer variables and it’s one of the main reasons to have them as part of your build process.

// Variable usage in Sass (SCSS) $ brandColor: red;  .marketing {   color: $ brandColor; }

So, do you even need to bother with native CSS custom properties then? Yes, you should. Here’s why in a nutshell:

  • Native CSS custom properties are more powerful then preprocessor variables. Their integration with the cascade in the DOM is something that preprocessor variables will never be able to do.
  • Native CSS custom properties are dynamic. When they change (perhaps via JavaScript, or with a media query), the browser repaints what it needs to. Preprocessor variables resolve to a value when they’re compiled and stay at that value.
  • Going with a native feature is good for the longevity of your code. You don’t need to preprocess native CSS.

I cover this in much more detail in the article “What is the difference between CSS variables and preprocessor variables?”

To be totally fair, there are little things that preprocessor variables can do that are hard or impossible with custom properties. Say you wanted to strip the units off a value for example. You can do that in Sass but you’ll have a much harder time with custom properties in CSS alone.

Can you preprocess custom properties?

Kinda. You can do this, with Sass just to pick one popular preprocessor:

$ brandColor: red; body {   --brandColor: $ brandColor; }

All that’s doing is moving a Sass variable to a custom property. That could be useful sometimes, but not terribly. Sass will just make --brandColor: red; there, not process the custom property away.

If a browser doesn’t support custom properties, that’s that. You can’t force a browser to do what custom properties do by CSS syntax transformations alone. There might be some kind of JavaScript polyfill that parses your CSS and replicates it, but I really don’t suggest that.

The PostCSS Custom Properties plugin, though, does do CSS syntax transforms to help. What it does is figure out the value to the best of it’s ability, and outputs that along with the custom property. So like:

:root {   --brandColor: red; } body {   color: var(--brandColor); }

Will output like this:

:root {   --brandColor: red; } body {   color: red;   color: var(--brandColor); }

That means you get a value that hopefully doesn’t seem broken in browsers that lack custom property support, but does not support any of the fancy things you can do with custom properties and will not even attempt to try. I’m a bit dubious about how useful that is, but I think this is about the best you can do and I like the spirit of attempting to not break things in older browsers or newer browsers.

Availiability

Another thing that is worth noting about the difference between is that with a CSS preprocessor, the variables are available only as you’re processing. Something like $ brandColor is meaningless in your HTML or JavaScript. But when you have custom properties in use, you can set inline styles that use those custom properties and they will work. Or you can use JavaScript to figure out their current values (in context), if needed.

Aside from some somewhat esoteric features of preprocessor variables (e.g. some math possibilities), custom properties are more capable and useful.

Custom properties and Web Components (Shadow DOM)

One of the most common and practical ways to style of Web Components (e.g. a <custom-component> with shadow DOM) is by using custom properties as styling hooks.

The main point of the shadow DOM is that it doesn’t “leak” styles in or out of it, offering style isolation in a way that nothing else offers, short of an <iframe>. Styles do still cascade their way inside, I just can’t select my way inside. This means custom properties will slide right in there.

Here’s an example:

Another common occurrence of the shadow DOM is with SVG and the <use> element.

Video: “CSS Custom Properties Penetrate the Shadow DOM”

Browser support

This browser support data is from Caniuse, which has more detail. A number indicates that browser supports the feature at that version and up.

Desktop

Chrome Firefox IE Edge Safari
49 31 No 16 10

Mobile / Tablet

Android Chrome Android Firefox Android iOS Safari
90 87 90 10.0-10.2

You can preprocess for deeper browser support, with heavy limitations.

@supports

If you would like to write conditional CSS for when a browser supports custom properties or not:

@supports (--custom: property) {   /* Isolated CSS for browsers that DOES support custom properties, assuming it DOES support @supports */ }  @supports not (--custom: property) {   /* Isolated CSS for browsers that DON'T support custom properties, assuming it DOES support @supports */ }

Credit

Thanks to Miriam Suzanne for co-authoring this with me!


The post A Complete Guide to Custom Properties appeared first on CSS-Tricks.

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

CSS-Tricks

, , ,
[Top]

Creating Custom Form Controls with ElementInternals

Ever since the dawn of time, humanity has dreamed of having more control over form elements. OK, I might be overselling it a tiny bit, but creating or customizing form components has been a holy grail of front-end web development for years.

One of the lesser-heralded, but most powerful features of custom elements (e.g. <my-custom-element>) has quietly made its way into Google Chrome as of version 77 and is working its way into other browsers. The ElementInternals standard is a very exciting set of features with a very unassuming name. Among the features internals adds are the ability to participate in forms and an API around accessibility controls.

In this article, we’re going to look at how to create a custom form control, integrate constraint validation, introduce the basics of internal accessibility and see a way to combine these features to create a highly-portable macro form control.

Let’s start by creating a very simple custom element that matches our design system. Our element will hold all of its styles within the shadow DOM and ensure some basic accessibility. We’ll use the wonderful LitElement library from the Polymer team at Google for our code examples and, although you definitely don’t need it, it does provide a great abstraction for writing custom elements.

In this Pen, we’ve created a <rad-input> that has some basic design to it. We have also added a second input to our form that is a vanilla HTML input, and added a default value (so you can simply press submit and see it work).

When we click our submit button a few things happen. First, the submit event’s preventDefault method is called, in this case, to ensure our page doesn’t reload. After this, we create a FormData object which gives us access to information about our form which we use to construct a JSON string and append it to an <output> element. Notice, however, that the only value added to our output is from the element with name="lame".

That’s because our element doesn’t know how to interact with the form just yet, so let’s set up our <rad-input> with an ElementInternals instance to help it live up to its name. To start, we’ll need to call our method’s attachInternals method in the element’s constructor, we’ll also be importing an ElementInternals polyfill into our page to work with browsers that don’t support the spec yet.

The attachInternals method returns a new element internals instance which contains some new APIs we can use in our method. In order to let our element take advantage of these APIs, we need to add a static formAssociated getter that returns true.

class RadInput extends LitElement {   static get formAssociated() {     return true;   }    constructor() {     super();     this.internals();   } }

Let’s take a look at some of the APIs in our element’s internals property:

  • setFormValue(value: string|FormData|File, state?: any): void — This method will set the element’s value on its parent form if one is present. If the value is null, the element will not participate in the form submission process.
  • form — A reference to our element’s parent form, if one exists.
  • setValidity(flags: Partial<ValidityState>, message?: string, anchor?: HTMLElement): void — The setValidity method will help control our element’s validity state within the form. If the form is invalid, a validation message must be present.
  • willValidate — Will be true if the element will be evaluated when the form is submitted.
  • validity — A validity object that matches the APIs and semantics attached to HTMLInputElement.prototype.validity.
  • validationMessage — If the control has been set as invalid with setValidity, this is the message that was passed in describing the error.
  • checkValidity — Will return true if the element is valid, otherwise this will return false and fire an invalid event on the element.
  • reportValidity — Does the same as checkValidity, and will report problems to the user if the event isn’t cancelled.
  • labels — A list of elements that label this element using the label[for] attribute.
  • A number of other controls used to set aria information on the element.

Setting a custom element’s value

Let’s modify our <rad-input> to take advantage of some of these APIs:

Here we’ve modified the element’s _onInput method to include a call to this.internals.setFormValue. This tells the form our element wants to register a value with the form under its given name (which is set as an attribute in our HTML). We’ve also added a firstUpdated method (loosely analogous with connectedCallback when not using LitElement) which sets the element’s value to an empty string whenever the element is done rendering. This is to make sure our element always has a value with the form (and though it is not necessary, you may want to exclude your element from the form by passing in a null value).

Now when we add a value to our input and submit the form, we will see that we have a radInput value in our <output> element. We can also see our element has been added to the HTMLFormElement’s radInput property. One thing you might have noticed, however, is that despite the fact that despite the fact that our element doesn’t have a value, it will still allow the form submission to take place. Let’s add some validation to our element next.

Adding constraint validation

In order to set our field’s validation, we need to modify our element a little bit to make use of the setValidity method on our element internals object. This method will take in three arguments (the second one is only required if the element is invalid, the third is always optional). The first argument is a partial ValidityState object. If any flag is set to true the control will be marked as invalid. If one of the built-in validity keys doesn’t meet your needs, there is a catch-all customError key that should work. Lastly, if the control is valid, we pass in an object literal ({}) to reset the control’s validity.

The second argument here is the control’s validity message. This argument is required if the control is invalid, and not allowed if the control is valid. The third argument is an optional validation target that will control the user’s focus if and when the form is submitted as invalid or reportValidity is called.

We’re going to introduce a new method to our <rad-input> that will take care of this logic for us:

_manageRequired() {   const { value } = this;   const input = this.shadowRoot.querySelector('input');   if (value === '' && this.required) {     this.internals.setValidity({       valueMissing: true     }, 'This field is required', input);   } else {     this.internals.setValidity({});   } }

This function gets the control’s value and input. If the value is equal to an empty string and the element is marked as required, we’ll call the internals.setValidity and toggle the control’s validity. Now we all we need to do is call this method in our firstUpdated and _onInput methods and we’ll have added some basic validation to our element.

Clicking the submit button before a value is entered into our <rad-input> will now display an error message in browsers that support the ElementInternals spec. Unfortunately, displaying validation errors is still not supported by the polyfill as there isn’t any reliable way to trigger the built-in validation popup in non-supporting browsers.

We’ve also added some basic accessibility information to our example by using our internals object. We’ve added an additional property to our element, _required, which will serve as a proxy for this.required and as a getter/setter for required.

get required() {   return this._required; }  set required(isRequired) {   this._required = isRequired;   this.internals.ariaRequired = isRequired; } 

By passing the required property to internals.ariaRequired, we are alerting screen readers that our element is currently expecting a value. In the polyfill, this is done by adding an aria-required attribute; however, in supporting browsers, the attribute won’t be added to the element because that property is inherent to the element.

Creating a micro-form

Now that we have a working input that meets our design system, we might want to begin composing our elements into patterns that we can reuse throughout several applications. One of the most compelling features for ElementInternals is that the setFormValue method can take not only string and file data, but also FormData objects. So let’s say we want to create a common address form that might be used in multiple organizations, we can do that easily with our newly-created elements.

In this example, we have a form created inside our element’s shadow root where we have composed four <rad-input> elements to make an address form. Instead of calling setFormValue with a string, this time we’ve chosen to pass along the entire value of our form. As a result, our element passes along the values of each individual element inside its child form to the outer form.


Adding constraint validation to this form would be a fairly straightforward process, as would providing additional styles, behaviors and slotting in content. Using these newer APIs finally allows developers to unlock a ton of potential inside custom elements and finally gives us free-range on controlling our user experiences.


The post Creating Custom Form Controls with ElementInternals appeared first on CSS-Tricks.

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

CSS-Tricks

, , , ,
[Top]