{"id":24054113,"url":"https://github.com/soujvnunes/themizer","last_synced_at":"2026-02-26T02:46:23.683Z","repository":{"id":271631196,"uuid":"914051642","full_name":"soujvnunes/themizer","owner":"soujvnunes","description":"Solution for verbose CSS utility classes, or scattered rules within media queries ","archived":false,"fork":false,"pushed_at":"2025-11-27T13:36:41.000Z","size":918,"stargazers_count":0,"open_issues_count":2,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-11-29T01:42:39.549Z","etag":null,"topics":["accessibility","cli-tool","css","css-in-js","css-variables","dark-mode","design-systems","design-tokens","media-queries","nextjs","reactjs","remix","responsive-design","semantic-css","tailwindcss","theme-generator","theming","type-safety","typescript-library","vite"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"isc","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/soujvnunes.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-01-08T21:18:26.000Z","updated_at":"2025-11-28T23:40:48.000Z","dependencies_parsed_at":"2025-05-09T01:14:54.574Z","dependency_job_id":"4cb9bcc7-e695-445c-852c-98ca501fab09","html_url":"https://github.com/soujvnunes/themizer","commit_stats":null,"previous_names":["soujvnunes/themizer"],"tags_count":12,"template":false,"template_full_name":null,"purl":"pkg:github/soujvnunes/themizer","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/soujvnunes%2Fthemizer","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/soujvnunes%2Fthemizer/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/soujvnunes%2Fthemizer/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/soujvnunes%2Fthemizer/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/soujvnunes","download_url":"https://codeload.github.com/soujvnunes/themizer/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/soujvnunes%2Fthemizer/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29848634,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-25T22:37:40.667Z","status":"online","status_checked_at":"2026-02-26T02:00:06.774Z","response_time":89,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["accessibility","cli-tool","css","css-in-js","css-variables","dark-mode","design-systems","design-tokens","media-queries","nextjs","reactjs","remix","responsive-design","semantic-css","tailwindcss","theme-generator","theming","type-safety","typescript-library","vite"],"created_at":"2025-01-09T03:01:31.624Z","updated_at":"2026-02-26T02:46:23.676Z","avatar_url":"https://github.com/soujvnunes.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# themizer\n\n\u003e Transform verbose CSS utility classes into semantic, maintainable design tokens\n\n[![npm version](https://img.shields.io/npm/v/themizer.svg)](https://www.npmjs.com/package/themizer)\n[![TypeScript](https://img.shields.io/badge/TypeScript-Ready-blue.svg)](https://www.typescriptlang.org/)\n[![License](https://img.shields.io/npm/l/themizer.svg)](https://github.com/soujvnunes/themizer/blob/main/LICENSE)\n\n## The Problem\n\nBuilding maintainable design systems requires design tokens, but most CSS workflows don't support them:\n\n**With utility classes:**\n\n```tsx\n// Button.tsx\n\u003cbutton className=\"bg-amber-500 hover:bg-amber-600 p-6 opacity-80 transition-all duration-200 ease-in-out\"\u003e\n  {/* Hardcoded spacing (p-6 = 1.5rem), opacity (80%), transition values */}\n\u003c/button\u003e\n\n// Card.tsx\n\u003cdiv className=\"p-10 md:p-16 opacity-60 bg-amber-950/80 dark:bg-amber-50\"\u003e\n  {/* Same spacing values in pixels/rem scattered everywhere */}\n\u003c/div\u003e\n```\n\n**With CSS-in-JS:**\n\n```tsx\nconst Button = styled.button`\n  background: color-mix(in srgb, oklch(6% 0.02 70) 80%, transparent);\n  padding: 1.5rem; /* Repeated in 20 files */\n  transition: 200ms cubic-bezier(0.25, 0.1, 0.25, 1); /* Complex easing repeated */\n\n  \u0026:hover {\n    background: oklch(66.6% 0.179 58.318);\n    opacity: 0.6; /* Magic number */\n  }\n\n  @media (width \u003e= 1024px) {\n    padding: 2.5rem; /* Desktop spacing hardcoded */\n    font-size: 4rem; /* Typography scales repeated */\n  }\n`\n```\n\nWithout design tokens, you get:\n\n- Hard-coded values scattered across components\n- No single source of truth for your design system\n- No way to rebrand without find-and-replace\n- Zero type safety\n\n## The Solution\n\n**themizer** generates type-safe design tokens and semantic aliases. Define tokens once, compose them into aliases, build components from aliases.\n\n```ts\n// themizer.config.ts\nimport themizer from 'themizer'\n\nconst alpha = (color: string, percentage: string) =\u003e `color-mix(in srgb, ${color} ${percentage}, transparent)`\n\nexport const theme = themizer(\n  {\n    prefix: 'theme',\n    medias: {\n      desktop: '(width \u003e= 1024px)',\n      desktopPortrait: '(width \u003e= 1024px) and (orientation: portrait)',\n      dark: '(prefers-color-scheme: dark)',\n      motion: '(prefers-reduced-motion: no-preference)',\n    },\n    tokens: {\n      // `palette` expands OKLCH colors into 7 shades\n      palette: {\n        /* palette.amber.lightest // oklch(98.92% 0.0102 81.8)\n         * palette.amber.lighter  // oklch(96.2% 0.059 95.617)\n         * palette.amber.light    // oklch(82.8% 0.189 84.429)\n         * palette.amber.base     // oklch(76.9% 0.188 70.08)\n         * palette.amber.dark     // oklch(66.6% 0.179 58.318)\n         * palette.amber.darker   // oklch(35% 0.0771 45.635)\n         * palette.amber.darkest  // oklch(14.92% 0.0268 85.77)\n         */\n        amber: 'oklch(76.9% 0.188 70.08)',\n      },\n      alphas: {\n        100: '100%',\n        80: '80%',\n        60: '60%',\n      },\n      // `units` generates numeric scales from [start, step, end]\n      units: {\n        /* units.rem[0]    // '0rem'\n         * units.rem[0.25] // '0.25rem'\n         * units.rem[0.5]  // '0.5rem'\n         * units.rem[0.75] // '0.75rem'\n         * units.rem[1]    // '1rem'\n         * units.rem[1.25] // '1.25rem'\n         * units.rem[1.5]  // '1.5rem'\n         * units.rem[1.75] // '1.75rem'\n         * units.rem[2]    // '2rem'\n         * units.rem[2.25] // '2.25rem'\n         * units.rem[2.5]  // '2.5rem'\n         * units.rem[2.75] // '2.75rem'\n         * units.rem[3]    // '3rem'\n         * units.rem[3.25] // '3.25rem'\n         * units.rem[3.5]  // '3.5rem'\n         * units.rem[3.75] // '3.75rem'\n         * units.rem[4]    // '4rem'\n         */\n        rem: [0, 0.25, 4],\n      },\n      transitions: {\n        bounce: '200ms cubic-bezier(0.5, -0.5, 0.25, 1.5)',\n        ease: '200ms cubic-bezier(0.25, 0.1, 0.25, 1)',\n      },\n    },\n  },\n  ({ palette, alphas, units, transitions }) =\u003e ({\n    // Semantic aliases composed from tokens\n    colors: {\n      main: palette.amber.base,\n      ground: {\n        fore: [{ dark: palette.amber.lightest }, alpha(palette.amber.darkest, alphas[80])],\n        back: [{ dark: palette.amber.darkest }, palette.amber.lightest],\n      },\n    },\n    typography: {\n      headline: [{ desktop: units.rem[4] }, units.rem[2.5]],\n      title: [{ desktop: units.rem[2.5] }, units.rem[1.5]],\n      body: units.rem[1],\n    },\n    spacing: {\n      section: [{ desktopPortrait: units.rem[4] }, units.rem[2.5]],\n      block: units.rem[1.5],\n    },\n    animations: {\n      bounce: [{ motion: transitions.bounce }],\n      ease: [{ motion: transitions.ease }],\n    },\n  }),\n)\n```\n\n**Build components with semantic aliases:**\n\n```tsx\n// Card with responsive spacing\n\u003cdiv className=\"bg-ground-back p-spacing-section\"\u003e\n  \u003ch1 className=\"text-typography-headline text-ground-fore\"\u003eTitle\u003c/h1\u003e\n  \u003cp className=\"text-typography-body mt-spacing-block\"\u003eContent\u003c/p\u003e\n\u003c/div\u003e\n\n// Hero section with animations\nimport { theme } from './themizer.config'\n\nconst Hero = styled.section`\n  background: ${theme.aliases.colors.main};\n  padding: ${theme.aliases.spacing.section}; /* Responsive: 2.5rem mobile, 4rem desktop portrait */\n  transition: ${theme.aliases.animations.bounce}; /* Only applies when motion is preferred */\n`\n\nconst Button = styled.button`\n  background: ${theme.aliases.colors.ground.back};\n  color: ${theme.aliases.colors.ground.fore}; /* Color with 80% alpha */\n  padding: ${theme.aliases.spacing.block};\n  font-size: ${theme.aliases.typography.title}; /* Responsive: 1.5rem mobile, 2.5rem desktop */\n  transition: ${theme.aliases.animations.ease};\n\n  \u0026:hover {\n    opacity: ${theme.tokens.alphas[60]};\n  }\n`\n```\n\nSingle source of truth. Responsive by default. Type-safe tokens.\n\n## Features\n\n### Token Types\n\n**themizer** supports both automatic expansion and manual definition:\n\n#### Automatic Expansion\n- **`palette`**: Single OKLCH color → 7 harmonious shades\n- **`units`**: `[from, step, to]` tuples → complete numeric scales\n\n#### Manual Definition\nDefine exact values when you need precise control:\n\n```ts\ntokens: {\n  // Auto-expand properties\n  palette: {\n    amber: 'oklch(76.9% 0.188 70.08)',  // → 7 shades\n    // blue: '#3b82f6',                 // ❌ Error: must be OKLCH\n  },\n  units: {\n    rem: [0, 0.25, 4],                   // → 17 values (0, 0.25, 0.5... 4rem)\n    px: [0, 4, 64],                      // → 17 values (0, 4, 8... 64px)\n  },\n\n  // Full control properties\n  colors: {\n    blue: {\n      50: '#eff6ff',\n      500: '#3b82f6',\n      950: '#172554',\n    },\n    brand: '#ff0000',\n  },\n  spacing: {\n    small: '0.5rem',\n    medium: '1rem',\n    large: '2rem',\n  },\n  anyPropertyName: {\n    // Your custom tokens\n  }\n}\n```\n\n### Atomic Design\n\nBuild from atoms (tokens) → molecules (aliases) → organisms (components):\n\n```ts\n// Atoms (tokens)\npalette.amber.base        // Raw expanded color value\nunits.rem[1.5]           // Raw spacing value (1.5rem)\n\n// Molecules (aliases)\ncolors.ground.fore       // Semantic color\nspacing.block            // Semantic spacing\n\n// Organisms (components)\n\u003cCard className=\"bg-ground-back p-spacing-block\" /\u003e\n```\n\n### Type-Safe\n\nFull TypeScript support with autocomplete:\n\n```ts\ntheme.aliases.colors.main  // ✓ Autocomplete\ntheme.aliases.colors.mian  // ✗ Type error\n```\n\n### Multiple Themes\n\nExport multiple themes for complex design systems with multi-brand support. All themes are combined into a single `theme.css` file:\n\n```ts\n// themizer.config.ts\nimport themizer from 'themizer'\n\n// Single theme (most common)\nexport const theme = themizer({ prefix: 'theme', tokens, medias }, () =\u003e ({}))\n\n// Or multiple themes for multi-brand design systems\nexport const cocaCola = themizer({ prefix: 'coke', tokens: cokeTokens, medias }, () =\u003e ({}))\nexport const nike = themizer({ prefix: 'nike', tokens: nikeTokens, medias }, () =\u003e ({}))\n```\n\nWhen you run `pnpm run themizer:theme`, all exported themes are combined:\n\n```bash\nthemizer: theme.css written to ./src/app (2 themes: cocaCola, nike)\n```\n\nEach theme uses its own prefix to avoid naming conflicts, and all CSS custom properties are combined into a single optimized file.\n\n**Note:** Themes are combined in alphabetical order (by export name) to ensure deterministic output. This means `theme.css` will always be identical for the same config, regardless of declaration order in the file.\n\n### Responsive by Default\n\nMedia queries configured once, applied everywhere:\n\n```ts\n// Define once\nmedias: {\n  desktop: '(width \u003e= 1024px)',\n  dark: '(prefers-color-scheme: dark)'\n}\n\n// Use in aliases\ntypography: {\n  title: [{ desktop: units.rem[2.5] }, units.rem[1.5]]  // 2.5rem on desktop, 1.5rem mobile\n}\n```\n\n### Production Ready\n\n#### Minification\n\nBase-52 encoding reduces CSS size by ~88%:\n\n```ts\ntheme.aliases.colors.ground.fore  // generates → --themea2\n```\n\n#### Source Maps\n\nDebug with `theme.css.map.json` which maps minified names to their object paths:\n\n```json\n{\n  \"--themea2\": \"--theme-aliases-colors-ground-fore\"\n}\n```\n\n#### CSS @property Registration\n\nBrowser-enforced type validation:\n\n```css\n@property --theme0 {\n  syntax: \"\u003ccolor\u003e\";\n  inherits: false;\n  initial-value: oklch(98.92% 0.0102 81.8);\n}\n```\n\n### Framework Agnostic\n\nWorks with any CSS framework - see [Framework Integration](#framework-integration).\n\n## Quick Start\n\n```bash\n# Install themizer\npnpm add themizer\n\n# Initialize (auto-detects your framework)\nnpx themizer init\n\n# Generate theme.css\npnpm run themizer:theme\n```\n\nThe `init` command creates `themizer.config.ts` with example tokens and adds a script to your `package.json`.\n\n## Generate CSS\n\n```bash\npnpm run themizer:theme        # Generate once\npnpm run themizer:theme:watch  # Watch mode (if configured with --watch)\n```\n\nExecutes your `themizer.config.ts` and generates minified CSS with:\n\n- CSS @property registration for type validation\n- Base-52 encoded variable names (`--theme0`, `--themea1`) for smaller bundles\n- Media query rules for responsive values and dark mode\n- Source map (`theme.css.map.json`) for debugging\n\n```css\n:root {\n  --theme0: oklch(98.92% 0.0102 81.8);  /* amber.lightest */\n  --theme3: oklch(76.9% 0.188 70.08);   /* amber.base */\n  --themea0: var(--theme3);              /* palette.main */\n  --themea2: color-mix(in srgb, var(--theme6) var(--themea), transparent); /* palette.ground.fore */\n}\n\n@media (prefers-color-scheme: dark) {\n  :root {\n    --themea2: var(--theme0);      /* palette.ground.fore switches */\n  }\n}\n```\n\n\n## Framework Integration\n\nImport the generated `theme.css` in your app's entry file:\n\n```tsx\n// app/layout.tsx, main.tsx, or _app.tsx\nimport './theme.css';\n```\n\nNow integrate with your styling solution.\n\n### Tailwind CSS\n\nExtend Tailwind's config with themizer aliases:\n\n```js\n// tailwind.config.js\nimport { theme } from './themizer.config';\n\nconst alpha = (color, percentage) =\u003e\n  `color-mix(in srgb, ${color} ${percentage}, transparent)`;\n\nexport default {\n  theme: {\n    extend: {\n      spacing: theme.aliases.spacing,\n      opacity: theme.tokens.alphas,\n      colors: {\n        main: theme.aliases.colors.main,\n        ground: {\n          fore: theme.aliases.colors.ground.fore,\n          back: theme.aliases.colors.ground.back,\n        },\n        primary: {\n          DEFAULT: theme.aliases.colors.main,\n          light: alpha(theme.aliases.colors.main, theme.tokens.alphas[60]),\n        },\n      },\n      fontSize: {\n        headline: theme.aliases.typography.headline,\n        title: theme.aliases.typography.title,\n        body: theme.aliases.typography.body,\n      },\n    },\n  },\n};\n```\n\nUse semantic classes in your components:\n\n```tsx\n\u003cbutton className=\"bg-ground-back text-ground-fore p-spacing-block\"\u003e\n  \u003ch1 className=\"text-headline\"\u003eWelcome\u003c/h1\u003e\n  \u003cp className=\"text-body opacity-60\"\u003eGet started with themizer\u003c/p\u003e\n\u003c/button\u003e\n```\n\n### Linaria (Zero-Runtime CSS-in-JS)\n\nUse theme values directly in your styled components:\n\n```tsx\nimport { styled } from '@linaria/react';\nimport { theme } from './themizer.config';\n\nconst Button = styled.button`\n  background: ${theme.aliases.colors.ground.back};\n  color: ${theme.aliases.colors.ground.fore};\n  padding: ${theme.aliases.spacing.block};\n  font-size: ${theme.aliases.typography.body};\n  transition: ${theme.aliases.animations.ease};\n\n  \u0026:hover {\n    opacity: ${theme.tokens.alphas[60]};\n  }\n`;\n```\n\n## CLI Commands\n\n### `themizer init`\n\nCreate configuration with auto-detected framework support.\n\n```bash\nthemizer init                    # Interactive setup\nthemizer init --watch            # Include watch mode script\nthemizer init --out-dir ./path   # Skip prompts, use custom path\n```\n\n**Detects:** Next.js (App/Pages Router), Remix, Vite, CRA, and more.\n\n### `themizer theme`\n\nGenerate theme.css from your configuration.\n\n```bash\nthemizer theme --out-dir ./src/app         # Generate once\nthemizer theme --out-dir ./src/app --watch # Watch for changes\n```\n\n## API\n\n### `themizer(options, aliases)`\n\nMain function to generate design tokens and aliases.\n\n#### Parameters\n\n- `options.prefix` - Prefix for CSS custom properties (e.g., `'theme'` → `--theme-*`)\n- `options.medias` - Media query definitions for responsive design\n- `options.tokens` - Design tokens object with special expansions:\n  - `palette.*`: OKLCH strings auto-expand to 7 shades (lightest, lighter, light, base, dark, darker, darkest)\n  - `units.*`: Object where each key is a unit type (rem, px, percentage, vh, vw, etc.) with `[from, step, to]` tuple values\n  - Other properties: Used as-is (no expansion)\n- `aliases` - Function that receives resolved tokens and returns semantic aliases\n\n#### Returns\n\n- `aliases` - Semantic aliases wrapped in `var()` for use in CSS/JS\n- `tokens` - Design tokens wrapped in `var()` for use in CSS/JS (with expansions applied)\n- `medias` - Media queries prefixed with `@media`\n\n#### Example\n\n```ts\nimport { theme } from './themizer.config'\n\n// Using aliases (semantic names)\ntheme.aliases.colors.main        // → \"var(--themea0, oklch(76.9% 0.188 70.08))\"\ntheme.aliases.typography.title    // → \"var(--themea5, 2.5rem)\"\n\n// Using tokens (raw values)\n// Expanded OKLCH color from palette:\ntheme.tokens.palette.amber.base    // → \"var(--theme3, oklch(76.9% 0.188 70.08))\"\ntheme.tokens.palette.amber.lightest // → \"var(--theme0, oklch(98.92% 0.0102 81.8))\"\n// Manual color definition:\ntheme.tokens.colors.blue[500]     // → \"var(--theme7, #3b82f6)\"\n// Expanded units:\ntheme.tokens.units.rem[1]          // → \"var(--themed, 1rem)\"\ntheme.tokens.units.rem[4]          // → \"var(--themeS, 4rem)\"\ntheme.tokens.units.px[16]          // → \"var(--themee, 16px)\"\n\n// Using media queries\ntheme.medias.desktop              // → \"@media (width \u003e= 1024px)\"\ntheme.medias.dark                 // → \"@media (prefers-color-scheme: dark)\"\n```\n\n### `unwrapAtom(atom)`\n\nExtract CSS custom property name from `var()` expression.\n\n#### Parameters\n\n- `atom` - A CSS custom property wrapped in `var()`\n\n#### Returns\n\nThe unwrapped custom property name (string)\n\n#### Example\n\n```ts\nimport { unwrapAtom } from 'themizer'\nimport { theme } from './themizer.config'\n\nunwrapAtom(theme.aliases.colors.main)  // → \"--themea0\"\n\n// Useful for scoped overrides:\n\u003cdiv style={{ [unwrapAtom(theme.aliases.colors.main)]: 'oklch(50% 0.2 180)' }}\u003e\n  This div has a custom main color\n\u003c/div\u003e\n```\n\n### `resolveAtom(atom)`\n\nExtract default value from `var()` expression.\n\n#### Parameters\n\n- `atom` - A CSS custom property with a default value\n\n#### Returns\n\nThe resolved default value (string or number)\n\n#### Example\n\n```ts\nimport { resolveAtom } from 'themizer'\nimport { theme } from './themizer.config'\n\nresolveAtom(theme.tokens.palette.amber.base)  // → \"oklch(76.9% 0.188 70.08)\"\nresolveAtom(theme.aliases.colors.main)      // → \"oklch(76.9% 0.188 70.08)\"\nresolveAtom(theme.tokens.units.rem[1])       // → \"1rem\"\n\n// Useful for non-CSS contexts:\nexport const viewport = {\n  themeColor: resolveAtom(theme.aliases.colors.main),\n}\n```\n\n## Links\n\n- [GitHub Repository](https://github.com/soujvnunes/themizer)\n- [npm Package](https://www.npmjs.com/package/themizer)\n- [Report Issues](https://github.com/soujvnunes/themizer/issues)\n\n## License\n\nISC\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsoujvnunes%2Fthemizer","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsoujvnunes%2Fthemizer","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsoujvnunes%2Fthemizer/lists"}