[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"verticals":3,"playground-css-property-registered-custom-properties":32},[4,20],{"id":5,"slug":6,"name":7,"tagline":8,"description":9,"accentFrom":10,"accentTo":11,"icon":12,"defaultLocale":13,"locales":14,"features":16,"position":19},"019fe637-3d33-714b-b57f-23e163ffca0c","dev","Web Development","Build. Learn. Ship.","Practical courses, engineering-grade articles and open-source tools for people who ship.","violet-500","cyan-400","◇","en",[13,15],"fa",{"courses":17,"paths":17,"articles":17,"exams":18,"flashcards":18,"packages":17,"community":17,"certificates":17,"teams":17,"commerce":17},true,false,0,{"id":21,"slug":22,"name":23,"tagline":24,"description":25,"accentFrom":26,"accentTo":10,"icon":27,"defaultLocale":13,"locales":28,"features":30,"position":31},"019fe637-3dc2-754c-8657-0f175bfee7c6","lang","Languages","Learn a language the way you learn a codebase.","Structured paths, listening drills and spaced repetition that actually sticks.","amber-400","⌘",[13,15,29],"es",{"courses":17,"paths":17,"articles":18,"exams":18,"flashcards":18,"packages":18,"community":17,"certificates":18,"teams":18,"commerce":17},2,{"slug":33,"title":34,"summary":35,"html":36,"css":37,"js":38,"config":39,"doc":41},"css-property-registered-custom-properties","@property — give a CSS variable a type",null,"\u003Cdiv class=\"wrap\">\n    \u003Ch1>🎮 A type is all that's missing\u003C\u002Fh1>\n    \u003Cp class=\"sub\">\n      Both sides run \u003Cem>identical\u003C\u002Fem> CSS — same \u003Ccode>var()\u003C\u002Fcode>, same \u003Ccode>transition\u003C\u002Fcode>,\n      same \u003Ccode>@keyframes\u003C\u002Fcode>. The only difference: the right side's variables are\n      \u003Cstrong>registered with \u003Ccode>@property\u003C\u002Fcode>\u003C\u002Fstrong>. Click and watch what a type unlocks.\n    \u003C\u002Fp>\n\n    \u003Cdiv class=\"note\" id=\"unsupported\" hidden>\n      ⚠️ This browser doesn't support \u003Ccode>@property\u003C\u002Fcode> \u002F \u003Ccode>CSS.registerProperty()\u003C\u002Fcode>,\n      so \u003Cem>both\u003C\u002Fem> sides will behave like the unregistered one. Try a recent Chrome, Edge, Safari, or Firefox.\n    \u003C\u002Fdiv>\n\n    \u003C!-- Demo 1 -->\n    \u003Cdiv class=\"panel\">\n      \u003Ch2>1 · Transition a color\u003C\u002Fh2>\n      \u003Cdiv class=\"controls\">\n        \u003Cbutton id=\"shift\">Shift the hue →\u003C\u002Fbutton>\n        \u003Cbutton id=\"reset1\" class=\"ghost\">Reset\u003C\u002Fbutton>\n        \u003Clabel>Duration\n          \u003Cinput type=\"range\" id=\"dur\" min=\"0.1\" max=\"1.5\" step=\"0.1\" value=\"0.6\" \u002F>\n          \u003Cspan id=\"durVal\" style=\"color:var(--ink)\">0.6s\u003C\u002Fspan>\n        \u003C\u002Flabel>\n      \u003C\u002Fdiv>\n      \u003Cdiv class=\"pair\">\n        \u003Cdiv class=\"side\">\n          \u003Cspan class=\"tag plain\">plain variable\u003C\u002Fspan>\n          \u003Cdiv class=\"swatch plain\" id=\"swPlain\">--hue\u003C\u002Fdiv>\n          \u003Cdiv class=\"hint\">the browser sees text → \u003Cb style=\"color:var(--bad)\">snaps\u003C\u002Fb>\u003C\u002Fdiv>\n        \u003C\u002Fdiv>\n        \u003Cdiv class=\"side\">\n          \u003Cspan class=\"tag reg\">@property &lt;number&gt;\u003C\u002Fspan>\n          \u003Cdiv class=\"swatch reg\" id=\"swReg\">--hue\u003C\u002Fdiv>\n          \u003Cdiv class=\"hint\">the browser sees a number → \u003Cb style=\"color:var(--good)\">eases\u003C\u002Fb>\u003C\u002Fdiv>\n        \u003C\u002Fdiv>\n      \u003C\u002Fdiv>\n    \u003C\u002Fdiv>\n\n    \u003C!-- Demo 2 -->\n    \u003Cdiv class=\"panel\" id=\"spinPanel\">\n      \u003Ch2>2 · Animate a conic gradient\u003C\u002Fh2>\n      \u003Cdiv class=\"controls\">\n        \u003Cbutton id=\"toggle\">Pause\u003C\u002Fbutton>\n        \u003Clabel>Speed\n          \u003Cinput type=\"range\" id=\"speed\" min=\"0.5\" max=\"4\" step=\"0.5\" value=\"2\" \u002F>\n          \u003Cspan id=\"speedVal\" style=\"color:var(--ink)\">2s \u002F turn\u003C\u002Fspan>\n        \u003C\u002Flabel>\n      \u003C\u002Fdiv>\n      \u003Cdiv class=\"pair\">\n        \u003Cdiv class=\"side\">\n          \u003Cspan class=\"tag plain\">plain variable\u003C\u002Fspan>\n          \u003Cdiv class=\"orb plain\">\u003C\u002Fdiv>\n          \u003Cdiv class=\"hint\">unregistered \u003Ccode>&lt;angle&gt;\u003C\u002Fcode> → \u003Cb style=\"color:var(--bad)\">frozen\u003C\u002Fb>\u003C\u002Fdiv>\n        \u003C\u002Fdiv>\n        \u003Cdiv class=\"side\">\n          \u003Cspan class=\"tag reg\">@property &lt;angle&gt;\u003C\u002Fspan>\n          \u003Cdiv class=\"orb reg\">\u003C\u002Fdiv>\n          \u003Cdiv class=\"hint\">typed \u003Ccode>&lt;angle&gt;\u003C\u002Fcode> → \u003Cb style=\"color:var(--good)\">spins\u003C\u002Fb>\u003C\u002Fdiv>\n        \u003C\u002Fdiv>\n      \u003C\u002Fdiv>\n\n      \u003Cdetails>\n        \u003Csummary>Show the CSS — the entire difference is these two blocks\u003C\u002Fsummary>\n\u003Cpre>\u003Ccode>\u003Cspan class=\"c\">\u002F* Register the variables. That's the whole fix. *\u002F\u003C\u002Fspan>\n\u003Cspan class=\"k\">@property\u003C\u002Fspan> --hue {\n  \u003Cspan class=\"k\">syntax\u003C\u002Fspan>: \u003Cspan class=\"v\">\"&lt;number&gt;\"\u003C\u002Fspan>; \u003Cspan class=\"k\">inherits\u003C\u002Fspan>: false; \u003Cspan class=\"k\">initial-value\u003C\u002Fspan>: \u003Cspan class=\"v\">220\u003C\u002Fspan>;\n}\n\u003Cspan class=\"k\">@property\u003C\u002Fspan> --spin {\n  \u003Cspan class=\"k\">syntax\u003C\u002Fspan>: \u003Cspan class=\"v\">\"&lt;angle&gt;\"\u003C\u002Fspan>; \u003Cspan class=\"k\">inherits\u003C\u002Fspan>: false; \u003Cspan class=\"k\">initial-value\u003C\u002Fspan>: \u003Cspan class=\"v\">0deg\u003C\u002Fspan>;\n}\n\n\u003Cspan class=\"c\">\u002F* ...and the rest is the CSS you already wrote. *\u002F\u003C\u002Fspan>\n.swatch { background: \u003Cspan class=\"k\">hsl\u003C\u002Fspan>(\u003Cspan class=\"k\">var\u003C\u002Fspan>(--hue) 78% 62%); \u003Cspan class=\"k\">transition\u003C\u002Fspan>: --hue .6s ease; }\n.orb    { background: \u003Cspan class=\"k\">conic-gradient\u003C\u002Fspan>(\u003Cspan class=\"k\">from\u003C\u002Fspan> \u003Cspan class=\"k\">var\u003C\u002Fspan>(--spin), …); \u003Cspan class=\"k\">animation\u003C\u002Fspan>: spin 2s linear infinite; }\n\u003Cspan class=\"k\">@keyframes\u003C\u002Fspan> spin { to { --spin: \u003Cspan class=\"v\">360deg\u003C\u002Fspan>; } }\u003C\u002Fcode>\u003C\u002Fpre>\n      \u003C\u002Fdetails>\n    \u003C\u002Fdiv>\n  \u003C\u002Fdiv>",":root {\n    --bg: #0d1117; --panel: #161b22; --ink: #e8ebf2; --muted: #9aa4b2;\n    --accent: #7c9cff; --good: #3fb950; --bad: #f0883e; --line: #232a35;\n    \u002F* transition duration for the color demo, driven by the slider *\u002F\n    --dur: 0.6s;\n    font-family: ui-sans-serif, system-ui, -apple-system, \"Segoe UI\", Roboto, sans-serif;\n  }\n  * { box-sizing: border-box; }\n  body { margin: 0; background: var(--bg); color: var(--ink); min-height: 100vh;\n    display: flex; align-items: flex-start; justify-content: center; padding: 18px; }\n  .wrap { width: 100%; max-width: 680px; }\n  h1 { font-size: 1.2rem; margin: 0 0 3px; letter-spacing: -0.01em; }\n  .sub { color: var(--muted); font-size: .9rem; margin: 0 0 14px; line-height: 1.4; }\n  .sub code { color: var(--accent); background: #ffffff10; padding: 1px 5px; border-radius: 5px; }\n\n  .panel { background: var(--panel); border: 1px solid var(--line);\n    border-radius: 14px; padding: 16px; margin-bottom: 14px; }\n  .panel h2 { font-size: .8rem; text-transform: uppercase; letter-spacing: .06em;\n    color: var(--muted); margin: 0 0 12px; font-weight: 700; }\n\n  .controls { display: flex; flex-wrap: wrap; gap: 12px; align-items: center; margin-bottom: 14px; }\n  button { font: inherit; font-weight: 600; color: #0b0d10; background: var(--accent);\n    border: 0; border-radius: 10px; padding: 9px 16px; cursor: pointer; }\n  button.ghost { background: transparent; color: var(--ink); border: 1px solid var(--line); }\n  button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }\n  label { color: var(--muted); font-size: .82rem; display: inline-flex; gap: 8px; align-items: center; }\n  input[type=range] { accent-color: var(--accent); width: 120px; }\n\n  .pair { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }\n  .side { border: 1px solid var(--line); border-radius: 11px; padding: 12px; text-align: center;\n    background: #0b0e14; }\n  .tag { font-size: .72rem; font-weight: 700; letter-spacing: .04em; margin-bottom: 10px;\n    display: inline-block; padding: 3px 9px; border-radius: 999px; }\n  .tag.plain { color: var(--bad); background: #f0883e1a; }\n  .tag.reg   { color: var(--good); background: #3fb9501a; }\n\n  \u002F* ---- Demo 1: color shift on a custom property ---- *\u002F\n  .swatch { height: 84px; border-radius: 10px; display: grid; place-items: center;\n    font-weight: 700; font-size: .95rem; color: #0b0d10; }\n  \u002F* Identical rule for both swatches. Only the property NAME differs, so the\n     right one can be registered while the left one stays plain text. *\u002F\n  .swatch.plain { --hue-plain: 220; background: hsl(var(--hue-plain) 78% 62%);\n    transition: --hue-plain var(--dur) ease; }\n  .swatch.reg   { --hue-reg: 220;   background: hsl(var(--hue-reg) 78% 62%);\n    transition: --hue-reg var(--dur) ease; }\n\n  \u002F* ---- Demo 2: conic-gradient angle animation ---- *\u002F\n  .orb { width: 108px; height: 108px; border-radius: 50%; margin: 4px auto 0;\n    box-shadow: 0 0 0 1px #ffffff14 inset; }\n  .orb.plain { --spin-plain: 0deg;\n    background: conic-gradient(from var(--spin-plain), oklch(72% 0.2 250), oklch(64% 0.28 320), oklch(72% 0.2 250));\n    animation: spinPlain var(--speed, 2s) linear infinite; }\n  .orb.reg { --spin-reg: 0deg;\n    background: conic-gradient(from var(--spin-reg), oklch(72% 0.2 250), oklch(64% 0.28 320), oklch(72% 0.2 250));\n    animation: spinReg var(--speed, 2s) linear infinite; }\n  .paused .orb { animation-play-state: paused; }\n  @keyframes spinPlain { to { --spin-plain: 360deg; } }\n  @keyframes spinReg   { to { --spin-reg: 360deg; } }\n\n  .hint { font-size: .78rem; color: var(--muted); margin-top: 9px; min-height: 1em; }\n\n  .note { font-size: .82rem; color: var(--muted); background: #f0883e14;\n    border: 1px solid #f0883e40; border-radius: 10px; padding: 10px 12px; margin-bottom: 14px; }\n  .note[hidden] { display: none; }\n\n  details { border-top: 1px dashed var(--line); margin-top: 6px; padding-top: 10px; }\n  summary { cursor: pointer; color: var(--muted); font-size: .82rem; }\n  pre { margin: 10px 0 0; overflow-x: auto; background: #0b0e14; border: 1px solid var(--line);\n    border-radius: 10px; padding: 12px; font-size: .78rem; line-height: 1.5; }\n  code { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }\n  .k { color: var(--accent); } .c { color: #6b7688; } .v { color: var(--good); }\n\n  @media (max-width: 470px) {\n    .pair { grid-template-columns: 1fr; }\n    input[type=range] { width: 100px; }\n  }","const $ = (id) => document.getElementById(id);\n\n  \u002F\u002F Register ONLY the right-hand side's properties. Left-hand names\n  \u002F\u002F (--hue-plain, --spin-plain) are deliberately never registered, so they\n  \u002F\u002F stay plain string substitution — exactly the \"before\" from the post.\n  let supported = typeof CSS !== \"undefined\" && typeof CSS.registerProperty === \"function\";\n  if (supported) {\n    try {\n      CSS.registerProperty({ name: \"--hue-reg\",  syntax: \"\u003Cnumber>\", inherits: false, initialValue: \"220\" });\n      CSS.registerProperty({ name: \"--spin-reg\", syntax: \"\u003Cangle>\",  inherits: false, initialValue: \"0deg\" });\n    } catch (e) {\n      \u002F\u002F Already registered (e.g. a hot reload) is fine; anything else → degrade.\n      if (!\u002Falready\u002Fi.test(e.message)) supported = false;\n    }\n  }\n  if (!supported) $(\"unsupported\").hidden = false;\n\n  \u002F\u002F ---- Demo 1: color shift ----\n  const swPlain = $(\"swPlain\"), swReg = $(\"swReg\");\n  let hot = false; \u002F\u002F false → 220, true → 340\n  function applyHue() {\n    const hue = hot ? 340 : 220;\n    swPlain.style.setProperty(\"--hue-plain\", hue);\n    swReg.style.setProperty(\"--hue-reg\", hue);\n    swPlain.textContent = swReg.textContent = \"--hue: \" + hue;\n  }\n  $(\"shift\").addEventListener(\"click\", () => { hot = !hot; applyHue(); });\n  $(\"reset1\").addEventListener(\"click\", () => { hot = false; applyHue(); });\n\n  $(\"dur\").addEventListener(\"input\", (e) => {\n    const d = e.target.value;\n    $(\"durVal\").textContent = d + \"s\";\n    document.documentElement.style.setProperty(\"--dur\", d + \"s\");\n  });\n\n  \u002F\u002F ---- Demo 2: conic gradient spin ----\n  const spinPanel = $(\"spinPanel\"), toggle = $(\"toggle\");\n  toggle.addEventListener(\"click\", () => {\n    const paused = spinPanel.classList.toggle(\"paused\");\n    toggle.textContent = paused ? \"Play\" : \"Pause\";\n  });\n  $(\"speed\").addEventListener(\"input\", (e) => {\n    const s = e.target.value;\n    $(\"speedVal\").textContent = s + \"s \u002F turn\";\n    document.documentElement.style.setProperty(\"--speed\", s + \"s\");\n  });\n\n  applyHue();",{"height":40,"autorun":17},520,{"slug":33,"title":42,"excerpt":43,"coverUrl":44},"You can't transition a CSS variable. @property says otherwise.","CSS custom properties are string substitution — the browser doesn't know a number from a color. Register one with @property and it gains a real type, an initial value, and the ability to animate.","\u002Fmedia\u002Fcovers\u002Fcss-property-registered-custom-properties.png"]