Skip to content
animorio
IT
All components

The message in a bottle contact form

Forms, SVG, HoverGSAP, CSS onlyAdvancedCompressed weight: Demo 15 KBGSAP 28 KBFreeDownload the zip

In the rich style, colourful and full of things: the contact form of a seaside inn, The Driftwood Inn. On the left a corner of the beach at sunset, with the sun going down, the waves, a sailing boat on the horizon and the water's edge washing up and back; on the sand an empty bottle, its cork, a coil of twine and a sheet of ruled paper. While you write the letter fills with ink: the words of the message become lines of handwriting with the nib running at the end, the email becomes the return address with a sand dollar stamp, the name the signature with its flourish. Sent, the sheet rolls up, is tied with twine and slides into the bottle, the cork flies into the neck, the bottle tips over, a wave runs up the beach, picks it up and carries it out towards the sun until it is a glint.

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 message in a bottle contact form</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/bricolage-grotesque@5.3.0/index.css">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/caveat@5.3.0/index.css">
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <main class="stage">
    <!-- the contact form of a seaside inn: beside it a corner of the beach at sunset, with an empty bottle standing in
         the sand, its cork and a sheet of paper. While you write, the letter fills with ink: the message line by line
         with a nib running at the end, the email as the return address with a sand dollar stamp, the name as the
         signature. Sent, the letter rolls up, is tied with twine and slides into the bottle, the cork flies in, the
         bottle tips over, rolls to the water and drifts away on the waves to the sun -->
    <section class="mare" aria-labelledby="mare-title" data-mare data-autoclic>
      <div class="mare__scene" aria-hidden="true">
        <svg class="scene" viewBox="0 0 300 360" data-svg>
          <defs>
            <linearGradient id="mare-sky" x1="0" y1="0" x2="0" y2="1">
              <stop offset="0" class="stop-sky-top"/>
              <stop offset="1" class="stop-sky"/>
            </linearGradient>
            <linearGradient id="mare-sea" x1="0" y1="0" x2="0" y2="1">
              <stop offset="0" class="stop-sea-far"/>
              <stop offset="1" class="stop-sea"/>
            </linearGradient>
            <linearGradient id="mare-sand" x1="0" y1="0" x2="0" y2="1">
              <stop offset="0" class="stop-sand-wet"/>
              <stop offset="0.16" class="stop-sand"/>
              <stop offset="1" class="stop-sand-low"/>
            </linearGradient>
            <linearGradient id="mare-glass" x1="0" y1="0" x2="1" y2="0">
              <stop offset="0" class="stop-glass-dark"/>
              <stop offset="0.3" class="stop-glass-light"/>
              <stop offset="0.55" class="stop-glass"/>
              <stop offset="1" class="stop-glass-dark"/>
            </linearGradient>
            <radialGradient id="mare-sun" cx="0.5" cy="0.5" r="0.5">
              <stop offset="0" class="stop-sun"/>
              <stop offset="0.45" class="stop-sun"/>
              <stop offset="1" class="stop-sun-glow"/>
            </radialGradient>
            <linearGradient id="mare-wood" x1="0" y1="0" x2="0" y2="1">
              <stop offset="0" class="stop-wood-light"/>
              <stop offset="1" class="stop-wood"/>
            </linearGradient>
            <clipPath id="mare-room">
              <rect x="0" y="0" width="300" height="360" rx="18"/>
            </clipPath>
            <clipPath id="mare-above">
              <rect x="0" y="0" width="300" height="170"/>
            </clipPath>
          </defs>

          <g clip-path="url(#mare-room)">
            <!-- the sky at sunset, two clouds drifting -->
            <rect x="0" y="0" width="300" height="172" fill="url(#mare-sky)"/>
            <g class="clouds" data-clouds>
              <g data-cloud="0" transform="translate(40 52)">
                <path class="cloud" d="M0 14 C 0 6, 9 2, 16 6 C 20 -2, 34 -3, 39 5 C 46 2, 56 7, 54 14 Z"/>
                <path class="cloud__light" d="M8 10 C 14 7, 20 7, 26 9"/>
              </g>
              <g data-cloud="1" transform="translate(196 34)">
                <path class="cloud" d="M0 11 C 1 5, 8 2, 13 5 C 17 -1, 28 -1, 31 5 C 37 3, 44 7, 42 11 Z"/>
              </g>
            </g>
            <!-- the sun going down behind the horizon -->
            <g clip-path="url(#mare-above)">
              <circle cx="220" cy="160" r="46" fill="url(#mare-sun)" class="sun__glow"/>
              <circle cx="220" cy="160" r="20" class="sun"/>
            </g>
            <!-- the sea: a band that darkens towards the beach, a sailing boat on the horizon, the glints under the sun -->
            <rect x="0" y="168" width="300" height="96" fill="url(#mare-sea)"/>
            <path d="M0 168.5 H300" class="horizon"/>
            <g class="boat" data-boat transform="translate(56 166)">
              <path d="M-7 0 H7 L5 2.6 H-5 Z" class="boat__hull"/>
              <path d="M0 -0.6 V-15" class="boat__mast"/>
              <path d="M0.8 -14 L8 -2 H0.8 Z" class="boat__sail"/>
              <path d="M-0.8 -11 L-6 -2 H-0.8 Z" class="boat__jib"/>
            </g>
            <g class="glints" data-glints></g>
            <g class="waves" data-waves></g>
            <!-- the bottle out at sea, drawn again small while it drifts -->
            <g data-floater opacity="0">
              <g data-floater-tilt>
                <path class="bottle__body" d="M-22 -9 C -22 -12, -19 -13, -16 -13 H6 C 10 -13, 12 -10, 14 -6 H22 V6 H14 C 12 10, 10 13, 6 13 H-16 C -19 13, -22 12, -22 9 Z"/>
                <rect x="22" y="-4.5" width="7" height="9" rx="2" class="cork"/>
                <rect x="-16" y="-4" width="26" height="8" rx="4" class="roll__paper"/>
                <path d="M-18 -9 H8" class="bottle__shine"/>
              </g>
              <path d="M-26 14 C -16 10, 16 10, 28 14" class="floater__ripple"/>
            </g>

            <!-- the beach: the water's edge that washes up and back, wet sand, then the dry sand -->
            <rect x="0" y="240" width="300" height="120" fill="url(#mare-sand)"/>
            <path class="wash" data-wash d="M0 248 C 60 252, 120 246, 180 250 C 230 253, 270 247, 300 250 V 236 H 0 Z"/>
            <path class="foam" data-foam d="M0 248 C 60 252, 120 246, 180 250 C 230 253, 270 247, 300 250"/>
            <g class="grains" data-grains></g>

            <!-- things on the sand: a piece of driftwood, two shells, pebbles, a coil of twine -->
            <g class="drift" transform="translate(196 342) rotate(-6)">
              <path d="M-70 -7 C -50 -12, 10 -11, 52 -8 C 60 -7, 64 -3, 62 2 C 40 6, -30 7, -66 5 C -74 4, -76 -4, -70 -7 Z" fill="url(#mare-wood)"/>
              <path d="M-60 -4 C -30 -6, 10 -6, 48 -4 M-50 1 C -20 0, 20 0, 44 2 M-8 -8 C -2 -6, 4 -6, 8 -8" class="drift__grain"/>
              <ellipse cx="22" cy="-2" rx="4" ry="2.2" class="drift__knot"/>
            </g>
            <g class="shell" transform="translate(26 318) rotate(-14)">
              <path d="M0 0 C -9 -2, -13 -11, -9 -17 C -5 -22, 5 -22, 9 -17 C 13 -11, 9 -2, 0 0 Z" class="shell__body"/>
              <path d="M0 -1 L-7 -16 M0 -1 L-3 -19 M0 -1 L1 -20 M0 -1 L5 -18 M0 -1 L8 -14" class="shell__rib"/>
              <path d="M-4 0 H4 L3 3 H-3 Z" class="shell__hinge"/>
            </g>
            <g class="shell shell--small" transform="translate(278 300) rotate(22) scale(0.7)">
              <path d="M0 0 C -9 -2, -13 -11, -9 -17 C -5 -22, 5 -22, 9 -17 C 13 -11, 9 -2, 0 0 Z" class="shell__body"/>
              <path d="M0 -1 L-7 -16 M0 -1 L-3 -19 M0 -1 L1 -20 M0 -1 L5 -18 M0 -1 L8 -14" class="shell__rib"/>
            </g>
            <ellipse cx="128" cy="344" rx="7" ry="4" class="pebble"/>
            <ellipse cx="139" cy="347" rx="4" ry="2.6" class="pebble pebble--light"/>
            <ellipse cx="56" cy="352" rx="5" ry="3" class="pebble pebble--light"/>

            <!-- the bottle on the sand: shadow, the glass behind, the letter when it goes in, the glass in front -->
            <g data-bottle-shadow>
              <ellipse cx="78" cy="309" rx="22" ry="5" class="bottle__shadow"/>
            </g>
            <g data-bottle>
              <g data-bottle-turn>
                <path class="bottle__back" d="M64 308 C 60 308, 58 305, 58 301 V 256 C 58 246, 66 240, 72 236 V 222 H 84 V 236 C 90 240, 98 246, 98 256 V 301 C 98 305, 96 308, 92 308 Z"/>
                <g data-inside></g>
                <path class="bottle__body" d="M64 308 C 60 308, 58 305, 58 301 V 256 C 58 246, 66 240, 72 236 V 222 H 84 V 236 C 90 240, 98 246, 98 256 V 301 C 98 305, 96 308, 92 308 Z"/>
                <path d="M71 222 H85 V 219 C 85 217.5, 84 217, 82.5 217 H 73.5 C 72 217, 71 217.5, 71 219 Z" class="bottle__lip"/>
                <path d="M63 258 C 63 250, 67 246, 72 242" class="bottle__shine"/>
                <path d="M63 266 V 298" class="bottle__shine"/>
                <path d="M93 270 V 296" class="bottle__shine bottle__shine--thin"/>
                <path d="M58 300 C 66 304, 90 304, 98 300" class="bottle__base"/>
                <!-- a scrap of an old label -->
                <path d="M64 270 L 92 268 L 91 284 L 66 285 Z" class="bottle__label"/>
                <path d="M68 275 H 86 M 69 279 H 82" class="bottle__label-line"/>
                <g data-cork-in opacity="0">
                  <path d="M72 223 C 72 213, 73 210, 74 209 H 82 C 83 210, 84 213, 84 223 Z" class="cork"/>
                  <path d="M74 213 H82 M73.5 217 H82.5" class="cork__dots"/>
                </g>
              </g>
            </g>

            <!-- the cork and the twine lying on the sand -->
            <g data-cork transform="translate(118 312) rotate(70)">
              <path d="M-5 -8 L 5 -8 L 6 8 L -6 8 Z" class="cork"/>
              <ellipse cx="0" cy="-8" rx="5" ry="1.8" class="cork__top"/>
              <circle cx="-1.5" cy="-2" r="0.7" class="cork__dot"/>
              <circle cx="2" cy="2" r="0.6" class="cork__dot"/>
              <circle cx="-2" cy="4.5" r="0.6" class="cork__dot"/>
            </g>
            <g class="twine" transform="translate(110 330)">
              <ellipse cx="0" cy="0" rx="9" ry="3.4" class="twine__coil"/>
              <ellipse cx="0" cy="-1.2" rx="6" ry="2.2" class="twine__coil"/>
              <path d="M8 1 C 14 3, 18 1, 22 4" class="twine__end"/>
            </g>

            <!-- the letter on a flat stone: ruled paper, the return address, the stamp, the ink lines, the signature -->
            <g data-letter-shadow>
              <path d="M146 268 L 282 262 L 286 342 L 150 348 Z" class="letter__shadow"/>
            </g>
            <g data-letter transform="translate(214 302) rotate(-3)">
              <g data-paper>
                <path d="M-66 -40 L 66 -40 L 66 40 C 40 42, -40 42, -66 40 Z" class="paper"/>
                <path d="M-66 -40 L 66 -40 L 66 -34 L -66 -34 Z" class="paper__top"/>
                <g class="paper__rules">
                  <path d="M-58 -16 H58 M-58 -6 H58 M-58 4 H58 M-58 14 H58 M-58 24 H58"/>
                </g>
                <path d="M-52 -40 V 40" class="paper__margin"/>
                <text x="-47" y="-25" class="paper__from" data-from></text>
                <g data-stamp transform="translate(52 -24)" opacity="0">
                  <circle r="9" class="stamp"/>
                  <g class="stamp__petals">
                    <ellipse cx="0" cy="-4" rx="1.4" ry="3"/>
                    <ellipse cx="0" cy="-4" rx="1.4" ry="3" transform="rotate(72)"/>
                    <ellipse cx="0" cy="-4" rx="1.4" ry="3" transform="rotate(144)"/>
                    <ellipse cx="0" cy="-4" rx="1.4" ry="3" transform="rotate(216)"/>
                    <ellipse cx="0" cy="-4" rx="1.4" ry="3" transform="rotate(288)"/>
                  </g>
                  <circle r="11.5" class="stamp__ring"/>
                </g>
                <g data-lines></g>
                <g data-blots></g>
                <text x="56" y="35" class="paper__sign" text-anchor="end" data-sign></text>
                <path d="M10 37 C 24 40, 40 38, 58 36" class="paper__flourish" data-flourish/>
                <g data-nib opacity="0">
                  <path d="M0 0 L 4 -11 L 7 -10 Z" class="nib"/>
                  <path d="M4 -11 L 14 -34" class="nib__quill"/>
                  <path d="M14 -34 C 8 -30, 6 -22, 5 -14 M14 -34 C 18 -28, 16 -20, 8 -12" class="nib__vane"/>
                </g>
              </g>
              <!-- the roll the letter becomes -->
              <g data-roll opacity="0">
                <rect x="-6" y="-40" width="12" height="80" rx="6" class="roll__paper"/>
                <ellipse cx="0" cy="-40" rx="6" ry="2.4" class="roll__end"/>
                <path d="M0 -40 m -3 0 a 3 1.2 0 1 0 6 0 a 1.6 0.6 0 1 0 -3 0" class="roll__spiral"/>
                <g data-tie opacity="0">
                  <path d="M-6.4 -3 H6.4 M-6.4 1 H6.4" class="tie"/>
                  <path d="M5 -1 C 12 -6, 14 2, 18 -2 M5 -1 C 11 4, 14 8, 19 6" class="tie"/>
                </g>
              </g>
            </g>

            <rect x="0.5" y="0.5" width="299" height="359" rx="17.5" class="scene__edge"/>
          </g>
        </svg>
      </div>

      <div class="mare__side">
        <form class="mare__form" novalidate data-form>
          <p class="mare__kicker">The Driftwood Inn</p>
          <h1 class="mare__title" id="mare-title">Write to us by sea</h1>

          <div class="field" data-field="name">
            <label class="field__label" for="mare-name">Your name</label>
            <input class="field__input" id="mare-name" name="name" type="text" autocomplete="name" aria-describedby="mare-err-name" data-input>
            <span class="field__err" id="mare-err-name" data-err></span>
          </div>

          <div class="field" data-field="email">
            <label class="field__label" for="mare-email">Email</label>
            <input class="field__input" id="mare-email" name="email" type="email" autocomplete="email" inputmode="email" aria-describedby="mare-err-email" data-input>
            <span class="field__err" id="mare-err-email" data-err></span>
          </div>

          <div class="field" data-field="message">
            <label class="field__label" for="mare-message">Message</label>
            <textarea class="field__input" id="mare-message" name="message" rows="2" aria-describedby="mare-err-message" data-input></textarea>
            <span class="field__err" id="mare-err-message" data-err></span>
          </div>

          <div class="mare__foot">
            <button class="send" type="submit" data-send>
              <svg class="send__wave" viewBox="0 0 22 14" aria-hidden="true">
                <path d="M1 9 C 4 5, 7 5, 9 8 C 11 11, 14 11, 16 8 C 18 5, 20 5, 21 6"/>
                <path d="M1 13 C 4 10, 7 10, 9 12 C 11 14, 14 14, 16 12" class="send__wave-low"/>
              </svg>
              <span class="send__word" data-word>Send by sea</span>
            </button>
            <span class="mare__note">We answer within a day.</span>
          </div>
        </form>

        <!-- what stays once the bottle is at sea -->
        <div class="done" data-done>
          <p class="done__title">Your bottle is at sea</p>
          <p class="done__text">It will reach the inn with the next tide. We write back to <span data-done-mail></span>.</p>
          <button class="again" type="button" data-again>Write another</button>
        </div>
      </div>
      <p class="sr" aria-live="polite" data-live></p>
    </section>
  </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.

