Skip to content
animorio
IT
All components

The goodnight button

Buttons, SVG, HoverCSS only, GSAPAdvancedCompressed weight: Demo 11 KBGSAP 28 KBFreeDownload the zip

The Log out of an app as a pill of sky with a small portrait drawn in SVG in its round end: awake, its eyes follow the pointer and it blinks at random moments. At a click the eyelids grow heavy, a yawn, the sky in the pill runs from day through the sunset to night, the sun goes down, the moon comes up with a few drawn stars and the portrait falls asleep with its head on one side, breathing, while small drawn z letters float up: Signed out · Sleep well. Another click wakes it with a start and the sun comes back: Good morning, Ada.

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 goodnight button</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/mona-sans@5.3.0/wdth.css">
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <main class="stage">
    <!-- the goodnight button: the Log out of an app, a pill of sky with a small drawn portrait in its round left end. The
         portrait is awake: its eyes follow the pointer and it blinks now and then. A click logs out: the eyelids grow
         heavy, a yawn, the sky inside the pill runs from day to night, the moon comes up and the portrait falls asleep
         with its head on one side, breathing. Another click signs in: it wakes with a start and the sun comes back -->
    <div class="night" data-night>
      <button class="night__btn" type="button" data-btn data-autoclic>
        <span class="night__fill" aria-hidden="true" data-fill>
          <span class="night__sky" data-sky></span>
          <span class="night__dark" data-dark></span>
        </span>
        <span class="night__clip" aria-hidden="true">
          <svg class="night__heavens" viewBox="0 0 40 60" data-heavens>
            <defs>
              <mask id="nb-crescent">
                <circle cx="20" cy="23" r="7.4" fill="#fff" />
                <circle cx="23.6" cy="20" r="6.6" fill="#000" />
              </mask>
            </defs>
            <g class="night__stars" data-stars>
              <path transform="translate(5 12) scale(0.9)" d="M0 -2.4Q0.35 -0.35 2.4 0Q0.35 0.35 0 2.4Q-0.35 0.35 -2.4 0Q-0.35 -0.35 0 -2.4Z" />
              <path transform="translate(34 42) scale(0.7)" d="M0 -2.4Q0.35 -0.35 2.4 0Q0.35 0.35 0 2.4Q-0.35 0.35 -2.4 0Q-0.35 -0.35 0 -2.4Z" />
              <path transform="translate(9 46) scale(0.55)" d="M0 -2.4Q0.35 -0.35 2.4 0Q0.35 0.35 0 2.4Q-0.35 0.35 -2.4 0Q-0.35 -0.35 0 -2.4Z" />
              <path transform="translate(33 8) scale(0.5)" d="M0 -2.4Q0.35 -0.35 2.4 0Q0.35 0.35 0 2.4Q-0.35 0.35 -2.4 0Q-0.35 -0.35 0 -2.4Z" />
            </g>
            <g data-sun>
              <g class="night__rays" data-rays>
                <path d="M20 12.2v-2.6M20 33.8v2.6M9.2 23h-2.6M30.8 23h2.6M12.4 15.4l-1.8-1.8M27.6 30.6l1.8 1.8M12.4 30.6l-1.8 1.8M27.6 15.4l1.8-1.8" />
              </g>
              <circle class="night__sun" cx="20" cy="23" r="7" />
            </g>
            <g data-moon>
              <rect class="night__moon" x="10" y="13" width="20" height="20" mask="url(#nb-crescent)" />
            </g>
          </svg>
          <svg class="night__zs" viewBox="0 0 14 21" data-zs>
            <path d="M0 0h3.4L0 3.8h3.4" />
            <path d="M0 0h3.4L0 3.8h3.4" />
            <path d="M0 0h3.4L0 3.8h3.4" />
          </svg>
        </span>
        <span class="night__portrait" aria-hidden="true">
          <svg viewBox="0 0 50 50" data-portrait>
            <defs>
              <clipPath id="nb-round"><circle cx="25" cy="25" r="23.6" /></clipPath>
              <clipPath id="nb-eye"><path d="M-2.7 0Q0 -2.7 2.7 0Q0 2.1 -2.7 0Z" /></clipPath>
            </defs>
            <g clip-path="url(#nb-round)">
              <circle class="pt-back" cx="25" cy="25" r="24" />
              <g data-body>
                <path class="pt-sweater" d="M2 52C3 41.5 12.5 37.6 25 37.6S47 41.5 48 52Z" />
                <path class="pt-neck" d="M21.3 30.5h7.4v6.6q-3.7 2.8 -7.4 0Z" />
                <path class="pt-collar" d="M19.4 38.2Q25 43 30.6 38.2" />
              </g>
              <g data-head>
                <path class="pt-hair" d="M12.2 26C11.2 14.2 17 8.6 25 8.6S38.8 14.2 37.8 26C37.6 31 36.4 33.8 34.6 34.6L15.4 34.6C13.6 33.8 12.4 31 12.2 26Z" />
                <ellipse class="pt-skin" cx="15.2" cy="25.4" rx="1.8" ry="2.5" />
                <ellipse class="pt-skin" cx="34.8" cy="25.4" rx="1.8" ry="2.5" />
                <ellipse class="pt-skin" cx="25" cy="24.6" rx="9.7" ry="11.1" />
                <g data-face>
                  <ellipse class="pt-cheek" cx="18.6" cy="28.6" rx="2" ry="1.2" />
                  <ellipse class="pt-cheek" cx="31.4" cy="28.6" rx="2" ry="1.2" />
                  <g class="pt-eye" transform="translate(21 24.6)">
                    <path class="pt-white" d="M-2.7 0Q0 -2.7 2.7 0Q0 2.1 -2.7 0Z" />
                    <g clip-path="url(#nb-eye)">
                      <g data-pupil>
                        <circle class="pt-iris" r="1.45" />
                        <circle class="pt-glint" cx="-0.45" cy="-0.5" r="0.42" />
                      </g>
                      <rect class="pt-lid" x="-3" y="-2.6" width="6" height="0" data-lid />
                    </g>
                    <path class="pt-lash" d="M-2.8 0Q0 -1.2 2.8 0" data-lash />
                  </g>
                  <g class="pt-eye" transform="translate(29 24.6)">
                    <path class="pt-white" d="M-2.7 0Q0 -2.7 2.7 0Q0 2.1 -2.7 0Z" />
                    <g clip-path="url(#nb-eye)">
                      <g data-pupil>
                        <circle class="pt-iris" r="1.45" />
                        <circle class="pt-glint" cx="-0.45" cy="-0.5" r="0.42" />
                      </g>
                      <rect class="pt-lid" x="-3" y="-2.6" width="6" height="0" data-lid />
                    </g>
                    <path class="pt-lash" d="M-2.8 0Q0 -1.2 2.8 0" data-lash />
                  </g>
                  <path class="pt-brow" d="M18.6 20.6Q21 19.4 23.2 20.4" data-brow />
                  <path class="pt-brow" d="M26.8 20.4Q29 19.4 31.4 20.6" data-brow />
                  <path class="pt-nose" d="M25.3 25.8Q26.2 27.6 24.9 28" />
                  <path class="pt-smile" d="M23 30.6Q25 32 27 30.6" data-smile />
                  <ellipse class="pt-yawn" cx="25" cy="31.2" rx="0" ry="0" data-yawn />
                </g>
                <path class="pt-hair" d="M14.8 23C14.6 14.6 19.6 11.4 25.6 11.4C31 11.4 35.4 14.4 35.6 22C32.6 18.4 28.6 16.6 24.2 17.4C20.4 18.2 17.2 20.2 14.8 23Z" data-fringe />
              </g>
              <rect class="pt-moonlight" x="0" y="0" width="50" height="50" data-moonlight />
            </g>
            <circle class="pt-ring" cx="25" cy="25" r="23.8" />
          </svg>
        </span>
        <span class="night__label">
          <span class="night__word" data-word data-wide="Log out">Log out</span>
        </span>
      </button>
      <p class="sr" aria-live="polite" data-live></p>
    </div>
  </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.

