Version 0.4.0 now released! See the release notes on GitHub Releases

→ A size that scales smoothly with the screen instead of jumping at fixed breakpoints.

Viewport Clamp

A viewport clamp is a size that scales smoothly with the screen instead of jumping at fixed breakpoints — a value that glides between a floor and a ceiling as the viewport grows. defineViewportClamp builds one for you: give it a reference screen size and it emits a native CSS clamp(), so you get fluid sizing with no stair-stepped media queries underneath.

It's the helper I use in every project — in most cases to build responsive variables, and now and then just to drop a fluid one-off value into a component. It's a helper like any other: a function that returns a value, resolved at build time, so the compiled CSS is a plain clamp(...) expression with nothing of Salty's left in it. If you want the deeper why behind that build-time line, the Compiler concept is the read.

What it's for

Most "responsive headline" code is three or four @media blocks doubling a font size at each breakpoint. The result is fine at the breakpoints and a little janky in between — and it's a fresh breakpoint to maintain every time the design shifts. CSS clamp() fixes the jank by scaling the value continuously between a floor and a ceiling. The catch is that typing the right clamp(min, fluid, max) for every size is tedious and easy to get subtly wrong. defineViewportClamp does that arithmetic for you: you give it a reference screen and a value, and it emits the clamp.

The practical payoff is that a size stays proportioned to the layout it was designed in. A headline keeps looking like the design a couple hundred pixels down from where you drew it — no new line breaks appearing, no element suddenly cramped — instead of holding one fixed size until a breakpoint snaps it to another. That means fewer breakpoints to write and far fewer awkward in-between states to babysit.

Two shapes of use cover almost everything:

  • Responsive design tokens — the main event. Define your spacing scale and type scale through a clamp, so the whole system scales together. This is where it earns its keep; there's a full worked version below.
  • A one-off fluid value — occasionally you just want this padding or this font size to be fluid, inline in a component, without promoting it to a token. That works too.

Defining a clamp

Create a clamp with defineViewportClamp, giving it a reference screen size. Define one per device target you care about — a desktop/HD reference and a mobile reference cover most projects.

/styles/helpers.css.ts
import { defineViewportClamp } from "@salty-css/core/helpers";

// Tuned for desktop/HD screens (1920px reference)
export const fhdClamp = defineViewportClamp({
  screenSize: 1920,
  minMultiplier: 1,
  maxMultiplier: 1.25,
});

// Tuned for mobile screens (640px reference)
export const mobileClamp = defineViewportClamp({
  screenSize: 640,
  minMultiplier: 0.75,
  maxMultiplier: 1,
});

// Mobile portrait, scaling on the vertical axis instead of width
export const mobilePortraitClamp = defineViewportClamp({
  screenSize: 375,
  minMultiplier: 0.75,
  maxMultiplier: 1,
  axis: "vertical",
});

The minMultiplier and maxMultiplier set how far the value is allowed to shrink or grow relative to the value you pass at the call site. A 1 / 1.25 pair means "never smaller than the reference value, up to 25% larger on big screens."

Using a clamp

Call the clamp with the value you want at the reference size. It returns a clamp() string, so it drops into any size property — this is not a font-size-only tool. Margins, padding, gaps, border radius, and positioning all take the same fluid treatment.

Example
import { styled } from "@salty-css/react/styled";
import { fhdClamp, mobileClamp } from "../styles/helpers.css";

export const ResponsiveText = styled("div", {
  base: {
    fontSize: fhdClamp(96),  // 96px at a 1920px viewport, fluid around it
    padding: fhdClamp(32),
    borderRadius: fhdClamp(8),

    // swap to a mobile-tuned clamp below a breakpoint if you want a different curve
    "@largeMobileDown": {
      fontSize: mobileClamp(48),
    },
  },
});

You don't have to pair it with a media query — a single clamp often carries a value all the way from phone to desktop on its own. The mobile clamp above is there for when you want a genuinely different scaling curve on small screens, not because the base clamp needs rescuing.

How it works

Under the hood the helper builds a CSS clamp(min, fluid, max) where:

  1. the minimum comes from minMultiplier (or an explicit min override, below),
  2. the fluid middle scales linearly with the viewport — vw on the horizontal axis, vh on the vertical,
  3. the maximum comes from maxMultiplier (or an explicit max override).

