[{"data":1,"prerenderedAt":853},["ShallowReactive",2],{"blog-\u002Fblog\u002Fevery-style-is-an-atom":3},{"id":4,"title":5,"body":6,"canonical":842,"date":843,"description":844,"draft":845,"duration":842,"extension":846,"lang":842,"meta":847,"navigation":848,"path":849,"place":842,"placeLink":842,"readingTime":842,"seo":850,"stem":851,"__hash__":852},"blog\u002Fblog\u002Fevery-style-is-an-atom.md","Every style is an atom",{"type":7,"value":8,"toc":832},"minimark",[9,14,26,38,41,44,49,52,156,167,174,198,201,205,208,211,305,308,371,385,389,392,398,401,408,417,420,423,450,459,463,466,497,504,507,510,642,650,653,657,660,663,691,694,697,701,704,733,740,746,773,787,794,801,805,812,815,828],[10,11,13],"h3",{"id":12},"how-panda-turns-your-styles-into-atoms-and-keeps-each-one-exactly-once","How Panda turns your styles into atoms, and keeps each one exactly once.",[15,16,17],"p",{},[18,19],"img",{"alt":20,"fetchPriority":21,"loading":22,"src":23,"width":24,"height":25},"Every style is an atom, on a cream background with yellow and teal glows, surrounded by the Panda logo and stickers reading (prop, value, cond), d_flex and hover:c_red.300.","high","eager","\u002Fimages\u002Fblog\u002Fevery-style-is-an-atom\u002Fcover.svg",1200,630,[15,27,28],{},[29,30,31,32,37],"em",{},"Panda internals, part 2. Part 1: ",[33,34,36],"a",{"href":35},"\u002Fblog\u002Fbuild-once-at-build-time","Build once, at build time",".",[15,39,40],{},"In part 1, I wrote about a generator I built that spat out 62,656 lines of CSS. It made a rule for every token, every utility, every condition. Every single one of those rules was an atom: one property, one value, one condition.",[15,42,43],{},"Panda works with the exact same unit. The difference is which atoms it keeps, and how many times it keeps them.",[45,46,48],"h2",{"id":47},"same-declaration-many-names","Same declaration, many names",[15,50,51],{},"Say you have three components:",[53,54,59],"pre",{"className":55,"code":56,"language":57,"meta":58,"style":58},"language-ts shiki shiki-themes github-light github-dark","const card = css({ display: 'flex', padding: '4', color: 'gray.700' })\nconst header = css({ display: 'flex', padding: '4', _hover: { color: 'red.300' } })\nconst footer = css({ display: 'flex', padding: '4' })\n","ts","",[60,61,62,105,134],"code",{"__ignoreMap":58},[63,64,67,71,75,78,82,86,90,93,96,99,102],"span",{"class":65,"line":66},"line",1,[63,68,70],{"class":69},"szBVR","const",[63,72,74],{"class":73},"sj4cs"," card",[63,76,77],{"class":69}," =",[63,79,81],{"class":80},"sScJk"," css",[63,83,85],{"class":84},"sVt8B","({ display: ",[63,87,89],{"class":88},"sZZnC","'flex'",[63,91,92],{"class":84},", padding: ",[63,94,95],{"class":88},"'4'",[63,97,98],{"class":84},", color: ",[63,100,101],{"class":88},"'gray.700'",[63,103,104],{"class":84}," })\n",[63,106,108,110,113,115,117,119,121,123,125,128,131],{"class":65,"line":107},2,[63,109,70],{"class":69},[63,111,112],{"class":73}," header",[63,114,77],{"class":69},[63,116,81],{"class":80},[63,118,85],{"class":84},[63,120,89],{"class":88},[63,122,92],{"class":84},[63,124,95],{"class":88},[63,126,127],{"class":84},", _hover: { color: ",[63,129,130],{"class":88},"'red.300'",[63,132,133],{"class":84}," } })\n",[63,135,137,139,142,144,146,148,150,152,154],{"class":65,"line":136},3,[63,138,70],{"class":69},[63,140,141],{"class":73}," footer",[63,143,77],{"class":69},[63,145,81],{"class":80},[63,147,85],{"class":84},[63,149,89],{"class":88},[63,151,92],{"class":84},[63,153,95],{"class":88},[63,155,104],{"class":84},[15,157,158,159,162,163,166],{},"With component-scoped CSS, each of these gets its own class. ",[60,160,161],{},"display: flex"," and ",[60,164,165],{},"padding: 1rem"," get written three times, under three different names. That's how component-scoped CSS is meant to work: one class per component, so the stylesheet grows with your components.",[15,168,169,170,173],{},"Panda counts something else. How many ",[29,171,172],{},"different"," declarations are in there?",[175,176,177,182,187,192],"ul",{},[178,179,180],"li",{},[60,181,161],{},[178,183,184],{},[60,185,186],{},"padding: 4",[178,188,189],{},[60,190,191],{},"color: gray.700",[178,193,194,197],{},[60,195,196],{},"color: red.300",", on hover",[15,199,200],{},"Four. That's all the CSS this code needs.",[45,202,204],{"id":203},"what-an-atom-is","What an atom is",[15,206,207],{},"An atom is the smallest piece of a style that still means something. A property, a value, and the conditions it applies under.",[15,209,210],{},"In v2 it's a real type in the Rust engine. Simplified, it looks like this:",[53,212,216],{"className":213,"code":214,"language":215,"meta":58,"style":58},"language-rust shiki shiki-themes github-light github-dark","struct Atom {\n    prop: Box\u003Cstr>,\n    value: AtomValue,\n    conditions: SmallVec\u003C[Box\u003Cstr>; 2]>,\n    important: bool,\n}\n","rust",[60,217,218,229,249,262,286,299],{"__ignoreMap":58},[63,219,220,223,226],{"class":65,"line":66},[63,221,222],{"class":69},"struct",[63,224,225],{"class":80}," Atom",[63,227,228],{"class":84}," {\n",[63,230,231,234,237,240,243,246],{"class":65,"line":107},[63,232,233],{"class":84},"    prop",[63,235,236],{"class":69},":",[63,238,239],{"class":80}," Box",[63,241,242],{"class":84},"\u003C",[63,244,245],{"class":80},"str",[63,247,248],{"class":84},">,\n",[63,250,251,254,256,259],{"class":65,"line":136},[63,252,253],{"class":84},"    value",[63,255,236],{"class":69},[63,257,258],{"class":80}," AtomValue",[63,260,261],{"class":84},",\n",[63,263,265,268,270,273,276,279,281,283],{"class":65,"line":264},4,[63,266,267],{"class":84},"    conditions",[63,269,236],{"class":69},[63,271,272],{"class":80}," SmallVec",[63,274,275],{"class":84},"\u003C[",[63,277,278],{"class":80},"Box",[63,280,242],{"class":84},[63,282,245],{"class":80},[63,284,285],{"class":84},">; 2]>,\n",[63,287,289,292,294,297],{"class":65,"line":288},5,[63,290,291],{"class":84},"    important",[63,293,236],{"class":69},[63,295,296],{"class":80}," bool",[63,298,261],{"class":84},[63,300,302],{"class":65,"line":301},6,[63,303,304],{"class":84},"}\n",[15,306,307],{},"If you don't read Rust, think of it as:",[53,309,311],{"className":55,"code":310,"language":57,"meta":58,"style":58},"type Atom = {\n  prop: string\n  value: string\n  conditions: string[]\n  important: boolean\n}\n",[60,312,313,324,335,344,357,367],{"__ignoreMap":58},[63,314,315,318,320,322],{"class":65,"line":66},[63,316,317],{"class":69},"type",[63,319,225],{"class":80},[63,321,77],{"class":69},[63,323,228],{"class":84},[63,325,326,330,332],{"class":65,"line":107},[63,327,329],{"class":328},"s4XuR","  prop",[63,331,236],{"class":69},[63,333,334],{"class":73}," string\n",[63,336,337,340,342],{"class":65,"line":136},[63,338,339],{"class":328},"  value",[63,341,236],{"class":69},[63,343,334],{"class":73},[63,345,346,349,351,354],{"class":65,"line":264},[63,347,348],{"class":328},"  conditions",[63,350,236],{"class":69},[63,352,353],{"class":73}," string",[63,355,356],{"class":84},"[]\n",[63,358,359,362,364],{"class":65,"line":288},[63,360,361],{"class":328},"  important",[63,363,236],{"class":69},[63,365,366],{"class":73}," boolean\n",[63,368,369],{"class":65,"line":301},[63,370,304],{"class":84},[15,372,373,376,377,380,381,384],{},[60,374,375],{},"conditions"," is an ordered list, outside to inside. ",[60,378,379],{},"{ md: { _hover: { color: 'red.300' } } }"," becomes ",[60,382,383],{},"['md', '_hover']",". A plain style with no condition gets an empty list.",[45,386,388],{"id":387},"into-the-set","Into the set",[15,390,391],{},"When Panda reads a file, the encoder walks every style object it extracted and turns each leaf into an atom. Then it puts the atom into a hash set.",[15,393,394,395,397],{},"A set only keeps one copy of anything. So the three ",[60,396,161],{}," declarations from earlier become one atom. The second and third inserts just find it already there.",[15,399,400],{},"There are two of these sets. Each file has its own. Then the project keeps one combined set, and counts how many files use each atom. That count matters in watch mode. Delete a file, and the atoms only that file used go away. The ones other files still need stay.",[15,402,403,404,407],{},"This isn't new to v2. v1 did the same thing in JavaScript, with a ",[60,405,406],{},"Set\u003Cstring>",". The key was a string built by gluing the parts together:",[53,409,411],{"className":55,"code":410,"language":57,"meta":58,"style":58},"'display]___[value:flex'\n",[60,412,413],{"__ignoreMap":58},[63,414,415],{"class":65,"line":66},[63,416,410],{"class":88},[15,418,419],{},"v2 keeps the same idea, but the key is a typed struct with its hash computed once and cached.",[15,421,422],{},"Ternaries work a little differently. Panda reads your code at build time, so it can't know which side of a ternary runs:",[53,424,426],{"className":55,"code":425,"language":57,"meta":58,"style":58},"css({ color: isActive ? 'red.300' : 'gray.700' })\n",[60,427,428],{"__ignoreMap":58},[63,429,430,433,436,439,442,445,448],{"class":65,"line":66},[63,431,432],{"class":80},"css",[63,434,435],{"class":84},"({ color: isActive ",[63,437,438],{"class":69},"?",[63,440,441],{"class":88}," 'red.300'",[63,443,444],{"class":69}," :",[63,446,447],{"class":88}," 'gray.700'",[63,449,104],{"class":84},[15,451,452,453,162,456,37],{},"So it keeps both. Two atoms, ",[60,454,455],{},"c_red.300",[60,457,458],{},"c_gray.700",[45,460,462],{"id":461},"from-atom-to-class","From atom to class",[15,464,465],{},"Each atom becomes one class name. By default they're readable:",[175,467,468,476,483,489],{},[178,469,470,472,473],{},[60,471,161],{}," → ",[60,474,475],{},"d_flex",[178,477,478,472,480],{},[60,479,186],{},[60,481,482],{},"p_4",[178,484,485,472,487],{},[60,486,191],{},[60,488,458],{},[178,490,491,493,494],{},[60,492,196],{}," on hover → ",[60,495,496],{},"hover:c_red.300",[15,498,499,500,503],{},"Conditions go in front, the property gets shortened, the value stays as you wrote it. If you'd rather ship short hashed names, there's ",[60,501,502],{},"hash: true"," in your config. It's off by default.",[15,505,506],{},"Either way, the same atom gives the same class name. Every build, every machine.",[15,508,509],{},"So the three components from earlier come out as this:",[53,511,514],{"className":512,"code":513,"language":432,"meta":58,"style":58},"language-css shiki shiki-themes github-light github-dark","@layer utilities {\n  .d_flex { display: flex }\n  .p_4 { padding: var(--spacing-4) }\n  .c_gray\\.700 { color: var(--colors-gray-700) }\n  .hover\\:c_red\\.300:is(:hover, [data-hover]) { color: var(--colors-red-300) }\n}\n",[60,515,516,524,544,568,595,638],{"__ignoreMap":58},[63,517,518,521],{"class":65,"line":66},[63,519,520],{"class":69},"@layer",[63,522,523],{"class":84}," utilities {\n",[63,525,526,529,532,535,538,541],{"class":65,"line":107},[63,527,528],{"class":80},"  .d_flex",[63,530,531],{"class":84}," { ",[63,533,534],{"class":73},"display",[63,536,537],{"class":84},": ",[63,539,540],{"class":73},"flex",[63,542,543],{"class":84}," }\n",[63,545,546,549,551,554,556,559,562,565],{"class":65,"line":136},[63,547,548],{"class":80},"  .p_4",[63,550,531],{"class":84},[63,552,553],{"class":73},"padding",[63,555,537],{"class":84},[63,557,558],{"class":73},"var",[63,560,561],{"class":84},"(",[63,563,564],{"class":328},"--spacing-4",[63,566,567],{"class":84},") }\n",[63,569,570,573,576,579,581,584,586,588,590,593],{"class":65,"line":264},[63,571,572],{"class":80},"  .c_gray",[63,574,575],{"class":73},"\\.",[63,577,578],{"class":80},"700",[63,580,531],{"class":84},[63,582,583],{"class":73},"color",[63,585,537],{"class":84},[63,587,558],{"class":73},[63,589,561],{"class":84},[63,591,592],{"class":328},"--colors-gray-700",[63,594,567],{"class":84},[63,596,597,600,603,606,608,611,613,616,619,622,625,627,629,631,633,636],{"class":65,"line":288},[63,598,599],{"class":80},"  .hover",[63,601,602],{"class":73},"\\:",[63,604,605],{"class":80},"c_red",[63,607,575],{"class":73},[63,609,610],{"class":80},"300:is",[63,612,561],{"class":84},[63,614,615],{"class":80},":hover",[63,617,618],{"class":84},", [",[63,620,621],{"class":80},"data-hover",[63,623,624],{"class":84},"]) { ",[63,626,583],{"class":73},[63,628,537],{"class":84},[63,630,558],{"class":73},[63,632,561],{"class":84},[63,634,635],{"class":328},"--colors-red-300",[63,637,567],{"class":84},[63,639,640],{"class":65,"line":301},[63,641,304],{"class":84},[15,643,644,645,162,647,649],{},"Four rules for three components. Add a fiftieth component with ",[60,646,161],{},[60,648,186],{},", and you get zero new rules.",[15,651,652],{},"So your stylesheet grows with the number of distinct atoms you use, not the number of components you write. And since your tokens are a finite set, there are only so many atoms to make.",[45,654,656],{"id":655},"order-without-import-order","Order, without import order",[15,658,659],{},"In plain CSS, when two rules have the same specificity, whichever one loaded last wins. Move an import, and your button changes color.",[15,661,662],{},"A set has no order. So before writing anything out, Panda sorts every rule by a fixed key:",[664,665,666,669,672,681],"ol",{},[178,667,668],{},"Plain rules first, then rules with a selector, then rules inside at-rules.",[178,670,671],{},"At-rules in a fixed order: supports, media, container, print. Size queries sort by their size.",[178,673,674,675,162,677,680],{},"Selector priority, from a table of pseudo-classes, so ",[60,676,615],{},[60,678,679],{},":focus"," land in a predictable order.",[178,682,683,684,687,688,690],{},"Property priority. Shorthands go before longhands, so ",[60,685,686],{},"padding-top"," comes after ",[60,689,553],{}," and wins.",[15,692,693],{},"Ties fall back to the property name and the value. Nothing in that list depends on which file Panda read first, or what you imported where.",[15,695,696],{},"Class names and CSS order are two separate things. The class name is what your component gets. Where the rule sits in the file is decided by the sorter, and only the sorter.",[45,698,700],{"id":699},"where-atoms-stop","Where atoms stop",[15,702,703],{},"Two places work differently.",[15,705,706,710,711,714,715,162,718,721,722,725,726,162,729,732],{},[707,708,709],"strong",{},"Recipes."," Config recipes, the ones you define under ",[60,712,713],{},"recipes"," in your Panda config, aren't atomic. A button recipe gives you ",[60,716,717],{},".button",[60,719,720],{},".button--size_md",", with the declarations grouped under those class names in ",[60,723,724],{},"@layer recipes",". They go through the same encoder walk, but come out grouped, in the order you wrote them. Atomic recipes, ",[60,727,728],{},"cva",[60,730,731],{},"sva",", are different. They do become atoms.",[15,734,735,736,739],{},"So \"everything in Panda is atomic\" isn't quite true. It also means recipe rules don't get the set's dedup for free. When the same recipe shows up from two places, say your app and a ",[60,737,738],{},"designSystem"," package, Panda has to dedupe those groups on its own.",[15,741,742,745],{},[707,743,744],{},"Last write wins."," In JavaScript, the later key wins:",[53,747,749],{"className":55,"code":748,"language":57,"meta":58,"style":58},"const style = { ...base, ...override }\n",[60,750,751],{"__ignoreMap":58},[63,752,753,755,758,760,762,765,768,770],{"class":65,"line":66},[63,754,70],{"class":69},[63,756,757],{"class":73}," style",[63,759,77],{"class":69},[63,761,531],{"class":84},[63,763,764],{"class":69},"...",[63,766,767],{"class":84},"base, ",[63,769,764],{"class":69},[63,771,772],{"class":84},"override }\n",[15,774,775,776,778,779,782,783,786],{},"Atomic classes don't work like that. If two classes both set ",[60,777,583],{},", the browser picks whichever rule comes later ",[29,780,781],{},"in the stylesheet",". Not by where it appears in your object or your ",[60,784,785],{},"className"," string.",[15,788,789,790,793],{},"Panda handles most of this for you. ",[60,791,792],{},"css(base, override)"," merges the objects first, then turns the result into atoms, so the override wins the way you'd expect. And with source transforms on, if Panda can't tell which value wins at build time, it doesn't guess. It leaves the call alone.",[15,795,796,797,800],{},"The thing to avoid is joining two class strings by hand and expecting the second one to win. Merge styles with ",[60,798,799],{},"css()",", not with string concatenation.",[45,802,804],{"id":803},"in-short","In short",[15,806,807,808,811],{},"So when you write ",[60,809,810],{},"css({ ... })",", you're not adding a stylesheet. You're adding atoms to a set.",[15,813,814],{},"Often, they're already in it.",[15,816,817],{},[29,818,819,820,824,825,827],{},"Next in the series: ",[33,821,823],{"href":822},"\u002Fblog\u002Flook-it-up-once","Look it up once",", why calling ",[60,826,799],{}," on every render stays cheap.",[829,830,831],"style",{},"html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}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 .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}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 .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"title":58,"searchDepth":107,"depth":107,"links":833},[834,835,836,837,838,839,840,841],{"id":12,"depth":136,"text":13},{"id":47,"depth":107,"text":48},{"id":203,"depth":107,"text":204},{"id":387,"depth":107,"text":388},{"id":461,"depth":107,"text":462},{"id":655,"depth":107,"text":656},{"id":699,"depth":107,"text":700},{"id":803,"depth":107,"text":804},null,"2026-10-17","How Panda breaks your styles into atoms, keeps each one exactly once, and where atoms stop.",false,"md",{},true,"\u002Fblog\u002Fevery-style-is-an-atom",{"title":5,"description":844},"blog\u002Fevery-style-is-an-atom","jXwtX49x22j0iAHygzUOqc_m36fPE477-0Q0d7GUgt0",1792585585719]