[{"data":1,"prerenderedAt":1342},["ShallowReactive",2],{"content:\u002Fcore-web-vitals-measurement\u002Freducing-cumulative-layout-shift-cls\u002Ffixing-cls-from-skeleton-screens-that-resize":3,"surroundings:\u002Fcore-web-vitals-measurement\u002Freducing-cumulative-layout-shift-cls\u002Ffixing-cls-from-skeleton-screens-that-resize":1333},{"id":4,"title":5,"body":6,"description":1314,"extension":1315,"meta":1316,"navigation":1327,"path":1328,"seo":1329,"stem":1331,"__hash__":1332},"content\u002Fcore-web-vitals-measurement\u002Freducing-cumulative-layout-shift-cls\u002Ffixing-cls-from-skeleton-screens-that-resize\u002Findex.md","Fixing CLS from Skeleton Screens That Resize",{"type":7,"value":8,"toc":1300},"minimark",[9,14,34,37,150,155,199,203,209,215,221,227,320,324,329,332,634,637,641,644,816,819,823,826,882,885,889,896,899,1004,1008,1011,1187,1198,1202,1212,1216,1240,1249,1258,1262,1285,1290,1293,1296],[10,11,13],"h1",{"id":12},"how-to-fix-layout-shift-from-skeleton-screens-that-do-not-match-real-content","How to Fix Layout Shift from Skeleton Screens That Do Not Match Real Content",[15,16,17,18,23,24,28,29,33],"p",{},"This guide covers a counter-intuitive CLS source, within ",[19,20,22],"a",{"href":21},"\u002Fcore-web-vitals-measurement\u002Freducing-cumulative-layout-shift-cls\u002F","Reducing Cumulative Layout Shift (CLS)"," in ",[19,25,27],{"href":26},"\u002Fcore-web-vitals-measurement\u002F","Core Web Vitals & Measurement",": the skeleton screen added specifically to ",[30,31,32],"em",{},"prevent"," layout shift turns out to cause it.",[15,35,36],{},"Skeletons work by occupying the space content will fill. They fail when the space they occupy is not the space the content needs. A skeleton card 280px tall replaced by a product card 340px tall moves everything below it by 60px — multiplied across a grid of 12 cards, the lower half of the page jumps. Because the swap happens when data arrives, long after any user input, the movement counts toward CLS in full. Teams often see CLS rise after introducing skeletons, then conclude skeletons are bad. The problem is sizing, not the pattern.",[15,38,39],{},[40,41,47,48,47,55,47,59,47,62,47,71,47,77,47,86,47,92,47,97,47,102,47,105,47,108,47,111,47,114,47,117,47,120,47,125,47,129,47,131,47,135,47,137,47,140,47,142,47,145,47,147,47],"svg",{"viewBox":42,"width":43,"role":44,"ariaLabel":45,"style":46},"0 0 760 228","100%","img","Comparison of a skeleton whose height differs from the real content, causing a shift, with a skeleton sized from the content model.","height:auto;max-width:760px;display:block;margin:1.75rem auto;font-family:inherit;color:var(--fp-svg-ink)"," ",[49,50],"rect",{"className":51,"x":53,"y":53,"width":43,"height":43,"fill":54},[52],"svg-canvas","0","#ffffff",[56,57,58],"title",{},"Mismatched vs matched skeleton",[60,61,45],"desc",{},[49,63],{"x":64,"y":64,"width":65,"height":66,"rx":67,"fill":68,"stroke":69,"style":70},"1","758","226","10","none","currentColor","stroke-opacity:0.18",[72,73,58],"text",{"x":74,"y":75,"fill":69,"style":76},"28.0","34.0","font-size:16px;font-weight:700",[49,78],{"x":74,"y":79,"width":80,"height":81,"rx":82,"fill":83,"stroke":84,"style":85},"56.0","340.0","150.0","6","#ffc300","#b8860b","fill-opacity:0.24;stroke-opacity:0.9",[72,87,91],{"x":88,"y":89,"fill":69,"style":90},"42.0","82.0","font-size:14px;font-weight:700","Generic skeleton (shifts)",[72,93,96],{"x":88,"y":94,"fill":69,"style":95},"108.0","font-size:12.5px;font-weight:700","•",[72,98,101],{"x":79,"y":94,"fill":69,"style":99,"textAnchor":100},"font-size:12.5px","start","Fixed 280px grey block per card",[72,103,96],{"x":88,"y":104,"fill":69,"style":95},"132.0",[72,106,107],{"x":79,"y":104,"fill":69,"style":99,"textAnchor":100},"Real card is 340px with two-line title",[72,109,96],{"x":88,"y":110,"fill":69,"style":95},"156.0",[72,112,113],{"x":79,"y":110,"fill":69,"style":99,"textAnchor":100},"Each row grows by 60px on swap",[72,115,96],{"x":88,"y":116,"fill":69,"style":95},"180.0",[72,118,119],{"x":79,"y":116,"fill":69,"style":99,"textAnchor":100},"Footer and later rows jump down",[49,121],{"x":122,"y":79,"width":80,"height":81,"rx":82,"fill":123,"stroke":123,"style":124},"392.0","#0466c8","fill-opacity:0.14;stroke-opacity:0.9",[72,126,128],{"x":127,"y":89,"fill":69,"style":90},"406.0","Content-shaped skeleton",[72,130,96],{"x":127,"y":94,"fill":69,"style":95},[72,132,134],{"x":133,"y":94,"fill":69,"style":99,"textAnchor":100},"420.0","Same aspect-ratio image box as real card",[72,136,96],{"x":127,"y":104,"fill":69,"style":95},[72,138,139],{"x":133,"y":104,"fill":69,"style":99,"textAnchor":100},"Title area reserves two lines",[72,141,96],{"x":127,"y":110,"fill":69,"style":95},[72,143,144],{"x":133,"y":110,"fill":69,"style":99,"textAnchor":100},"Price and rating rows fixed height",[72,146,96],{"x":127,"y":116,"fill":69,"style":95},[72,148,149],{"x":133,"y":116,"fill":69,"style":99,"textAnchor":100},"Swap changes pixels, not geometry",[151,152,154],"h2",{"id":153},"rapid-diagnosis","Rapid Diagnosis",[156,157,158,166,172,187,193],"ul",{},[159,160,161,165],"li",{},[162,163,164],"strong",{},"Throttle the data request."," In DevTools, use request blocking or a slow custom network profile on the API call alone, so the skeleton stays visible long enough to compare with the final layout.",[159,167,168,171],{},[162,169,170],{},"Screenshot both states at the same scroll position."," Overlay them (or toggle quickly) and note which edges move.",[159,173,174,177,178,182,183,186],{},[162,175,176],{},"Read the shift sources."," In the Performance panel, select the layout shift and inspect ",[179,180,181],"code",{},"previousRect"," and ",[179,184,185],{},"currentRect"," for each source node. A consistent height delta per card points at skeleton sizing.",[159,188,189,192],{},[162,190,191],{},"Check variable-length content."," Titles that wrap to one, two or three lines, optional badges, and prices with or without a discount line all change card height.",[159,194,195,198],{},[162,196,197],{},"Check responsive breakpoints."," A skeleton tuned for desktop often mismatches on mobile, where text wraps more.",[151,200,202],{"id":201},"root-cause-analysis","Root Cause Analysis",[15,204,205,208],{},[162,206,207],{},"1. Generic placeholder dimensions."," Skeleton components shared across the app use fixed heights that approximate \"a card\" rather than the specific card that will render.",[15,210,211,214],{},[162,212,213],{},"2. Variable content height."," Real content height depends on data: text length, optional fields, image aspect ratios. A skeleton can only match it if the variability is constrained.",[15,216,217,220],{},[162,218,219],{},"3. Images without intrinsic ratio."," The skeleton reserves a 1:1 box, but the real image component renders at its natural aspect ratio once loaded, or has no reserved ratio and collapses to zero until it loads.",[15,222,223,226],{},[162,224,225],{},"4. Different element counts."," A skeleton list renders 6 placeholders; the API returns 9 items. The extra three rows push the footer down. Or it returns 2, and the page shrinks — content below moves up, which also counts.",[15,228,229],{},[40,230,47,233,47,236,47,239,47,241,47,244,47,246,47,256,47,263,47,271,47,276,47,280,47,284,47,288,47,292,47,297,47,301,47,305,47,309,47,313,47],{"viewBox":231,"width":43,"role":44,"ariaLabel":232,"style":46},"0 0 760 216","Bar chart of CLS caused by four kinds of skeleton mismatch on a product listing page on mobile.",[49,234],{"className":235,"x":53,"y":53,"width":43,"height":43,"fill":54},[52],[56,237,238],{},"CLS contribution by skeleton mismatch type",[60,240,232],{},[49,242],{"x":64,"y":64,"width":65,"height":243,"rx":67,"fill":68,"stroke":69,"style":70},"214",[72,245,238],{"x":74,"y":75,"fill":69,"style":76},[247,248],"line",{"x1":249,"y1":250,"x2":249,"y2":251,"stroke":84,"strokeWidth":252,"strokeDashArray":253,"style":255},"526.9","72.0","194.0","1.5",[254,254],"4","stroke-opacity:0.9",[72,257,262],{"x":258,"y":259,"fill":69,"style":260,"textAnchor":261},"162.0","92.0","font-size:13px","end","Card height too short",[49,264],{"x":265,"y":266,"width":267,"height":268,"rx":269,"fill":83,"stroke":84,"style":270},"174.0","78.0","494.0","19","3","fill-opacity:0.7;stroke-opacity:0.9",[72,272,275],{"x":273,"y":259,"fill":69,"style":274},"674.0","font-size:12px;font-weight:600","0.14",[72,277,279],{"x":258,"y":278,"fill":69,"style":260,"textAnchor":261},"123.0","Image ratio unreserved",[49,281],{"x":265,"y":282,"width":283,"height":268,"rx":269,"fill":83,"stroke":84,"style":270},"109.0","388.1",[72,285,287],{"x":286,"y":278,"fill":69,"style":274},"568.1","0.11",[72,289,291],{"x":258,"y":290,"fill":69,"style":260,"textAnchor":261},"154.0","Item count differs",[49,293],{"x":265,"y":294,"width":295,"height":268,"rx":269,"fill":123,"stroke":123,"style":296},"140.0","211.7","fill-opacity:0.55;stroke-opacity:0.9",[72,298,300],{"x":299,"y":290,"fill":69,"style":274},"391.7","0.06",[72,302,304],{"x":258,"y":303,"fill":69,"style":260,"textAnchor":261},"185.0","Breakpoint mismatch",[49,306],{"x":265,"y":307,"width":308,"height":268,"rx":269,"fill":123,"stroke":123,"style":296},"171.0","141.1",[72,310,312],{"x":311,"y":303,"fill":69,"style":274},"321.1","0.04",[72,314,319],{"x":249,"y":315,"fill":316,"style":317,"textAnchor":318},"68.0","#8a6608","font-size:12px;font-weight:700","middle","CLS 0.1",[151,321,323],{"id":322},"step-by-step-resolution","Step-by-Step Resolution",[325,326,328],"h3",{"id":327},"_1-build-skeletons-from-the-same-layout-as-the-real-component","1. Build skeletons from the same layout as the real component",[15,330,331],{},"Render the real component's layout with placeholder content, rather than a separate skeleton component. Every box keeps its real dimensions; only the content is replaced by shimmer.",[333,334,339],"pre",{"className":335,"code":336,"language":337,"meta":338,"style":338},"language-jsx shiki shiki-themes github-light-high-contrast github-dark-high-contrast github-light-high-contrast","function ProductCard({ product }) {\n  const loading = !product;\n  return (\n    \u003Carticle className=\"card\" aria-busy={loading}>\n      \u003Cdiv className=\"card-media\">{loading ? \u003Cspan className=\"shimmer\" \u002F> : \u003Cimg {...product.image} \u002F>}\u003C\u002Fdiv>\n      \u003Ch3 className=\"card-title\">{loading ? \u003Cspan className=\"shimmer-text\" \u002F> : product.name}\u003C\u002Fh3>\n      \u003Cp className=\"card-price\">{loading ? \u003Cspan className=\"shimmer-text short\" \u002F> : product.price}\u003C\u002Fp>\n    \u003C\u002Farticle>\n  );\n}\n\u002F\u002F trade-off: one component handling both states adds a branch to every field.\n\u002F\u002F For very complex cards, a separate skeleton is fine IF it imports the same\n\u002F\u002F layout CSS and its dimensions are covered by a visual regression test.\n","jsx","",[179,340,341,364,383,392,427,497,545,593,603,609,615,622,628],{"__ignoreMap":338},[342,343,345,349,353,357,361],"span",{"class":247,"line":344},1,[342,346,348],{"class":347},"sPARh","function",[342,350,352],{"class":351},"smZ65"," ProductCard",[342,354,356],{"class":355},"saISM","({ ",[342,358,360],{"class":359},"sQw3B","product",[342,362,363],{"class":355}," }) {\n",[342,365,367,370,374,377,380],{"class":247,"line":366},2,[342,368,369],{"class":347},"  const",[342,371,373],{"class":372},"sPXB4"," loading",[342,375,376],{"class":347}," =",[342,378,379],{"class":347}," !",[342,381,382],{"class":355},"product;\n",[342,384,386,389],{"class":247,"line":385},3,[342,387,388],{"class":347},"  return",[342,390,391],{"class":355}," (\n",[342,393,395,398,402,405,408,412,415,418,421,424],{"class":247,"line":394},4,[342,396,397],{"class":355},"    \u003C",[342,399,401],{"class":400},"sZBmE","article",[342,403,404],{"class":372}," className",[342,406,407],{"class":347},"=",[342,409,411],{"class":410},"sZ8jY","\"card\"",[342,413,414],{"class":372}," aria-busy",[342,416,417],{"class":347},"={",[342,419,420],{"class":355},"loading",[342,422,423],{"class":347},"}",[342,425,426],{"class":355},">\n",[342,428,430,433,436,438,440,443,446,449,452,455,458,460,462,464,467,470,473,475,477,480,483,485,488,490,493,495],{"class":247,"line":429},5,[342,431,432],{"class":355},"      \u003C",[342,434,435],{"class":400},"div",[342,437,404],{"class":372},[342,439,407],{"class":347},[342,441,442],{"class":410},"\"card-media\"",[342,444,445],{"class":355},">",[342,447,448],{"class":347},"{",[342,450,451],{"class":355},"loading ",[342,453,454],{"class":347},"?",[342,456,457],{"class":355}," \u003C",[342,459,342],{"class":400},[342,461,404],{"class":372},[342,463,407],{"class":347},[342,465,466],{"class":410},"\"shimmer\"",[342,468,469],{"class":355}," \u002F> ",[342,471,472],{"class":347},":",[342,474,457],{"class":355},[342,476,44],{"class":400},[342,478,479],{"class":347}," {...",[342,481,482],{"class":355},"product.image",[342,484,423],{"class":347},[342,486,487],{"class":355}," \u002F>",[342,489,423],{"class":347},[342,491,492],{"class":355},"\u003C\u002F",[342,494,435],{"class":400},[342,496,426],{"class":355},[342,498,500,502,504,506,508,511,513,515,517,519,521,523,525,527,530,532,534,537,539,541,543],{"class":247,"line":499},6,[342,501,432],{"class":355},[342,503,325],{"class":400},[342,505,404],{"class":372},[342,507,407],{"class":347},[342,509,510],{"class":410},"\"card-title\"",[342,512,445],{"class":355},[342,514,448],{"class":347},[342,516,451],{"class":355},[342,518,454],{"class":347},[342,520,457],{"class":355},[342,522,342],{"class":400},[342,524,404],{"class":372},[342,526,407],{"class":347},[342,528,529],{"class":410},"\"shimmer-text\"",[342,531,469],{"class":355},[342,533,472],{"class":347},[342,535,536],{"class":355}," product.name",[342,538,423],{"class":347},[342,540,492],{"class":355},[342,542,325],{"class":400},[342,544,426],{"class":355},[342,546,548,550,552,554,556,559,561,563,565,567,569,571,573,575,578,580,582,585,587,589,591],{"class":247,"line":547},7,[342,549,432],{"class":355},[342,551,15],{"class":400},[342,553,404],{"class":372},[342,555,407],{"class":347},[342,557,558],{"class":410},"\"card-price\"",[342,560,445],{"class":355},[342,562,448],{"class":347},[342,564,451],{"class":355},[342,566,454],{"class":347},[342,568,457],{"class":355},[342,570,342],{"class":400},[342,572,404],{"class":372},[342,574,407],{"class":347},[342,576,577],{"class":410},"\"shimmer-text short\"",[342,579,469],{"class":355},[342,581,472],{"class":347},[342,583,584],{"class":355}," product.price",[342,586,423],{"class":347},[342,588,492],{"class":355},[342,590,15],{"class":400},[342,592,426],{"class":355},[342,594,596,599,601],{"class":247,"line":595},8,[342,597,598],{"class":355},"    \u003C\u002F",[342,600,401],{"class":400},[342,602,426],{"class":355},[342,604,606],{"class":247,"line":605},9,[342,607,608],{"class":355},"  );\n",[342,610,612],{"class":247,"line":611},10,[342,613,614],{"class":355},"}\n",[342,616,618],{"class":247,"line":617},11,[342,619,621],{"class":620},"sjfSM","\u002F\u002F trade-off: one component handling both states adds a branch to every field.\n",[342,623,625],{"class":247,"line":624},12,[342,626,627],{"class":620},"\u002F\u002F For very complex cards, a separate skeleton is fine IF it imports the same\n",[342,629,631],{"class":247,"line":630},13,[342,632,633],{"class":620},"\u002F\u002F layout CSS and its dimensions are covered by a visual regression test.\n",[15,635,636],{},"Expected outcome: skeleton and content share CSS, so geometry can only diverge where content length varies.",[325,638,640],{"id":639},"_2-constrain-variable-length-content","2. Constrain variable-length content",[15,642,643],{},"Reserve space for the maximum lines you will show, and clamp longer text, so card height is fixed regardless of data.",[333,645,649],{"className":646,"code":647,"language":648,"meta":338,"style":338},"language-css shiki shiki-themes github-light-high-contrast github-dark-high-contrast github-light-high-contrast",".card-media { aspect-ratio: 4 \u002F 3; }                  \u002F* fixed image box *\u002F\n.card-title {\n  min-height: calc(2 * 1.3em);                         \u002F* always two lines tall *\u002F\n  line-height: 1.3;\n  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;\n  overflow: hidden;\n}\n.card-price { min-height: 2.75rem; }                   \u002F* room for price + \"was\" price *\u002F\n\u002F* trade-off: line-clamp truncates long names. Expose the full name via the\n   title attribute or on the product page; never clamp text that carries\n   information users need to choose (sizes, variants). *\u002F\n","css",[179,650,651,678,686,716,729,761,773,777,801,806,811],{"__ignoreMap":338},[342,652,653,656,659,662,665,667,670,672,675],{"class":247,"line":344},[342,654,655],{"class":372},".card-media",[342,657,658],{"class":355}," { ",[342,660,661],{"class":372},"aspect-ratio",[342,663,664],{"class":355},": ",[342,666,254],{"class":372},[342,668,669],{"class":355}," \u002F ",[342,671,269],{"class":372},[342,673,674],{"class":355},"; }                  ",[342,676,677],{"class":620},"\u002F* fixed image box *\u002F\n",[342,679,680,683],{"class":247,"line":366},[342,681,682],{"class":372},".card-title",[342,684,685],{"class":355}," {\n",[342,687,688,691,693,696,699,702,705,708,710,713],{"class":247,"line":385},[342,689,690],{"class":372},"  min-height",[342,692,664],{"class":355},[342,694,695],{"class":372},"calc",[342,697,698],{"class":355},"(",[342,700,701],{"class":372},"2",[342,703,704],{"class":347}," *",[342,706,707],{"class":372}," 1.3",[342,709,30],{"class":347},[342,711,712],{"class":355},");                         ",[342,714,715],{"class":620},"\u002F* always two lines tall *\u002F\n",[342,717,718,721,723,726],{"class":247,"line":394},[342,719,720],{"class":372},"  line-height",[342,722,664],{"class":355},[342,724,725],{"class":372},"1.3",[342,727,728],{"class":355},";\n",[342,730,731,734,736,739,742,745,747,749,751,754,756,759],{"class":247,"line":429},[342,732,733],{"class":372},"  display",[342,735,664],{"class":355},[342,737,738],{"class":372},"-webkit-box",[342,740,741],{"class":355},"; ",[342,743,744],{"class":372},"-webkit-line-clamp",[342,746,664],{"class":355},[342,748,701],{"class":372},[342,750,741],{"class":355},[342,752,753],{"class":372},"-webkit-box-orient",[342,755,664],{"class":355},[342,757,758],{"class":372},"vertical",[342,760,728],{"class":355},[342,762,763,766,768,771],{"class":247,"line":499},[342,764,765],{"class":372},"  overflow",[342,767,664],{"class":355},[342,769,770],{"class":372},"hidden",[342,772,728],{"class":355},[342,774,775],{"class":247,"line":547},[342,776,614],{"class":355},[342,778,779,782,784,787,789,792,795,798],{"class":247,"line":595},[342,780,781],{"class":372},".card-price",[342,783,658],{"class":355},[342,785,786],{"class":372},"min-height",[342,788,664],{"class":355},[342,790,791],{"class":372},"2.75",[342,793,794],{"class":347},"rem",[342,796,797],{"class":355},"; }                   ",[342,799,800],{"class":620},"\u002F* room for price + \"was\" price *\u002F\n",[342,802,803],{"class":247,"line":605},[342,804,805],{"class":620},"\u002F* trade-off: line-clamp truncates long names. Expose the full name via the\n",[342,807,808],{"class":247,"line":611},[342,809,810],{"class":620},"   title attribute or on the product page; never clamp text that carries\n",[342,812,813],{"class":247,"line":617},[342,814,815],{"class":620},"   information users need to choose (sizes, variants). *\u002F\n",[15,817,818],{},"Expected outcome: real cards render at exactly the skeleton height; per-card shifts disappear.",[325,820,822],{"id":821},"_3-render-the-right-number-of-placeholders","3. Render the right number of placeholders",[15,824,825],{},"Request the count you will show (page size) and render that many skeletons. If the final count is unknown, contain the list so a different count cannot move content outside it.",[333,827,829],{"className":646,"code":828,"language":648,"meta":338,"style":338},".results { min-height: calc(3 * var(--card-row-height)); contain: layout; }\n\u002F* trade-off: a min-height for three rows leaves empty space when there are\n   fewer results. Show an explicit \"No more results\" message to fill it\n   rather than letting the page collapse. *\u002F\n",[179,830,831,867,872,877],{"__ignoreMap":338},[342,832,833,836,838,840,842,844,846,848,850,853,855,858,861,864],{"class":247,"line":344},[342,834,835],{"class":372},".results",[342,837,658],{"class":355},[342,839,786],{"class":372},[342,841,664],{"class":355},[342,843,695],{"class":372},[342,845,698],{"class":355},[342,847,269],{"class":372},[342,849,704],{"class":347},[342,851,852],{"class":372}," var",[342,854,698],{"class":355},[342,856,857],{"class":359},"--card-row-height",[342,859,860],{"class":355},")); ",[342,862,863],{"class":372},"contain",[342,865,866],{"class":355},": layout; }\n",[342,868,869],{"class":247,"line":366},[342,870,871],{"class":620},"\u002F* trade-off: a min-height for three rows leaves empty space when there are\n",[342,873,874],{"class":247,"line":385},[342,875,876],{"class":620},"   fewer results. Show an explicit \"No more results\" message to fill it\n",[342,878,879],{"class":247,"line":394},[342,880,881],{"class":620},"   rather than letting the page collapse. *\u002F\n",[15,883,884],{},"Expected outcome: the footer and anything below the list stay put whether the API returns 2 or 12 items within the reserved range.",[325,886,888],{"id":887},"_4-swap-without-changing-scroll-anchoring","4. Swap without changing scroll anchoring",[15,890,891,892,895],{},"When content replaces skeletons above the viewport (the user has scrolled down), browsers' scroll anchoring usually compensates. Make sure you have not disabled it with ",[179,893,894],{},"overflow-anchor: none",", and avoid removing and re-adding the whole list — update in place so the anchor node survives.",[15,897,898],{},"Expected outcome: content above the viewport can change height without moving what the user is looking at.",[15,900,901],{},[40,902,47,905,47,908,47,911,47,913,47,916,47,918,47,922,47,928,47,933,47,936,47,939,47,943,47,946,47,950,47,954,47,957,47,961,47,965,47,971,47,975,47,979,47,984,47,986,47,989,47,992,47,995,47,998,47,1001,47],{"viewBox":903,"width":43,"role":44,"ariaLabel":904,"style":46},"0 0 760 318","Four checks to apply to every skeleton screen so its swap to real content is shift-free.",[49,906],{"className":907,"x":53,"y":53,"width":43,"height":43,"fill":54},[52],[56,909,910],{},"Skeleton checklist that keeps CLS at zero",[60,912,904],{},[49,914],{"x":64,"y":64,"width":65,"height":915,"rx":67,"fill":68,"stroke":69,"style":70},"316",[72,917,910],{"x":74,"y":75,"fill":69,"style":76},[49,919],{"x":250,"y":79,"width":920,"height":921,"rx":82,"fill":123,"stroke":123,"style":124},"660.0","51.0",[72,923,927],{"x":924,"y":925,"fill":69,"style":926,"textAnchor":100},"86.0","77.0","font-size:13px;font-weight:700","Same layout CSS as the real component",[72,929,932],{"x":924,"y":930,"fill":69,"style":931,"textAnchor":100},"94.0","font-size:12px","Shimmer replaces content",[49,934],{"x":250,"y":935,"width":920,"height":921,"rx":82,"fill":123,"stroke":123,"style":124},"119.0",[72,937,938],{"x":924,"y":294,"fill":69,"style":926,"textAnchor":100},"Fixed media ratios",[72,940,942],{"x":924,"y":941,"fill":69,"style":931,"textAnchor":100},"157.0","aspect-ratio on every image and video container",[49,944],{"x":250,"y":945,"width":920,"height":921,"rx":82,"fill":123,"stroke":123,"style":124},"182.0",[72,947,949],{"x":924,"y":948,"fill":69,"style":926,"textAnchor":100},"203.0","Clamped text with reserved lines",[72,951,953],{"x":924,"y":952,"fill":69,"style":931,"textAnchor":100},"220.0","min-height equals the clamped line count",[49,955],{"x":250,"y":956,"width":920,"height":921,"rx":82,"fill":123,"stroke":123,"style":124},"245.0",[72,958,960],{"x":924,"y":959,"fill":69,"style":926,"textAnchor":100},"266.0","Placeholder count equals page size",[72,962,964],{"x":924,"y":963,"fill":69,"style":931,"textAnchor":100},"283.0","List contained so counts cannot move the footer",[247,966],{"x1":967,"y1":968,"x2":967,"y2":969,"stroke":69,"strokeWidth":252,"style":970},"43.0","95.5","130.5","stroke-opacity:0.3",[247,972],{"x1":967,"y1":973,"x2":967,"y2":974,"stroke":69,"strokeWidth":252,"style":970},"158.5","193.5",[247,976],{"x1":967,"y1":977,"x2":967,"y2":978,"stroke":69,"strokeWidth":252,"style":970},"221.5","256.5",[980,981],"circle",{"cx":967,"cy":982,"r":983,"fill":123},"81.5","13",[72,985,64],{"x":967,"y":924,"fill":54,"style":926,"textAnchor":318},[980,987],{"cx":967,"cy":988,"r":983,"fill":123},"144.5",[72,990,701],{"x":967,"y":991,"fill":54,"style":926,"textAnchor":318},"149.0",[980,993],{"cx":967,"cy":994,"r":983,"fill":123},"207.5",[72,996,269],{"x":967,"y":997,"fill":54,"style":926,"textAnchor":318},"212.0",[980,999],{"cx":967,"cy":1000,"r":983,"fill":123},"270.5",[72,1002,254],{"x":967,"y":1003,"fill":54,"style":926,"textAnchor":318},"275.0",[151,1005,1007],{"id":1006},"verification","Verification",[15,1009,1010],{},"Add a visual regression test that renders each skeleton and the corresponding loaded component with fixture data at three breakpoints, and asserts their bounding boxes are equal. That catches drift as designs change.",[333,1012,1016],{"className":1013,"code":1014,"language":1015,"meta":338,"style":338},"language-javascript shiki shiki-themes github-light-high-contrast github-dark-high-contrast github-light-high-contrast","\u002F\u002F Playwright: compare skeleton and loaded card geometry at a mobile width.\nawait page.setViewportSize({ width: 390, height: 844 });\nawait page.goto('\u002Ftest\u002Fproduct-card?state=loading');\nconst before = await page.locator('.card').first().boundingBox();\nawait page.goto('\u002Ftest\u002Fproduct-card?state=loaded&fixture=long-title');\nconst after = await page.locator('.card').first().boundingBox();\nexpect(Math.abs(after.height - before.height)).toBeLessThan(1);\n\u002F\u002F trade-off: fixture-based tests only cover the fixtures you write. Include\n\u002F\u002F the longest real product name and the \"with discount\" variant.\n","javascript",[179,1017,1018,1023,1049,1066,1104,1119,1148,1177,1182],{"__ignoreMap":338},[342,1019,1020],{"class":247,"line":344},[342,1021,1022],{"class":620},"\u002F\u002F Playwright: compare skeleton and loaded card geometry at a mobile width.\n",[342,1024,1025,1028,1031,1034,1037,1040,1043,1046],{"class":247,"line":366},[342,1026,1027],{"class":347},"await",[342,1029,1030],{"class":355}," page.",[342,1032,1033],{"class":351},"setViewportSize",[342,1035,1036],{"class":355},"({ width: ",[342,1038,1039],{"class":372},"390",[342,1041,1042],{"class":355},", height: ",[342,1044,1045],{"class":372},"844",[342,1047,1048],{"class":355}," });\n",[342,1050,1051,1053,1055,1058,1060,1063],{"class":247,"line":385},[342,1052,1027],{"class":347},[342,1054,1030],{"class":355},[342,1056,1057],{"class":351},"goto",[342,1059,698],{"class":355},[342,1061,1062],{"class":410},"'\u002Ftest\u002Fproduct-card?state=loading'",[342,1064,1065],{"class":355},");\n",[342,1067,1068,1071,1074,1076,1079,1081,1084,1086,1089,1092,1095,1098,1101],{"class":247,"line":394},[342,1069,1070],{"class":347},"const",[342,1072,1073],{"class":372}," before",[342,1075,376],{"class":347},[342,1077,1078],{"class":347}," await",[342,1080,1030],{"class":355},[342,1082,1083],{"class":351},"locator",[342,1085,698],{"class":355},[342,1087,1088],{"class":410},"'.card'",[342,1090,1091],{"class":355},").",[342,1093,1094],{"class":351},"first",[342,1096,1097],{"class":355},"().",[342,1099,1100],{"class":351},"boundingBox",[342,1102,1103],{"class":355},"();\n",[342,1105,1106,1108,1110,1112,1114,1117],{"class":247,"line":429},[342,1107,1027],{"class":347},[342,1109,1030],{"class":355},[342,1111,1057],{"class":351},[342,1113,698],{"class":355},[342,1115,1116],{"class":410},"'\u002Ftest\u002Fproduct-card?state=loaded&fixture=long-title'",[342,1118,1065],{"class":355},[342,1120,1121,1123,1126,1128,1130,1132,1134,1136,1138,1140,1142,1144,1146],{"class":247,"line":499},[342,1122,1070],{"class":347},[342,1124,1125],{"class":372}," after",[342,1127,376],{"class":347},[342,1129,1078],{"class":347},[342,1131,1030],{"class":355},[342,1133,1083],{"class":351},[342,1135,698],{"class":355},[342,1137,1088],{"class":410},[342,1139,1091],{"class":355},[342,1141,1094],{"class":351},[342,1143,1097],{"class":355},[342,1145,1100],{"class":351},[342,1147,1103],{"class":355},[342,1149,1150,1153,1156,1159,1162,1165,1168,1171,1173,1175],{"class":247,"line":547},[342,1151,1152],{"class":351},"expect",[342,1154,1155],{"class":355},"(Math.",[342,1157,1158],{"class":351},"abs",[342,1160,1161],{"class":355},"(after.height ",[342,1163,1164],{"class":347},"-",[342,1166,1167],{"class":355}," before.height)).",[342,1169,1170],{"class":351},"toBeLessThan",[342,1172,698],{"class":355},[342,1174,64],{"class":372},[342,1176,1065],{"class":355},[342,1178,1179],{"class":247,"line":595},[342,1180,1181],{"class":620},"\u002F\u002F trade-off: fixture-based tests only cover the fixtures you write. Include\n",[342,1183,1184],{"class":247,"line":605},[342,1185,1186],{"class":620},"\u002F\u002F the longest real product name and the \"with discount\" variant.\n",[15,1188,1189,1190,1193,1194,1197],{},"In the field, attribute CLS with the ",[179,1191,1192],{},"web-vitals"," attribution build; ",[179,1195,1196],{},"largestShiftTarget"," should stop pointing at list containers and cards once the fix is live.",[151,1199,1201],{"id":1200},"when-skeletons-are-the-wrong-tool","When Skeletons Are the Wrong Tool",[15,1203,1204,1205,1208,1209,1211],{},"Skeletons are best for content whose shape is known and stable: product cards, profile headers, table rows. They are a poor fit for content of genuinely unknown shape — a rich-text article body, user-generated posts with optional media, search results mixing several card types. For those, two alternatives usually work better. Either render nothing in that region until data arrives while containing it (",[179,1206,1207],{},"contain: layout"," with a fixed ",[179,1210,786],{},") so nothing outside can move, or fetch the data on the server and remove the loading state from the initial render entirely. Server rendering also helps LCP, since the content is in the first paint rather than a client-side swap. Reserve skeletons for repeat interactions in client-rendered views, where they make waiting feel shorter without costing stability.",[151,1213,1215],{"id":1214},"faq","FAQ",[1217,1218,1221,1225],"details",{"className":1219},[1220],"faq-item",[1222,1223,1224],"summary",{},"Do shimmer animations affect performance?",[15,1226,1227,1228,1231,1232,1235,1236,1239],{},"A shimmer implemented with a ",[179,1229,1230],{},"transform","-animated gradient overlay is composited and cheap. One implemented by animating ",[179,1233,1234],{},"background-position"," across many elements triggers paint every frame and can cost noticeable main-thread and GPU time on low-end devices — enough to delay the very data rendering it is waiting for. Prefer a single transformed pseudo-element, and stop the animation under ",[179,1237,1238],{},"prefers-reduced-motion",".",[1217,1241,1243,1246],{"className":1242},[1220],[1222,1244,1245],{},"Should skeletons appear immediately or after a delay?",[15,1247,1248],{},"Show them only if loading takes longer than roughly 200–300ms. For faster responses, a skeleton that flashes for one frame is visual noise. Delaying them does not affect CLS as long as the reserved space exists from the first render — reserve the space immediately, show the shimmer after the delay.",[1217,1250,1252,1255],{"className":1251},[1220],[1222,1253,1254],{},"How do I size skeletons for content whose length varies a lot?",[15,1256,1257],{},"Pick a reserved size from the distribution of real content, not the maximum. Measure the rendered height of a few hundred real items at each breakpoint and reserve the 75th–90th percentile height, then clamp anything longer. That keeps nearly every swap shift-free without leaving large gaps for short items. For truly unbounded content such as user reviews, reserve space only for the first screenful and let the rest expand below the fold, where it cannot move anything the user is looking at.",[151,1259,1261],{"id":1260},"related","Related",[156,1263,1264,1271,1278],{},[159,1265,1266,1270],{},[19,1267,1269],{"href":1268},"\u002Fcore-web-vitals-measurement\u002Freducing-cumulative-layout-shift-cls\u002Freserving-space-for-embeds-with-aspect-ratio\u002F","Reserving space for embeds with aspect-ratio"," — the same reservation principle for iframes and media.",[159,1272,1273,1277],{},[19,1274,1276],{"href":1275},"\u002Fcore-web-vitals-measurement\u002Freducing-cumulative-layout-shift-cls\u002Ffixing-cls-on-infinite-scroll-and-load-more\u002F","Fixing CLS on infinite scroll and load more"," — skeletons at the end of growing lists.",[159,1279,1280,1284],{},[19,1281,1283],{"href":1282},"\u002Fcore-web-vitals-measurement\u002Fsoft-navigations-and-spa-metrics\u002Fwhy-spa-cls-accumulates-across-routes\u002F","Why SPA CLS accumulates across routes"," — skeleton swaps on every client-side navigation.",[1286,1287,1289],"script",{"type":1288},"application\u002Fld+json","\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"HowTo\",\n  \"name\": \"How to Fix Layout Shift from Skeleton Screens That Do Not Match Real Content\",\n  \"description\": \"Why loading placeholders shift layout when content arrives, and how to size, constrain and contain them so the swap is shift-free.\",\n  \"step\": [\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 1,\n      \"name\": \"Build skeletons from the same layout as the real component\",\n      \"text\": \"Render the real component's layout with placeholder content, rather than a separate skeleton component.\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 2,\n      \"name\": \"Constrain variable-length content\",\n      \"text\": \"Reserve space for the maximum lines you will show, and clamp longer text, so card height is fixed regardless of data.\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 3,\n      \"name\": \"Render the right number of placeholders\",\n      \"text\": \"Request the count you will show (page size) and render that many skeletons.\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 4,\n      \"name\": \"Swap without changing scroll anchoring\",\n      \"text\": \"When content replaces skeletons above the viewport (the user has scrolled down), browsers' scroll anchoring usually compensates.\"\n    }\n  ]\n}\n",[1286,1291,1292],{"type":1288},"\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"TechArticle\",\n  \"headline\": \"How to Fix Layout Shift from Skeleton Screens That Do Not Match Real Content\",\n  \"description\": \"Why loading placeholders shift layout when content arrives, and how to size, constrain and contain them so the swap is shift-free.\",\n  \"datePublished\": \"2026-10-06\",\n  \"dateModified\": \"2026-10-06\",\n  \"author\": {\n    \"@type\": \"Organization\",\n    \"name\": \"frontend-performance.com\"\n  },\n  \"publisher\": {\n    \"@type\": \"Organization\",\n    \"name\": \"frontend-performance.com\"\n  },\n  \"mainEntityOfPage\": {\n    \"@type\": \"WebPage\",\n    \"@id\": \"https:\u002F\u002Ffrontend-performance.com\u002Fcore-web-vitals-measurement\u002Freducing-cumulative-layout-shift-cls\u002Ffixing-cls-from-skeleton-screens-that-resize\u002F\"\n  }\n}\n",[1286,1294,1295],{"type":1288},"\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"BreadcrumbList\",\n  \"itemListElement\": [\n    {\n      \"@type\": \"ListItem\",\n      \"position\": 1,\n      \"name\": \"Home\",\n      \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002F\"\n    },\n    {\n      \"@type\": \"ListItem\",\n      \"position\": 2,\n      \"name\": \"Core Web Vitals & Measurement\",\n      \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fcore-web-vitals-measurement\u002F\"\n    },\n    {\n      \"@type\": \"ListItem\",\n      \"position\": 3,\n      \"name\": \"Reducing Cumulative Layout Shift (CLS)\",\n      \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fcore-web-vitals-measurement\u002Freducing-cumulative-layout-shift-cls\u002F\"\n    },\n    {\n      \"@type\": \"ListItem\",\n      \"position\": 4,\n      \"name\": \"Fixing CLS from Skeleton Screens That Resize\",\n      \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fcore-web-vitals-measurement\u002Freducing-cumulative-layout-shift-cls\u002Ffixing-cls-from-skeleton-screens-that-resize\u002F\"\n    }\n  ]\n}\n",[1297,1298,1299],"style",{},"html pre.shiki code .sPARh, html code.shiki .sPARh{--shiki-default:#A0111F;--shiki-dark:#FF9492;--shiki-light:#A0111F}html pre.shiki code .smZ65, html code.shiki .smZ65{--shiki-default:#622CBC;--shiki-dark:#DBB7FF;--shiki-light:#622CBC}html pre.shiki code .saISM, html code.shiki .saISM{--shiki-default:#0E1116;--shiki-dark:#F0F3F6;--shiki-light:#0E1116}html pre.shiki code .sQw3B, html code.shiki .sQw3B{--shiki-default:#702C00;--shiki-dark:#FFB757;--shiki-light:#702C00}html pre.shiki code .sPXB4, html code.shiki .sPXB4{--shiki-default:#023B95;--shiki-dark:#91CBFF;--shiki-light:#023B95}html pre.shiki code .sZBmE, html code.shiki .sZBmE{--shiki-default:#024C1A;--shiki-dark:#72F088;--shiki-light:#024C1A}html pre.shiki code .sZ8jY, html code.shiki .sZ8jY{--shiki-default:#032563;--shiki-dark:#ADDCFF;--shiki-light:#032563}html pre.shiki code .sjfSM, html code.shiki .sjfSM{--shiki-default:#66707B;--shiki-dark:#BDC4CC;--shiki-light:#66707B}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 .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}",{"title":338,"searchDepth":366,"depth":366,"links":1301},[1302,1303,1304,1310,1311,1312,1313],{"id":153,"depth":366,"text":154},{"id":201,"depth":366,"text":202},{"id":322,"depth":366,"text":323,"children":1305},[1306,1307,1308,1309],{"id":327,"depth":385,"text":328},{"id":639,"depth":385,"text":640},{"id":821,"depth":385,"text":822},{"id":887,"depth":385,"text":888},{"id":1006,"depth":366,"text":1007},{"id":1200,"depth":366,"text":1201},{"id":1214,"depth":366,"text":1215},{"id":1260,"depth":366,"text":1261},"Why loading placeholders shift layout when content arrives, and how to size, constrain and contain them so the swap is shift-free.","md",{"slug":1317,"type":401,"breadcrumb":1318,"datePublished":1326,"dateModified":1326},"fixing-cls-from-skeleton-screens-that-resize",[1319,1322,1323,1324],{"name":1320,"url":1321},"Home","\u002F",{"name":27,"url":26},{"name":22,"url":21},{"name":5,"url":1325},"\u002Fcore-web-vitals-measurement\u002Freducing-cumulative-layout-shift-cls\u002Ffixing-cls-from-skeleton-screens-that-resize\u002F","2026-10-06",true,"\u002Fcore-web-vitals-measurement\u002Freducing-cumulative-layout-shift-cls\u002Ffixing-cls-from-skeleton-screens-that-resize",{"title":5,"description":1330},"Skeleton screens meant to prevent layout shift often cause it when real content is a different size. Size skeletons from data to keep CLS under 0.1.","core-web-vitals-measurement\u002Freducing-cumulative-layout-shift-cls\u002Ffixing-cls-from-skeleton-screens-that-resize\u002Findex","rHEA_Se10JYqW4PAJyaLHKPVvsIvCpzydSvzz-vi0Uc",[1334,1338],{"title":1335,"path":1336,"stem":1337,"children":-1},"Fixing CLS from Cookie Consent Banners","\u002Fcore-web-vitals-measurement\u002Freducing-cumulative-layout-shift-cls\u002Ffixing-cls-from-cookie-consent-banners","core-web-vitals-measurement\u002Freducing-cumulative-layout-shift-cls\u002Ffixing-cls-from-cookie-consent-banners\u002Findex",{"title":1339,"path":1340,"stem":1341,"children":-1},"Fixing CLS on Infinite Scroll and Load More","\u002Fcore-web-vitals-measurement\u002Freducing-cumulative-layout-shift-cls\u002Ffixing-cls-on-infinite-scroll-and-load-more","core-web-vitals-measurement\u002Freducing-cumulative-layout-shift-cls\u002Ffixing-cls-on-infinite-scroll-and-load-more\u002Findex",1791308073827]