Skip to content
animorio
IT
Back to the library

The word with handles

Text, HoverGSAP, CSS onlyAdvancedCompressed weight: Demo 9.5 KBGSAP 28 KBFreeDownload the zip

A word inside a selection frame, like a layer in a design tool: the four handles work, and pulling a corner sets the word again to fill the frame, using the font’s width axis.

The code

A folder that runs on its own: index.html, style.css, script.js and the libraries from a CDN. The React and Vue tabs hold the same demo as a ready component, which switches its animations off when it leaves the page.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>The word with handles</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/anybody@5.3.0/wdth.css">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/geist-mono@5.3.0/index.css">
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <!-- the board is one screen: two rulers, the word inside its selection frame, a colleague's cursor that types -->
  <main class="board" data-board>
    <div class="ruler ruler--x" data-ruler="x" aria-hidden="true">
      <span class="ruler__band"></span>
      <span class="ruler__numbers" data-numbers="x"></span>
      <span class="ruler__mark" data-mark="x"></span>
    </div>
    <div class="ruler ruler--y" data-ruler="y" aria-hidden="true">
      <span class="ruler__band"></span>
      <span class="ruler__numbers" data-numbers="y"></span>
      <span class="ruler__mark" data-mark="y"></span>
    </div>
    <span class="ruler__corner" aria-hidden="true"></span>

    <header class="top">
      <p class="brand">Offcut</p>
      <p class="top__line">A small studio for printed things</p>
    </header>

    <!-- dashed guides along the frame's edges, shown while a corner is being pulled -->
    <div class="guides" aria-hidden="true"><i class="guide guide--left"></i><i class="guide guide--right"></i><i class="guide guide--top"></i><i class="guide guide--bottom"></i></div>

    <div class="frame" data-frame>
      <h1 class="word" data-word aria-label="Offcut"><span class="word__letter" aria-hidden="true">O</span><span class="word__letter" aria-hidden="true">F</span><span class="word__letter" aria-hidden="true">F</span><span class="word__letter" aria-hidden="true">C</span><span class="word__letter" aria-hidden="true">U</span><span class="word__letter" aria-hidden="true">T</span></h1>
      <span class="frame__edge frame__edge--top" aria-hidden="true"></span>
      <span class="frame__edge frame__edge--right" aria-hidden="true"></span>
      <span class="frame__edge frame__edge--bottom" aria-hidden="true"></span>
      <span class="frame__edge frame__edge--left" aria-hidden="true"></span>
      <button class="handle handle--nw" type="button" data-handle="nw" aria-label="Top left corner: drag, or use the arrow keys, to resize the word"></button>
      <button class="handle handle--ne" type="button" data-handle="ne" aria-label="Top right corner: drag, or use the arrow keys, to resize the word"></button>
      <button class="handle handle--sw" type="button" data-handle="sw" aria-label="Bottom left corner: drag, or use the arrow keys, to resize the word"></button>
      <button class="handle handle--se" type="button" data-handle="se" aria-label="Bottom right corner: drag, or use the arrow keys, to resize the word"></button>
      <p class="frame__size" data-size aria-live="off"><span data-size-box>0 × 0</span><span class="frame__axis" data-size-axis>width 100</span></p>
    </div>

    <!-- a colleague: the cursor, the name and the bubble where the greeting is typed -->
    <div class="peer" data-peer aria-hidden="true">
      <svg class="peer__arrow" viewBox="0 0 24 26" width="24" height="26"><path d="M2 2 L2 21 L7.4 16.2 L11 24 L14.6 22.4 L11.2 14.8 L18.6 14.4 Z"/></svg>
      <span class="peer__name">Ines</span>
      <span class="peer__bubble" data-bubble><span data-typed></span><i class="peer__caret" data-caret></i></span>
    </div>

    <p class="hint" data-hint>Pull a corner: the word is set again to fill its frame.</p>
    <button class="replay" type="button" data-replay data-autoclic>Replay</button>
  </main>

  <script src="https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/gsap.min.js"></script>
  <script src="script.js"></script>
</body>
</html>

Playground

Change values, colours and text with the controls and watch the animation respond live. Or write in the code yourself. Then download the folder with your changes.

36 values to change

Or ask Claude Code for it

With the Animorio connector, Claude Code takes it from here, guide included, and fits it into your project. Paste this request:

Take “The word with handles” from Animorio and fit it into my site

What it does

The blue board of an invented studio, squared like a blueprint, with two rulers. A colleague’s cursor, Ines, comes in and her bubble types “hello, we are”; then the six letters of OFFCUT arrive one at a time from the right, on a spring, and the selection frame draws itself around them: four yellow lines, four handles, and its measurements underneath. Ines rubs out the greeting, takes the bottom right corner and pulls: the word widens, then narrows, always flush with the frame. From there on the handles are yours: every corner can be dragged with the mouse, with a finger or with the arrow keys, and the rulers mark how wide and how tall the frame is.