30 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 goodnight button” from Animorio and fit it into my site

What it does

On a white background a pill 60 pixels tall made of daytime sky, blue at the top and almost white at the foot, with a thread of light on the rim and a soft shadow. In the round left end, inside a white ring, the portrait of a drawn person: a dark bob with a fringe, a face with rosy cheeks, a nose in one line, a smile and a terracotta sweater. In the middle, in midnight blue Mona Sans, Log out; in the right end a yellow sun with its rays. On arrival the button rises gently and the portrait opens its eyes. The eyes follow the pointer anywhere on the page: the pupils move in the eyes with a lively dart, the face follows them a little, the fringe less, the head turns a hair, so the portrait has depth. It blinks at random moments, now and then twice in a row. With the mouse over it, it smiles more and raises its brows, the words widen and the sky swells with a bounce. At a click the words become Logging out…: the eyelids grow heavy and the brows give, a yawn opens the mouth and tips the head back, then the mouth closes and the lids fall; meanwhile the sky runs inside the pill, from day to a peach sunset, then mauve, then midnight blue, the sun goes down out of the pill, the moon comes up in its place and four small drawn stars appear, still; the words turn pale and the portrait, lit blue by the moon, falls asleep with its head on one side and breathes slowly, its shoulders rising and falling, while small drawn z letters float up and fade beside it: Signed out · Sleep well, then Sign in. Another click: it wakes with a start, the eyes snap open, the brows jump, the head comes straight with a jolt, the sky runs back down fast, the moon goes down and the sun comes back up with a bounce, turning its rays: Signing in…, then Good morning, Ada with a broad smile, then Log out again. With the keyboard the focus shows a midnight ring, the words widen and the portrait smiles; Enter or Space work like the click. On a phone the eyes follow the finger and the tap signs out or in.

How it works

The portrait in SVG

