A cross-browser function as of the discharge of Firefox 128 in July 2024 is a brand new at-rule – @property
– which permits defining varieties in addition to inheritance and an preliminary worth in your {custom} properties.
We’ll study when and why conventional fallback values can fail, and the way @property
options enable us to jot down safer, extra resilient CSS {custom} property definitions.
Normal Use of Customized Properties
Customized properties – aka “CSS variables” – are helpful as a result of they permit creating references to values much like variables in different programming languages.
Think about the next situation which creates and assigns the --color-blue
property, which then is carried out as a category and utilized to a paragraph.
CSS for “Normal use case for {custom} properties”
:root {
--color-blue: blue;
}
.color-blue {
shade: var(--color-blue);
}
The paragraph renders as blue. Glorious! Ship it.
Widespread Error Circumstances Utilizing Customized Properties
Now, you and I each know that “blue” is a shade. And it additionally could appear apparent that you’d solely apply the category that makes use of this shade to textual content we intend to be blue.
However, the actual world is not good, and typically the downstream shoppers of our CSS end-up with a purpose to re-define the worth. Or maybe they by accident introduce a typo that impacts the unique worth.
The result of both of those situations might be:
- the textual content finally ends up a shade in addition to blue, as that writer meant
- the textual content surprisingly renders as black
If the rendered shade is surprisingly black, it is seemingly that we have hit the distinctive situation of invalid at computed worth time.
When the browser is assessing CSS guidelines and figuring out what worth to use to every property primarily based on the cascade, inheritance, specificity and so forth, it’s going to retain a {custom} property because the profitable worth so long as it understands the overall means it is getting used.
In our --color-blue
instance, the browser positively understands the shade
property, so it assumes all might be pleased with using the variable as effectively.
However, what occurs if somebody redefines --color-blue
to an invalid shade?
CSS for “Defining an invalid shade”
:root {
--color-blue: blue;
}
.color-blue {
shade: var(--color-blue);
}
p {
--color-blue: notacolor;
}
Uh oh – it is surprisingly rendering as black.
Why Conventional Fallbacks Can Fail
Okay, earlier than we study what that scary-sounding phrase means, let’s have a look in DevTools and see if it offers us a clue about what is going on on.
That appears fairly regular, and does not appear to disclose that something is flawed, making troubleshooting the error lots trickier.
You would possibly know that {custom} properties enable a fallback worth as second parameter, so maybe that can assist! Let’s attempt.
CSS for “Try decision with {custom} property fallback”
:root {
--color-blue: blue;
}
.color-blue {
shade: var(--color-blue, blue);
}
p {
--color-blue: notacolor;
}
Sadly, the textual content nonetheless renders as black.
Okay, however our good buddy the cascade exists, and again within the day we used to place issues like vendor prefixed properties previous to the unprefixed ones. So, maybe if we use the same methodology and provide an additional shade
definition earlier than the one which has the {custom} property it will probably fallback to that?
CSS for “Try decision with further shade definition”
:root {
--color-blue: blue;
}
.color-blue {
shade: blue;
shade: var(--color-blue);
}
p {
--color-blue: notacolor;
}
Bummer, we are not making progress in the direction of stopping this problem.
That is due to the (barely scary sounding) situation invalid at computed worth time.
Though the browser has stored our definition that expects a {custom} property worth, it is not till later that the browser tries to truly compute that worth.
On this case, it seems at each the .color-blue
class and the worth supplied for the p
factor rule and makes an attempt to use the computed worth of notacolor
. At this stage, it has discarded the alternate worth of blue
initially supplied by the category. Consequently, since notacolor
is in actual fact not a shade and due to this fact invalid, the very best it will probably do is use both:
- an inherited worth if the property is allowed to inherit and an ancestor has supplied a price; or
- the preliminary worth as outlined within the CSS spec
Whereas shade
is an inheritable property, we’ve not outlined it on any ancestors, so the rendered shade of black
is because of the preliminary worth.
Seek advice from this earlier Trendy CSS article about how {custom} property values are computed and study extra deeply concerning the situation of invalid at computed worth time.
Defining Sorts for Safer CSS
It is time to introduce @property
to assist remedy this problem of what chances are you’ll understand as a shocking rendered worth.
The crucial options @property
gives are:
- defining acceptable varieties for particular {custom} properties
- enabling or disabling inheritance
- offering an preliminary worth as a failsafe for invalid or undefined values
This at-rule is outlined on a per-custom property foundation, that means a novel definition is required for every property for which you wish to leverage these advantages.
It’s not a requirement, and you may actually proceed utilizing {custom} properties with out ever bringing in @property
.
Please notice that at time of writing,
@property
could be very newly cross-browser and I might advise you to think about it a progressive enhancement to learn customers in supporting browsers.
Let’s apply it to our blue dilemma and see the way it fixes the problem of the in any other case invalid shade equipped within the factor rule.
CSS for “Apply @property to –color-blue”
@property --color-blue {
syntax: "<shade>";
inherits: true;
initial-value: blue;
}
Success, our textual content continues to be blue regardless of the invalid definition!
Moreover, DevTools is now useful once more:
We will observe each that the invalid worth is clearly an error, and we are also supplied the complete definition of the {custom} property through the hover overlay.
Offering Sorts through syntax
Why would we’d like varieties for {custom} properties? Listed below are just a few causes:
- varieties assist confirm what makes a legitimate vs. invalid worth
- with out varieties, {custom} properties are very open-ended and may take practically any worth, together with a clean house
- lack of varieties prevents browser DevTools from offering the optimum stage of element about which worth is in use for a {custom} property
In our @property
definition, the syntax
descriptor permits offering the allowed varieties for the {custom} property. We used "<shade>"
, however different varieties embody:
"<size>"
– numbers with items connected, ex.4px
or3vw
"<integer>"
– decimal items 0 via 9 (aka “complete numbers”)"<quantity>"
– numbers which can have a fraction, ex.1.25
"<proportion>"
– numbers with a proportion signal connected, ex.24%
"<length-percentage>"
– accepts legitimate<size>
or<proportion>
values
A particular case is "*"
which stands for “common syntax” and permits accepting any worth, much like the default conduct. This implies you skip the typing profit, however maybe need the inheritance and/or preliminary worth management.
These varieties and extra are listed for the syntax descriptor on MDN.
The kind applies to the computed worth of the {custom} property, so the "<shade>"
kind can be proud of each blue
in addition to light-dark(blue, cyan)
(though solely a type of is accepted into the initial-value
as we’ll quickly study).
Stronger Typing With Lists
To illustrate we wish to present slightly flexibility for our --color-blue
{custom} property.
We will use a listing to supply legitimate choices. Something aside from these precise values can be thought of invalid, and use the initial-value
as a substitute (if inheritance did not apply). These are referred to as “{custom} idents”, are case delicate, and could be any worth.
CSS for “Defining a listing inside syntax”
@property --color-blue cyan
.color-blue {
shade: var(--color-blue);
}
.demo p {
--color-blue: dodgerblue;
}
Typing for Combined Values
The pipe character (|
) used within the earlier record signifies an “or” situation. Whereas we used specific shade names, it can be used to say “any of those syntax varieties are legitimate.”
syntax: "<shade> | <size>";
Typing for A number of Values
To date, we have solely typed {custom} properties that anticipate a single worth.
Two further instances could be lined with a further “multiplier” character, which ought to instantly comply with the syntax part title.
- Use
+
to assist a space-separated record, ex."<size>+"
- Use
#
to assist a comma-separated record, ex."<size>#"
This may be helpful for properties that enable a number of definitions, akin to background-image
.
CSS for “Help a number of values for syntax”
@property --bg-gradient{
syntax: "<picture>#";
inherits: false;
initial-value:
repeating-linear-gradient(to proper, blue 10px 12px, clear 12px 22px),
repeating-linear-gradient(to backside, blue 10px 12px, clear 12px 22px);
}
.field {
background-image: var(--bg-gradient);
inline-size: 5rem;
aspect-ratio: 1;
border-radius: 4px;
border: 1px strong;
}
Typing for Multi-Half Combined Values
Some properties settle for combined varieties to develop the complete worth, akin to box-shadow
which has potential forms of inset
, a sequence of <size>
values, and a <shade>
.
Presently, it is not potential to kind this in a single @property
definition, though chances are you’ll try and attempt one thing like "<size>+ <shade>"
. Nevertheless, this successfully invalidates the @property
definition itself.
One different is to interrupt up the {custom} property definitions in order that we will enable a sequence of lengths, after which enable a shade. Whereas barely extra cumbersome, this permits us to nonetheless get the good thing about typing which relieves the potential errors we lined earlier.
CSS for “Help multi-part combined values for syntax”
@property --box-shadow-length {
syntax: "<size>+";
inherits: false;
initial-value: 0px 0px 8px 2px;
}
@property --box-shadow-color {
syntax: "<shade>";
inherits: false;
initial-value: hsl(0 0% 75%);
}
.field {
box-shadow: var(--box-shadow-length) var(--box-shadow-color);
inline-size: 5rem;
aspect-ratio: 1;
border-radius: 4px;
}
If you happen to’re much less involved concerning the “kind” of a property for one thing like box-shadow
and care extra about inheritance or the preliminary worth, you’ll be able to as a substitute use the common syntax definition to permit any worth. This negates the issue we simply mitigated by splitting up the components.
@property --box-shadow {
syntax: "*";
inherits: false;
initial-value: 0px 0px 8px 2px hsl(0 0% 75%);
}
As a result of the common syntax accepts any worth, a further “multiplier” just isn’t wanted.
Be aware: The initial-value
continues to be required to be computationally impartial as we’ll find out about quickly below limitations of initial-value.
A subset of CSS properties are inheritable, akin to shade
. The inherits
descriptor in your @property
registration permits you to management that conduct in your {custom} property.
If true
, the computed worth can look to an ancestor for its worth if the property just isn’t explicitly set, and if a price is not discovered it’s going to use the preliminary worth.
If false
, the computed worth will use the preliminary worth if the property just isn’t explicitly set for the factor, akin to through a category or factor rule.
On this demonstration, the --box-bg
has been set to inherits: false
, and solely the outer field has an specific definition through the utilized class. The interior field makes use of the preliminary worth since inheritance just isn’t allowed.
CSS for “Results of setting inherits: false”
@property --box-bg {
syntax: "<shade>";
inherits: false;
initial-value: cyan;
}
.field {
background-color: var(--box-bg);
aspect-ratio: 1;
border-radius: 4px;
padding: 1.5rem;
}
.outer-box {
--box-bg: purple;
}
Legitimate Use of initial-value
Except your syntax is open to any worth utilizing the common syntax definition – "*"
– then it’s required to set an initial-value
to achieve the advantages of registering a {custom} property.
As we have already skilled, use of initial-value
was crucial in stopping the situation of a very damaged render attributable to invalid at computed worth time. Listed below are another advantages of utilizing @property
with an initial-value
.
When constructing design programs or UI libraries, it is vital to make sure your {custom} properties are sturdy and dependable. Offering an initial-value
might help forestall a damaged expertise. Plus, typing properties additionally meshes properly with holding the intent of design tokens which can be expressed as {custom} properties.
Dynamic computation situations akin to using clamp()
have the potential to incorporate an invalid worth, whether or not via an error or from the browser not supporting one thing throughout the operate. Having a fallback through initial-value
ensures that your design stays purposeful. This fallback conduct is a safeguard for unsupported options as effectively, although that may be restricted by whether or not the @property
rule is supported within the browser getting used.
Evaluation further methods to forestall invalid at computed time that could be extra applicable in your browser assist matrix, particularly for crucial situations.
Incorporating @property
with initial-value
not solely enhances the reliability of your CSS but in addition opens the door to the potential for higher tooling round {custom} properties. We have previewed the conduct change in browser DevTools, however I am eager for an enlargement of tooling together with IDE plugins.
The added layer of safety from utilizing @property
with initial-value
helps keep the intent of your design, even when it is not good for each context.
Limitations of initial-value
The initial-value
is topic to the syntax
you outline for @property
. Past that, syntax
itself does not assist each potential worth mixture, which we beforehand lined. So, typically slightly creativity is required to get the profit.
Additionally, initial-value
values have to be what the spec calls computationally impartial. Simplified, this implies relative values like em
or dynamic capabilities like clamp()
or light-dark()
are sadly not allowed. Nevertheless, in these situations you’ll be able to nonetheless set an appropriate preliminary worth, after which use a relative or dynamic worth while you use the {custom} property, akin to within the :root
task.
@property --heading-font-size {
syntax: "<size>";
inherits: true;
initial-value: 24px;
}
:root {
--heading-font-size: clamp(1.25rem, 5cqi, 2rem);
}
This limitation on relative items or dynamic capabilities additionally means different {custom} properties can’t be used within the initial-value
task. The earlier method can nonetheless be used to mitigate this, the place the popular consequence consists in using the property.
Lastly, {custom} properties registered through @property
are nonetheless locked into the principles of standard properties, akin to that they can’t be used to allow variables in media or container question at-rules. For instance, @media (min-width: var(--mq-md))
would nonetheless be invalid.
Unsupported initial-value
Can Crash the Web page
As of time of writing, utilizing a property or operate worth {that a} browser might not assist as a part of the initial-value
definition may cause your entire web page to crash!
Fortuitously, we will use @helps
to check for ultra-modern properties or options earlier than we attempt to use them because the initial-value
.
@helps ([property|feature]) {
}
@helps not ([property|feature]) {
}
There should still be some surprises the place @helps
stories true, however testing will reveal a crash or different error (ex. currentColor
used with color-mix()
in Safari). You’ll want to check your options cross-browser!
Be taught extra about methods to check function assist for contemporary CSS.
Exceptions to Dynamic Limitations
There are just a few circumstances which can really feel like exceptions to the requirement of “computationally impartial” values when used for the initial-value
.
First, currentColor
is accepted. Not like a relative worth akin to em
which requires computing font-size
of ancestors to compute itself, the worth of currentColor
could be computed with out relying on context.
CSS for “Use of currentColor as initial-value”
@property --border-color {
syntax: "<shade>";
inherits: false;
initial-value: currentColor;
}
h2 {
shade: blue;
border: 3px strong var(--border-color);
}
My border is about to currentColor
Second, use of "<length-percentage>"
permits using calc()
, which is talked about within the spec. This enables a calculation that features what is taken into account a world, computationally impartial unit set despite the fact that we regularly use them for dynamic conduct. That’s, using viewport items.
For a situation akin to fluid kind, this gives a greater fallback that retains the spirit of the meant consequence despite the fact that it is total much less excellent for many situations.
CSS for “Use of calc() with vi for initial-value”
@property --heading-font-size {
syntax: "<length-percentage>";
inherits: true;
initial-value: calc(18px + 1.5vi);
}
h2 {
font-size: var(--heading-font-size);
}
Resize the window to see the fluid conduct
Be aware: Whereas we usually advocate utilizing rem
for font-size
definitions, it’s thought of a relative worth and never accepted to be used in initial-value
, therefore using px
within the calculation.
Penalties of Setting initial-value
In some situations, registering a property with out the common syntax – which implies an initial-value
is required – has penalties, and limits the property’s use.
Some causes for preferring non-compulsory part properties embody:
- to make use of the common {custom} property fallback methodology in your default worth, particularly if the fallback ought to be one other {custom} property (ex. a design token)
- an
initial-value
might lead to an undesirable default situation, notably since it will probably’t embody one other {custom} property
A way I really like to make use of for versatile part kinds is together with an deliberately undefined {custom} property in order that variants can effectively be created simply by updating the {custom} property worth. Or, purposely utilizing completely undefined properties to make the bottom class extra inclusive of varied situations by treating {custom} properties like a part type API.
For instance, if I registered --button-background
right here as a shade, it might by no means use the right fallback when my intention was for the default variant to make use of the fallback.
.button {
background-color: var(--button-background, var(--color-primary));
border-radius: var(--button-border-radius);
}
.button--secondary {
--button-background: var(--color-secondary);
}
.button--rounded {
--button-border-radius: 4px;
}
If you happen to even have these situations, chances are you’ll think about using a combined strategy of typing your primitive properties – like --color-primary
– however not the component-specific properties.
Concerns For Utilizing @property
Whereas a number of the demos on this article deliberately have been set as much as have the rendered output use solely the initial-value
, in apply it might be finest to individually outline the {custom} property. Once more, that is presently a brand new function, so with out a further definition akin to in :root
you threat not having a price in any respect when you swap to solely counting on initial-value
.
You must also bear in mind that it’s potential to register the identical property a number of instances, and that cascade guidelines imply the final one will win. This raises the potential for conflicts from unintended overrides. There is not a approach to “scope” the @property
rule inside a selector.
Nevertheless, use of cascade layers can modify this conduct since unlayered kinds win over layered kinds, which incorporates at-rules. Cascade layers could be a approach to handle registration of @property
guidelines when you assign a “properties” layer early on and decide to assigning all registrations to that layer.
Customized properties can be registered through JavaScript. Actually, this was the unique approach to do it since this functionality was initially coupled with the Houdini APIs. If a property is registered through JS, that definition is more likely to win over the one in your stylesheets. That stated, in case your precise intent is to vary a {custom} property worth through JS, study the extra applicable approach to entry and set {custom} properties with JS.
Use of @property
has the potential for strengthening container type queries, particularly in case you are registering properties to behave as toggles or enums. On this instance, using @property
helps by typing our theme values, and ensures a fallback of “gentle”.
@property --theme darkish";
inherits: true;
initial-value: gentle;
:root {
--theme: darkish;
}
@container type(--theme: darkish) {
physique {
background-color: black;
shade: white;
}
}
Be taught extra about this explicit concept of utilizing type queries for simplified darkish mode.
Though it is a bit outdoors the scope of this text, one other advantage of typing {custom} properties is that they change into animatable. It’s because the kind turns the worth into one thing CSS is aware of how one can work with, vs. the mysterious open-ended worth it might in any other case be. Here is a CodePen instance of how registering a shade {custom} property permits animating a spread of colours for the background.
Use of @property
permits writing safer CSS {custom} properties, which improves the reliability of your system design, and defends in opposition to errors that might impression person expertise. A reminder that for now they’re a progressive enhancement and may virtually all the time be used at the side of an specific definition of the property.
You’ll want to check to make sure your meant consequence of each the allowed syntax, and the end result if the initial-value
is used within the remaining render.