Shuffle letters in a word when hovered over, then unscramble them back to their original order

I have noticed a fascinating effect on several websites. To give you an idea, here are two websites that showcase this effect: Locomotive and TUX. Locomotive is particularly interesting to look at as the desired effect can be seen immediately when hovering over the navigation items, although the effect is used consistently throughout the site.

An example of the effect is having a word like "Careers" which, upon hover, scrambles its letters and then animates back to the original order.

I have been experimenting with my own version of this effect (see attached) but it relies on jQuery, which I do not use elsewhere in my projects. Moreover, the letters don't seem as scrambled as the first one because the width of the word changes.

I would really appreciate some assistance in creating an effect similar to the examples provided!

jQuery('document').ready(function($) {
  // Set effect velocity in ms
  var velocity = 50;

  var shuffleElement = $('.shuffle');

  $.each(shuffleElement, function(index, item) {
    $(item).attr('data-text', $(item).text());
  });

  var shuffle = function(o) {
    for (var j, x, i = o.length; i; j = parseInt(Math.random() * i), x = o[--i], o[i] = o[j], o[j] = x);
    return o;
  };

  var shuffleText = function(element, originalText) {
    var elementTextArray = [];
    var randomText = [];

    for (i = 0; i < originalText.length; i++) {
      elementTextArray.push(originalText.charAt([i]));
    };

    var repeatShuffle = function(times, index) {
      if (index == times) {
        element.text(originalText);
        return;
      }

      setTimeout(function() {
        randomText = shuffle(elementTextArray);
        for (var i = 0; i < index; i++) {
          randomText[i] = originalText[i];
        }
        randomText = randomText.join('');
        element.text(randomText);
        index++;
        repeatShuffle(times, index);
      }, velocity);
    }
    repeatShuffle(element.text().length, 0);
  }

  shuffleElement.mouseenter(function() {
    shuffleText($(this), $(this).data('text'));
  });
});
body {
  font-family: helvetica;
  font-size: 16px;
  padding: 48px;
}

a {
  color: black;
  text-decoration: none;
}

ul {
  list-style: none;
  margin: 0;
  padding: 0;
}

