styleenv
v1.1.0
Published
Environment-tinted favicons for Next.js and Vite — see at a glance whether a tab is dev, preview, staging, or production.
Maintainers
Readme
Five browser tabs, all the same favicon: dev, preview, production.. and you keep editing the wrong one. styleenv tints your existing favicon per environment at build time, Vercel-style: same mark, different color. Production is never touched.
Install
pnpm add styleenv
# or: npm install styleenvUsage
Next.js
// next.config.ts
import { withEnvStyles } from 'styleenv'
export default withEnvStyles(nextConfig)That's the only change needed. No layout edits, no manual favicon files.
Vite
// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { envStyle } from 'styleenv/vite'
export default defineConfig({
plugins: [react(), envStyle()],
})Default colors
| environment | color |
|---------------|--------------------|
| development | #3b82f6 blue |
| preview | #f59e0b amber |
| anything else | #6b7280 gray |
| production | never touched |
Environment names are plain strings: whatever detection yields (e.g.
ENV_STYLES_ENV=staging, or a Vercel custom environment via VERCEL_TARGET_ENV) is
used as the color key. Any env without a color gets the gray fallback — except
production, which is never touched.
Options
enabled?: boolean— kill switch for the whole tool. Defaulttrue.color?: Partial<Record<string, string>>— override the tint color per environment.environment?: string— force the environment instead of detecting it.excludeColors?: string[]— keep pixels near these colors untinted (e.g. white backgrounds or marks in an icon that shouldn't shift).icon?: string | Partial<Record<string, string>>— path to a ready-made icon, or a per-environment map of paths (e.g.{ staging: 'staging-icon.png' }), served as-is for styled environments. Tinting andexcludeColorsare skipped entirely. An env missing from the map falls back to normal tinting.fallbackShape?: "circle" | "square" | "rounded-square" | "ring"— shape of the fallback icon when no source icon is found. Default"circle".runtime?: boolean | { hostnamePatterns?: Partial<Record<string, string>> }— enable runtime restyle for promoted builds. Pre-generates icons for all environments and injects a client-side script that swaps the favicon based on hostname. Defaultfalse.
export default withEnvStyles(nextConfig, {
color: { staging: '#ff00ff' },
excludeColors: ['#fff'],
fallbackShape: 'ring',
})Runtime restyle
When a build is promoted from preview to production, the favicon stays tinted — a
single build can't re-tint. The runtime option solves this by pre-generating icons
for all environments at build time and injecting a small client-side script that swaps
the favicon based on hostname patterns.
// Next.js
export default withEnvStyles(nextConfig, {
runtime: {
hostnamePatterns: {
preview: '-git-.*\\.vercel\\.app$',
staging: 'staging\\.myapp\\.com$',
},
},
})// Vite
envStyle({
runtime: {
hostnamePatterns: {
preview: '-git-.*\\.vercel\\.app$',
},
},
})Default hostname patterns match localhost → development and Vercel preview URLs →
preview. Custom patterns are merged with the defaults.
For Next.js, the runtime script is exported via process.env.ENV_STYLE_RUNTIME_SCRIPT
so you can inject it into your root layout:
// app/layout.tsx
export default function RootLayout({ children }) {
return (
<html>
<head>
<script dangerouslySetInnerHTML={{ __html: process.env.ENV_STYLE_RUNTIME_SCRIPT }} />
</head>
<body>{children}</body>
</html>
)
}Doctor CLI
Inspect what styleenv would do without running a build:
npx styleenv doctorOutput shows detected environment, tint color, fallback shape, source icon, rewrites, and whether the tool is active or disabled (production = zero footprint).
Options:
-e, --environment <env>— force environment--color <env>=<hex>— override color per env--exclude-colors <c1,c2>— colors to exclude from tinting--icon <path>— custom icon path--fallback-shape <shape>— circle, square, rounded-square, or ring
Multi-resolution .ico
The tool generates multi-resolution .ico files (16, 32, 48, 64px) alongside the
tinted .png. The /favicon.ico route automatically serves the .ico version.
Next.js Metadata API
If your project uses app/icon.tsx or app/apple-icon.tsx (Next.js Metadata API),
styleenv automatically detects these and adds the corresponding rewrites (/icon,
/apple-icon).
How it works
- Detects the environment:
environmentoption →ENV_STYLES_ENV→VERCEL_TARGET_ENV→VERCEL_ENV. If none is set, Next.js falls back toNODE_ENV; Vite falls back to the command (dev server = development,vite build= production — setENV_STYLES_ENVto tint a build). - Tints your existing favicon with
sharpand writes it topublic/__styleenv/, which self-gitignores. - Next.js: serves the tinted icon at your normal favicon URLs via
beforeFilesrewrites, withCache-Control: no-storeso stale icons don't linger after switching envs. - Vite: rewrites
<link rel="icon">tags inindex.htmlto point at the tinted icon (injecting one if none exists) and serves it withno-storefrom dev-server middleware. - In runtime mode, icons for all environments are pre-generated and a client-side script swaps the favicon based on hostname patterns.
Known limits
- Environment is resolved at build time — a single build promoted across environments
won't restyle (unless
runtimeis enabled). - CDN-only favicons aren't intercepted; on Next.js, custom metadata
iconspaths aren't either. - Icon source changes need a dev server or build restart to pick up.
Development
pnpm test— unit tests (vitest);pnpm typecheck— TypeScript.pnpm lint— linting (biome);pnpm format— auto-fix.pnpm test:visual— fetches real brand favicons and writes a before/after tint gallery to.tmp/visual/index.html(needs network; Node >= 22.18 to run the.tsscript directly).- Runnable examples:
cd examples/nextjs && pnpm install && pnpm dev(same forexamples/vite-react) — the tab favicon should render tinted green.
License
MIT
