惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

F
Full Disclosure
WordPress大学
WordPress大学
小众软件
小众软件
Cloudbric
Cloudbric
AWS News Blog
AWS News Blog
腾讯CDC
量子位
人人都是产品经理
人人都是产品经理
大猫的无限游戏
大猫的无限游戏
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
V
Vulnerabilities – Threatpost
Scott Helme
Scott Helme
Hugging Face - Blog
Hugging Face - Blog
博客园_首页
C
CXSECURITY Database RSS Feed - CXSecurity.com
The Hacker News
The Hacker News
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
IT之家
IT之家
Jina AI
Jina AI
Attack and Defense Labs
Attack and Defense Labs
S
SegmentFault 最新的问题
Simon Willison's Weblog
Simon Willison's Weblog
The Cloudflare Blog
阮一峰的网络日志
阮一峰的网络日志
T
Tailwind CSS Blog
Last Week in AI
Last Week in AI
博客园 - 【当耐特】
Google Online Security Blog
Google Online Security Blog
美团技术团队
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
V
Visual Studio Blog
罗磊的独立博客
L
LINUX DO - 最新话题
博客园 - Franky
博客园 - 叶小钗
Apple Machine Learning Research
Apple Machine Learning Research
The Last Watchdog
The Last Watchdog
J
Java Code Geeks
AI
AI
C
Cisco Blogs
酷 壳 – CoolShell
酷 壳 – CoolShell
C
Cyber Attacks, Cyber Crime and Cyber Security
Cisco Talos Blog
Cisco Talos Blog
博客园 - 三生石上(FineUI控件)
雷峰网
雷峰网
Help Net Security
Help Net Security
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
云风的 BLOG
云风的 BLOG
I
Intezer
S
Securelist

Little Things

Madeira — May 2025 | Little Things OpenClaw Agent Deploys OpenClaw to a Phone via SSH | Little Things From Failed Simulation to Daily Copilot - My OpenClaw Setup | Little Things Building Agentic Workflows for my HomeLab | Little Things Starting a photography series | Little Things Exploring the browser rendering process | Little Things Managing Interactive Demos in MDX | Little Things Interactive post on OKLCH color space | Little Things A Guide to animations that feels right | Little Things No Authentication Like Button | Little Things Interactive Story - A Journey of Choices | Little Things A Time Before Algorithms Took Over the 90's Web | Little Things Why does this site exist? | Little Things
Animations - Liquid background hover effect | Little Things
2024-09-12 · via Little Things

The other day, I was working on a project and realized how much a simple thing like a menu’s movement can change the whole feel of a site. So I decided to mimic the animation and simulate smooth, natural motion using physics concepts, making the animation feel lifelike, similar to how objects move and decelerate in nature. Below is a demo of what I am talking about.

Demo

We’ll walk through how the menu’s background handle smoothly follows the active link when hovering, resizing and repositioning in a natural way.

Dependencies

Make sure you have these dependencies installed:

npm install solid-js gsap clsx

Code Overview

We will be working with the below navigation data. You can replace this with your own data.

const items = [
  { TEXT: 'Home', HREF: '/' },
  { TEXT: 'Blog', HREF: '/blog' },
  { TEXT: 'Projects', HREF: '/projects' },
  { TEXT: 'About', HREF: '/about' },
];

Create the component and import dependencies

We will create a new component called NavigationMenu and import the necessary dependencies. It will accept props - currentPath, slug, containerClass, and items.

import { createSignal, onMount } from 'solid-js';
import gsap from 'gsap';
import clsx from 'clsx';

const Props {
  currentPath: string;
  slug: string;
  containerClass?: string;
  handleClass?: string;
  items: { TEXT: string; HREF: string }[];
}

export default function NavigationMenu(props: Props) {
  const { currentPath, slug, containerClass, items,handleClass } = props;
  //...

  return (
    <div class="p-2 inline-block rounded-full">
      <div class="relative">
        <nav
          id="nav-menu"
          class="p-0 m-0 flex cursor-pointer relative z-10 text-xs"
          onMouseOver={}
        >
          {items.map((item) => (
            <a
              class={clsx('px-4 py-2.5 m-0 text-sm duration-500')}
              href={item.HREF}
            >
              {item.TEXT}
            </a>
          ))}
        </nav>
        <span
          class={clsx(
            'rounded-full absolute left-0 h-full top-0 handle',
            handleClass
          )}></span>
      </div>
    </div>
  );
}

This is how it looks:

Here, we create a state using createSignal function which tracks the index of the active link - activeIndex. When a user hovers over a link, we call activateLink, which calculates its index and sets activeIndex.

const [activeIndex, setActiveIndex] = createSignal(0);
let original: HTMLAnchorElement | null = null;

