JAVASCRIPT · TUTORIAL

Replace onmouseover with JavaScript Event Listeners

Move hover behavior out of HTML, avoid child-element flicker, and support keyboard focus alongside pointer interaction.

Older websites often contain image swaps, highlights or information panels controlled by inline onmouseover and onmouseout attributes. Those snippets can work, but they scatter behavior throughout the markup and make repeated updates harder to manage. Moving the behavior into a JavaScript file gives each component a clearer structure and lets several elements share the same logic.

The goal is more than replacing one spelling with another. A useful refactor preserves what visitors can do, reduces unexpected event behavior and provides a keyboard equivalent where the interaction matters. Start with one small component, confirm its intended behavior, and then apply the pattern to other pages. A decorative highlight and an interactive menu should not automatically receive the same implementation.

Understand the existing interaction first

Suppose a linked product image changes when the pointer moves over it. Before editing the code, check whether the change belongs to the whole card, only the image, or every child element inside the link. Check whether another script also uses the same attributes. Removing an inline handler without identifying its side effects can leave a second component depending on behavior that no longer runs.

For a simple color, border or opacity change, CSS is usually enough. Combining :hover with :focus-visible can give pointer users and keyboard users comparable feedback without JavaScript. Save event listeners for behavior that really requires code, such as changing a data-driven image or coordinating several elements.

Use a real link and data attributes

The example below keeps the destination available even when JavaScript is disabled. The alternate image path is stored as data rather than executable code. Width and height reserve space so the image swap does not move surrounding content. Replace both example image paths with files that actually exist on your site.

<a class="preview-card" href="/projects/"
   data-preview="/images/project-preview.jpg">
  <img src="/images/project-original.jpg"
       width="320" height="240" alt="Explore programming projects">
  <span>Programming projects</span>
</a>

The visible label and link explain the destination independently of the image. The alternate picture should add visual interest, not contain essential instructions that appear only while hovering. If the user must read additional information to decide what to do, show that information in ordinary page content or behind an explicit button.

Choose boundary events for a whole card

A card can contain an image, caption and icon. Moving between those children should not repeatedly restore and replace the image. The mouseenter and mouseleave events fit this boundary behavior. In this implementation, listeners attach directly to each card; these events do not bubble like mouseover and mouseout.

document.querySelectorAll('.preview-card').forEach((card) => {
  const image = card.querySelector('img');
  const preview = card.dataset.preview;
  if (!image || !preview) return;

  const original = image.getAttribute('src');
  let hovered = false;
  let focused = false;

  function updateImage() {
    image.setAttribute('src', hovered || focused ? preview : original);
  }

  card.addEventListener('mouseenter', () => {
    hovered = true;
    updateImage();
  });
  card.addEventListener('mouseleave', () => {
    hovered = false;
    updateImage();
  });
  card.addEventListener('focus', () => {
    focused = true;
    updateImage();
  });
  card.addEventListener('blur', () => {
    focused = false;
    updateImage();
  });
});

The two state variables prevent one input from undoing the other. If a keyboard-focused card is also hovered, moving the pointer away keeps the preview visible until focus leaves. Without those states, a mouseleave handler could restore the original image while the visitor is still using the focused link.

Load the code when the elements exist

Place the listener code in a dedicated JavaScript file and load it with defer. That allows the document to be parsed before initialization runs. If a shared file serves many different page types, an empty selector result is normal; the loop simply has nothing to initialize. Do not treat every missing optional component as a site error.

<script src="/javascript/project-previews.js" defer></script>

The example uses ordinary images with a single source. Responsive images with srcset need a coordinated swap because the browser can choose a source independently of src. Another option is two fixed image layers with CSS controlling visibility. Choose the approach that matches the component you already have rather than layering competing source rules onto it.

Make the touch and keyboard behavior intentional

A phone visitor may never trigger a meaningful hover. This card remains useful because tapping its link navigates normally. There is no requirement to reveal the alternate image before opening the destination. If your current design requires a first tap to show hidden controls and a second tap to activate them, simplify that flow with a visible control and an explicit expanded state.

A popup menu needs more work than an image preview. It may need focus management, an accessible name, an Escape-key response and a way to close it without using a pointer. Do not copy this decorative swap into a menu and assume that adding a focus listener completes the interaction. Describe what the component does and implement the appropriate keyboard behavior for that job.

Test the refactor before repeating it

Move the pointer from the image to the caption and then outside the card. Tab onto the link and away from it. Combine keyboard focus with pointer movement to confirm the image remains consistent. Test a missing alternate file, since a broken preview should not make the destination inaccessible. Check the browser console for syntax errors and verify that the original link still works without the script.

Once the component behaves correctly, remove the old inline attributes and reuse the class on other cards. Avoid initializing the same node twice if your application replaces content dynamically. A named initialization function or a marker can make that lifecycle explicit. Continue with the addEventListener() tutorial. MDN documents mouseenter and listener registration in more detail.