Fluid heading
A heading that scales along a line through two points, with slope and intercept printed.
npx shadcn@latest add https://ui.goodtabs.co/r/fluid-heading.json
With the @goodtabs namespace in components.json: npx shadcn@latest add @goodtabs/fluid-heading
Type that scales
slope 0.01458, intercept 13.33px
clamp(1.125rem, 0.833rem + 1.458vw, 2rem)
18px at 320 to 32px at 1280, with the derived clamp() shown.
Large
Fluid between 390 and 1280
slope 0.02022, intercept 22.11px
clamp(1.875rem, 1.382rem + 2.022vw, 3rem)
30px at 390 to 48px at 1280 in a bordered box.
Usage
import { FluidHeading } from "@/components/ui/fluid-heading"
<FluidHeading minPx={30} maxPx={48} minVw={390} maxVw={1280}>
Heading
</FluidHeading>Props
| Prop | Type | Default | Description |
|---|---|---|---|
| minPx | number | 24 | Size in pixels at the small viewport. |
| maxPx | number | 48 | Size in pixels at the large viewport. |
| minVw | number | 320 | Small viewport width in pixels. |
| maxVw | number | 1280 | Large viewport width in pixels. |
| showFormula | boolean | true | Shows slope, intercept and the clamp() value under the heading. |
Source
fluid-heading/fluid-heading.tsx
import * as React from "react";
import { cn } from "cn";
type FluidHeadingProps = React.ComponentProps<"h2"> & {
/** Size in px at the small viewport. */
minPx?: number;
/** Size in px at the large viewport. */
maxPx?: number;
/** Small viewport width in px. */
minVw?: number;
/** Large viewport width in px. */
maxVw?: number;
/** Prints slope, intercept and the clamp() under the heading. */
showFormula?: boolean;
};
const round = (n: number, d = 3) => Number(n.toFixed(d));
/**
* A heading whose size is a straight line through two points:
* slope = (max - min) / (maxVw - minVw), intercept = min - slope * minVw.
*/
export function FluidHeading({
minPx = 24,
maxPx = 48,
minVw = 320,
maxVw = 1280,
showFormula = true,
className,
children,
...props
}: FluidHeadingProps) {
const slope = (maxPx - minPx) / (maxVw - minVw);
const intercept = minPx - slope * minVw;
const value = `clamp(${round(minPx / 16)}rem, ${round(intercept / 16)}rem + ${round(slope * 100)}vw, ${round(maxPx / 16)}rem)`;
return (
<div className="flex flex-col gap-2">
<h2 className={cn("font-medium tracking-tight text-balance", className)} style={{ fontSize: value }} {...props}>
{children}
</h2>
{showFormula ? (
<p className="font-mono text-xs leading-relaxed text-muted-foreground tabular-nums">
slope {round(slope, 5)}, intercept {round(intercept, 2)}px
<br />
{value}
</p>
) : null}
</div>
);
}
For AI agents
Hand your agent this page, the markdown at /components/fluid-heading/prompt.md, or the Copy AI prompt button above.
When to use
- Page headings that should scale smoothly between two viewports
- Docs that teach where a clamp() value comes from
When not to use
- Body text: keep it fixed so zooming behaves
- Pairs where the vw coefficient passes about 4, which fails zoom at 200 percent
Agent notes
- Server component. The size is a clamp() computed at render, so there is no layout shift
- Set showFormula to false in production and keep it for teaching pages
- It renders an h2, pass className to restyle it
The maths
- Slope = (maxPx - minPx) / (maxVw - minVw), so 14 over 960 is 0.01458.
- Intercept = minPx - slope × minVw, so 18 - 0.01458 × 320 is 13.33px.
- Size = clamp(min, intercept + slope × 100vw, max).