Uses CSS clamp to scale type linearly between viewport sizes.
Fluid Type for Framer: responsive typography with clamp
Reusable Framer component for responsive type scaling with CSS clamp, built for effects, CMS lists, and layouts where Framer Fit Text falls short.
Works in situations where Framer Fit Text is too limited for production use.
Better for layered effects, reusable systems, and CMS collection lists.
Where it earns its place
- Responsive editorial headlines that need predictable scaling.
- CMS collection lists where Framer Fit Text cannot be used.
- Hero sections, cards, and sections that need type effects or more control.
A practical starting point
- Add the Fluid Type component where you would normally place a heading or text layer.
- Set the minimum size, maximum size, and viewport range to define the scaling curve.
- Use the tag and styling controls to keep semantic structure while matching the design system.
What It Is For
Fluid Type is a reusable Framer component for responsive typography. It uses CSS clamp() to scale text linearly between defined viewport values, which makes it useful when you want type that feels intentional across desktop and mobile without manually tuning every breakpoint.
It is especially valuable in builds where typography is doing more than simply filling a box. If the text needs layered effects, motion treatment, or use inside repeated CMS-driven layouts, this approach holds up better than relying on Framer’s built-in Fit Text.
Access Methods
1. Direct Import
import FluidType from "https://framer.com/m/FluidType-48DkXW.js@RYOH4zMcULQdfTPXy5gk"
2. URL
https://framer.com/m/FluidType-48DkXW.js@RYOH4zMcULQdfTPXy5gk
3. Source Code
import { addPropertyControls, ControlType, RenderTarget } from "framer"
import { motion } from "framer-motion"
function fluidClamp(
minSize: string,
maxSize: string,
minViewport: string = "320px",
maxViewport: string = "1280px",
viewportUnit: string = "vw"
): string {
const parseValueUnit = (str: string) => {
const match = str.match(/^([\d.]+)(.*)$/)
if (!match) throw new Error(`Invalid size format: ${str}`)
return { value: parseFloat(match[1]), unit: match[2] || "px" }
}
// Helper to convert everything to a single scale (pixels) for the math
const toPx = (val: number, unit: string) =>
unit === "rem" ? val * 16 : val
const min = parseValueUnit(minSize)
const max = parseValueUnit(maxSize)
const minVp = parseValueUnit(minViewport)
const maxVp = parseValueUnit(maxViewport)
// Convert values to pixels for accurate slope/intercept calculation
const minPx = toPx(min.value, min.unit)
const maxPx = toPx(max.value, max.unit)
const minVpPx = toPx(minVp.value, minVp.unit)
const maxVpPx = toPx(maxVp.value, maxVp.unit)
// Calculate slope based on pixel values
const slope = (maxPx - minPx) / (maxVpPx - minVpPx)
// Calculate y-intercept in pixels
const yIntersectionPx = minPx - slope * minVpPx
// Convert y-intercept back to the font's original unit (rem or px)
const yIntersection =
min.unit === "rem" ? yIntersectionPx / 16 : yIntersectionPx
const slopePercent = slope * 100
const slopeSign = slopePercent >= 0 ? "+ " : "- "
const preferredValue =
yIntersection === 0
? `${Math.abs(slopePercent).toFixed(4)}${viewportUnit}`
: `${yIntersection.toFixed(4)}${min.unit} ${slopeSign}${Math.abs(slopePercent).toFixed(4)}${viewportUnit}`
return `clamp(${minSize}, ${preferredValue}, ${maxSize})`
}
interface FluidTypeProps {
text: string
font: any
color: string
tag: string
textWrap: "wrap" | "balance" | "pretty"
textTransform: "string"
nudgeX: number
nudgeY: number
fluid: boolean
unit: "rem" | "px" | "vw" | "vh"
minSize: number
maxSize: number
minViewport: number
maxViewport: number
viewportUnit: "vw" | "vh" | "vmin" | "vmax"
}
/**
* @framerDisableUnlink
* @framerSupportedLayoutWidth fixed
* @framerSupportedLayoutHeight auto
*/
export default function FluidType(props: FluidTypeProps) {
const {
text,
font,
color,
tag,
textTransform,
textWrap,
nudgeX,
nudgeY,
fluid,
unit,
minSize,
maxSize,
minViewport,
maxViewport,
viewportUnit,
} = props
let fontSize = font.fontSize
// If fluid is enabled, calculate the clamp expression
if (fluid) {
const minSizeStr = `${minSize}${unit}`
const maxSizeStr = `${maxSize}${unit}`
const minViewportStr = `${minViewport}px`
const maxViewportStr = `${maxViewport}px`
fontSize = fluidClamp(
minSizeStr,
maxSizeStr,
minViewportStr,
maxViewportStr,
viewportUnit
)
}
let Tag = motion[tag]
const transform =
nudgeX !== 0 || nudgeY !== 0
? `translate(${nudgeX}em, ${nudgeY}em)`
: undefined
return (
<Tag
layout
style={{
// width: "fit-content",
margin: 0,
transform,
textWrap,
textTransform,
...font,
color,
// in canvas, viewport = browser/app window
fontSize: fluid
? RenderTarget.current() == RenderTarget.preview
? fontSize
: minSize == null
? font.fontSize
: `${minSize}${unit}`
: font.fontSize,
}}
>
{text}
</Tag>
)
}
let hidden = (props: FluidTypeProps) => !props.fluid
addPropertyControls(FluidType, {
text: {
type: ControlType.String,
title: "Text",
defaultValue: "Fluid type",
},
font: {
type: ControlType.Font,
title: "Font",
defaultValue: {
fontSize: 16,
lineHeight: 1.5,
},
defaultFontType: "sans-serif",
displayFontSize: true,
controls: "extended",
},
color: {
type: ControlType.Color,
title: "Color",
},
tag: {
type: ControlType.Enum,
title: "Tag",
options: ["h1", "h2", "h3", "h4", "h5", "h6", "p", "span"],
defaultValue: "span",
},
textWrap: {
type: ControlType.Enum,
title: "Wrap",
options: ["wrap", "nowrap", "balance", "pretty"],
optionTitles: ["Wrap", "No wrap", "Balance", "Pretty"],
defaultValue: "wrap",
},
textTransform: {
type: ControlType.Enum,
title: "Transform",
options: ["none", "uppercase", "lowercase", "capitalize"],
optionTitles: ["None", "UPPERCASE", "lowercase", "Capitalize"],
defaultValue: "none",
},
// ─── Nudge ────────────────────────────────────────────────────────────
nudgeX: {
type: ControlType.Number,
title: "Nudge X",
defaultValue: 0,
step: 0.01,
description: "Horizontal offset in em — scales with font size",
},
nudgeY: {
type: ControlType.Number,
title: "Nudge Y",
defaultValue: 0,
step: 0.01,
description: "Vertical offset in em — scales with font size",
},
fluid: {
type: ControlType.Boolean,
title: "Fluid",
defaultValue: false,
description: "Enable fluid typography scaling",
},
unit: {
type: ControlType.Enum,
title: "Unit",
options: ["rem", "px", "vw", "vh"],
defaultValue: "rem",
hidden,
description: "Unit for min/max sizes",
},
minSize: {
type: ControlType.Number,
title: "Min Size",
defaultValue: 1,
min: 0,
step: 0.1,
hidden,
description: "Minimum font size",
},
maxSize: {
type: ControlType.Number,
title: "Max Size",
defaultValue: 2,
min: 0,
step: 0.1,
hidden,
description: "Maximum font size",
},
minViewport: {
type: ControlType.Number,
title: "Min Viewport",
defaultValue: 320,
min: 0,
step: 10,
hidden,
description: "Minimum viewport width (px)",
},
maxViewport: {
type: ControlType.Number,
title: "Max Viewport",
defaultValue: 1280,
min: 0,
step: 10,
hidden,
description: "Maximum viewport width (px)",
},
viewportUnit: {
type: ControlType.Enum,
title: "Viewport Unit",
options: ["vw", "vh", "vmin", "vmax"],
defaultValue: "vw",
hidden,
description: "Viewport unit for scaling",
displaySegmentedControl: true,
},
})
Why It Matters
Framer Fit Text can be useful for quick resizing, but it breaks down in some of the situations that matter most in production. It does not support every effect pattern cleanly, and it cannot be used inside CMS collection lists.
That limitation shows up fast in real work:
- reusable cards that need consistent headline behavior
- editorial sections with expressive type treatment
- CMS collections where type still needs to feel designed
Fluid Type solves the problem by making the scaling rule explicit. Instead of letting Framer squeeze text to fit, you define the range and let the browser interpolate it.
How To Use It
Use Fluid Type anywhere you need responsive type with more control than a standard text layer provides.
- Set a minimum size for the smallest viewport you care about.
- Set a maximum size for the largest viewport in the layout.
- Define the viewport range so the scaling happens predictably between those two points.
- Apply your effects, semantics, and layout styles without depending on Fit Text.
In practice, it works well for:
- hero headlines
- section titles
- card titles in CMS collection lists
- oversized editorial typography
Why Clamp Wins Here
The value is not just that clamp() is responsive. The value is that it is legible as a system. You can define a range once, reuse it across components, and know exactly how the type will behave from mobile to desktop.
That makes Fluid Type useful in Framer projects where typography carries part of the visual identity.
FAQ
Why use Fluid Type instead of Framer Fit Text?
Framer Fit Text is useful for simple cases, but it does not support many effect-driven layouts and it cannot be used inside CMS collection lists. Fluid Type gives you predictable responsive scaling without those constraints.
Does Fluid Type replace a design system text scale?
No. It works best as a responsive layer on top of a deliberate type system. Use it where linear scaling improves readability or composition across breakpoints.
What makes clamp useful in Framer?
Clamp lets type interpolate between a minimum and maximum size across a viewport range, which makes responsive typography easier to control than fixed breakpoint overrides.