So fhdClamp(96) defined with screenSize: 1920, minMultiplier: 1, maxMultiplier: 1.25 resolves to a value that is 96px at a 1920px viewport, can grow to 120px (96 × 1.25) on larger screens, and scales proportionally between. The screenSize is the anchor: it's the viewport width at which the fluid middle exactly equals the value you passed.

You can override the min and max per call, as the second and third arguments — handy for the occasional value that needs a hard floor or ceiling the multipliers don't give:

Example
fhdClamp(96, 42, 240)             // exact min 42px, exact max 240px
fhdClamp(96, undefined, 240)      // keep the multiplier-based min, override only the max

Worked example

For fhdClamp defined with screenSize: 1920, minMultiplier: 0.5, maxMultiplier: 1.25, calling fhdClamp(96) produces roughly clamp(48px, 5vw, 120px). Resolved at a few common widths:

Viewport widthLinear value (5vw)After clamp(48, …, 120)
480px24px48px (min)
1024px51.2px51.2px
1366px68.3px68.3px
1920px96px96px (reference)
2560px128px120px (max)

The value glides linearly through the middle and holds flat at both ends. The reference value — 96px at 1920px here — is the point where the fluid formula matches your input exactly.

Building responsive tokens

This is the pattern that makes viewport clamps worth adopting rather than sprinkling. Instead of calling a clamp at every style, call it once inside your variables, so your whole spacing and type system scales fluidly and every component just reads a plain token.

/styles/variables.css.ts
import { defineVariables } from "@salty-css/core/factories";
import { fhdClamp, mobileClamp } from "./helpers.css";

export default defineVariables({
  responsive: {
    base: {
      spacing: {
        small: fhdClamp(8),
        medium: fhdClamp(20),
        large: fhdClamp(36),
        pageMargin: fhdClamp(120),
      },
      fontSize: {
        headline: { small: fhdClamp(24), regular: fhdClamp(36), large: fhdClamp(64) },
        body: { small: fhdClamp(14), regular: fhdClamp(16), large: fhdClamp(24) },
      },
    },
    "@largeMobileDown": {
      spacing: {
        pageMargin: mobileClamp(30),
      },
      fontSize: {
        headline: { small: mobileClamp(24), regular: mobileClamp(32), large: mobileClamp(42) },
      },
    },
  },
});

Components then reference the tokens by path and never mention a clamp at all:

Example
import { styled } from "@salty-css/react/styled";

export const Header = styled("header", {
  base: {
    padding: "{spacing.large}",
    fontSize: "{fontSize.body.regular}",
  },
});

Now every dimension in the system scales proportionally, the clamp definitions live in one file, and swapping to mobile-tuned curves at a breakpoint is a change in one place rather than across every component. (For more on the responsive scope itself, see Variables & Tokens.)

Reference

Clamp options — defineViewportClamp(options)

OptionDescriptionDefault
screenSizeReference screen width/height in pixelsRequired
minMultiplierMultiplier for the minimum output (e.g. 0.75 = 75% of value)Optional
maxMultiplierMultiplier for the maximum output (e.g. 1.25 = 125% of value)Optional
axisAxis for scaling — 'horizontal' (vw) or 'vertical' (vh)'horizontal'

Call arguments — clamp(value, min?, max?)

ArgumentDescriptionDefault
0Value the property should have at screenSizeRequired
1Minimum override (pass undefined to skip)Optional
2Maximum overrideOptional

Edge cases

  • Min greater than max. If your multipliers or overrides flip the order, the browser still honors clamp(a, b, c) semantics — the larger end wins. Salty doesn't reorder for you, so double-check the multipliers.
  • Reference size larger than every real viewport. If screenSize: 1920 is bigger than any screen your users have, the value pins to min everywhere — probably not what you want. Drop screenSize to the upper end of your actual target range.
  • Negative multipliers. Allowed (the value can go negative — useful for shifting something off-screen), but rarely what you mean for a size. Use 0 if you want the value to collapse to zero on small screens.
  • Axis selection. 'horizontal' uses vw, 'vertical' uses vh. For portrait mobile layouts where height varies more than width, vertical is often the better anchor.

Best practices

Define a small set of clamps — an HD one and a mobile one go a long way — and use them consistently so everything scales in proportion. Set the multipliers to control how much a value may shrink or grow, and reach for the per-call min/max overrides only for the exceptions. And remember it applies to any size property, not just type: unifying spacing, gaps, and radii under the same clamps is what makes a whole layout feel like it scales as one piece.