Eject input: a clear button that throws the old search out

Press the gold cross and the old search jumps out of the box letter by letter. The field is empty at once, the cursor is ready, and underneath it is a plain labelled search input.

A customer on your lunch menu searches for "spicy noodle soup", reads the result, then wants to look for something else. On most sites they tap a tiny grey cross and the words simply vanish, which is easy to miss on a phone. This search box throws the old words out instead: each letter jumps up and tumbles away, the field is empty, and the cursor is already waiting for the next search.

Press the cross, or Escape, to start a new search.

  • Spicy noodle soup $14
  • Miso soup with tofu $6
  • Chicken rice bowl $15
  • Cold noodle salad, spicy $13
  • Garden salad $11
  • Pork and sesame dumplings $12

Live demo. It runs on exactly the code further down this page.

How it works

  1. A real search box. It is a normal input type="search" with a label, and the dish list filters as you type. The browser's own small cross is hidden so there is only one clear button.
  2. Copy the text. When you press the gold cross or Escape, the script reads the current text and empties the input straight away, so the real value is already clear before any animation starts.
  3. Lay letters on top. Each character is copied into its own span inside a layer that sits exactly over the input text, shifted by the input's scroll position so long searches line up too.
  4. Throw them. Every letter gets a random upward kick, a sideways push and a spin. The script works out a short arc with simple gravity and hands it to the Web Animations API, which runs it smoothly without a loop of our own.
  5. Tidy up. When the last letter finishes, the layer is emptied. Focus goes back to the input and a live region announces how many dishes are now shown.

The code

Three short files, no libraries; the search filter is included so you can see how the effect sits next to real behaviour.

HTML
<div class="ej" data-lab="eject-input">
  <div class="ej-card">
    <label class="ej-label" for="ej-q">Search the lunch menu</label>
    <div class="ej-field">
      <svg class="ej-icon" viewBox="0 0 24 24" aria-hidden="true"><circle cx="10.5" cy="10.5" r="6.5"/><path d="M15.5 15.5 21 21"/></svg>
      <input class="ej-input" id="ej-q" type="search" autocomplete="off" spellcheck="false" value="spicy noodle soup" aria-describedby="ej-help">
      <button class="ej-clear" type="button" aria-label="Clear search">
        <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M7 7l10 10M17 7 7 17"/></svg>
      </button>
      <div class="ej-fly" aria-hidden="true"></div>
    </div>
    <p class="ej-help" id="ej-help">Press the cross, or Escape, to start a new search.</p>
    <ul class="ej-list">
      <li data-k="spicy noodle soup chilli broth">Spicy noodle soup <span>$14</span></li>
      <li data-k="miso soup tofu">Miso soup with tofu <span>$6</span></li>
      <li data-k="chicken rice bowl">Chicken rice bowl <span>$15</span></li>
      <li data-k="spicy tofu noodle salad">Cold noodle salad, spicy <span>$13</span></li>
      <li data-k="garden salad vegan">Garden salad <span>$11</span></li>
      <li data-k="sesame dumplings pork">Pork and sesame dumplings <span>$12</span></li>
    </ul>
    <p class="ej-count" aria-live="polite"></p>
  </div>
