A custom property is any property whose name starts with --. It cascades and inherits, so a value set on :root is visible everywhere and can be overridden for one subtree. var(--name, fallback) reads it, using the fallback only when it is undefined. Names are case-sensitive; scripts set them with el.style.setProperty().
An unregistered custom property is a plain token string, checked only after substitution. If --size: red lands in width: var(--size), the declaration is invalid at computed-value time and width falls back to its inherited or initial value, not to an earlier width rule. @property (Baseline newly available, 2024) registers a type, an inheritance flag and an initial value (required unless the syntax is "*"), so the browser validates, computes and interpolates the value. Only a registered property animates smoothly; an unregistered one flips halfway.
<style>
@property --p { syntax: "<percentage>"; inherits: false; initial-value: 0%; }
@keyframes fill { from { --p: 0%; --q: 0%; } to { --p: 100%; --q: 100%; } }
div { display: inline-grid; place-items: center; width: 100px; height: 100px; margin: 4px;
vertical-align: top; border-radius: 50%; font: 13px sans-serif;
animation: fill 2s linear -0.7s paused; }
</style>
<div style="background: conic-gradient(#1565c0 var(--p), #eceff1 0)">--p: 35%</div>
<div style="background: conic-gradient(#1565c0 var(--q), #eceff1 0)">--q: 0%</div>