li {
  display: inline-block;
  margin: 0 16px 8px 0;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<ul>
  <li>
    <a class="shuffle" href="#">shuffle</a>
  </li>
  <li>
    <a class="shuffle" href="#">texts</a>
  </li>
  <li>
    <a class="shuffle" href="#">hover</a>
  </li>
</ul>

Answer №1

To prevent unexpected layout shifts, it is important to also turn off kerning and ligatures - as the width of words can vary based on different letter combinations like fi (ligature) or Av (kerning).

.shuffle{
  font-kerning: none;
  font-feature-settings: "liga" 0;
}

Below is a simple vanilla JavaScript function:

let shuffledEls = document.querySelectorAll(".shuffle");
let duration = 50;
let framesMax = 7

shuffledEls.forEach((shuffledEl) => {
  let textOrig = shuffledEl.textContent;
  let inter;

  shuffledEl.addEventListener("mouseover", (e) => {
    let text = e.currentTarget.textContent;
    let charArr = text.split("");
    let frame = 0;

    // shuffle at given speed
    inter = setInterval(() => {
      if(frame<framesMax){
        let charArrShuff = shuffleArr(charArr);
        shuffledEl.textContent = charArrShuff.join("");
        frame++
      }else{
        clearInterval(inter);
        shuffledEl.textContent = textOrig;
      }
    }, duration);

  });

  // stop
  shuffledEl.addEventListener("mouseleave", (e) => {
    e.currentTarget.textContent = textOrig;
    clearInterval(inter);
  });
});

function shuffleArr(arr) {
  return arr.reduce(
    ([a, b]) => (
      b.push(...a.splice((Math.random() * a.length) | 0, 1)), [a, b]
    ),
    [[...arr], []]
  )[1];
}
body {
  font-family: helvetica;
  font-size: 10vmin;
  padding: 48px;
}

a {
  color: black;
  text-decoration: none;
}

ul {
  list-style: none;
  margin: 0;
  padding: 0;
}

li {
  display: inline-block;
  margin: 0 16px 8px 0;
}

.shuffle{
  font-kerning: none;
  font-feature-settings: "liga" 0;
}
<ul>
  <li>
    <a class="shuffle" href="#">shuffle</a>
  </li>
  <li>
    <a class="shuffle" href="#">texts</a>
  </li>
  <li>
    <a class="shuffle" href="#">hover</a>
  </li>
</ul>

<h3>Inline</h3>
<p><span class="shuffle">shuffle</span> <span class="shuffle">texts</span> <span class="shuffle">hover</span> </p>


<h3>Whole paragraph (causes layout shifts!)</h3>
<p class="shuffle">
One morning, when Gregor Samsa woke from troubled dreams, he found himself transformed in his bed into a horrible vermin.
</p>

Answer №2

When you slow down the velocity, the first letter starts to change gradually. Adjusting the width is simple - just add some letter-spacing. You can also experiment with font-size or font-weight for different effects.

I included a transition effect on all anchors to show how it works, but you can customize it further as needed.

In the JavaScript section, I introduced a class that is removed at the end of the "animation" event, resulting in added letter spacing through that class.

If you're looking to make the text even more scrambled, consider calling the shuffle function twice to create a longer shuffling effect. Additionally, instead of replacing the text completely when resetting it, try arranging the letters back to their original positions for a smoother transition.

jQuery('document').ready(function($) {
  // Set effect velocity in ms
  var velocity = 50;

  var shuffleElement = $('.shuffle');

  $.each(shuffleElement, function(index, item) {
    $(item).attr('data-text', $(item).text());
  });

  var shuffle = function(o) {
    for (var j, x, i = o.length; i; j = parseInt(Math.random() * i), x = o[--i], o[i] = o[j], o[j] = x);
    return o;
  };

  var shuffleText = function(element, originalText) {
    var elementTextArray = [];
    var randomText = [];

    for (i = 0; i < originalText.length; i++) {
      elementTextArray.push(originalText.charAt([i]));
    };

    var repeatShuffle = function(times, index) {
      if (index == times) {
        element.text(originalText);
        element.removeClass("active");
        return;
      }

      setTimeout(function() {
        randomText = shuffle(elementTextArray);
        for (var i = 0; i < index; i++) {
          randomText[i] = originalText[i];
        }
        randomText = randomText.join('');
        element.text(randomText);
        index++;
        repeatShuffle(times, index);
      }, velocity);
    }
    repeatShuffle(element.text().length, 0);
  }

  shuffleElement.mouseenter(function() {
    $(this).addClass("active");
    shuffleText($(this), $(this).data('text'));
  });
});
body {
  font-family: helvetica;
  font-size: 16px;
  padding: 48px;
}

a {
  color: black;
  text-decoration: none;
  transition: all 0.2s;
}
a.active {
  letter-spacing: 2px;
}

ul {
  list-style: none;
  margin: 0;
  padding: 0;
}

li {
  display: inline-block;
  margin: 0 16px 8px 0;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<ul>
  <li>
    <a class="shuffle" href="#">shuffle</a>
  </li>
  <li>
    <a class="shuffle" href="#">texts</a>
  </li>
  <li>
    <a class="shuffle" href="#">hover</a>
  </li>
</ul>

Answer №3

Identify the initial word in dataset - this is to retrieve the starting word after shuffling, and also to allow for the application of styles to control the width of the element so that neighboring words do not appear to be "jumping" during the animation:

document.querySelectorAll('.shuffle').forEach(el => {
  el.addEventListener('mouseenter', () => {
    if (el.intervalAnimationWord) {
      return;
    }
    const word = el.textContent;
    el.dataset.word = word;
    el.intervalAnimationWord = setInterval(() => {
      el.textContent = shuffleWord(word);
    }, 60); // 60ms - updating time
    el.timeoutAnimationWord = setTimeout(clear, 600); // 600ms - animation duration
  })
  el.addEventListener('mouseleave', clear)

  function clear() {
    clearInterval(el.intervalAnimationWord);
    clearTimeout(el.timeoutAnimationWord);
    el.intervalAnimationWord = undefined;
    el.timeoutAnimationWord = undefined;
    el.textContent = el.dataset.word;
  }
})

function shuffleWord(word) {
  const letters = word.split('');
  letters.sort(() => Math.random() - 0.5);
  return letters.join('');
}
body {
  font-family: helvetica;
  font-size: 24px;
  padding: 48px;
}

ul {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  gap: 16px;
}

.shuffle {
  color: black;
  text-decoration: none;
  display: inline-flex;
  flex-direction: column;
}

.shuffle[data-word]:after {
  content: attr(data-word);
  height: 0;
  overflow: hidden;
}
<ul>
  <li>
    <a class="shuffle" href="#">shuffle</a>
  </li>
  <li>
    <a class="shuffle" href="#">texts</a>
  </li>
  <li>
    <a class="shuffle" href="#">hover</a>
  </li>
</ul>

Answer №4

I believe it would create a more visually captivating experience if individual "correct" letters were revealed one at a time, similar to how computers are often portrayed slowly decoding passwords in movies.

DAMPING = 30
DELAY = 2

function* shuffle(word) {
  let abc = 'abcdefghijklmnopqrstuvwxyz'
  let w = [...word]
  let steps = (w.length + 1) * DAMPING

  for (let step = 0; step < steps; step++) {
    for (let k = 0; k < w.length; k++) {
      if (step >= steps - w.length * DAMPING + k * DAMPING)
        w[k] = word[k]
      else
        w[k] = abc[0 | (Math.random() * abc.length)]
    }
    yield w.join('')
  }
}


async function delay(n) {
  return new Promise(r => setTimeout(r, n))
}


async function animate(el) {
  let word = el.textContent
  for (let w of shuffle(word)) {
    if (!el.matches(':hover')) break
    el.textContent = w
    await delay(DELAY)
  }
  el.textContent = word;
  
}

window.onload = function() {
  document.querySelectorAll('a').forEach(a => {
    a.addEventListener('mouseenter', () => animate(a))
  })
}
a {
font-size: 30px;
text-decoration: none;
font-family: monospace;
}
<ul>
  <li>
    <a class="shuffle" href="#">shuffle</a>
  </li>
  <li>
    <a class="shuffle" href="#">texts</a>
  </li>
  <li>
    <a class="shuffle" href="#">hover</a>
  </li>
</ul>

It is recommended to use a monospace font for optimal viewing. Feel free to adjust the constants at the top to customize the effect further.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Implementing Event Listeners in Vue 3.0: A Guide to Attaching to the Parent Element

How can I attach an addEventListener to the parent element in Vue 3.x? I discovered that you can access the parent by using this code: import { getCurrentInstance, onMounted } from 'vue' onMounted(() => { console.log(getCurrentInstance() ...

Unleashing the Power of Wildcards in HTML with XPath

When using XPath to extract values from DOM elements, I've encountered inconsistent XPaths. To select all DOM elements on the same level, I've resorted to some wildcard magic. For instance, in an HTML document, my XPaths may look like: //div[@i ...

The design of the Bootstrap alert button is not working properly as it fails to close when clicked on

While experimenting with Bootstrap, I encountered an issue with the bootstrap alert button that seems to be malfunctioning. I simply copied the code from Bootstrap's official website: https://i.sstatic.net/DRLIg.png However, upon testing it on my pag ...

Employing asynchronous operations and the power of async/await for achieving seamless integration

I am currently facing an issue where I need to retrieve queries from a database stored in Postgres. A function is utilized, which employs a callback mechanism. In order to fetch rows from the database, there exists a function called getRecipesByCategoryFo ...

How to make Jquery skip over elements with a particular data attribute

I am looking to select all elements that are labeled with the 'tag' class. Once these items have been selected, I would like to remove any items from the list that contain the attribute 'data-tag-cat'. var tags = $('.tag'); c ...

What are the steps to set up i18next in my HTML?

I have a project using nodejs, express, and HTML on the client side. I am looking to localize my application by incorporating i18next. So far, I have successfully implemented i18next on the nodejs side by requiring it and setting it up. var i18n = require ...

Questions from beginners regarding the use of canvas elements with imported images, including concerns about caching and altering colors

I have a couple of beginner questions regarding the canvas element in HTML. Firstly, I was wondering if images imported into a canvas element are cached? Is this consistent across different browsers? Secondly, is it possible to import a black and white PN ...

What is the best way to populate nested elements with jQuery?

I am trying to showcase the latest NEWS headlines on my website by populating them using Jquery. Despite writing the necessary code, I am facing an issue where nothing appears on the webpage. Below is the HTML code snippet: <div class="col-md-4"> ...

Struggling to accurately differentiate between a CSS and Bootstrap code

I'm attempting to create a visually appealing underline under the active tab, but I am facing challenges in targeting the specific class. Since I am utilizing bootstrap and unable to adjust the width and margin of text-decoration or recognize an eleme ...

Hiding labels using JQuery cannot be concealed

Why isn't this working? -_- The alert is showing, but nothing else happens. <asp:Content ID="HeaderContent" runat="server" ContentPlaceHolderID="HeaderContent"> <script type="text/javascript"> if (navigator.userA ...

What is the process for changing the background color when a button or div is pressed, and reverting the color back when another button or div is

Looking to customize a list of buttons or divs in your project? Check out this sample layout: <button type="button" class="btn" id="btn1">Details1</button> <button type="button" class="btn" id="btn2">Details2</button> <button ty ...

What is the best way to retain changes made to a checkbox that is used for email notifications?

Users on my website receive email notifications when someone comments on their profile or blog. To give users control over these notifications, I have created an email settings page with checkboxes that allow them to choose whether or not they want to rece ...

Sending form data after a successful AJAX request with jQuery in ASP.NET MVC 5

Within my Asp.net MVC 5 project, there exists a form equipped with a Submit button. Upon clicking this Submit button, the following tasks are to be executed: Client-side validation must occur using jQuery on various fields (ensuring required fields are ...

Ensure that the user's credentials are accessible to all views in Node.js

I'm currently developing an Android application that utilizes Node.js web services. The initial interface requires the user to connect to a host using an IP address, login, and password in order to access all databases. I aim to save these credentials ...

Deploying NextJS: Error in type causes fetch-routing malfunction

Encountering a mysterious and funky NextJS 13.4 Error that has me stumped. Here's a summary of what I've observed: The issue only pops up after running npm run build & npm run start, not during npm run dev The problem occurs both locally and ...

The error message "Unexpected token < in JSON at position 0" is indicating a SyntaxError in the

I am facing an issue with the API on this specific page. Although the API is working fine on other pages, it seems to be encountering a problem here. I'm not sure what's causing the issue. Below is the code snippet: export async function getStati ...

Using jQuery to combine the values of text inputs and checkboxes into a single array or string

I need to combine three different types of items into a single comma-separated string or array, which I plan to use later in a URL. Is there a way to merge these three types of data together into one string or array? An existing POST string User input f ...

Unable to make a successful POST request using the JQuery $.ajax() function

I am currently working with the following HTML code: <select id="options" title="prd_name1" name="options" onblur="getpricefromselect(this);" onchange="getpricefromselect(this);"></select> along with an: <input type="text" id="prd_price" ...

Navigating through different states and managing the URL manually can be a breeze with AngularJS

In my current setup using the meanjs stack boilerplate, I am facing an issue with user authentication. When users log in, their state remains 'logged-in' as long as they navigate within the app starting from the base root URL '/'. This ...

What causes the picturesArray to remain consistently void?

const fetch = require("node-fetch"); let images = []; fetch('http://www.vorohome.com//images/assets/159314_887955.png') .then(response => response.buffer()) .then(buffer => { const data = "data:" + response.headers.get ...