[{"data":1,"prerenderedAt":1156},["ShallowReactive",2],{"blog-2026-04-12-on-grids-that-fail-gracefully":3,"blog-related":460},{"id":4,"title":5,"body":6,"date":451,"description":452,"extension":453,"meta":454,"navigation":329,"path":455,"pinned":329,"seo":456,"stem":457,"tags":458,"__hash__":459},"blog\u002Fblog\u002F2026-04-12-on-grids-that-fail-gracefully.md","On Grids That Fail Gracefully",{"type":7,"value":8,"toc":441},"minimark",[9,14,18,21,24,28,31,135,145,148,152,208,212,215,372,375,379,382,396,403,409,416,423,427,437],[10,11,13],"h2",{"id":12},"the-problem-with-perfect-grids","The Problem with Perfect Grids",[15,16,17],"p",{},"Every grid looks great in a mockup. The columns align, the gutters breathe, the content fits neatly in every cell. Then reality arrives.",[15,19,20],{},"Real content is messy. Titles run long. Images arrive at unexpected aspect ratios. Dynamic data can have zero items or two hundred. A grid that works only when the content cooperates is not a robust grid — it's a fragile prototype.",[15,22,23],{},"The key insight is that a grid's failure modes are part of its specification, not exceptions to it.",[10,25,27],{"id":26},"how-grid-placement-actually-works","How Grid Placement Actually Works",[15,29,30],{},"CSS Grid's auto-placement algorithm is smarter than most developers give it credit for.",[32,33,38],"pre",{"className":34,"code":35,"language":36,"meta":37,"style":37},"language-css shiki shiki-themes github-light github-dark",".grid {\n  display: grid;\n  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));\n  gap: 1.5rem;\n}\n","css","",[39,40,41,54,70,113,129],"code",{"__ignoreMap":37},[42,43,46,50],"span",{"class":44,"line":45},"line",1,[42,47,49],{"class":48},"sScJk",".grid",[42,51,53],{"class":52},"sVt8B"," {\n",[42,55,57,61,64,67],{"class":44,"line":56},2,[42,58,60],{"class":59},"sj4cs","  display",[42,62,63],{"class":52},": ",[42,65,66],{"class":59},"grid",[42,68,69],{"class":52},";\n",[42,71,73,76,78,81,84,87,90,93,95,98,102,104,107,110],{"class":44,"line":72},3,[42,74,75],{"class":59},"  grid-template-columns",[42,77,63],{"class":52},[42,79,80],{"class":59},"repeat",[42,82,83],{"class":52},"(",[42,85,86],{"class":59},"auto-fill",[42,88,89],{"class":52},", ",[42,91,92],{"class":59},"minmax",[42,94,83],{"class":52},[42,96,97],{"class":59},"240",[42,99,101],{"class":100},"szBVR","px",[42,103,89],{"class":52},[42,105,106],{"class":59},"1",[42,108,109],{"class":100},"fr",[42,111,112],{"class":52},"));\n",[42,114,116,119,121,124,127],{"class":44,"line":115},4,[42,117,118],{"class":59},"  gap",[42,120,63],{"class":52},[42,122,123],{"class":59},"1.5",[42,125,126],{"class":100},"rem",[42,128,69],{"class":52},[42,130,132],{"class":44,"line":131},5,[42,133,134],{"class":52},"}\n",[15,136,137,138,140,141,144],{},"The ",[39,139,86],{}," keyword tells the browser to create as many columns as will fit. The ",[39,142,143],{},"minmax(240px, 1fr)"," constraint ensures each column is at least 240px wide but grows proportionally. This single rule handles breakpoints automatically.",[15,146,147],{},"When items overflow, the algorithm wraps them. When there are no items, the grid simply disappears — its container collapses to zero height without leaving phantom whitespace.",[10,149,151],{"id":150},"mermaid-content-layout-decision-tree","Mermaid: Content → Layout Decision Tree",[32,153,157],{"className":154,"code":155,"language":156,"meta":37,"style":37},"language-mermaid shiki shiki-themes github-light github-dark","graph LR\n  A[Content arrives] --> B{Count?}\n  B -->|0 items| C[Empty state component]\n  B -->|1–3 items| D[Single-row grid]\n  B -->|4+ items| E[Multi-row auto-fill grid]\n  D --> F{All fit?}\n  E --> F\n  F -->|Yes| G[Render as-is]\n  F -->|No| H[Truncate + show count]\n","mermaid",[39,158,159,164,169,174,179,184,190,196,202],{"__ignoreMap":37},[42,160,161],{"class":44,"line":45},[42,162,163],{},"graph LR\n",[42,165,166],{"class":44,"line":56},[42,167,168],{},"  A[Content arrives] --> B{Count?}\n",[42,170,171],{"class":44,"line":72},[42,172,173],{},"  B -->|0 items| C[Empty state component]\n",[42,175,176],{"class":44,"line":115},[42,177,178],{},"  B -->|1–3 items| D[Single-row grid]\n",[42,180,181],{"class":44,"line":131},[42,182,183],{},"  B -->|4+ items| E[Multi-row auto-fill grid]\n",[42,185,187],{"class":44,"line":186},6,[42,188,189],{},"  D --> F{All fit?}\n",[42,191,193],{"class":44,"line":192},7,[42,194,195],{},"  E --> F\n",[42,197,199],{"class":44,"line":198},8,[42,200,201],{},"  F -->|Yes| G[Render as-is]\n",[42,203,205],{"class":44,"line":204},9,[42,206,207],{},"  F -->|No| H[Truncate + show count]\n",[10,209,211],{"id":210},"responsive-without-media-queries","Responsive Without Media Queries",[15,213,214],{},"The common pattern of writing three breakpoint rules for a grid can often be replaced with a single intrinsic rule. Consider the difference:",[32,216,218],{"className":34,"code":217,"language":36,"meta":37,"style":37},"\u002F* Fragile: assumes specific viewport widths *\u002F\n.grid { grid-template-columns: 1fr; }\n@media (min-width: 640px) { .grid { grid-template-columns: 1fr 1fr; } }\n@media (min-width: 1024px) { .grid { grid-template-columns: 1fr 1fr 1fr; } }\n\n\u002F* Resilient: responds to available space, not assumed device *\u002F\n.grid { grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); }\n",[39,219,220,226,245,286,325,331,336],{"__ignoreMap":37},[42,221,222],{"class":44,"line":45},[42,223,225],{"class":224},"sJ8bj","\u002F* Fragile: assumes specific viewport widths *\u002F\n",[42,227,228,230,233,236,238,240,242],{"class":44,"line":56},[42,229,49],{"class":48},[42,231,232],{"class":52}," { ",[42,234,235],{"class":59},"grid-template-columns",[42,237,63],{"class":52},[42,239,106],{"class":59},[42,241,109],{"class":100},[42,243,244],{"class":52},"; }\n",[42,246,247,250,253,256,258,261,263,266,268,270,272,274,276,278,281,283],{"class":44,"line":72},[42,248,249],{"class":100},"@media",[42,251,252],{"class":52}," (",[42,254,255],{"class":59},"min-width",[42,257,63],{"class":52},[42,259,260],{"class":59},"640",[42,262,101],{"class":100},[42,264,265],{"class":52},") { ",[42,267,49],{"class":48},[42,269,232],{"class":52},[42,271,235],{"class":59},[42,273,63],{"class":52},[42,275,106],{"class":59},[42,277,109],{"class":100},[42,279,280],{"class":59}," 1",[42,282,109],{"class":100},[42,284,285],{"class":52},"; } }\n",[42,287,288,290,292,294,296,299,301,303,305,307,309,311,313,315,317,319,321,323],{"class":44,"line":115},[42,289,249],{"class":100},[42,291,252],{"class":52},[42,293,255],{"class":59},[42,295,63],{"class":52},[42,297,298],{"class":59},"1024",[42,300,101],{"class":100},[42,302,265],{"class":52},[42,304,49],{"class":48},[42,306,232],{"class":52},[42,308,235],{"class":59},[42,310,63],{"class":52},[42,312,106],{"class":59},[42,314,109],{"class":100},[42,316,280],{"class":59},[42,318,109],{"class":100},[42,320,280],{"class":59},[42,322,109],{"class":100},[42,324,285],{"class":52},[42,326,327],{"class":44,"line":131},[42,328,330],{"emptyLinePlaceholder":329},true,"\n",[42,332,333],{"class":44,"line":186},[42,334,335],{"class":224},"\u002F* Resilient: responds to available space, not assumed device *\u002F\n",[42,337,338,340,342,344,346,348,350,352,354,356,358,361,363,365,367,369],{"class":44,"line":192},[42,339,49],{"class":48},[42,341,232],{"class":52},[42,343,235],{"class":59},[42,345,63],{"class":52},[42,347,80],{"class":59},[42,349,83],{"class":52},[42,351,86],{"class":59},[42,353,89],{"class":52},[42,355,92],{"class":59},[42,357,83],{"class":52},[42,359,360],{"class":59},"280",[42,362,101],{"class":100},[42,364,89],{"class":52},[42,366,106],{"class":59},[42,368,109],{"class":100},[42,370,371],{"class":52},")); }\n",[15,373,374],{},"The second approach also handles edge cases the first doesn't: a sidebar layout, a narrower viewport, an unexpected container width from a parent component.",[10,376,378],{"id":377},"handling-long-content","Handling Long Content",[15,380,381],{},"The most common grid failure: a title wraps unexpectedly and pushes the grid item's height, misaligning adjacent items.",[15,383,384,385,388,389,392,393,395],{},"Grid's ",[39,386,387],{},"align-items: start"," (versus the default ",[39,390,391],{},"stretch",") lets items be their natural height without forcing alignment on siblings. For card layouts where visual alignment matters more than content height uniformity, ",[39,394,391],{}," is often the wrong default.",[15,397,398],{},[399,400],"img",{"alt":401,"src":402},"Grid failure mode diagram showing misaligned card heights","\u002Fimages\u002Fblog\u002Fgrid-failure-modes.png",[10,404,406,407],{"id":405},"the-design-intent-behind-fr","The Design Intent Behind ",[39,408,109],{},[15,410,411,412,415],{},"Fractional units communicate design intent in a way percentages cannot. When you write ",[39,413,414],{},"grid-template-columns: 1.4fr 1fr",", you're saying the first column should be 40% more prominent than the second — not that it should be 58.33% of the container width. The intent is preserved even when the container changes size.",[15,417,418,419,422],{},"This portfolio uses ",[39,420,421],{},"grid-cols-[1.4fr_1fr]"," on the blog list for exactly this reason: the post list should feel heavier and more primary than the sidebar, in all viewport conditions.",[10,424,426],{"id":425},"summary","Summary",[15,428,429,430,89,432,89,434,436],{},"Grids fail gracefully when you treat failure as a design constraint rather than an edge case. The tools are already in CSS Grid's vocabulary — ",[39,431,86],{},[39,433,92],{},[39,435,387],{},", fractional units. The practice is recognizing which failure modes your layout will encounter and specifying them upfront.",[438,439,440],"style",{},"html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}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 .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}",{"title":37,"searchDepth":56,"depth":56,"links":442},[443,444,445,446,447,448,450],{"id":12,"depth":56,"text":13},{"id":26,"depth":56,"text":27},{"id":150,"depth":56,"text":151},{"id":210,"depth":56,"text":211},{"id":377,"depth":56,"text":378},{"id":405,"depth":56,"text":449},"The Design Intent Behind fr",{"id":425,"depth":56,"text":426},"2026-04-12","How CSS Grid behaves when content breaks the layout — and how to design for failure modes from the start.","md",{},"\u002Fblog\u002F2026-04-12-on-grids-that-fail-gracefully",{"title":5,"description":452},"blog\u002F2026-04-12-on-grids-that-fail-gracefully",[36],"2sBIOUNWcTDepVOt-qygOE_tyjtxVCudmjiVsYa4bPM",[461,788],{"id":4,"title":5,"body":462,"date":451,"description":452,"extension":453,"meta":785,"navigation":329,"path":455,"pinned":329,"seo":786,"stem":457,"tags":787,"__hash__":459},{"type":7,"value":463,"toc":776},[464,466,468,470,472,474,476,542,548,550,552,592,594,596,734,736,738,740,748,752,756,760,764,766,774],[10,465,13],{"id":12},[15,467,17],{},[15,469,20],{},[15,471,23],{},[10,473,27],{"id":26},[15,475,30],{},[32,477,478],{"className":34,"code":35,"language":36,"meta":37,"style":37},[39,479,480,486,496,526,538],{"__ignoreMap":37},[42,481,482,484],{"class":44,"line":45},[42,483,49],{"class":48},[42,485,53],{"class":52},[42,487,488,490,492,494],{"class":44,"line":56},[42,489,60],{"class":59},[42,491,63],{"class":52},[42,493,66],{"class":59},[42,495,69],{"class":52},[42,497,498,500,502,504,506,508,510,512,514,516,518,520,522,524],{"class":44,"line":72},[42,499,75],{"class":59},[42,501,63],{"class":52},[42,503,80],{"class":59},[42,505,83],{"class":52},[42,507,86],{"class":59},[42,509,89],{"class":52},[42,511,92],{"class":59},[42,513,83],{"class":52},[42,515,97],{"class":59},[42,517,101],{"class":100},[42,519,89],{"class":52},[42,521,106],{"class":59},[42,523,109],{"class":100},[42,525,112],{"class":52},[42,527,528,530,532,534,536],{"class":44,"line":115},[42,529,118],{"class":59},[42,531,63],{"class":52},[42,533,123],{"class":59},[42,535,126],{"class":100},[42,537,69],{"class":52},[42,539,540],{"class":44,"line":131},[42,541,134],{"class":52},[15,543,137,544,140,546,144],{},[39,545,86],{},[39,547,143],{},[15,549,147],{},[10,551,151],{"id":150},[32,553,554],{"className":154,"code":155,"language":156,"meta":37,"style":37},[39,555,556,560,564,568,572,576,580,584,588],{"__ignoreMap":37},[42,557,558],{"class":44,"line":45},[42,559,163],{},[42,561,562],{"class":44,"line":56},[42,563,168],{},[42,565,566],{"class":44,"line":72},[42,567,173],{},[42,569,570],{"class":44,"line":115},[42,571,178],{},[42,573,574],{"class":44,"line":131},[42,575,183],{},[42,577,578],{"class":44,"line":186},[42,579,189],{},[42,581,582],{"class":44,"line":192},[42,583,195],{},[42,585,586],{"class":44,"line":198},[42,587,201],{},[42,589,590],{"class":44,"line":204},[42,591,207],{},[10,593,211],{"id":210},[15,595,214],{},[32,597,598],{"className":34,"code":217,"language":36,"meta":37,"style":37},[39,599,600,604,620,654,692,696,700],{"__ignoreMap":37},[42,601,602],{"class":44,"line":45},[42,603,225],{"class":224},[42,605,606,608,610,612,614,616,618],{"class":44,"line":56},[42,607,49],{"class":48},[42,609,232],{"class":52},[42,611,235],{"class":59},[42,613,63],{"class":52},[42,615,106],{"class":59},[42,617,109],{"class":100},[42,619,244],{"class":52},[42,621,622,624,626,628,630,632,634,636,638,640,642,644,646,648,650,652],{"class":44,"line":72},[42,623,249],{"class":100},[42,625,252],{"class":52},[42,627,255],{"class":59},[42,629,63],{"class":52},[42,631,260],{"class":59},[42,633,101],{"class":100},[42,635,265],{"class":52},[42,637,49],{"class":48},[42,639,232],{"class":52},[42,641,235],{"class":59},[42,643,63],{"class":52},[42,645,106],{"class":59},[42,647,109],{"class":100},[42,649,280],{"class":59},[42,651,109],{"class":100},[42,653,285],{"class":52},[42,655,656,658,660,662,664,666,668,670,672,674,676,678,680,682,684,686,688,690],{"class":44,"line":115},[42,657,249],{"class":100},[42,659,252],{"class":52},[42,661,255],{"class":59},[42,663,63],{"class":52},[42,665,298],{"class":59},[42,667,101],{"class":100},[42,669,265],{"class":52},[42,671,49],{"class":48},[42,673,232],{"class":52},[42,675,235],{"class":59},[42,677,63],{"class":52},[42,679,106],{"class":59},[42,681,109],{"class":100},[42,683,280],{"class":59},[42,685,109],{"class":100},[42,687,280],{"class":59},[42,689,109],{"class":100},[42,691,285],{"class":52},[42,693,694],{"class":44,"line":131},[42,695,330],{"emptyLinePlaceholder":329},[42,697,698],{"class":44,"line":186},[42,699,335],{"class":224},[42,701,702,704,706,708,710,712,714,716,718,720,722,724,726,728,730,732],{"class":44,"line":192},[42,703,49],{"class":48},[42,705,232],{"class":52},[42,707,235],{"class":59},[42,709,63],{"class":52},[42,711,80],{"class":59},[42,713,83],{"class":52},[42,715,86],{"class":59},[42,717,89],{"class":52},[42,719,92],{"class":59},[42,721,83],{"class":52},[42,723,360],{"class":59},[42,725,101],{"class":100},[42,727,89],{"class":52},[42,729,106],{"class":59},[42,731,109],{"class":100},[42,733,371],{"class":52},[15,735,374],{},[10,737,378],{"id":377},[15,739,381],{},[15,741,384,742,388,744,392,746,395],{},[39,743,387],{},[39,745,391],{},[39,747,391],{},[15,749,750],{},[399,751],{"alt":401,"src":402},[10,753,406,754],{"id":405},[39,755,109],{},[15,757,411,758,415],{},[39,759,414],{},[15,761,418,762,422],{},[39,763,421],{},[10,765,426],{"id":425},[15,767,429,768,89,770,89,772,436],{},[39,769,86],{},[39,771,92],{},[39,773,387],{},[438,775,440],{},{"title":37,"searchDepth":56,"depth":56,"links":777},[778,779,780,781,782,783,784],{"id":12,"depth":56,"text":13},{"id":26,"depth":56,"text":27},{"id":150,"depth":56,"text":151},{"id":210,"depth":56,"text":211},{"id":377,"depth":56,"text":378},{"id":405,"depth":56,"text":449},{"id":425,"depth":56,"text":426},{},{"title":5,"description":452},[36],{"id":789,"title":790,"body":791,"date":1146,"description":1147,"extension":453,"meta":1148,"navigation":329,"path":1149,"pinned":1150,"seo":1151,"stem":1152,"tags":1153,"__hash__":1155},"blog\u002Fblog\u002F2025-12-24-animating-with-restraint.md","Animating with Restraint",{"type":7,"value":792,"toc":1138},[793,797,800,803,806,810,813,820,826,832,835,839,933,936,939,943,946,953,1037,1040,1045,1048,1124,1127,1129,1135],[10,794,796],{"id":795},"why-most-animation-fails","Why Most Animation Fails",[15,798,799],{},"The common failure mode of UI animation is not technical — it's philosophical. Animations fail because they exist to impress the developer, not to help the user.",[15,801,802],{},"A loading spinner that spins for 300ms on a 50ms operation. A page transition that takes longer than the attention span it's meant to hold. A card hover effect that makes every card feel equally special, which means none of them are.",[15,804,805],{},"Restraint is not the absence of animation. It's the discipline of animating only when motion communicates something that static design cannot.",[10,807,809],{"id":808},"the-three-legitimate-uses","The Three Legitimate Uses",[15,811,812],{},"After surveying dozens of production interfaces, motion serves exactly three legitimate purposes:",[15,814,815,819],{},[816,817,818],"strong",{},"1. Orientation"," — telling the user where they are and how they got there. A modal sliding in from the right implies it came from a right-side trigger. A list item expanding in place implies it contains the content that was revealed.",[15,821,822,825],{},[816,823,824],{},"2. Causality"," — connecting cause to effect. When a button click causes a drawer to open, the transition that links them helps the user's mental model: pressing this caused that.",[15,827,828,831],{},[816,829,830],{},"3. Feedback"," — confirming that an action was received. A button that briefly scales down on press, a form field that shakes on invalid input. These are motion as error message.",[15,833,834],{},"Everything else is decoration. Decoration has a cost: it draws attention, consumes processing, and after the first viewing, delays the user.",[10,836,838],{"id":837},"duration-as-a-design-decision","Duration as a Design Decision",[32,840,842],{"className":34,"code":841,"language":36,"meta":37,"style":37},"\u002F* System-level duration tokens *\u002F\n:root {\n  --duration-instant: 80ms;   \u002F* state changes: checked, focused *\u002F\n  --duration-fast: 150ms;     \u002F* micro-interactions: hover, press *\u002F\n  --duration-standard: 250ms; \u002F* component transitions: modal, drawer *\u002F\n  --duration-deliberate: 400ms; \u002F* page-level transitions *\u002F\n}\n",[39,843,844,849,856,876,894,912,929],{"__ignoreMap":37},[42,845,846],{"class":44,"line":45},[42,847,848],{"class":224},"\u002F* System-level duration tokens *\u002F\n",[42,850,851,854],{"class":44,"line":56},[42,852,853],{"class":48},":root",[42,855,53],{"class":52},[42,857,858,862,864,867,870,873],{"class":44,"line":72},[42,859,861],{"class":860},"s4XuR","  --duration-instant",[42,863,63],{"class":52},[42,865,866],{"class":59},"80",[42,868,869],{"class":100},"ms",[42,871,872],{"class":52},";   ",[42,874,875],{"class":224},"\u002F* state changes: checked, focused *\u002F\n",[42,877,878,881,883,886,888,891],{"class":44,"line":115},[42,879,880],{"class":860},"  --duration-fast",[42,882,63],{"class":52},[42,884,885],{"class":59},"150",[42,887,869],{"class":100},[42,889,890],{"class":52},";     ",[42,892,893],{"class":224},"\u002F* micro-interactions: hover, press *\u002F\n",[42,895,896,899,901,904,906,909],{"class":44,"line":131},[42,897,898],{"class":860},"  --duration-standard",[42,900,63],{"class":52},[42,902,903],{"class":59},"250",[42,905,869],{"class":100},[42,907,908],{"class":52},"; ",[42,910,911],{"class":224},"\u002F* component transitions: modal, drawer *\u002F\n",[42,913,914,917,919,922,924,926],{"class":44,"line":186},[42,915,916],{"class":860},"  --duration-deliberate",[42,918,63],{"class":52},[42,920,921],{"class":59},"400",[42,923,869],{"class":100},[42,925,908],{"class":52},[42,927,928],{"class":224},"\u002F* page-level transitions *\u002F\n",[42,930,931],{"class":44,"line":192},[42,932,134],{"class":52},[15,934,935],{},"These are not arbitrary values. They're derived from the perceptual threshold research: below 100ms feels instantaneous, 100–300ms feels responsive, 300–500ms is noticeable, above 500ms is slow.",[15,937,938],{},"Most UI transitions should live in the 150–250ms range. Page transitions can reach 300–400ms when they establish strong spatial metaphors. Beyond 500ms requires exceptional justification.",[10,940,942],{"id":941},"easing-communicates-character","Easing Communicates Character",[15,944,945],{},"A linear ease says: this system doesn't understand physics. An ease-out says: this object has momentum and decelerates naturally. An ease-in-out says: this transition is smooth in both directions.",[15,947,948,949,952],{},"For most UI transitions, ",[39,950,951],{},"ease-out"," is correct. Objects entering the screen carry momentum from outside the viewport; they should decelerate as they arrive. Objects leaving should accelerate (ease-in) — they're departing, gathering speed.",[32,954,956],{"className":34,"code":955,"language":36,"meta":37,"style":37},".enter { animation: slideIn 200ms cubic-bezier(0.0, 0.0, 0.2, 1); }\n.leave { animation: slideOut 150ms cubic-bezier(0.4, 0.0, 1, 1); }\n",[39,957,958,1000],{"__ignoreMap":37},[42,959,960,963,965,968,971,974,976,979,981,984,986,988,990,993,995,997],{"class":44,"line":45},[42,961,962],{"class":48},".enter",[42,964,232],{"class":52},[42,966,967],{"class":59},"animation",[42,969,970],{"class":52},": slideIn ",[42,972,973],{"class":59},"200",[42,975,869],{"class":100},[42,977,978],{"class":59}," cubic-bezier",[42,980,83],{"class":52},[42,982,983],{"class":59},"0.0",[42,985,89],{"class":52},[42,987,983],{"class":59},[42,989,89],{"class":52},[42,991,992],{"class":59},"0.2",[42,994,89],{"class":52},[42,996,106],{"class":59},[42,998,999],{"class":52},"); }\n",[42,1001,1002,1005,1007,1009,1012,1014,1016,1018,1020,1023,1025,1027,1029,1031,1033,1035],{"class":44,"line":56},[42,1003,1004],{"class":48},".leave",[42,1006,232],{"class":52},[42,1008,967],{"class":59},[42,1010,1011],{"class":52},": slideOut ",[42,1013,885],{"class":59},[42,1015,869],{"class":100},[42,1017,978],{"class":59},[42,1019,83],{"class":52},[42,1021,1022],{"class":59},"0.4",[42,1024,89],{"class":52},[42,1026,983],{"class":59},[42,1028,89],{"class":52},[42,1030,106],{"class":59},[42,1032,89],{"class":52},[42,1034,106],{"class":59},[42,1036,999],{"class":52},[15,1038,1039],{},"Note the asymmetry: entrances are slightly slower (200ms) and use ease-out; exits are faster (150ms) and use ease-in. Users spend more time watching things arrive than depart. Give them time to orient.",[10,1041,1043],{"id":1042},"prefers-reduced-motion",[39,1044,1042],{},[15,1046,1047],{},"Every animation system should respect the operating system's accessibility setting:",[32,1049,1051],{"className":34,"code":1050,"language":36,"meta":37,"style":37},"@media (prefers-reduced-motion: reduce) {\n  *, *::before, *::after {\n    animation-duration: 0.01ms !important;\n    transition-duration: 0.01ms !important;\n  }\n}\n",[39,1052,1053,1060,1083,1100,1115,1120],{"__ignoreMap":37},[42,1054,1055,1057],{"class":44,"line":45},[42,1056,249],{"class":100},[42,1058,1059],{"class":52}," (prefers-reduced-motion: reduce) {\n",[42,1061,1062,1066,1068,1071,1074,1076,1078,1081],{"class":44,"line":56},[42,1063,1065],{"class":1064},"s9eBZ","  *",[42,1067,89],{"class":52},[42,1069,1070],{"class":1064},"*",[42,1072,1073],{"class":48},"::before",[42,1075,89],{"class":52},[42,1077,1070],{"class":1064},[42,1079,1080],{"class":48},"::after",[42,1082,53],{"class":52},[42,1084,1085,1088,1090,1093,1095,1098],{"class":44,"line":72},[42,1086,1087],{"class":59},"    animation-duration",[42,1089,63],{"class":52},[42,1091,1092],{"class":59},"0.01",[42,1094,869],{"class":100},[42,1096,1097],{"class":100}," !important",[42,1099,69],{"class":52},[42,1101,1102,1105,1107,1109,1111,1113],{"class":44,"line":115},[42,1103,1104],{"class":59},"    transition-duration",[42,1106,63],{"class":52},[42,1108,1092],{"class":59},[42,1110,869],{"class":100},[42,1112,1097],{"class":100},[42,1114,69],{"class":52},[42,1116,1117],{"class":44,"line":131},[42,1118,1119],{"class":52},"  }\n",[42,1121,1122],{"class":44,"line":186},[42,1123,134],{"class":52},[15,1125,1126],{},"This is not optional. Some users experience vestibular disorders that make motion literally nauseating. The reduce variant should collapse transitions to instant rather than removing them — state still needs to change, just without the motion.",[10,1128,426],{"id":425},[15,1130,1131,1132,1134],{},"Animation earns its place by communicating orientation, causality, or feedback. Everything else should be cut. When you do animate, match duration to perception (150–250ms for most transitions), match easing to physics (ease-out for entries), and always respect ",[39,1133,1042],{},". The goal is an interface where users never consciously notice the animations — only their absence.",[438,1136,1137],{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}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 .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}",{"title":37,"searchDepth":56,"depth":56,"links":1139},[1140,1141,1142,1143,1144,1145],{"id":795,"depth":56,"text":796},{"id":808,"depth":56,"text":809},{"id":837,"depth":56,"text":838},{"id":941,"depth":56,"text":942},{"id":1042,"depth":56,"text":1042},{"id":425,"depth":56,"text":426},"2025-12-24","Why most UI animation fails, and a practical framework for motion that earns its place.",{},"\u002Fblog\u002F2025-12-24-animating-with-restraint",false,{"title":790,"description":1147},"blog\u002F2025-12-24-animating-with-restraint",[1154],"motion","tM7jY9eZSmIpZLF7ypMhPz3l9CmjC63QjEHNUIIOSB4",1780894152891]