Fit title
One-line title that sizes itself to fill the available width, measured with the real font once it has loaded.
FitTitle, fill
Installation
npx shadcn add https://www.reelcn.dev/r/fit-title.jsonFiles 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,
}}
/>
);
}