fluid-px
v3.1.0
Published
Fluid responsive CSS values using linear interpolation — scale any CSS property smoothly between two screen sizes with calc(). Zero-config Tailwind v3 and v4 support. Works with inline styles and Tailwind arbitrary values. No media queries, no clamp, no l
Maintainers
Keywords
Readme
fluid-px
Fluid responsive values for any CSS property — no media queries, no breakpoints, no clamp.
Scales smoothly between two screen sizes using a single CSS max() expression. Define a value at mobile and desktop, it interpolates in between — and keeps growing proportionally on screens larger than your max so the design stays consistent on ultrawide displays.
import { fluid, tw, cls } from 'fluid-px'
import cls from 'fluid-px/cls'
// inline style
<h1 style={{ fontSize: fluid(24, 56) }}>
// tailwind arbitrary value
<h1 className={`text-[${tw(24, 56)}]`}>
// tailwind class builder (cleanest)
<h1 className={cls.text(24, 56)}>Works with React, Next.js, Vue, Angular, Svelte, Payload CMS — any framework, JavaScript or TypeScript.
What's included
| | |
|---|---|
| fluid() | Inline style value — grows proportionally above max screen |
| tw() | Tailwind arbitrary value — no spaces, same behavior |
| fluid-px/cls | Tailwind class builders — cls.text(24, 56) instead of `text-[${tw(24, 56)}]` |
| at() | Real px number at any screen width, including overflow |
| configure() | Set global screen defaults at runtime |
| npx fluid-px scan | Tailwind v4 — generate fluid-classes.css from your source files |
| npx fluid-px generate | Generate CSS vars + Tailwind tokens from a config file |
| npx fluid-px preview | Print a value table including overflow widths |
| npx fluid-px audit | Scan your codebase for breakpoint-heavy classNames |
| fluid-px/image | Generate sizes strings for Next.js <Image> |
How it works
You pass a pixel value for mobile and a pixel value for desktop. The package computes a max() expression that:
- Scales linearly between your two screen sizes
- Keeps growing proportionally above your max screen — so the design looks the same on ultrawide monitors
fluid(24, 56) with screens 375px → 1440px
At 375px → 24px (your mobile baseline)
At 768px → 35.8px (smooth interpolation)
At 1440px → 56px (your desktop baseline)
At 1920px → 74.7px (proportional — keeps growing)
At 2560px → 99.6px (proportional — keeps growing)
At 3840px → 149px (proportional — keeps growing)The CSS value looks like this:
max(calc(4.7324px + 3.0047vw), calc(2.5vw))- Below max screen: the
calc(intercept + slope*vw)linear term wins - Above max screen: the
calc(proportionalVw)term wins — seamless transition, no jump - No JavaScript runs in the browser
- No media queries
- No layout shifts
Installation
npm install fluid-pxThat's it. The postinstall script detects your Tailwind version and wires everything up automatically.
What happens on install
Tailwind v3
Your tailwind.config.js is patched automatically:
// Added automatically — no manual step needed
const { withFluidPx } = require('fluid-px/extractor')
// ...your existing config...
module.exports = withFluidPx(module.exports)Every build re-evaluates your cls.*() and tw() calls and generates the matching CSS. Zero ongoing maintenance.
Tailwind v4
Three things happen automatically:
- Your source files (
./src,./app, etc.) are scanned forcls.*()andtw()calls fluid-classes.cssis written alongside your CSS entry with@source inline(...)directives@import "./fluid-classes.css"is injected into yourglobals.css
/* globals.css — @import injected automatically */
@import "tailwindcss";
@import "./fluid-classes.css";One additional step for v4: add fluid-px scan to your prebuild script so the CSS file stays up to date when you add new fluid-px calls:
{
"scripts": {
"prebuild": "fluid-px scan",
"build": "next build"
}
}No Tailwind
If you're not using Tailwind, use fluid() for inline styles — no configuration needed.
Optional: set your screen sizes
npx fluid-px setup📐 fluid-px — set your default screen sizes
Press Enter to keep the shown default.
Min screen width in px [430]: 375
Max screen width in px [1920]: 1440
✅ fluid-px defaults saved: 375px → 1440pxRe-run at any time to change your defaults. Default until you run setup: 430px → 1920px.
API
fluid(min, max, options?)
Returns a max() CSS value with spaces — use as an inline style value.
Scales linearly from min at minScreen to max at maxScreen, then continues growing proportionally above maxScreen.
import { fluid } from 'fluid-px'
// font size that scales from 16px to 48px, keeps growing beyond max screen
<h1 style={{ fontSize: fluid(16, 48) }}>
// padding that scales from 16px to 80px on both sides
<nav style={{ paddingLeft: fluid(16, 80), paddingRight: fluid(16, 80) }}>
// gap that scales from 8px to 24px
<div style={{ gap: fluid(8, 24) }}>
// border radius that scales from 4px to 16px
<div style={{ borderRadius: fluid(4, 16) }}>
// any CSS property — you control the property name
<div style={{ marginTop: fluid(24, 64), letterSpacing: fluid(-0.5, -1) }}>Output:
fluid(16, 48) → "max(calc(4.7324px + 3.0047vw), calc(3.3333vw))"tw(min, max, options?)
Returns the same max() value without spaces — use inside Tailwind arbitrary value brackets [...].
Tailwind breaks on spaces inside arbitrary values, so use tw() instead of fluid() for classNames.
import { tw } from 'fluid-px'
// font size
<h1 className={`text-[${tw(16, 48)}]`}>
// padding
<nav className={`px-[${tw(16, 80)}]`}>
// multiple fluid values in one className
<div className={`gap-[${tw(8, 24)}] rounded-[${tw(4, 16)}] h-[${tw(60, 100)}]`}>
// combine with regular tailwind classes
<section className={`flex flex-col items-center py-[${tw(32, 96)}]`}>Output:
tw(16, 48) → "max(calc(4.7324px+3.0047vw),calc(3.3333vw))"fluid-px/cls — Tailwind class builders
A set of helpers that return complete, ready-to-use Tailwind class strings so you don't have to wrap tw() in template literal brackets yourself.
// Before (tw() — still valid)
<h1 className={`text-[${tw(24, 56)}]`}>
// After (cls — cleaner)
<h1 className={cls.text(24, 56)}>Import
// default import — cls object
import cls from 'fluid-px/cls'
// named imports — individual helpers
import { text, px, gap } from 'fluid-px/cls'Usage
import cls from 'fluid-px/cls'
// typography
<h1 className={cls.text(24, 56)}>
<p className={cls.text(14, 16)}>
<p className={`${cls.text(14, 16)} ${cls.leading(20, 28)}`}>
// padding
<nav className={cls.px(16, 80)}>
<section className={cls.py(32, 96)}>
// gap
<div className={`flex flex-col ${cls.gap(12, 32)}`}>
// sizing
<img className={`${cls.w(79, 170)} ${cls.h(32, 62)}`}>
// border radius
<div className={cls.rounded(8, 20)}>
// combine with regular Tailwind classes
<section className={`flex flex-col items-center ${cls.py(32, 96)} ${cls.gap(16, 40)}`}>All helpers pass options through to tw():
cls.text(24, 56, { minScreen: 375, maxScreen: 1440 })
cls.px(16, 80, { unit: 'rem' })
cls.gap(8, 24, { minScreen: 320, maxScreen: 768 })Available helpers
| Import | Returns | CSS property |
|--------|---------|--------------|
| cls.text | text-[...] | font-size |
| cls.leading | leading-[...] | line-height |
| cls.tracking | tracking-[...] | letter-spacing |
| cls.p | p-[...] | padding |
| cls.px | px-[...] | padding-left + right |
| cls.py | py-[...] | padding-top + bottom |
| cls.pt | pt-[...] | padding-top |
| cls.pb | pb-[...] | padding-bottom |
| cls.pl | pl-[...] | padding-left |
| cls.pr | pr-[...] | padding-right |
| cls.ps | ps-[...] | padding-inline-start |
| cls.pe | pe-[...] | padding-inline-end |
| cls.m | m-[...] | margin |
| cls.mx | mx-[...] | margin-left + right |
| cls.my | my-[...] | margin-top + bottom |
| cls.mt | mt-[...] | margin-top |
| cls.mb | mb-[...] | margin-bottom |
| cls.ml | ml-[...] | margin-left |
| cls.mr | mr-[...] | margin-right |
| cls.ms | ms-[...] | margin-inline-start |
| cls.me | me-[...] | margin-inline-end |
| cls.gap | gap-[...] | gap |
| cls.gapX | gap-x-[...] | column-gap |
| cls.gapY | gap-y-[...] | row-gap |
| cls.w | w-[...] | width |
| cls.h | h-[...] | height |
| cls.size | size-[...] | width + height |
| cls.minW | min-w-[...] | min-width |
| cls.maxW | max-w-[...] | max-width |
| cls.minH | min-h-[...] | min-height |
| cls.maxH | max-h-[...] | max-height |
| cls.rounded | rounded-[...] | border-radius |
| cls.border | border-[...] | border-width |
| cls.top | top-[...] | top |
| cls.right | right-[...] | right |
| cls.bottom | bottom-[...] | bottom |
| cls.left | left-[...] | left |
| cls.inset | inset-[...] | inset |
Options
Both fluid() and tw() accept an optional third argument:
fluid(min, max, { minScreen?, maxScreen?, unit? })
tw(min, max, { minScreen?, maxScreen?, unit? })Per-call screen override
Override the screen range for a single value without changing global defaults:
// this value only scales between 320px and 768px
fluid(14, 18, { minScreen: 320, maxScreen: 768 })
// useful for components that only appear on mobile
tw(12, 16, { minScreen: 320, maxScreen: 600 })unit: 'rem'
Output rem units instead of px. Pass px values as always — division by 16 is handled automatically.
fluid(16, 48, { unit: 'rem' })
// → "max(calc(0.2958rem + 0.1878vw), calc(0.2083vw))"
tw(16, 48, { unit: 'rem' })
// → "max(calc(0.2958rem+0.1878vw),calc(0.2083vw))"Useful when your design system or browser accessibility settings depend on rem for font sizes.
unit: 'cqi'
Output cqi units — scales relative to the parent container's width instead of the viewport. Use this for components inside sidebars, grids, cards, or modals where viewport width doesn't reflect the component's actual space.
fluid(14, 24, { unit: 'cqi' })
// → "calc(4.7324px + 3.0047cqi)"Note:
cqiuses plaincalc()— container sizes are bounded by their parent so proportional overflow does not apply.
Requires container-type: inline-size on the parent:
.sidebar {
container-type: inline-size;
}<aside className="sidebar">
<h2 style={{ fontSize: fluid(14, 24, { unit: 'cqi' }) }}>
Scales with sidebar width, not viewport
</h2>
</aside>at(min, max, screenWidth, options?)
Returns the actual rendered pixel value at a given screen width. Matches exactly what the browser computes from fluid() — including proportional overflow above maxScreen.
import { at } from 'fluid-px'
at(16, 48, 375) // → 16 (mobile baseline)
at(16, 48, 768) // → 27.81
at(16, 48, 1440) // → 48 (desktop baseline)
at(16, 48, 1920) // → 64 (proportional overflow)
at(16, 48, 2560) // → 85.33 (proportional overflow)
at(16, 48, 3840) // → 128 (proportional overflow)
// with screen override
at(16, 48, 768, { minScreen: 375, maxScreen: 1440 }) // → 27.81configure(options)
Override global screen defaults at runtime. Call once at your app's entry point before any fluid() or tw() calls.
import { configure } from 'fluid-px'
configure({ minScreen: 375, maxScreen: 1440 })Where to call it:
// Next.js App Router — app/layout.tsx
import { configure } from 'fluid-px'
configure({ minScreen: 375, maxScreen: 1440 })
// Next.js Pages Router — pages/_app.tsx
import { configure } from 'fluid-px'
configure({ minScreen: 375, maxScreen: 1440 })
// React / Vite — main.tsx
import { configure } from 'fluid-px'
configure({ minScreen: 375, maxScreen: 1440 })If you already ran
npx fluid-px setup, you don't needconfigure()— your screen sizes are already saved.
Defaults priority
Screen sizes are resolved in this order (highest to lowest priority):
| Source | How |
|--------|-----|
| configure() | Runtime JS call |
| Env vars | FLUID_MIN_SCREEN / FLUID_MAX_SCREEN |
| npx fluid-px setup | Interactive prompt |
| Built-in fallback | 430px → 1920px |
Container / CI / Cloud Run environments
Set environment variables — no interactive prompt needed.
FLUID_MIN_SCREEN=375
FLUID_MAX_SCREEN=1440Set them in:
- Google Cloud Run → Edit Service → Variables & Secrets
- Docker →
ENV FLUID_MIN_SCREEN=375in Dockerfile - GitHub Actions / CI → repository secrets or env block
Runtime env vars are read when your app starts, so no rebuild is required when you change them.
Validation warnings
In development (NODE_ENV !== 'production'), the package warns you in the console when something looks wrong:
fluid(16, 16)
// ⚠ [fluid-px] min and max are equal (16) — no fluid scaling will occur
fluid(undefined, 48)
// ⚠ [fluid-px] min or max is not a number
fluid(16, 48, { minScreen: 1920, maxScreen: 375 })
// ⚠ [fluid-px] minScreen (1920) must be less than maxScreen (375) — scaling will be brokenWarnings are silenced in production automatically.
TypeScript
Types are included — no @types package needed.
import { fluid, tw, at, configure } from 'fluid-px'
const fontSize: string = fluid(16, 48)
const twClass: string = tw(16, 48)
const value: number = at(16, 48, 2560) // 85.33 — accounts for overflowCLI
npx fluid-px setup
Set or update your default screen sizes interactively.
npx fluid-px setupnpx fluid-px scan [dir]
Tailwind v4 only. Scans your source files for cls.*() and tw() calls, evaluates each one, and writes fluid-classes.css with @source inline(...) directives so Tailwind v4 generates the correct CSS.
npx fluid-px scan # scan ./src ./app ./pages ./components
npx fluid-px scan ./src # scan a specific directory
npx fluid-px scan --output ./app/globals/fluid-classes.css
npx fluid-px scan --watch # re-scan on file changes (dev mode)| Flag | Default | Description |
|------|---------|-------------|
| [dir] | ./src ./app ./pages ./components | Directory (or directories) to scan |
| --output <path> | ./fluid-classes.css | Output CSS file path |
| --watch | — | Re-scan automatically when source files change |
Generated output:
/* AUTO-GENERATED by fluid-px — do not edit manually */
/* Re-run: npx fluid-px scan */
@source inline("text-[max(calc(...))] px-[max(...)] gap-[max(...)]");Add to your prebuild script to keep it current:
{
"scripts": {
"prebuild": "fluid-px scan",
"dev": "fluid-px scan --watch & next dev"
}
}npx fluid-px preview <min> <max>
Print a table of actual pixel values at common screen widths, including overflow widths above your max screen. Useful for verifying a value before using it.
npx fluid-px preview 16 48
npx fluid-px preview 16 48 --minScreen 375 --maxScreen 1440Output:
📐 fluid(16, 48) [screens: 375px → 1440px]
320px → 14.35px
375px → 16px ← min
430px → 17.65px
768px → 27.81px
1024px → 35.5px
1280px → 43.19px
1440px → 48px ← max
1920px → 64px
Overflow (proportional beyond 1440px):
2560px → 85.33px ← grows with screen
3840px → 128px ← grows with screennpx fluid-px generate
Generate CSS custom properties and Tailwind tokens from a config file. See the Config-file workflow section below.
npx fluid-px generate
npx fluid-px generate --config ./path/to/fluid.config.jsnpx fluid-px audit [dir]
Scan your codebase for elements that use 3 or more Tailwind breakpoint variants on the same CSS property — exactly the cases that fluid-px can replace with a single smooth value.
npx fluid-px audit ./src
npx fluid-px audit ./src --min-breakpoints 4
npx fluid-px audit ./src --jsonOptions:
| Flag | Default | Description |
|------|---------|-------------|
| [dir] | . | Directory to scan (recursive) |
| --min-breakpoints <n> | 3 | Minimum number of responsive breakpoints to flag |
| --json | — | Output machine-readable JSON instead of the human report |
Example output:
Scanning 87 files in src...
components/HeroText.js:5
text-* → 5 breakpoints
xs:text-4xl min-[800px]:text-[40px] min-[1023px]:text-3xl min-[1270px]:text-5xl 2xl:text-[2.7rem]
Suggestion (30px → 48px):
inline style: style={{ fontSize: fluid(30, 48) }}
tailwind arb: className={`text-[${tw(30, 48)}]`}
config token: fontSize: { 'name': [30, 48] }
Found 14 elements with 3+ breakpoints across 8 files.What it skips: node_modules/, .next/, .git/, dist/, build/.
Manual Tailwind setup
The postinstall script handles this automatically. If you need to configure manually (e.g. monorepo, custom setup):
Tailwind v3
// tailwind.config.js
const { withFluidPx } = require('fluid-px/extractor')
module.exports = withFluidPx({
content: ['./src/**/*.{js,ts,jsx,tsx}'],
theme: { extend: {} },
plugins: [],
})// tailwind.config.ts (ESM)
import { withFluidPx } from 'fluid-px/extractor'
const config = {
content: ['./src/**/*.{js,ts,jsx,tsx}'],
theme: { extend: {} },
plugins: [],
}
export default withFluidPx(config)Tailwind v4
Run the scan manually, then import the output in your CSS entry:
npx fluid-px scan/* globals.css */
@import "tailwindcss";
@import "./fluid-classes.css";fluid-px/image — Next.js image sizes
import { imageSizes, configureImage } from 'fluid-px/image'Generates the sizes attribute string for Next.js <Image> (or any <img> with srcSet). The sizes attribute tells the browser how wide the image will be at each viewport width, so it downloads the correctly-sized source.
imageSizes uses the same max() formula as fluid() — including proportional overflow above maxScreen — so the browser always downloads the right image size even on ultrawide screens.
Setup (optional)
If you use named tokens from fluid.config.js, call configureImage once at your app entry point:
// Next.js App Router — app/layout.js
import { configureImage } from 'fluid-px/image'
import fluidConfig from './fluid.config.js'
configureImage({ config: fluidConfig })After that, token names resolve automatically everywhere.
imageSizes(minPx, maxPx, options?)
Generate from explicit pixel values — mirrors the fluid(min, max) signature.
import { imageSizes } from 'fluid-px/image'
// Image that scales from 79px on mobile to 170px on desktop, keeps growing on ultrawide
<Image
src={logo}
sizes={imageSizes(79, 170)}
/>
// → "(max-width: 375px) 79px, max(calc(46.9577px + 8.5446vw), calc(11.8056vw))"imageSizes('token-name', options?)
Generate from a named token in your fluid.config.js. Keeps image sizes automatically in sync with your layout tokens — change the token, the sizes string updates on next build.
// fluid.config.js
export default {
width: {
'navbar-logo-w': [79, 170],
'hero-image-w': [320, 900],
}
}
// Component
import { imageSizes } from 'fluid-px/image'
<Image src={logo} sizes={imageSizes('navbar-logo-w')} />
<Image src={heroImage} sizes={imageSizes('hero-image-w')} />
// Per-call config (if configureImage was not called)
import fluidConfig from './fluid.config.js'
<Image src={logo} sizes={imageSizes('navbar-logo-w', { config: fluidConfig })} />imageSizes({ mobile, desktop }, options?)
Generate from viewport fractions — for images that span a fraction of the viewport.
// Full-width on mobile, half-width on desktop
<Image sizes={imageSizes({ mobile: '100vw', desktop: '50vw' })} />
// → "(max-width: 768px) 100vw, 50vw"
// Custom breakpoint
<Image sizes={imageSizes({ mobile: '100vw', desktop: '33vw' }, { breakpoint: 1024 })} />
// → "(max-width: 1024px) 100vw, 33vw"Options
All forms accept an options object:
| Option | Type | Description |
|--------|------|-------------|
| minScreen | number | Override min screen for this call |
| maxScreen | number | Override max screen for this call |
| config | FluidConfig | Pass config inline (token form only) |
| breakpoint | number | Viewport fraction switch point (fraction form only, default 768) |
Config-file workflow (optional)
The config-file workflow lets you define all fluid values in one place as named tokens, then use them as Tailwind utility classes throughout the codebase.
Instead of writing fluid(24, 56) in every component, you define heading once and use text-heading everywhere.
1. Create fluid.config.js
export default {
screens: { min: 375, max: 1440 },
output: {
css: './src/styles/fluid.css',
tokens: './src/utils/fluid.tokens.js',
},
units: {
fontSize: 'rem', // optional — output rem instead of px for this category
},
fontSize: {
heading: [24, 56],
body: [14, 16],
// per-token screen override:
caption: { min: 10, max: 13, minScreen: 320, maxScreen: 768 },
},
spacing: {
'page-px': [16, 80],
sm: [8, 16],
},
borderRadius: {
card: [8, 20],
},
height: {
navbar: [60, 100],
},
// size generates .fluid-size-* CSS classes (square or rectangle)
size: {
icon: [24, 40], // square — width + height
avatar: [40, 80, 40, 80], // rectangle — [minW, maxW, minH, maxH]
},
}Supported token categories: fontSize, lineHeight, letterSpacing, spacing, borderRadius, width, height, size
2. Generate
npx fluid-px generateThis writes two files:
fluid.css— CSS custom properties on:rootusingmax()— proportional overflow above max screen is baked into each value, no extra media query neededfluid.tokens.js— token map for Tailwind (var(--fluid-...)strings)
3. Import CSS
/* globals.css */
@import './fluid.css';4. Wire up Tailwind
// tailwind.config.js
import { generateTailwindTokens } from 'fluid-px/tailwind'
import fluidConfig from './fluid.config.js'
const fluid = generateTailwindTokens(fluidConfig)
export default {
theme: {
extend: {
fontSize: fluid.fontSize,
spacing: fluid.spacing,
borderRadius: fluid.borderRadius,
height: fluid.height,
},
},
}5. Use in components
// named tailwind classes — values come from fluid.css via CSS vars
<nav className="h-navbar px-page-px">
<h1 className="text-heading">My App</h1>
<p className="text-body">Subtitle</p>
</nav>
// fluid-size-* utility class
<img className="fluid-size-avatar" />The math
For fluid(24, 56) with screens 375px → 1440px:
slope = (56 - 24) / (1440 - 375) = 0.03005
intercept = 24 - 0.03005 × 375 = 12.7
slopeVw = 0.03005 × 100 = 3.005vw
linear: calc(12.7px + 3.005vw)
At 375px → 12.7 + 3.005 × 3.75 = 24px ✓
At 768px → 12.7 + 3.005 × 7.68 = 35.8px
At 1440px → 12.7 + 3.005 × 14.4 = 56px ✓
overflowVw = 56 / 1440 × 100 = 3.889vw
overflow: calc(3.889vw)
At 1440px → 3.889 × 14.4 = 56px (equal — seamless handoff)
At 1920px → 3.889 × 19.2 = 74.7px (keeps growing)
At 2560px → 3.889 × 25.6 = 99.6px (keeps growing)
result: max(calc(12.7px + 3.005vw), calc(3.889vw))
Below 1440px: linear wins (linear > proportional in the interpolation range)
Above 1440px: overflow wins (proportional grows faster beyond max screen)License
MIT