How it works

The font, measured once

Once the font is in, the script takes three measures on a canvas: how tall the capitals are, how far the font rises and how far it drops, all as shares of the type size. Then it sets the word at 200 px and reads its width at every step of the wdth axis, from 50 to 150 every 10: eleven measures, in ems, good for any size.

From the height, the size

The frame’s height, less a little air above and below, is how tall the capitals must be: the type size is that height divided by the share of the size the capitals take. The line is as tall as the capitals, and a shift downwards (half of: capitals, minus ascent, plus descent) sets them exactly inside it.

From the width, the axis

The width that is left, divided by the type size, says how many ems wide the word must be. Among the eleven measures the script finds the two that hold it and interpolates the font-stretch value. If the frame asks for less than wdth 50 or more than wdth 150 the axis stops there and a scaleX makes up the rest: “stretched” shows under the frame.

The handles

Each handle is a button. On pointerdown it captures the pointer and keeps the distance between the point touched and the corner; while dragging, that corner follows and the opposite one stays put, between a minimum of 150 by 64 px and the edges of the board. The arrow keys move it by 10 px, 40 with Shift. On release the letters settle with an elastic; two clicks on the frame put it back as it was.

The rulers

Four CSS variables on the board, --ox, --oy, --fw and --fh, say where the frame is: the frame itself, the rulers and the dashed guides all read them. The ticks are three gradients repeated every 10, 50 and 100 px, moved with background-position so that zero falls on the frame’s corner; a yellow band covers its width and height, and a red line follows the pointer with gsap.quickTo, in 0.2 seconds.

The opening

One timeline. The greeting is typed in a second, by a counter that slices the sentence, and the text cursor blinks in steps; the letters start 40 px to the right, at scale 0.9, 0.03 seconds apart, on a curve that is the solution of a spring (stiffness 400, damping 40, mass 4); then lines, handles, measurements. The colleague pulls the corner by calling the same function the handles use. Touch a handle before it ends and everything stops: you are in charge.

Values to change

NameFileDefaultWhat it changes
WEIGHTscript.js780The word’s weight.
AXISscript.js[50, 150]The two ends of the width axis: change them if you change the font.
PADscript.js[0.07, 0.13]The air between word and frame, as shares of the frame’s height: at the sides, and above and below.
MINscript.js[150, 64]The smallest frame, in pixels.
GREETING, TYPE_TIMEscript.js'hello, we are', 1What the colleague types, and in how many seconds.
LETTER_GAP, SPRINGscript.js0.03, 400 / 40 / 4The seconds between one letter and the next, and the spring they arrive on: stiffness, damping, mass.
KEY_STEPscript.js10The pixels per arrow key (four times as much with Shift).
--blue, --yellowstyle.css#2343e6, #ffd21fThe board’s colour, and that of the frame, bands and measurements.

Try them in the Playground

When to use it

For the name of a studio, a product or a person at the top of a page about design: a portfolio, an agency, a foundry, the page of a variable font.

On phones

One ruler only, at the top. The frame starts taller, with the word narrow, and the corners are dragged with a finger: around each handle there is a 46 px area. The colleague pulls less to the right, because there is little room.

With reduced motion

No opening: word, frame and measurements are there at once, the colleague does not appear and the letters do not bounce. The handles work, and the button at the bottom puts the frame back as it was.

Pitfalls

  • The measures must be taken after the font has arrived (document.fonts.load): before that, canvas and page measure the fallback and the word spills out of the frame.
  • With a line height smaller than the type size the text is not centred on the capitals but on ascent plus descent: without the computed shift, the word sits higher or lower than the frame depending on the font.
  • The letters are inline-block, to move them one at a time, and there is no kerning between one span and the next: the width has to be measured on the word built that way, not on plain text.
  • The handles need touch-action: none, or on a phone the browser takes the first movement and a pointercancel comes in.
  • The point touched is never the exact corner: if that distance is not kept, the frame jumps at the first movement.
  • The opening and the visitor’s hands write the same variables: as soon as a handle is touched the timeline must be stopped and everything taken to its final state, or the two fight over the frame.

Libraries

GSAP 3.15.0, Anybody (font variabile) 5.3.0, Geist Mono (font variabile) 5.3.0.

Compressed with gzip, the three demo files weigh 9.5 KB; the libraries: GSAP 28 KB.

Testing

Tested on 30 September 2026

  • Desktop: Done
  • Phone: Done
  • Reduced motion: Done
  • Safari on iPhone: Done

License

Use it in your own projects and in your clients’ projects. Do not resell it or redistribute it as is. GSAP stays under its own Standard license. Full license.

All animations