The portrait is a 50 unit SVG clipped to a circle: the sweater and the neck in one group, the head (hair behind, ears, face, features, the fringe in front) in another that tilts round the neck. Each eye is a white almond holding, clipped to the same almond, the pupil with its glint and the lid: a skin coloured rectangle that grows in height. The edge of the lid is a single curve that comes down with it and turns over: arched when open, the curve of sleep when shut.

The face as numbers

The face is an object of numbers (the gaze, the lids, the blink, the smile, the yawn, the brows, the tilt, the jolt, the breath, the moonlight) that GSAP animates and one function writes into the SVG: transforms, the height of the lids, the paths of the lid edge, the brows and the mouth rebuilt on each frame. A key made of the numbers rounded to the hundredth says whether anything changed: if not, nothing is written and the loop stops.

The gaze

The direction from the middle of the portrait to the pointer, full beyond 90 pixels and in proportion closer in, is where the gaze is going; the drawn gaze follows it on a quick spring (4.2 swings a second, damping 0.62), so the eyes dart and settle. The pupils move 1.1 units, the features 0.9, the fringe 0.45 and the head turns 1.2 degrees: the parallax between the layers gives the depth. LOOK scales them all.

Blinks at random

A GSAP delayedCall calls itself again at a random moment between the two numbers of BLINK; the blink comes down in 0.07 seconds and goes back up in 0.15, and about one time in five it does it again at once. It is a number apart from the heavy lids, and the more shut of the two counts, so a blink never opens the eyes of someone sleepy.

The running sky

The sky is a gradient four pills tall (day, peach sunset, mauve, night) inside the fill with overflow: hidden; logging out slides it up by yPercent −75 in DUSK seconds, and signing in brings it back down. The sun goes down 60 units, the moon (a rectangle with a crescent mask: a white circle less a black one moved aside) comes up in its place, the four pointed stars come in with back.out and stay still, the colour of the words goes through a CSS variable animated by GSAP, and a blue veil on the portrait makes the moonlight.

The states

The button is a real button, with aria-disabled while it signs out or in and an aria-live region that says Logging out…, Signed out · Sleep well, Signing in… and Good morning, Ada; it is all simulated with GSAP's timing. Asleep, the breath is an endless yoyo tween and the z letters a repeating timeline of three drawn letters; waking stops them. The pill takes the measured width of the words with back.out, leaving room for the sun on the right, and when it narrows it waits for the old words to be gone.

Values to change

NameFileDefaultWhat it changes
LOG_OUTscript.jsLog outThe words at rest, signed in.
LOGGING_OUTscript.jsLogging out…The words while it logs out; the aria-live region says them too.
SLEEP_WELLscript.jsSigned out · Sleep wellThe words once signed out, before Sign in.
SIGN_INscript.jsSign inThe words at rest, signed out.
SIGNING_INscript.jsSigning in…The words while it signs in.
MORNINGscript.jsGood morning, AdaThe greeting once signed in. The sign in is simulated: in a real site this is the name your server returns.
DUSKscript.js1.8The seconds the sky takes to go from day to night.
BLINKscript.js[1.6, 4.8]In how many seconds, at least and at most, the next blink comes.
LOOKscript.js1How far the eyes and the face turn to the pointer: 0 they stay still.
TILTscript.js13How far the head falls on one side asleep, in degrees.
BREATHscript.js3.4The seconds of one breath asleep.

Try them in the Playground

When to use it

For the sign out of an evening or quiet app: sleep and meditation, reading, music, a journal, an app for children or families, or simply a product with a gentle voice. The portrait is meant to be replaced by the user's avatar or the product's mascot; the real sign out has to be wired to your server: here it is simulated.

On phones

The pill is between about 190 and 300 pixels wide and always fits at 390; if the words are too long the type gets smaller. The eyes follow a finger resting on the page, a tap signs out or in; the portrait, the sun and the moon keep their size.

With reduced motion

The eyes do not follow the pointer or blink, no yawn, no breath and no z letters, the pill does not swell and the words do not widen. At a click the portrait fades out and back in already asleep, the night fades in over the day, the sun fades out and the moon and the stars fade in; another click brings back the day and the waking portrait the same way.

Pitfalls

  • The sun with a pale round halo, on the blue sky, looked like a glowing dot: now it is a yellow disc with drawn rays, and the rays turn only as it rises.
  • Setting by 46 units, the sun's halo still peeped over the bottom edge of the pill at night: now the sun and the moon go down 60.
  • The z letters were under four pixels tall and night coloured on the night: now they are bigger, pale, and rise from beside the head without ever touching the words.
  • The pill narrowed from Good morning, Ada to Log out while the old words faded, and for a moment their last letters passed under the sun: now when it narrows it waits for the old words to be gone.
  • A blink that ended with the lids open opened the eyes of someone yawning: the blink is a number apart and the more shut of the two is drawn.
  • GSAP turns SVG elements round their own box: the sun's rays turned round a corner. svgOrigin with the middle of the sun fixes it.

Libraries

GSAP 3.15.0, Mona Sans (font variabile) 5.3.0.

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

Testing

Tested on 12 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