37 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 message in a bottle contact form” from Animorio and fit it into my site

What it does

A real contact form, with name, email and message, and a beach beside it that tells its story. Every field writes on the letter on the sand: the message word by word in handwriting, with the nib running at the end and vanishing when you stop; the email at the top as the return address, and when it is right the sand dollar stamp lands; the name at the bottom as the signature, with the flourish drawn once it is long enough. A field left wrong drops an ink blot, shakes the field and rustles the sheet. With «Send by sea» the sheet rolls up from the right, is tied with twine, flies over the bottle and slides in; the cork flips and goes into the neck, the bottle tips over on the sand, a wave runs up the beach, lifts it and carries it away, and the bottle drifts out rocking, smaller and smaller, towards the sun.

How it works

The beach in SVG, the sea drawn again

The beach is a 300 by 360 SVG: sky, sun, sea and sand are fixed, while the waves, the sun's glints and the water's edge are drawn again on every frame from GSAP's ticker with a few sines.

Every word, a line of handwriting

Every word of the message becomes a stroke of handwriting: a zig-zag curve with a loop up for the tall letters (b, d, l, t…) and down for the low ones (g, p, y…), wrapping when the line is full; the nib sits at the end of the last word.

The moving pieces written by hand

The moving parts (letter, roll, bottle, cork, bottle at sea) each have an object of numbers: GSAP tweens them and the ticker writes them into the transform attribute, so they turn round the right point even inside groups that are already moved.

