reelcn
GitHubBack to landing

Fit title

One-line title that sizes itself to fill the available width, measured with the real font once it has loaded.

FitTitle, fill
FitTitle, fill thumbnail, 16:9

Installation

npx shadcn add https://www.reelcn.dev/r/fit-title.json

Files land in src/reelcn/ with the items it builds on.

Usage

<Center>
  <FitTitle text="Season finale" effect="mask" />
</Center>

Props

PropTypeDescription
font"mono" | "heading" | "body" Default "heading".
delaynumberFrames to wait before entering.
durationnumberEnter duration in frames. Defaults to 0.6s. Some items reuse duration for an unrelated local timing. See that item's own prop docs.
exitnumber | booleanExit at the end of the parent `<Sequence>`. false keeps it on screen, a number sets the exit length in frames.
motionMotionPersonalityOverride the theme's motion personality.
posterbooleanRenders fully entered, with no exit: for a still frame or thumbnail. Defaults to false.
holdFramesnumberDelays the start of the exit window by this many frames, for a minimum on-screen hold. Defaults to 0.
staggernumberFrames between units. Defaults at 30fps: char 1, word 2, line 5.
styleCSSProperties
classNamestring
text*string
effectTextRevealEffect
weightnumber
colorstring
accentColorstring
accentWordsstring[]Words painted in the accent color. Case and punctuation are ignored.
align"left" | "right" | "center"
from"left" | "right" | "below" | "above"Where rise, blur and mask units travel in from.
orderStaggerOrderWhich unit enters first.
scaleFromnumberStarting scale for the scale effect: below 1 grows in, above 1 slams down.
maxSizenumberLargest font size in design units, reached when the text is short. Default 240.
maxWidthnumberWidth to fill, in design units. Defaults to the safe-zone width.
minSizenumberSmallest font size in design units. A title that would shrink below it wraps onto more lines instead.
split"word" | "char"

Built from

Use

  • Big one-word or short titles that should fill the frame at every aspect ratio
  • Titles of unknown length, such as data-driven names or chapter titles

Avoid

  • Headlines that should wrap onto several lines. Use text-reveal

Guides

Components take their colors from a theme, their timing from motion props and their layout from the canvas.

Source

registry/items/fit-title.tsx
/**
 * @title Fit Title
 * @category text
 * @description One-line title that sizes itself to fill the available width, measured with the real font once it has loaded.
 * @duration 30
 * @use Big one-word or short titles that should fill the frame at every aspect ratio
 * @use Titles of unknown length, such as data-driven names or chapter titles
 * @avoid Headlines that should wrap onto several lines. Use `text-reveal`
 * @tags fit, auto size, responsive, title, big type
 * @example
 * <Center>
 *   <FitTitle text="Season finale" effect="mask" />
 * </Center>
 */
import { graphemes, useTextMetrics, useTheme, useViewport } from "./core";
import { TextReveal, type TextRevealProps } from "./text-reveal";

export type FitTitleProps = Omit<TextRevealProps, "size" | "split"> & {
  /** Largest font size in design units, reached when the text is short. */
  maxSize?: number;
  /** Width to fill, in design units. Defaults to the safe-zone width. */
  maxWidth?: number;
  /** Smallest font size in design units. A title that would shrink below it wraps onto more lines instead. */
  minSize?: number;
  split?: "word" | "char";
};

export function FitTitle({
  text,
  maxSize = 240,
  maxWidth,
  minSize,
  font = "heading",
  weight,
  style,
  ...props
}: FitTitleProps) {
  const theme = useTheme();
  const { u, scale, width, safe } = useViewport();
  const stack = theme.fonts[font];
  const fontWeight = weight ?? (font === "heading" ? theme.headingWeight : 500);
  const metrics = useTextMetrics(text, { fontFamily: stack, fontSize: 100, fontWeight: fontWeight });
  const ready = metrics.ready;

  const available = maxWidth === undefined ? width - safe.x * 2 : u(maxWidth);
  // Matches TextReveal's tracking: -0.025em, none for mono.
  const tracking = font === "mono" ? 0 : -0.025;
  const perPx = ready ? metrics.width / 100 + graphemes(text).length * tracking : 0;
  // 2% headroom: each word or character is its own box, so kerning across boxes is lost and the line runs a bit wider.
  const fitted = perPx > 0 ? Math.min((available * 0.98) / perPx, u(maxSize)) : u(maxSize);
  const wraps = minSize !== undefined && fitted < u(minSize);
  const px = wraps ? u(minSize) : fitted;

  return (
    <TextReveal
      {...props}
      text={text}
      font={font}
      weight={fontWeight}
      size={px / scale}
      style={{
        whiteSpace: wraps ? undefined : "nowrap",
        maxWidth: wraps ? available : "none",
        visibility: ready ? undefined : "hidden",
        ...style,
      }}
    />
  );
}

On this page