{"_id":"shipstyles","name":"shipstyles","dist-tags":{"latest":"1.0.0"},"versions":{"1.0.0":{"name":"shipstyles","description":"Zero-config runtime styles for UI packages: ship scoped CSS without consumer imports or bundler setup.","version":"1.0.0","homepage":"https://ealush.github.io/shipstyles","license":"MIT","repository":{"type":"git","url":"git+https://github.com/ealush/shipstyles.git"},"bugs":{"url":"https://github.com/ealush/shipstyles/issues"},"keywords":["CSS-in-JS","component-library","UI-packages","scoped-styles","CSS-delivery","zero-config","SSR","Shadow-DOM","CSP","bundler-agnostic","CSS-variables","atomic-CSS","style","StyleSheet","reusable"],"main":"./dist/index.js","devDependencies":{"@swc/core":"^1.3.102","@types/node":"^26.4.1","@typescript-eslint/eslint-plugin":"^8.69.0","@typescript-eslint/parser":"^8.69.0","@semantic-release/changelog":"^7.0.0","@semantic-release/git":"^11.0.1","@semantic-release/github":"^12.0.9","@semantic-release/npm":"^13.1.5","conventional-changelog-conventionalcommits":"^8.0.0","eslint":"^8.56.0","eslint-config-prettier":"^10.1.8","jsdom":"^30.0.1","prettier":"^3.1.1","semantic-release":"^25.0.9","tsup":"^8.0.1","typescript":"^5.3.3","vite":"^8.2.2","vitest":"^5.0.0"},"scripts":{"build":"tsup ./src/index.ts --target es5","lint":"eslint --ext .ts ./src","test":"vitest --watch=false","test:acceptance":"npm run build && node --test acceptance/*.test.mjs","test:all":"npm run test && npm run test:acceptance","prepublish":"npm run test && npm run build"},"tsup":{"sourcemap":true,"dts":true,"clean":true,"legacyOutput":true,"format":["cjs","esm"]},"exports":{"./package.json":"./package.json",".":{"import":{"types":"./dist/index.d.ts","default":"./dist/esm/index.js"},"require":{"types":"./dist/index.d.ts","default":"./dist/index.js"}}},"types":"./dist/index.d.ts","overrides":{"esbuild":"^0.28.1"},"gitHead":"b3f10339bc9befb83d9e4e4c7110f100f69b377a","_id":"shipstyles@1.0.0","_nodeVersion":"24.16.0","_npmVersion":"11.13.0","dist":{"integrity":"sha512-1AcihfXo/mSxqeOdoKqq/5TNv+vSH0dBYPFq88PUX5FBkK5pRmY5TP9SuMNio85PQ+euUI67+KpSVk1cUVDCgA==","shasum":"8fd95f4c095748a1a47086ea5ce447405d779531","tarball":"https://registry.npmjs.org/shipstyles/-/shipstyles-1.0.0.tgz","fileCount":9,"unpackedSize":372102,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQCyeoCAtqVgZSUCgBGHN+34NwW4fmjU9WSVq2obZFjkLwIgVhILIzc/CDHCYQvgXq0ui5Pur8zQpwpGYCJc2VFO1rY="}]},"_npmUser":{"name":"ealush","email":"code@ealush.com"},"directories":{},"maintainers":[{"name":"ealush","email":"code@ealush.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/shipstyles_1.0.0_1788982771045_0.42807253353569763"},"_hasShrinkwrap":false}},"time":{"created":"2026-09-09T19:39:30.952Z","1.0.0":"2026-09-09T19:39:31.486Z","modified":"2026-09-09T19:39:31.755Z"},"maintainers":[{"name":"ealush","email":"code@ealush.com"}],"description":"Zero-config runtime styles for UI packages: ship scoped CSS without consumer imports or bundler setup.","homepage":"https://ealush.github.io/shipstyles","keywords":["CSS-in-JS","component-library","UI-packages","scoped-styles","CSS-delivery","zero-config","SSR","Shadow-DOM","CSP","bundler-agnostic","CSS-variables","atomic-CSS","style","StyleSheet","reusable"],"repository":{"type":"git","url":"git+https://github.com/ealush/shipstyles.git"},"bugs":{"url":"https://github.com/ealush/shipstyles/issues"},"license":"MIT","readme":"<picture>\n  <source media=\"(prefers-color-scheme: dark)\" srcset=\"brand/svg/shipstyles-lockup-horizontal-white.svg\">\n  <img src=\"brand/svg/shipstyles-lockup-horizontal.svg\" alt=\"ShipStyles\" width=\"320\">\n</picture>\n\nShip styled UI packages without CSS imports or bundler setup.\n\n[![npm version](https://img.shields.io/npm/v/shipstyles.svg)](https://www.npmjs.com/package/shipstyles)\n[![CI](https://github.com/ealush/shipstyles/actions/workflows/test.yml/badge.svg)](https://github.com/ealush/shipstyles/actions)\n[![license](https://img.shields.io/github/license/ealush/shipstyles.svg)](LICENSE)\n[![bundle size](https://img.shields.io/bundlephobia/minzip/shipstyles.svg)](https://bundlephobia.com/package/shipstyles)\n[![TypeScript](https://img.shields.io/npm/types/shipstyles.svg)](https://www.npmjs.com/package/shipstyles)\n\n## Why ShipStyles?\n\nApplications can dictate their stack; packages cannot. A shared component\nmust bring its styles along and behave in bundlers, frameworks, and server\nruntimes it has never seen:\n\n- **No consumer setup** — styles ship inside the JavaScript. No CSS files to\n  import, no bundler plugins to configure.\n- **No collisions** — every declaration becomes its own hashed, deduplicated\n  atomic class, scoped to your package.\n- **SSR from day one** — `sheet.getStyle()` returns the whole stylesheet as\n  a string; the client adopts the server tag instead of mounting a duplicate.\n- **Portable** — works in the browser, in Shadow DOM, and with CSP nonces.\n\n## Installation\n\n```bash\nnpm install shipstyles\n```\n\n## Usage\n\n```javascript\nimport { createSheet, cx } from 'shipstyles';\n\nconst sheet = createSheet('MyComponent');\n\nconst styles = sheet.create({\n  button: {\n    color: 'red',\n    ':hover': {\n      color: 'blue',\n    },\n  },\n});\n\nconst Button = () => <button className={cx(styles.button)}>Hover Me</button>;\n```\n\n## Environment support\n\nBrowser, SSR, Shadow DOM, and CSP nonce environments. Ships ESM and CJS\nfrom the same package:\n\n```javascript\nimport { createSheet, cx } from 'shipstyles'; // ESM\nconst { createSheet, cx } = require('shipstyles'); // CJS\n```\n\n## Migrating from FlairUp\n\nUpgrading from the `flairup` package? See [MIGRATION.md](MIGRATION.md). The\nAPI and runtime styling behavior are unchanged.\n\n## Battle-tested\n\nShipStyles is the continuation of FlairUp, which ships in production inside\n[Emoji-Picker-React](https://github.com/ealush/emoji-picker-react).\n\n## What `create()` returns\n\n`create()` returns a `Record` mapping each scope name to a `Set` of class\nnames — one entry per scope, not a single `Set`:\n\n```javascript\nconst styles = sheet.create({\n  button: { color: 'red' },\n  title: { color: 'blue' },\n});\n\nstyles.button; // Set { 'MyComponent_abc123' }\nstyles.title; // Set { 'MyComponent_def456' }\n\ncx(styles.button); // \"MyComponent_abc123\"\n```\n\nOne atomic class is generated per CSS declaration:\n\n- Each plain declaration (for example `color: 'red'`) gets its own class and\n  its own single-declaration rule. Identical declarations are emitted once\n  and shared across scopes. Distributive shorthands are the exception:\n  `margin: '2px'` expands into one class per physical side (never\n  logicals), multi-value forms distribute positionally, and anything\n  that cannot be split soundly stays one atomic class — see the\n  shorthand rules under `cx()` below.\n- Each conditional declaration (inside a pseudo selector, postcondition, or\n  media query) gets its own class, so conditional styles compose\n  independently.\n- Each CSS variable gets its own single-declaration rule — variables are no\n  longer grouped into one class per scope.\n\nProperty names are written in camelCase and emitted in dash-case\n(`backgroundColor` → `background-color`), so both spellings share one\ndeduplication entry.\n\n## `cx()` composition\n\n`cx()` accepts strings, arrays, `Set`s, and objects (truthy keys win), and\nreturns a single class string:\n\n```javascript\ncx('a', ['b', 'c'], { d: true, e: false }); // \"a b c d\"\ncx(styles.button, styles.title);\n```\n\nWhen classes conflict, the last one passed to `cx()` wins — regardless of the\norder the styles were created in:\n\n```javascript\nconst red = sheet.create({ red: { color: 'red' } });\nconst blue = sheet.create({ blue: { color: 'blue' } });\n\ncx(red.red, blue.blue); // \"...blue...\" — blue wins\ncx(blue.blue, red.red); // \"...red...\" — red wins\n```\n\nConflict rules:\n\n- Conflicts are resolved per context. The same property set globally,\n  under `:hover`, and under a media query never conflicts with itself —\n  each context keeps its class.\n- Shorthand/longhand overlap in the same context follows `cx()` order per\n  side: distributive shorthands (`margin`, `padding`, `inset`, `gap`,\n  `overflow`, border widths/styles/colors, `border-radius`) expand into\n  one atomic class per longhand, so a later `margin-top` wins only its\n  side while the other sides survive. Box shorthands emit only the four\n  physical sides (they set no logicals); multi-value forms distribute\n  positionally per CSS, and elliptical radii translate each corner to\n  its space-separated form. Values that cannot be split soundly (any\n  top-level bare `var()`/`env()`/`attr()` reference, over-count values,\n  repeated slashes) and non-distributive shorthands (`background`,\n  `font`, `border`, ...) stay one atomic class.\n- Each CSS variable is its own conflict domain: overriding `--tone` keeps\n  an unrelated `--space` from the same scope.\n- Classes from different contexts (pseudo selectors, media queries) are\n  always preserved.\n- Unknown classes (for example utility classes) pass through untouched, and\n  repeated classes are deduped.\n\nEdge cases:\n\n- Partial overlap drops the whole earlier class. An atomic shorthand that\n  stays atomic (`background`, `font`, `border`, an opaque `var()` margin)\n  resolves as one unit: if a later class overlaps any of its conflict keys,\n  the earlier class is removed entirely — deterministically, regardless of\n  creation order. That means non-overlapping declarations go with it:\n  `cx({ background: 'red url(a.png)' }, { backgroundColor: 'blue' })`\n  yields blue with no image. When those declarations matter, give each\n  visual state its own complete scope instead of layering.\n- `border` also resets `border-image`, so `border-image` (and its\n  longhands) belong to `border`'s conflict set: a later `border` replaces\n  an earlier image, and a later image replaces an earlier `border`.\n- `!important` survives shorthand expansion: `margin: '2px !important'`\n  still emits four longhands, each carrying the flag.\n\n## `createSheet()` options and mounting\n\n```javascript\nconst sheet = createSheet('MyComponent');\n```\n\nWith explicit mount options:\n\n```javascript\nconst sheet = createSheet('MyComponent', { rootNode, nonce });\n```\n\nCalling `createSheet(name)` mounts one `<style id=\"flairup-{name}\">` tag into\n`document.head` and keeps it synchronized after every `create()` and\n`keyframes()` call. `getStyle()` returns the sheet's CSS text, and\n`isApplied()` reports whether a style tag is mounted.\n\nThe second argument controls where (and whether) the tag mounts:\n\n- `{ rootNode }` — mount under a specific `HTMLElement` or `ShadowRoot`,\n  so styles work inside Shadow DOM. A bare element may also be passed\n  directly as the second argument.\n- `{ rootNode: null }` — never mount a tag. Use this on the server (or in\n  tests) and read the CSS with `getStyle()`.\n- `{ nonce }` — set a `nonce` attribute on the style tag for\n  Content-Security-Policy environments. The nonce is also applied to an\n  adopted server-rendered tag.\n\nSheets with the same name and the same mount target share one style element\nand its CSS state: writes from any handle are visible through every handle,\nand identical declarations are still emitted only once. Different names, or\nthe same name under different roots, stay isolated.\n\n## Keyframes\n\n`sheet.keyframes()` defines animations and returns a `Record` mapping each\nkeyframe name to its generated (hashed) animation name. Names are prefixed\nwith the sheet name and numbered independently of created styles:\n\n```javascript\nconst sheet = createSheet('Spinner');\n\nconst { spin } = sheet.keyframes({\n  spin: {\n    from: { transform: 'rotate(0deg)' },\n    to: { transform: 'rotate(360deg)' },\n  },\n});\n// spin === 'Spinner_0_spin'\n\nconst styles = sheet.create({\n  icon: {\n    color: 'red',\n    animationName: spin,\n    animationDuration: '1s',\n    animationIterationCount: 'infinite',\n  },\n});\n```\n\nThe `@keyframes` rules are written into the same style element as the rest of\nthe sheet's CSS.\n\n## Conditions\n\n### Scoping styles under a known class name (preconditions)\n\nNest scopes under a condition key to apply them only beneath a known\nselector:\n\n```javascript\nconst styles = sheet.create({\n  '.theme-dark': {\n    button: {\n      color: 'red',\n      ':hover': {\n        color: 'blue',\n      },\n    },\n  },\n  button: {\n    color: 'green',\n  },\n});\n```\n\n### Scoping lower level styles under a selector (postconditions)\n\nNest condition keys inside a scope to target lower level elements. All of\nthe following prefixes are supported:\n\n- `.class` — descendant class: `'.menu'` → `.hash .menu`\n- `:pseudo` / `::element` — pseudo selectors and elements: `':hover'`,\n  `'::before'`\n- `>` / `+` / `~` — combinators: `'> .icon'`, `'+ .next'`, `'~ .sibling'`\n- `*` — universal selector, with or without a suffix\n- `&.class` — same element plus a class, no space: `'.hash.active'`\n- `&:pseudo` — same element plus a pseudo selector, no space:\n  `'.hash:hover'`\n\n```javascript\nconst styles = sheet.create({\n  button: {\n    '.menu': {\n      color: 'red',\n    },\n    '&.active': {\n      color: 'blue',\n    },\n  },\n});\n```\n\nPostconditions chain with preconditions and media queries, joining nested\nlevels with a space (except `&` conditions, which attach directly).\n\n### Media queries\n\n```javascript\nconst styles = sheet.create({\n  button: {\n    color: 'red',\n    '@media (max-width: 600px)': {\n      color: 'blue',\n    },\n  },\n});\n```\n\nThe same declaration inside and outside a media query gets distinct classes,\nso global and media rules never clash or collapse into each other.\n\n### Pseudo selectors and pseudo elements\n\n```javascript\nconst styles = sheet.create({\n  button: {\n    color: 'red',\n    ':hover': {\n      color: 'blue',\n    },\n    '::before': {\n      content: '•',\n    },\n  },\n});\n```\n\n### CSS variables\n\n```javascript\nconst styles = sheet.create({\n  button: {\n    '--': {\n      '--color': 'red',\n      '--hover-color': 'blue',\n    },\n  },\n});\n```\n\nEach variable produces its own single-declaration rule, so one variable can\nbe overridden (via `cx()` order) without affecting the others.\n\n### Custom class names\n\n```javascript\nconst styles = sheet.create({\n  button: {\n    '.': 'my-button', // or [\"my-button\", \"button-main\"]\n    color: 'red',\n  },\n});\n```\n\nCustom classes are added to the scope's `Set` as-is, alongside the generated\nclasses.\n\n## SSR\n\nRender the sheet's CSS into a style tag whose `id` matches the sheet\n(`flairup-{name}`). On the client, `createSheet()` with the same name adopts\nthat tag instead of mounting a duplicate, preserving the server CSS and\nappending only new rules:\n\n> ShipStyles 1.x intentionally retains the legacy `flairup-` style-tag\n> prefix for hydration compatibility. Treat the generated ID as an\n> implementation detail; it may change in a future major version.\n\n```jsx\nimport { createSheet } from 'shipstyles';\n\nconst sheet = createSheet('MyComponent');\n\nexport function SSRStyles() {\n  if (sheet.isApplied()) {\n    return null;\n  }\n\n  return (\n    <style\n      id=\"flairup-MyComponent\"\n      suppressHydrationWarning\n      dangerouslySetInnerHTML={{ __html: sheet.getStyle() }}\n    />\n  );\n}\n```\n\nPlace `<SSRStyles />` anywhere inside your component. When there is no\nserver tag (pure client render), the sheet mounts its own tag as usual.\n\n## What does the output look like?\n\nGiven this input:\n\n```javascript\nimport { createSheet } from 'shipstyles';\n\nconst sheet = createSheet('Button', null);\n\nconst styles = sheet.create({\n  button: {\n    color: 'red',\n    '--': { '--bg': 'white' },\n    ':hover': { color: 'blue' },\n    '@media (max-width: 600px)': { color: 'green' },\n  },\n});\n\nconst { fade } = sheet.keyframes({\n  fade: { from: { opacity: '0' }, to: { opacity: '1' } },\n});\n```\n\n`sheet.getStyle()` returns exactly this CSS:\n\n```css\n.Button_wqxq0q {color:red;}\n.Button_x5i9n8 {--bg:white;}\n.Button_-w97goy:hover {color:blue;}\n@media (max-width: 600px) {\n.Button_se7zu2 {color:green;}\n}\n@keyframes Button_0_fade {\nfrom { opacity:0; }\nto { opacity:1; }\n}\n```\n\nEach declaration produces one atomic class with one single-declaration\nrule, conditional declarations get their own classes, and keyframes are\nnamed `{sheet}_{n}_{name}`. Exact class hashes vary with the sheet name and\ncreation order.\n","readmeFilename":"README.md","_rev":"1-1e49601000f16e7523d3a68cf80ad77c"}