</div>
CSS
.ej {
  display: grid;
  justify-items: center;
  padding: 8px 0;
  font-family: system-ui, sans-serif;
  color: #101a14;
}
.ej-card {
  width: 100%;
  max-width: 420px;
  box-sizing: border-box;
  padding: 22px;
  border: 1px solid #c9cec4;
  border-radius: 16px;
  background: #fff;
}
.ej-label { display: block; margin: 0 0 8px; font: 700 17px/1.3 system-ui, sans-serif; color: #133621; }

/* The field wraps the input so the flying letters can start exactly where the text sits. */
.ej-field { position: relative; }
.ej-icon {
  position: absolute; left: 14px; top: 50%;
  width: 18px; height: 18px; margin-top: -9px;
  fill: none; stroke: #46534a; stroke-width: 2; stroke-linecap: round;
  pointer-events: none;
}
.ej .ej-input {
  display: block;
  width: 100%;
  box-sizing: border-box;
  height: 48px;
  margin: 0;
  padding: 0 48px 0 42px;
  border: 1px solid #c9cec4;
  border-radius: 12px;
  background: #fff;
  color: #101a14;
  font: 16px/1 system-ui, sans-serif;
  -webkit-appearance: none;
  appearance: none;
}
.ej-input::-webkit-search-cancel-button { -webkit-appearance: none; display: none; }
.ej .ej-card .ej-input:focus, .ej .ej-card .ej-input:focus-visible { outline: 2px solid #133621; outline-offset: 1px; border-color: #133621; }
.ej-clear {
  position: absolute; right: 6px; top: 6px;
  width: 36px; height: 36px; padding: 0;
  display: grid; place-items: center;
  border: 0; border-radius: 9px;
  background: #f4d96c; color: #133621;
  cursor: pointer;
  transition: transform .15s ease, opacity .15s ease;
}
.ej-clear svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 2.6; stroke-linecap: round; }
.ej-clear:hover { transform: scale(1.06); }
.ej .ej-card .ej-clear:focus-visible { outline: 2px solid #133621; outline-offset: 2px; }
.ej-clear[hidden] { display: none; }

/* Letters are copies laid over the input, then thrown. */
.ej-fly {
  position: absolute; inset: 0;
  padding-left: 43px;
  display: flex; align-items: center;
  font: 16px/1 system-ui, sans-serif;
  color: #101a14;
  white-space: pre;
  pointer-events: none;
  overflow: visible;
}
.ej-fly span { display: inline-block; will-change: transform, opacity; }

.ej .ej-card p.ej-help { padding: 8px 0 14px; font-size: 13px; color: #46534a; }
.ej .ej-card ul.ej-list { margin: 0; padding: 0; list-style: none; border-top: 1px solid #c9cec4; }
.ej .ej-card ul.ej-list li {
  display: flex; justify-content: space-between; gap: 12px;
  margin: 0; padding: 10px 2px;
  border-bottom: 1px solid #c9cec4;
  font-size: 15px; line-height: 1.35;
}
.ej-list li span { color: #46534a; font-variant-numeric: tabular-nums; }
.ej .ej-card ul.ej-list li[hidden] { display: none; }
.ej .ej-card p.ej-count { padding: 10px 0 0; font-size: 13px; color: #46534a; }

@media (prefers-reduced-motion: reduce) {
  .ej-clear { transition: none; }
  .ej-clear:hover { transform: none; }
}
JavaScript
(function () {
  var root = document.querySelector('[data-lab="eject-input"]');
  if (!root) return;
  var input = root.querySelector('.ej-input');
  var clear = root.querySelector('.ej-clear');
  var fly = root.querySelector('.ej-fly');
  var items = root.querySelectorAll('.ej-list li');
  var count = root.querySelector('.ej-count');
  var DURATION = 750;   // how long each letter flies, in milliseconds
  var GRAVITY = 900;    // pixels per second squared pulling letters down
  var calm = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;

  // Show only dishes that contain every word typed.
  function filter() {
    var words = input.value.toLowerCase().split(/\s+/).filter(Boolean);
    var shown = 0;
    for (var i = 0; i < items.length; i++) {
      var k = items[i].getAttribute('data-k');
      var ok = words.every(function (w) { return k.indexOf(w) > -1; });
      items[i].hidden = !ok;
      if (ok) shown++;
    }
    count.textContent = shown + (shown === 1 ? ' dish' : ' dishes') + (words.length ? (shown === 1 ? ' matches' : ' match') : ' on the menu');
    clear.hidden = !input.value;
  }

  // Copy each letter into its own span, laid exactly over the input text.
  function eject(text) {
    fly.innerHTML = '';
    fly.style.transform = 'translateX(' + (-input.scrollLeft) + 'px)';
    for (var i = 0; i < text.length; i++) {
      var s = document.createElement('span');
      s.textContent = text.charAt(i);
      fly.appendChild(s);
    }
    var spans = fly.children, done = 0;
    if (!spans.length || !spans[0].animate) { fly.innerHTML = ''; return; }
    for (var j = 0; j < spans.length; j++) {
      // Each letter gets its own kick: up and outwards, then gravity takes over.
      var vx = 80 + Math.random() * 220, vy = -(170 + Math.random() * 190);
      var spin = (Math.random() - 0.5) * 720, frames = [];
      for (var f = 0; f <= 10; f++) {
        var t = (f / 10) * DURATION / 1000;
        frames.push({
          transform: 'translate(' + (vx * t).toFixed(1) + 'px,' + (vy * t + 0.5 * GRAVITY * t * t).toFixed(1) + 'px) rotate(' + (spin * f / 10).toFixed(0) + 'deg)',
          opacity: f < 6 ? 1 : 1 - (f - 5) / 5
        });
      }
      if (calm) frames = [{ opacity: 1 }, { opacity: 0 }];
      var a = spans[j].animate(frames, { duration: calm ? 250 : DURATION, delay: calm ? 0 : j * 18, easing: 'linear', fill: 'forwards' });
      a.onfinish = function () { if (++done === spans.length) fly.innerHTML = ''; };
    }
  }

  function clearSearch() {
    var text = input.value;
    if (!text) return;
    input.value = '';
    eject(text);
    filter();
    input.focus();
  }

  input.addEventListener('input', filter);
  clear.addEventListener('click', clearSearch);
  input.addEventListener('keydown', function (e) {
    if (e.key === 'Escape' && input.value) { e.preventDefault(); clearSearch(); }
  });
  filter();
})();

When to use it, and when not to

It fits places where people search more than once in a visit: a menu, a product catalogue, a list of services or a store locator. The flying letters make the reset obvious, which helps on small screens where a field quietly going blank can look like a glitch. It also gives a playful brand a small moment of character in a spot that is usually dull.

It is a poor fit for forms where clearing means losing work, such as a long message or an address. There, a dramatic exit makes losing text feel fun when it should feel careful, and an undo is more useful than an animation. Keep it off password and payment fields entirely.

For accessibility, the effect is only decoration. The letters are marked aria-hidden, the clear button has a proper label, and screen reader users hear the new result count instead of the animation. People who ask their device for reduced motion get a quick fade with no flying or spinning.

Make it yours

  • Speed of the throw: change DURATION for how long letters fly and GRAVITY for how fast they fall. A higher gravity gives a short hop, a lower one a long float.
  • Direction: the kick for each letter is set by vx and vy inside eject(). Make vx negative to throw letters to the left.
  • Your own list: edit the li items and their data-k words, or replace filter() with a call to your site's real search.

Frequently asked questions

Does it work on phones?

Yes. The clear button is a normal button that responds to taps, and the letters are moved with transforms, which phones animate smoothly.

Will the animation slow down my search?

No. The input is emptied and the list is updated before the letters start to fly, so the result is ready at once and the animation runs on top of it.

Can I use it on a search box from my website builder?

Often, yes. Point the script at your builder's search input and clear button instead of the demo's classes, as long as you can add custom code to the page.

What should your website do?

Describe a feature you wish your site had. If it is useful to other owners too, we build it here as a free Lab post with a live demo and code.

We never publish your name, email or website. Privacy

More from the Website Lab

All experiments