const activateLink = (element: HTMLAnchorElement) => {
  if (element.parentElement !== null) {
    setActiveIndex([...element?.parentElement.children].indexOf(element)); 
  }
};

const onMouseOver = (event: MouseEvent) => {
  const hoveredElement = event.target; 
  if (hoveredElement.tagName === 'A') {
    activateLink(hoveredElement);
  }
};

return (
  ...
    <nav
      ...
      onMouseOver={onMouseOver}
    >
      ...
    </nav>
  ...
);

I am priting the activeIndex in this demo. This is how it looks:

Activating the background handle

To activate the background handle, we need to update the handle’s position and size based on the active link. We’ll create a function called updateHandlePosition that takes the active link element as an argument and animates the handle to match the link’s position and size.

const activateLink = (element: HTMLAnchorElement) => {
  ...
  updateHandlePosition(element); // update when hover
};

const updateHandlePosition = (element: HTMLAnchorElement) => {
  if (!handle || !element) return;

  const rect = element.getBoundingClientRect();
  const parentRect = element.parentElement?.getBoundingClientRect();
  if (!parentRect) return;

  const offsetX = rect.left - parentRect.left;

  handle.style.width = `${rect.width}px`;
  handle.style.transform = `translateX(${offsetX}px) scaleX(1)`;
};

This is how it looks:

Animating the Handle

We use GSAP to move and resize the handle. We will retrieve the width and left offset of the element using getBoundingClientRect() which provides the link’s dimensions and its position relative to the viewport.

const rect = element.getBoundingClientRect(); 

Similarly, it retrieves the bounding rectangle of the element’s parent (nav). The parent’s position is necessary to calculate the exact offset of the link relative to its container, not the whole page.

const parentRect = element.parentElement?.getBoundingClientRect();
const updateHandlePosition = (element: HTMLAnchorElement) => {
  if (!handle || !element) return;

  const rect = element.getBoundingClientRect();
  const parentRect = element.parentElement?.getBoundingClientRect();
  if (!parentRect) return;

  const offsetX = rect.left - parentRect.left;

  gsap.killTweensOf(handle);

  gsap.fromTo(
    handle,
    { scaleX: 1.5 },
    {
      width: rect.width,
      x: offsetX,
      scaleX: 1,
      opacity: 1,
      duration: 0.3,
      ease: 'power1.out'
    }
  );
};

This is how it looks:

GSAP’s killTweensOf(handle) is called to stop any ongoing animations that might still be affecting the handle. This ensures that no overlapping or unfinished animations disrupt the new one.

Animate the Handle with GSAP: The fromTo() method accepts three arguments and is used to animate the handle:

  • Parameter 1: The element you want to animate.
  • Parameter 2: An object containing the initial properties of the element.
  • Parameter 3: An object containing the final properties of the element.

In this demo, lets assume that the user is on the /blog page. Just like in React we have useEffect, in Solid we have onMount which is called when the component is mounted. We will initialize the active link inside onMount method.

const activateLink = (element: HTMLAnchorElement) => {
  ...
  updateHandlePosition(element);
};

onMount(() => {
  const activeLink = LINKS.find(
    ({ HREF }) => HREF === slug || currentPath === HREF
  )?.HREF;

  const activeElement = document.querySelector(
    `#nav-menu a[href='${activeLink}']`
  );
  original = activeElement as HTMLAnchorElement;
  activateLink(original);
});

You will notice that we save the active link in the original variable. This is because we will need to reset the active link when the mouse leaves the link. And finally, when we move the mouse away, we will reset the active link based on the current URL - /blog.

const onMouseOver = (event: MouseEvent) => {
  const hoveredElement = event.target as HTMLAnchorElement;
  if (hoveredElement.tagName === 'A') {
    activateLink(hoveredElement);
    // Reset the active link when mouse leaves
    hoveredElement.addEventListener('mouseleave', () => { 
      activateLink(original);
    }); 
  }
};

This is how it looks:

When the user hovers over a link, it activates the hovered link by calling activateLink. It also adds an event listener to detect when the mouse leaves the link, at which point activateLink is called to reset the active link with the original link.


Conclusion

Small details often have a significant impact, and animations are a perfect example of this. While animations themselves are not a recent innovation, they used to come with their own set of challenges, particularly around performance. In the past, incorporating animations into websites or applications often resulted in slow, laggy experiences, especially on less powerful devices or older browsers. Developers had to make trade-offs, either sacrificing smoothness or limiting the use of animations altogether.

However, this is rapidly changing. Modern browsers have evolved significantly, now offering improved processing capabilities and access to powerful APIs that streamline the way animations are rendered. Animations have become an integral part of user experience design that can significantly influence engagement and usability.