The sheet that rolls up and slides into the bottle

To roll up, the sheet squeezes towards its left edge (scaleX round that edge) while the roll runs from right to left; then a twin roll, drawn behind the glass, slides down into the bottle.

The wave that takes the bottle away

The wave is the water's edge itself rising: a number added to the height of the edge makes it run up to the bottle and pull back, taking it away.

Values to change

NameFileDefaultWhat it changes
ROLL_TIMEscript.js0.9How many seconds the letter takes to roll up.
DRIFT_TIMEscript.js4.2How many seconds the bottle takes to drift out to the sun.
WAVE_HEIGHTscript.js1How high the waves on the sea are: 0 is a flat calm.
SURFscript.js3How many pixels the water's edge washes up and back.
INK_WIDTHscript.js2.5How wide one letter of handwriting is on the paper.
WAITscript.js0.4How many seconds the pretend sending lasts before the letter rolls up.
TYPE_SPEEDscript.js0.07Seconds per letter when the preview writes by itself.

Try them in the Playground

When to use it

The contact page of a seaside hotel or holiday home, a beach club, a sailing school, a beach bar: where writing in should feel like a small holiday gesture.

On phones

On the phone the beach and the form stay side by side, the beach narrower; the errors go under the field and the line «We answer within a day» disappears to make room.

With reduced motion

With reduced motion the sea stands still, the ink, the stamp and the signature appear with no springs; sent, the letter and the bottle on the sand fade and the bottle appears already out at sea.

Pitfalls

  • GSAP transforms on nested SVG groups turn round the corner of their box: the letter and the cork slid away. Here each piece has its numbers and the transform is written by hand, with the rotation round the right point (rotate(r 98 308) for the bottle's foot).
  • The roll cannot pass behind the glass if it stays in the letter's group: there is a twin roll inside the bottle, between the back glass and the front glass, that takes the other's place above the neck.
  • Gradient stops with stop-color="var(--…)" as an attribute stay black: the stops have a class and the colour sits in the CSS.
  • The preview clicks the card every few seconds: the pretend writing only starts if one is not already running, otherwise it would start again halfway.

Libraries

GSAP 3.15.0, Bricolage Grotesque (font variabile) 5.3.0, Caveat (font variabile) 5.3.0.

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

Testing

Tested on 23 October 2026

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

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 components