[{"data":1,"prerenderedAt":1275},["ShallowReactive",2],{"page-\u002Fcss\u002F13-animations-and-transitions":3},{"id":4,"title":5,"body":6,"description":1268,"extension":1269,"meta":1270,"navigation":508,"path":1271,"seo":1272,"stem":1273,"__hash__":1274},"content\u002Fcss\u002F13-animations-and-transitions.md","13 — Animations & Transitions: The Compositor Pipeline",{"type":7,"value":8,"toc":1251},"minimark",[9,13,45,50,177,185,338,342,352,357,432,598,605,778,785,836,843,949,953,1030,1034,1134,1138,1195,1247],[10,11,5],"h1",{"id":12},"_13-animations-transitions-the-compositor-pipeline",[14,15,16,17,21,22,25,26,29,30,29,33,29,36,39,40,44],"p",{},"The render pipeline is: style → layout → paint → composite. Animating ",[18,19,20],"code",{},"transform"," and ",[18,23,24],{},"opacity"," skips layout and paint — they're compositor-only (GPU). Animating ",[18,27,28],{},"width","\u002F",[18,31,32],{},"height",[18,34,35],{},"top",[18,37,38],{},"left"," triggers layout ",[41,42,43],"em",{},"every frame"," → janky. This is the single most important performance rule in CSS.",[46,47,49],"h2",{"id":48},"transitions-smooth-property-interpolation","Transitions — smooth property interpolation",[51,52,54],"code-wrapper",{"language":53},"css",[55,56,60],"pre",{"className":57,"code":58,"language":53,"meta":59,"style":59},"language-css shiki shiki-themes github-light github-dark",".button {\n  background: blue;\n  transition: background 0.3s ease, transform 0.2s ease;  \u002F* list SPECIFIC properties *\u002F\n}\n.button:hover { background: darkblue; transform: scale(1.05); }\n\u002F* ⚠️ transition: all is a footgun — animates unexpected properties (background-image, etc.) *\u002F\n","",[18,61,62,75,91,130,136,171],{"__ignoreMap":59},[63,64,67,71],"span",{"class":65,"line":66},"line",1,[63,68,70],{"class":69},"sIsaT",".button",[63,72,74],{"class":73},"ssxIu"," {\n",[63,76,78,82,85,88],{"class":65,"line":77},2,[63,79,81],{"class":80},"snvgF","  background",[63,83,84],{"class":73},": ",[63,86,87],{"class":80},"blue",[63,89,90],{"class":73},";\n",[63,92,94,97,99,103,106,110,113,116,119,121,123,126],{"class":65,"line":93},3,[63,95,96],{"class":80},"  transition",[63,98,84],{"class":73},[63,100,102],{"class":101},"sMKGj","background",[63,104,105],{"class":80}," 0.3",[63,107,109],{"class":108},"svdQ7","s",[63,111,112],{"class":80}," ease",[63,114,115],{"class":73},", transform ",[63,117,118],{"class":80},"0.2",[63,120,109],{"class":108},[63,122,112],{"class":80},[63,124,125],{"class":73},";  ",[63,127,129],{"class":128},"sdCPZ","\u002F* list SPECIFIC properties *\u002F\n",[63,131,133],{"class":65,"line":132},4,[63,134,135],{"class":73},"}\n",[63,137,139,142,145,147,149,152,155,157,159,162,165,168],{"class":65,"line":138},5,[63,140,141],{"class":69},".button:hover",[63,143,144],{"class":73}," { ",[63,146,102],{"class":80},[63,148,84],{"class":73},[63,150,151],{"class":80},"darkblue",[63,153,154],{"class":73},"; ",[63,156,20],{"class":80},[63,158,84],{"class":73},[63,160,161],{"class":80},"scale",[63,163,164],{"class":73},"(",[63,166,167],{"class":80},"1.05",[63,169,170],{"class":73},"); }\n",[63,172,174],{"class":65,"line":173},6,[63,175,176],{"class":128},"\u002F* ⚠️ transition: all is a footgun — animates unexpected properties (background-image, etc.) *\u002F\n",[46,178,180,181,184],{"id":179},"timing-functions-cubic-bezier-for-overshoot","Timing Functions — ",[18,182,183],{},"cubic-bezier"," for overshoot",[51,186,187],{"language":53},[55,188,190],{"className":57,"code":189,"language":53,"meta":59,"style":59},":root {\n  --ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);  \u002F* y>1 → overshoots target, settles (bounce) *\u002F\n  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);         \u002F* fast start, slow settle *\u002F\n}\n.modal { transition: transform 0.3s var(--ease-spring); }\n\u002F* steps(n, start|end): discrete steps — sprite animation, loading bars *\u002F\n.sprite { transition: background-position 0.5s steps(8, end); }\n",[18,191,192,199,236,269,273,300,305],{"__ignoreMap":59},[63,193,194,197],{"class":65,"line":66},[63,195,196],{"class":69},":root",[63,198,74],{"class":73},[63,200,201,205,207,209,211,214,217,220,222,225,227,230,233],{"class":65,"line":77},[63,202,204],{"class":203},"sCrzJ","  --ease-spring",[63,206,84],{"class":73},[63,208,183],{"class":80},[63,210,164],{"class":73},[63,212,213],{"class":80},"0.34",[63,215,216],{"class":73},", ",[63,218,219],{"class":80},"1.56",[63,221,216],{"class":73},[63,223,224],{"class":80},"0.64",[63,226,216],{"class":73},[63,228,229],{"class":80},"1",[63,231,232],{"class":73},");  ",[63,234,235],{"class":128},"\u002F* y>1 → overshoots target, settles (bounce) *\u002F\n",[63,237,238,241,243,245,247,250,252,254,256,259,261,263,266],{"class":65,"line":93},[63,239,240],{"class":203},"  --ease-out",[63,242,84],{"class":73},[63,244,183],{"class":80},[63,246,164],{"class":73},[63,248,249],{"class":80},"0.16",[63,251,216],{"class":73},[63,253,229],{"class":80},[63,255,216],{"class":73},[63,257,258],{"class":80},"0.3",[63,260,216],{"class":73},[63,262,229],{"class":80},[63,264,265],{"class":73},");         ",[63,267,268],{"class":128},"\u002F* fast start, slow settle *\u002F\n",[63,270,271],{"class":65,"line":132},[63,272,135],{"class":73},[63,274,275,278,280,283,286,288,290,293,295,298],{"class":65,"line":138},[63,276,277],{"class":69},".modal",[63,279,144],{"class":73},[63,281,282],{"class":80},"transition",[63,284,285],{"class":73},": transform ",[63,287,258],{"class":80},[63,289,109],{"class":108},[63,291,292],{"class":80}," var",[63,294,164],{"class":73},[63,296,297],{"class":203},"--ease-spring",[63,299,170],{"class":73},[63,301,302],{"class":65,"line":173},[63,303,304],{"class":128},"\u002F* steps(n, start|end): discrete steps — sprite animation, loading bars *\u002F\n",[63,306,308,311,313,315,318,321,323,326,328,331,333,336],{"class":65,"line":307},7,[63,309,310],{"class":69},".sprite",[63,312,144],{"class":73},[63,314,282],{"class":80},[63,316,317],{"class":73},": background-position ",[63,319,320],{"class":80},"0.5",[63,322,109],{"class":108},[63,324,325],{"class":80}," steps",[63,327,164],{"class":73},[63,329,330],{"class":80},"8",[63,332,216],{"class":73},[63,334,335],{"class":80},"end",[63,337,170],{"class":73},[46,339,341],{"id":340},"the-performance-rule-compositor-only-properties","The Performance Rule — compositor-only properties",[51,343,345],{"language":344},"text",[55,346,350],{"className":347,"code":349,"language":344,"meta":59},[348],"language-text","Property          Cost              Animate?\ntransform         Compositor (GPU)  ✅ 60fps\nopacity           Compositor (GPU)  ✅ 60fps\nfilter            Compositor (mostly) ✅\ncolor\u002Fbg-color    Paint             ⚠️ OK\nbox-shadow        Paint             ⚠️ (use pseudo-element opacity instead)\nwidth\u002Fheight      Layout            ❌ expensive\ntop\u002Fleft\u002Fmargin   Layout            ❌ expensive\n",[18,351,349],{"__ignoreMap":59},[353,354,356],"h3",{"id":355},"anti-pattern-animating-height-triggers-layout","Anti-pattern: animating height triggers layout",[51,358,359],{"language":53},[55,360,362],{"className":57,"code":361,"language":53,"meta":59,"style":59},"\u002F* ❌ height triggers layout every frame → janky. And height:auto can't interpolate (auto isn't a length). *\u002F\n.menu { height: 0; overflow: hidden; transition: height 0.3s ease; }\n.menu.open { height: 200px; }  \u002F* fixed value — breaks if content changes *\u002F\n",[18,363,364,369,409],{"__ignoreMap":59},[63,365,366],{"class":65,"line":66},[63,367,368],{"class":128},"\u002F* ❌ height triggers layout every frame → janky. And height:auto can't interpolate (auto isn't a length). *\u002F\n",[63,370,371,374,376,378,380,383,385,388,390,393,395,397,400,402,404,406],{"class":65,"line":77},[63,372,373],{"class":69},".menu",[63,375,144],{"class":73},[63,377,32],{"class":80},[63,379,84],{"class":73},[63,381,382],{"class":80},"0",[63,384,154],{"class":73},[63,386,387],{"class":80},"overflow",[63,389,84],{"class":73},[63,391,392],{"class":80},"hidden",[63,394,154],{"class":73},[63,396,282],{"class":80},[63,398,399],{"class":73},": height ",[63,401,258],{"class":80},[63,403,109],{"class":108},[63,405,112],{"class":80},[63,407,408],{"class":73},"; }\n",[63,410,411,414,416,418,420,423,426,429],{"class":65,"line":93},[63,412,413],{"class":69},".menu.open",[63,415,144],{"class":73},[63,417,32],{"class":80},[63,419,84],{"class":73},[63,421,422],{"class":80},"200",[63,424,425],{"class":108},"px",[63,427,428],{"class":73},"; }  ",[63,430,431],{"class":128},"\u002F* fixed value — breaks if content changes *\u002F\n",[51,433,434],{"language":53},[55,435,437],{"className":57,"code":436,"language":53,"meta":59,"style":59},"\u002F* ✓ transform: scaleY (compositor-only, GPU, no layout). transform-origin: top anchors it. *\u002F\n.menu { transform: scaleY(0); transform-origin: block-start; transition: transform 0.3s ease; }\n.menu.open { transform: scaleY(1); }\n\n\u002F* ✓ grid-template-rows: 0fr → 1fr (animates to content height, interpolable, fits any content) *\u002F\n.menu-wrap { display: grid; grid-template-rows: 0fr; transition: grid-template-rows 0.3s ease; }\n.menu-wrap.open { grid-template-rows: 1fr; }\n.menu { overflow: hidden; }  \u002F* clip the collapsing content *\u002F\n\u002F* Browser support: grid-template-rows 0fr↔1fr transition: Chrome 107+, Safari 16+, Firefox 66+. *\u002F\n",[18,438,439,444,486,504,510,515,557,574,592],{"__ignoreMap":59},[63,440,441],{"class":65,"line":66},[63,442,443],{"class":128},"\u002F* ✓ transform: scaleY (compositor-only, GPU, no layout). transform-origin: top anchors it. *\u002F\n",[63,445,446,448,450,452,454,457,459,461,464,467,469,472,474,476,478,480,482,484],{"class":65,"line":77},[63,447,373],{"class":69},[63,449,144],{"class":73},[63,451,20],{"class":80},[63,453,84],{"class":73},[63,455,456],{"class":80},"scaleY",[63,458,164],{"class":73},[63,460,382],{"class":80},[63,462,463],{"class":73},"); ",[63,465,466],{"class":80},"transform-origin",[63,468,84],{"class":73},[63,470,471],{"class":80},"block-start",[63,473,154],{"class":73},[63,475,282],{"class":80},[63,477,285],{"class":73},[63,479,258],{"class":80},[63,481,109],{"class":108},[63,483,112],{"class":80},[63,485,408],{"class":73},[63,487,488,490,492,494,496,498,500,502],{"class":65,"line":93},[63,489,413],{"class":69},[63,491,144],{"class":73},[63,493,20],{"class":80},[63,495,84],{"class":73},[63,497,456],{"class":80},[63,499,164],{"class":73},[63,501,229],{"class":80},[63,503,170],{"class":73},[63,505,506],{"class":65,"line":132},[63,507,509],{"emptyLinePlaceholder":508},true,"\n",[63,511,512],{"class":65,"line":138},[63,513,514],{"class":128},"\u002F* ✓ grid-template-rows: 0fr → 1fr (animates to content height, interpolable, fits any content) *\u002F\n",[63,516,517,520,522,525,527,530,532,535,537,539,542,544,546,549,551,553,555],{"class":65,"line":173},[63,518,519],{"class":69},".menu-wrap",[63,521,144],{"class":73},[63,523,524],{"class":80},"display",[63,526,84],{"class":73},[63,528,529],{"class":80},"grid",[63,531,154],{"class":73},[63,533,534],{"class":80},"grid-template-rows",[63,536,84],{"class":73},[63,538,382],{"class":80},[63,540,541],{"class":108},"fr",[63,543,154],{"class":73},[63,545,282],{"class":80},[63,547,548],{"class":73},": grid-template-rows ",[63,550,258],{"class":80},[63,552,109],{"class":108},[63,554,112],{"class":80},[63,556,408],{"class":73},[63,558,559,562,564,566,568,570,572],{"class":65,"line":307},[63,560,561],{"class":69},".menu-wrap.open",[63,563,144],{"class":73},[63,565,534],{"class":80},[63,567,84],{"class":73},[63,569,229],{"class":80},[63,571,541],{"class":108},[63,573,408],{"class":73},[63,575,577,579,581,583,585,587,589],{"class":65,"line":576},8,[63,578,373],{"class":69},[63,580,144],{"class":73},[63,582,387],{"class":80},[63,584,84],{"class":73},[63,586,392],{"class":80},[63,588,428],{"class":73},[63,590,591],{"class":128},"\u002F* clip the collapsing content *\u002F\n",[63,593,595],{"class":65,"line":594},9,[63,596,597],{"class":128},"\u002F* Browser support: grid-template-rows 0fr↔1fr transition: Chrome 107+, Safari 16+, Firefox 66+. *\u002F\n",[46,599,601,604],{"id":600},"keyframes-animations",[18,602,603],{},"@keyframes"," Animations",[51,606,607],{"language":53},[55,608,610],{"className":57,"code":609,"language":53,"meta":59,"style":59},"@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }\n@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }\n\n.spinner { animation: spin 1s linear infinite; }\n.pulse { animation: pulse 2s ease-in-out infinite; }\n\n\u002F* animation: name duration timing-function delay iteration-count direction fill-mode play-state *\u002F\n\u002F* animation-fill-mode: forwards → retains the END state after the animation (default: none → snaps back) *\u002F\n\u002F* negative animation-delay: -1s on a 2s animation starts it at the 1s mark (already halfway) → staggered *\u002F\n",[18,611,612,664,706,710,735,759,763,768,773],{"__ignoreMap":59},[63,613,614,616,619,621,624,626,628,630,633,635,637,640,643,646,648,650,652,654,656,659,661],{"class":65,"line":66},[63,615,603],{"class":108},[63,617,618],{"class":203}," spin",[63,620,144],{"class":73},[63,622,623],{"class":69},"from",[63,625,144],{"class":73},[63,627,20],{"class":80},[63,629,84],{"class":73},[63,631,632],{"class":80},"rotate",[63,634,164],{"class":73},[63,636,382],{"class":80},[63,638,639],{"class":108},"deg",[63,641,642],{"class":73},"); } ",[63,644,645],{"class":69},"to",[63,647,144],{"class":73},[63,649,20],{"class":80},[63,651,84],{"class":73},[63,653,632],{"class":80},[63,655,164],{"class":73},[63,657,658],{"class":80},"360",[63,660,639],{"class":108},[63,662,663],{"class":73},"); } }\n",[63,665,666,668,671,673,676,678,681,683,685,687,689,692,695,697,699,701,703],{"class":65,"line":77},[63,667,603],{"class":108},[63,669,670],{"class":203}," pulse",[63,672,144],{"class":73},[63,674,675],{"class":69},"0%",[63,677,216],{"class":73},[63,679,680],{"class":69},"100%",[63,682,144],{"class":73},[63,684,24],{"class":80},[63,686,84],{"class":73},[63,688,229],{"class":80},[63,690,691],{"class":73},"; } ",[63,693,694],{"class":69},"50%",[63,696,144],{"class":73},[63,698,24],{"class":80},[63,700,84],{"class":73},[63,702,320],{"class":80},[63,704,705],{"class":73},"; } }\n",[63,707,708],{"class":65,"line":93},[63,709,509],{"emptyLinePlaceholder":508},[63,711,712,715,717,720,723,725,727,730,733],{"class":65,"line":132},[63,713,714],{"class":69},".spinner",[63,716,144],{"class":73},[63,718,719],{"class":80},"animation",[63,721,722],{"class":73},": spin ",[63,724,229],{"class":80},[63,726,109],{"class":108},[63,728,729],{"class":80}," linear",[63,731,732],{"class":80}," infinite",[63,734,408],{"class":73},[63,736,737,740,742,744,747,750,752,755,757],{"class":65,"line":138},[63,738,739],{"class":69},".pulse",[63,741,144],{"class":73},[63,743,719],{"class":80},[63,745,746],{"class":73},": pulse ",[63,748,749],{"class":80},"2",[63,751,109],{"class":108},[63,753,754],{"class":80}," ease-in-out",[63,756,732],{"class":80},[63,758,408],{"class":73},[63,760,761],{"class":65,"line":173},[63,762,509],{"emptyLinePlaceholder":508},[63,764,765],{"class":65,"line":307},[63,766,767],{"class":128},"\u002F* animation: name duration timing-function delay iteration-count direction fill-mode play-state *\u002F\n",[63,769,770],{"class":65,"line":576},[63,771,772],{"class":128},"\u002F* animation-fill-mode: forwards → retains the END state after the animation (default: none → snaps back) *\u002F\n",[63,774,775],{"class":65,"line":594},[63,776,777],{"class":128},"\u002F* negative animation-delay: -1s on a 2s animation starts it at the 1s mark (already halfway) → staggered *\u002F\n",[46,779,781,784],{"id":780},"will-change-the-gpu-hint-use-sparingly",[18,782,783],{},"will-change"," — the GPU hint (use sparingly)",[51,786,787],{"language":53},[55,788,790],{"className":57,"code":789,"language":53,"meta":59,"style":59},".modal { will-change: transform, opacity; }  \u002F* hint: promote to a GPU layer NOW *\u002F\n.modal.closed { will-change: auto; }          \u002F* remove after → frees GPU memory *\u002F\n\u002F* ⚠️ Each will-change element gets a GPU layer (RAM). * { will-change: transform; } = OOM on mobile. *\u002F\n\u002F* Add will-change just before the animation, remove after. Never leave it permanently. *\u002F\n",[18,791,792,806,826,831],{"__ignoreMap":59},[63,793,794,796,798,800,803],{"class":65,"line":66},[63,795,277],{"class":69},[63,797,144],{"class":73},[63,799,783],{"class":80},[63,801,802],{"class":73},": transform, opacity; }  ",[63,804,805],{"class":128},"\u002F* hint: promote to a GPU layer NOW *\u002F\n",[63,807,808,811,813,815,817,820,823],{"class":65,"line":77},[63,809,810],{"class":69},".modal.closed",[63,812,144],{"class":73},[63,814,783],{"class":80},[63,816,84],{"class":73},[63,818,819],{"class":80},"auto",[63,821,822],{"class":73},"; }          ",[63,824,825],{"class":128},"\u002F* remove after → frees GPU memory *\u002F\n",[63,827,828],{"class":65,"line":93},[63,829,830],{"class":128},"\u002F* ⚠️ Each will-change element gets a GPU layer (RAM). * { will-change: transform; } = OOM on mobile. *\u002F\n",[63,832,833],{"class":65,"line":132},[63,834,835],{"class":128},"\u002F* Add will-change just before the animation, remove after. Never leave it permanently. *\u002F\n",[46,837,839,842],{"id":838},"prefers-reduced-motion-mandatory-accessibility",[18,840,841],{},"prefers-reduced-motion"," — mandatory accessibility",[51,844,845],{"language":53},[55,846,848],{"className":57,"code":847,"language":53,"meta":59,"style":59},"@media (prefers-reduced-motion: reduce) {\n  *, *::before, *::after {\n    animation-duration: 0.01ms !important;\n    animation-iteration-count: 1 !important;\n    transition-duration: 0.01ms !important;\n    scroll-behavior: auto !important;\n  }\n}\n",[18,849,850,858,881,899,912,927,940,945],{"__ignoreMap":59},[63,851,852,855],{"class":65,"line":66},[63,853,854],{"class":108},"@media",[63,856,857],{"class":73}," (prefers-reduced-motion: reduce) {\n",[63,859,860,864,866,869,872,874,876,879],{"class":65,"line":77},[63,861,863],{"class":862},"sk71V","  *",[63,865,216],{"class":73},[63,867,868],{"class":862},"*",[63,870,871],{"class":69},"::before",[63,873,216],{"class":73},[63,875,868],{"class":862},[63,877,878],{"class":69},"::after",[63,880,74],{"class":73},[63,882,883,886,888,891,894,897],{"class":65,"line":93},[63,884,885],{"class":80},"    animation-duration",[63,887,84],{"class":73},[63,889,890],{"class":80},"0.01",[63,892,893],{"class":108},"ms",[63,895,896],{"class":108}," !important",[63,898,90],{"class":73},[63,900,901,904,906,908,910],{"class":65,"line":132},[63,902,903],{"class":80},"    animation-iteration-count",[63,905,84],{"class":73},[63,907,229],{"class":80},[63,909,896],{"class":108},[63,911,90],{"class":73},[63,913,914,917,919,921,923,925],{"class":65,"line":138},[63,915,916],{"class":80},"    transition-duration",[63,918,84],{"class":73},[63,920,890],{"class":80},[63,922,893],{"class":108},[63,924,896],{"class":108},[63,926,90],{"class":73},[63,928,929,932,934,936,938],{"class":65,"line":173},[63,930,931],{"class":80},"    scroll-behavior",[63,933,84],{"class":73},[63,935,819],{"class":80},[63,937,896],{"class":108},[63,939,90],{"class":73},[63,941,942],{"class":65,"line":307},[63,943,944],{"class":73},"  }\n",[63,946,947],{"class":65,"line":576},[63,948,135],{"class":73},[46,950,952],{"id":951},"tips-tricks","💡 Tips & Tricks",[954,955,956,967,982,995,1003],"ul",{},[957,958,959,84,963,966],"li",{},[960,961,962],"strong",{},"Idiom",[18,964,965],{},"cubic-bezier(0.34, 1.56, 0.64, 1)"," for overshoot\u002Fbounce — the y-value >1 makes the animation go past the target and settle. Playful, spring-like.",[957,968,969,84,971,974,975,978,979,981],{},[960,970,962],{},[18,972,973],{},"animation-fill-mode: forwards"," (or ",[18,976,977],{},"both",") to retain the end state — without it, the element snaps back to pre-animation state. ",[18,980,977],{}," also applies the start state during the delay.",[957,983,984,986,987,990,991,994],{},[960,985,962],{},": negative ",[18,988,989],{},"animation-delay"," for staggered animations that appear already in progress — ",[18,992,993],{},"animation-delay: -1s"," on a 2s animation starts it halfway through.",[957,996,997,84,999,1002],{},[960,998,962],{},[18,1000,1001],{},"steps(8)"," for sprite-sheet animation — 8 discrete steps cycle through a sprite sheet, the classic CSS sprite technique.",[957,1004,1005,1007,1008,29,1010,1012,1013,29,1015,29,1017,29,1019,1021,1022,1025,1026,1029],{},[960,1006,962],{},": animate ",[18,1009,20],{},[18,1011,24],{}," (GPU), never ",[18,1014,28],{},[18,1016,32],{},[18,1018,35],{},[18,1020,38],{}," (layout). ",[18,1023,1024],{},"transform: translateX()"," for position, ",[18,1027,1028],{},"scale()"," for size.",[46,1031,1033],{"id":1032},"️-edge-cases-gotchas","⚠️ Edge Cases & Gotchas",[954,1035,1036,1049,1069,1088,1101,1109,1126],{},[957,1037,1038,1044,1045,1048],{},[960,1039,1040,1043],{},[18,1041,1042],{},"transition: all"," is a footgun",": animates every property change, including non-interpolable ones (",[18,1046,1047],{},"background-image","). List specific properties.",[957,1050,1051,84,1058,1061,1062,1064,1065,1068],{},[960,1052,1053,1055,1056],{},[18,1054,282],{}," doesn't work on ",[18,1057,524],{},[18,1059,1060],{},"display: none → block"," can't transition. Use ",[18,1063,24],{}," + ",[18,1066,1067],{},"visibility",", or a transform.",[957,1070,1071,84,1077,1079,1080,1083,1084,1087],{},[960,1072,1073,1076],{},[18,1074,1075],{},"height: auto"," can't transition",[18,1078,819],{}," isn't a length → not interpolable. Use ",[18,1081,1082],{},"transform: scaleY()"," or ",[18,1085,1086],{},"grid-template-rows: 0fr → 1fr",".",[957,1089,1090,1096,1097,1100],{},[960,1091,1092,1095],{},[18,1093,1094],{},"animation-fill-mode: none"," (default) snaps back",": after the animation, the element returns to its pre-animation state. Use ",[18,1098,1099],{},"forwards"," to keep the end state.",[957,1102,1103,1108],{},[960,1104,1105,1107],{},[18,1106,783],{}," on too many elements wastes memory",": each gets a GPU layer. Use only for elements about to animate, remove after.",[957,1110,1111,1121,1122,1125],{},[960,1112,1113,1115,1116,29,1118,1120],{},[18,1114,603],{}," without ",[18,1117,623],{},[18,1119,675],{}," uses the element's current state",": if you only specify ",[18,1123,1124],{},"to { }",", the animation starts from the computed style. Useful for \"animate to this state.\"",[957,1127,1128,1133],{},[960,1129,1130,1132],{},[18,1131,282],{}," reverses from the current point",": if a property changes and changes back mid-transition, the transition reverses from the current (mid-animation) state, not the start.",[46,1135,1137],{"id":1136},"spot-the-bug","🧠 Spot the Bug",[51,1139,1140],{"language":53},[55,1141,1143],{"className":57,"code":1142,"language":53,"meta":59,"style":59},".menu { height: 0; overflow: hidden; transition: height 0.3s ease; }\n.menu.open { height: 200px; }\n",[18,1144,1145,1179],{"__ignoreMap":59},[63,1146,1147,1149,1151,1153,1155,1157,1159,1161,1163,1165,1167,1169,1171,1173,1175,1177],{"class":65,"line":66},[63,1148,373],{"class":69},[63,1150,144],{"class":73},[63,1152,32],{"class":80},[63,1154,84],{"class":73},[63,1156,382],{"class":80},[63,1158,154],{"class":73},[63,1160,387],{"class":80},[63,1162,84],{"class":73},[63,1164,392],{"class":80},[63,1166,154],{"class":73},[63,1168,282],{"class":80},[63,1170,399],{"class":73},[63,1172,258],{"class":80},[63,1174,109],{"class":108},[63,1176,112],{"class":80},[63,1178,408],{"class":73},[63,1180,1181,1183,1185,1187,1189,1191,1193],{"class":65,"line":77},[63,1182,413],{"class":69},[63,1184,144],{"class":73},[63,1186,32],{"class":80},[63,1188,84],{"class":73},[63,1190,422],{"class":80},[63,1192,425],{"class":108},[63,1194,408],{"class":73},[1196,1197,1198,1202],"details",{},[1199,1200,1201],"summary",{},"Answer",[14,1203,1204,1205,1207,1208,1211,1212,1214,1215,1218,1219,1221,1222,1225,1226,1229,1230,1232,1233,29,1235,29,1237,29,1239,1241,1242,1083,1244,1087],{},"Animating ",[18,1206,32],{}," triggers ",[960,1209,1210],{},"layout"," every frame — the browser recomputes the layout of ",[18,1213,373],{}," and all descendants. On mobile (slower CPUs), this drops frames → janky. Also, ",[18,1216,1217],{},"height: 200px"," is a fixed value — if the content changes, it breaks, and ",[18,1220,1075],{}," can't transition (auto isn't interpolable). Fix: ",[18,1223,1224],{},"transform: scaleY(0)"," → ",[18,1227,1228],{},"scaleY(1)"," (compositor-only, GPU, 60fps) or ",[18,1231,1086],{}," (animates to content height, interpolable). The lesson: never animate ",[18,1234,28],{},[18,1236,32],{},[18,1238,35],{},[18,1240,38],{}," — use ",[18,1243,20],{},[18,1245,1246],{},"grid-template",[1248,1249,1250],"style",{},"html pre.shiki code .sIsaT, html code.shiki .sIsaT{--shiki-default:#6F42C1;--shiki-github-dark:#B392F0}html pre.shiki code .ssxIu, html code.shiki .ssxIu{--shiki-default:#24292E;--shiki-github-dark:#E1E4E8}html pre.shiki code .snvgF, html code.shiki .snvgF{--shiki-default:#005CC5;--shiki-github-dark:#79B8FF}html pre.shiki code .sMKGj, html code.shiki .sMKGj{--shiki-default:#B31D28;--shiki-default-font-style:italic;--shiki-github-dark:#FDAEB7;--shiki-github-dark-font-style:italic}html pre.shiki code .svdQ7, html code.shiki .svdQ7{--shiki-default:#D73A49;--shiki-github-dark:#F97583}html pre.shiki code .sdCPZ, html code.shiki .sdCPZ{--shiki-default:#6A737D;--shiki-github-dark:#6A737D}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .github-dark .shiki span {color: var(--shiki-github-dark);background: var(--shiki-github-dark-bg);font-style: var(--shiki-github-dark-font-style);font-weight: var(--shiki-github-dark-font-weight);text-decoration: var(--shiki-github-dark-text-decoration);}html.github-dark .shiki span {color: var(--shiki-github-dark);background: var(--shiki-github-dark-bg);font-style: var(--shiki-github-dark-font-style);font-weight: var(--shiki-github-dark-font-weight);text-decoration: var(--shiki-github-dark-text-decoration);}html pre.shiki code .sCrzJ, html code.shiki .sCrzJ{--shiki-default:#E36209;--shiki-github-dark:#FFAB70}html pre.shiki code .sk71V, html code.shiki .sk71V{--shiki-default:#22863A;--shiki-github-dark:#85E89D}",{"title":59,"searchDepth":77,"depth":77,"links":1252},[1253,1254,1256,1259,1261,1263,1265,1266,1267],{"id":48,"depth":77,"text":49},{"id":179,"depth":77,"text":1255},"Timing Functions — cubic-bezier for overshoot",{"id":340,"depth":77,"text":341,"children":1257},[1258],{"id":355,"depth":93,"text":356},{"id":600,"depth":77,"text":1260},"@keyframes Animations",{"id":780,"depth":77,"text":1262},"will-change — the GPU hint (use sparingly)",{"id":838,"depth":77,"text":1264},"prefers-reduced-motion — mandatory accessibility",{"id":951,"depth":77,"text":952},{"id":1032,"depth":77,"text":1033},{"id":1136,"depth":77,"text":1137},"Which properties animate on the GPU (transform\u002Fopacity) vs which trigger layout, cubic-bezier overshoot curves, the height:auto trap, grid-template-rows 0fr to 1fr, will-change memory cost, and prefers-reduced-motion. Code-first reference for 60fps animation.","md",{},"\u002Fcss\u002F13-animations-and-transitions",{"title":5,"description":1268},"css\u002F13-animations-and-transitions","gx08A_pxp82YhbKoRZX5H2neqMM5T7i_ZhWF0dxB1JQ",1789924644827]