|
| 1 | +--- |
| 2 | +section: 50-Guides |
| 3 | +name: Local Development Atomic Clashes |
| 4 | +order: 21 |
| 5 | +--- |
| 6 | + |
| 7 | +# Local Development Atomic Clashes |
| 8 | + |
| 9 | +When consuming Compiled packages from npm in local development, you may encounter atomic class clashes that cause unexpected styling behavior. |
| 10 | +This happens when **runtime styles** (from your local code) mix with **extracted styles** (from npm packages), resulting in [atomic specificity conflicts](/how-it-works#extracted-styles). |
| 11 | + |
| 12 | +This page explains why these clashes occur and how to fix them in your configuration. |
| 13 | + |
| 14 | +## Understanding the problem |
| 15 | + |
| 16 | +### Compiled's atomic style ordering |
| 17 | + |
| 18 | +Compiled's atomic CSS system relies on a specific ordering of styles to work correctly. |
| 19 | +Without this ordering, CSS shorthand properties would override their longhand counterparts, and pseudo-selectors and media queries wouldn't apply as expected. |
| 20 | + |
| 21 | +The sorting order looks like this: |
| 22 | + |
| 23 | +1. `all` property |
| 24 | +2. Shorthand properties (e.g., `border`, `margin`, `padding`, `font`) |
| 25 | +3. More specific shorthands (e.g., `border-block`) |
| 26 | +4. Even more specific shorthands (e.g., `border-block-end`) |
| 27 | +5. Longhand properties (e.g., `color`, `border-block-end-color`, `font-weight`) |
| 28 | +6. Pseudo-selectors in LVFHA order: |
| 29 | + - `&:link` |
| 30 | + - `&:visited` |
| 31 | + - `&:focus-within` |
| 32 | + - `&:focus` |
| 33 | + - `&:focus-visible` |
| 34 | + - `&:hover` |
| 35 | + - `&:active` |
| 36 | +7. At-rules like `@media` (sorted by breakpoint dimensions) |
| 37 | +8. Other at-rules (`@keyframes`, `@supports`, etc.) |
| 38 | + |
| 39 | +Within each at-rule group (like `@media` queries), the same property ordering is applied recursively, so `font` always comes before `font-weight`. |
| 40 | + |
| 41 | +This creates approximately 15–25 distinct atomic groups on average (technically infinite due to media query combinations). |
| 42 | + |
| 43 | +### Why ordering matters |
| 44 | + |
| 45 | +This ordering only works correctly when **all** Compiled styles are in a single sorted set. |
| 46 | +CSS cascade rules mean that when two rules have the same specificity, the last one defined wins. |
| 47 | + |
| 48 | +**Stylesheet extraction** combines and deduplicates all Compiled atomic classes from your codebase into `compiled.css` files. These files are then injected as `<style>` tags in your HTML's `<head>`, creating a single unified set of sorted styles. This is typically enabled in production builds for optimal performance. |
| 49 | + |
| 50 | +In development, extraction is usually disabled (for better HMR performance), and Compiled maintains the same ordering by inserting styles into multiple style buckets at runtime. |
| 51 | + |
| 52 | +This unified, sorted set of styles is what enables the atomic CSS system to work correctly. |
| 53 | + |
| 54 | +## The local development issue |
| 55 | + |
| 56 | +Compiled's atomic CSS ordering only works when **all Compiled styles are in a single sorted set**. This is what bundlers like Parcel and Webpack ensure in production. |
| 57 | + |
| 58 | +However, when running a local project in development mode while consuming production-built Compiled packages from npm, you have **two separate sets of Compiled styles**: |
| 59 | + |
| 60 | +- **Npm package styles**: Injected as `<style>` tags from their pre-built `compiled.css` files |
| 61 | +- **Your local code styles**: Injected as `<style>` buckets at runtime (extraction disabled by default in dev) |
| 62 | + |
| 63 | +These two sets are not sorted together as one unified stylesheet. Whichever set comes last in the document will take CSS cascade precedence, causing your local styles to unexpectedly override npm package styles. |
| 64 | + |
| 65 | +### What happens |
| 66 | + |
| 67 | +1. **Npm packages** are built with extraction enabled, so their styles are in `compiled.css` files that get injected as `<style>` tags when the package loads |
| 68 | +2. **Your local project** runs in development mode with extraction disabled (default in most bundlers), so Compiled uses [runtime styles](/how-it-works#runtime-styles-unperformant) injected into style buckets. |
| 69 | +3. Both sets of styles are injected as separate `<style>` tags in the document `<head>`, but they're **not sorted together as one unified set** |
| 70 | +4. Because Compiled uses the same deterministic algorithm to generate class names, both sources generate **identical class names** for identical CSS declarations |
| 71 | +5. **Result**: Your local runtime styles override npm package styles unexpectedly, breaking components from the npm packages |
| 72 | + |
| 73 | +### Example scenario |
| 74 | + |
| 75 | +Imagine you have a design system package from npm with this component: |
| 76 | + |
| 77 | +```jsx |
| 78 | +// From npm package: @your-org/design-system |
| 79 | +import { css } from '@compiled/react'; |
| 80 | + |
| 81 | +const navStyles = css({ |
| 82 | + display: 'none', // Hidden by default |
| 83 | + '@media (min-width: 768px)': { |
| 84 | + display: 'flex', // Shown on larger screens |
| 85 | + }, |
| 86 | +}); |
| 87 | + |
| 88 | +export const Nav = () => <nav css={navStyles}>Navigation</nav>; |
| 89 | +``` |
| 90 | + |
| 91 | +In your local project, you create a component that also uses `display: none`: |
| 92 | + |
| 93 | +```jsx |
| 94 | +// Your local component |
| 95 | +import { css } from '@compiled/react'; |
| 96 | + |
| 97 | +const modalStyles = css({ |
| 98 | + display: 'none', |
| 99 | +}); |
| 100 | + |
| 101 | +export const Modal = () => <div css={modalStyles}>Modal</div>; |
| 102 | +``` |
| 103 | + |
| 104 | +Both generate the same atomic class for `display: none` (e.g., `._1p2d3e4f`), since the hash is based on the CSS property and value. |
| 105 | + |
| 106 | +> **Style Buckets**: At runtime, Compiled organizes styles into multiple `<style>` buckets ordered by CSS property precedence (shorthands before longhands), pseudo-selectors, and at-rules. This ensures proper cascade order. |
| 107 | +
|
| 108 | +In the browser, you'll see: |
| 109 | + |
| 110 | +```html |
| 111 | +<head> |
| 112 | + <!-- From npm package (extracted styles injected as style tags) --> |
| 113 | + <style> |
| 114 | + ._1p2d3e4f { display: none; } |
| 115 | + </style> |
| 116 | + <style> |
| 117 | + @media (min-width: 768px) { ._9q2r4s7t { display: flex; } } |
| 118 | + </style> |
| 119 | + |
| 120 | + <!-- From your local development (runtime style buckets) --> |
| 121 | + <style data-bucket="default"> |
| 122 | + ._1p2d3e4f { display: none; } |
| 123 | + </style> |
| 124 | +</head> |
| 125 | + |
| 126 | +<body> |
| 127 | + <nav class="_1p2d3e4f _9q2r4s7t">...</nav> |
| 128 | + <!-- ❌ The local runtime bucket's ._1p2d3e4f comes last in document order, |
| 129 | + so it takes CSS cascade precedence, breaking the media query --> |
| 130 | + |
| 131 | + <div class="_1p2d3e4f">...</div> <!-- ✅ display: none as expected --> |
| 132 | +</body> |
| 133 | +``` |
| 134 | + |
| 135 | +The Nav component from the npm package is broken because you have two separate sets of Compiled styles that aren't sorted together. The local runtime style buckets come after the npm package's styles in document order, so due to CSS cascade rules (source order), the `display: none` from your local code overrides the npm package's rules, preventing the media query's `display: flex` from taking effect. |
| 136 | + |
| 137 | +## Potential Solution |
| 138 | + |
| 139 | +Use `classHashPrefix` in development to create different class names for your local project. |
| 140 | +This prevents your local runtime styles from colliding with extracted styles from npm packages. |
| 141 | + |
| 142 | +### Configuration |
| 143 | + |
| 144 | +In your `compiled.config.js`: |
| 145 | + |
| 146 | +```javascript |
| 147 | +const isLocalDev = process.env.NODE_ENV !== 'production'; |
| 148 | + |
| 149 | +export default { |
| 150 | + // Extraction should be disabled in development (default in most bundlers) |
| 151 | + // and enabled in production to generate compiled.css |
| 152 | + extract: !isLocalDev, |
| 153 | + |
| 154 | + // Add classHashPrefix in development to salt the atomic group hash |
| 155 | + // This ensures your local classes have different names than npm package classes |
| 156 | + classHashPrefix: isLocalDev ? 'my-app' : undefined, |
| 157 | + |
| 158 | + // Other recommended options |
| 159 | + importReact: true, |
| 160 | + sortShorthand: true, |
| 161 | + addComponentName: true, |
| 162 | + parserBabelPlugins: ['typescript', 'jsx'], |
| 163 | +}; |
| 164 | +``` |
| 165 | + |
| 166 | +### How it fixes the problem |
| 167 | + |
| 168 | +With `classHashPrefix: 'my-app'` in development, your local code generates different class names: |
| 169 | + |
| 170 | +- **Npm package** `display: none`: `._1p2d3e4f` (original atomic group hash) |
| 171 | +- **Your local** `display: none`: `._7x9w3e4f` (modified salt atomic group hash) |
| 172 | + |
| 173 | +Even though both use the same CSS declaration, the classHashPrefix reduces the likelihood of a hash collision. |
| 174 | + |
| 175 | +Since the two sets of styles aren't sorted together, having different class names means they can coexist without interfering: |
| 176 | + |
| 177 | +```html |
| 178 | +<head> |
| 179 | + <!-- From npm package (extracted styles) --> |
| 180 | + <style> |
| 181 | + ._1p2d3e4f { display: none; } |
| 182 | + </style> |
| 183 | + <style> |
| 184 | + @media (min-width: 768px) { ._9q2r4s7t { display: flex; } } |
| 185 | + </style> |
| 186 | + |
| 187 | + <!-- From your local development (runtime style buckets) --> |
| 188 | + <style data-bucket="default"> |
| 189 | + ._7x9w3e4f { display: none; } |
| 190 | + </style> |
| 191 | +</head> |
| 192 | + |
| 193 | +<body> |
| 194 | + <nav class="_1p2d3e4f _9q2r4s7t">...</nav> |
| 195 | + <!-- ✅ display: flex (._9q2r4s7t from media query) --> |
| 196 | + |
| 197 | + <div class="_7x9w3e4f">...</div> <!-- ✅ display: none (local class) --> |
| 198 | +</body> |
| 199 | +``` |
| 200 | + |
| 201 | +## Related issues |
| 202 | + |
| 203 | +- [Parcel doesn't pick up new CSS changes on local development when stylesheet extraction is enabled (Issue #1306)](https://github.com/atlassian-labs/compiled/issues/1306) |
| 204 | + |
| 205 | +## See also |
| 206 | + |
| 207 | +- [How it Works](/how-it-works) - Understanding runtime vs extracted styles |
| 208 | +- [Atomic CSS](/atomic-css) - Understanding Compiled's atomic CSS system |
| 209 | +- [CSS Extraction](/css-extraction-webpack) - How stylesheet extraction works in production |
| 210 | +- [Babel Plugin Configuration](/pkg-babel-plugin) - All available configuration options |
0 commit comments