Elliot Li
Resource

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.

Uses CSS clamp to scale type linearly between viewport sizes.

Works in situations where Framer Fit Text is too limited for production use.

Better for layered effects, reusable systems, and CMS collection lists.

Use It For

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.
How To Use It

A practical starting point

  1. Add the Fluid Type component where you would normally place a heading or text layer.
  2. Set the minimum size, maximum size, and viewport range to define the scaling curve.
  3. 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.

  1. Set a minimum size for the smallest viewport you care about.
  2. Set a maximum size for the largest viewport in the layout.
  3. Define the viewport range so the scaling happens predictably between those two points.
  4. 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.