Browse by section

Web Design 日本語

Element Methods in JavaScript: What Is Actually New

The genuinely new additions around Element are three: Element.checkVisibility() (Baseline March 2024), the Popover API (Baseline January 2025), and Node.moveBefore() (2025, not yet in Safari).

A caveat first. You will find attachShadow() and requestFullscreen() presented as “new Element methods.” All of those have been available since the 2010s. They are not new.

This article separates what actually became available recently from what has existed for years but is frequently used incorrectly, with working code for both.

Sponsored

When each of these became available

Articles about DOM manipulation age badly, so checking when a feature landed is a habit worth having.

Method / API Available since Status
Element.attachShadow() around 2016 Established
Element.requestFullscreen() early 2010s (prefixed) Established
Element.setPointerCapture() around 2015 Established
Element.getAnimations() 2019–2020 Established
Element.checkVisibility() Baseline March 2024 New
Popover API Baseline January 2025 New
Node.moveBefore() Chrome/Edge 133 (Feb 2025), Firefox 144. Not in Safari Newest

Element.checkVisibility(): is this element actually rendered?

One line replaces the old dance of reading offsetParent and calling getComputedStyle() several times.

const el = document.querySelector('#target');

// false if display:none or content-visibility:hidden
el.checkVisibility();

// also treat opacity:0 and visibility:hidden as "not visible"
el.checkVisibility({
  opacityProperty: true,
  visibilityProperty: true,
  contentVisibilityAuto: true,
});
Option When true, returns false for
opacityProperty opacity: 0
visibilityProperty visibility: hidden / collapse
contentVisibilityAuto rendering skipped by content-visibility: auto

Even with no options, display: none and content-visibility: hidden return false.

One important limitation: true does not mean the element is on screen. An element outside the viewport, or covered by another element, still returns true. What it reports is only whether CSS is rendering it.

For “has it scrolled into view”, you want the Intersection Observer API instead—that is a different question.

// avoid trying to focus something that is hidden
function focusIfVisible(el) {
  if (!el.checkVisibility({ opacityProperty: true, visibilityProperty: true })) {
    return false;
  }
  el.focus();
  return true;
}

Sponsored

Popover API: tooltips and menus with no JavaScript

This is the most practically useful addition of the last few years. Two attributes give you an element in the top layer.

<button popovertarget="menu">Open menu</button>

<div id="menu" popover>
  <ul>
    <li><a href="#">Profile</a></li>
    <li><a href="#">Settings</a></li>
  </ul>
</div>

That alone gives you:

  • Light dismiss—clicking outside closes it
  • Escape closes it
  • Top layer placement, so no z-index stacking problems
  • Only one popover open at a time (unless you use popover="manual")

There are methods for script control too.

const menu = document.getElementById('menu');

menu.showPopover();
menu.hidePopover();
menu.togglePopover();

menu.addEventListener('toggle', (e) => {
  console.log(e.oldState, '→', e.newState); // 'closed' → 'open'
});

Popover or dialog?

popover <dialog> + showModal()
Background interaction Allowed Blocked
Focus trapping No Yes
Outside click closes it By default Needs closedby="any"
Best for Tooltips, menus, notifications Confirmations, blocking input

If you need to stop the user, use a dialog. If you are showing something supplementary, use a popover. Dialog implementation is covered in the HTML dialog element.

Node.moveBefore(): move a node without resetting it

Moving an element with appendChild() or insertBefore() removes it from the DOM first, which resets its state. Focus is lost, playing video stops, CSS animations restart.

moveBefore() moves it without detaching, so state survives.

const list = document.getElementById('list');
const item = document.getElementById('item');
const ref  = document.getElementById('ref');

// old: focus and video playback are lost
list.insertBefore(item, ref);

// new: state is preserved
list.moveBefore(item, ref);

What survives:

  • Focus (a text box being typed into stays active)
  • Playing <video> / <audio>
  • Open popovers, modals, and fullscreen
  • Running CSS transitions and animations
  • iframe content (no reload)

Safari does not support it as of September 2026, so feature-detect and fall back.

function moveElement(parent, node, ref) {
  if (typeof parent.moveBefore === 'function') {
    parent.moveBefore(node, ref);
  } else {
    parent.insertBefore(node, ref); // works, but loses state
  }
}

This solves the classic drag-to-reorder problem where the animation on the element you are holding breaks the moment it moves.

Sponsored

attachShadow(): encapsulated components

From here on, these are the long-established APIs. attachShadow() creates a subtree that outside CSS cannot reach.

class CustomButton extends HTMLElement {
  constructor() {
    super();
    const shadow = this.attachShadow({ mode: 'open' });

    // adoptedStyleSheets is the modern way to pass styles
    const sheet = new CSSStyleSheet();
    sheet.replaceSync(`
      button {
        background: #2a7fff;
        color: #fff;
        border: none;
        border-radius: 8px;
        padding: 10px 20px;
        font: inherit;
        cursor: pointer;
      }
      button:focus-visible { outline: 3px solid #ffb300; outline-offset: 2px; }
    `);
    shadow.adoptedStyleSheets = [sheet];

    const button = document.createElement('button');
    button.textContent = this.getAttribute('label') ?? 'Click Me';
    shadow.appendChild(button);
  }
}

customElements.define('custom-button', CustomButton);
<custom-button label="Submit"></custom-button>
  • Do not push styles in as an innerHTML string. With adoptedStyleSheets, the sheet is parsed once and shared across instances
  • mode: 'open' vs 'closed': with open, outside code can reach element.shadowRoot. closed hides it but provides no real security, and makes testing harder. open is the usual choice

getAnimations(): grab CSS-defined animations too

getAnimations() returns animations and transitions defined in CSS as well as ones created in script. That is what makes it useful.

const box = document.querySelector('.box');

box.animate(
  [{ transform: 'translateX(0)' }, { transform: 'translateX(100px)' }],
  { duration: 1000, iterations: Infinity }
);

document.querySelector('#stop-animation').addEventListener('click', () => {
  box.getAnimations().forEach((anim) => anim.pause());
});

// wait for every animation on the page to finish
await Promise.all(
  document.getAnimations().map((a) => a.finished.catch(() => {}))
);

finished rejects when an animation is cancelled, so add the catch when waiting on many at once. More on this in the Web Animations API animate() method.

requestFullscreen(): it returns a promise, so catch it

requestFullscreen() returns a promise and can fail—without user activation, or in an iframe lacking allow="fullscreen".

const content = document.querySelector('#content');

document.querySelector('#fullscreen-button').addEventListener('click', async () => {
  try {
    if (document.fullscreenElement) {
      await document.exitFullscreen();
    } else {
      await content.requestFullscreen({ navigationUI: 'hide' });
    }
  } catch (err) {
    console.warn('Fullscreen request failed:', err);
  }
});

// also fires when the user leaves fullscreen with Escape
document.addEventListener('fullscreenchange', () => {
  const isFull = Boolean(document.fullscreenElement);
  document.body.classList.toggle('is-fullscreen', isFull);
});

Users will exit with Escape regardless of the button you provide. Track state with document.fullscreenElement and the fullscreenchange event, never with your own boolean.

setPointerCapture(): avoiding the classic drag bug

setPointerCapture() keeps sending pointer events to an element even when the pointer leaves it, so dragging does not “let go” unexpectedly.

But feeding event.clientX straight into translate() is wrong—the element jumps to the cursor the instant you grab it.

// element teleports on grab
draggable.addEventListener('pointermove', (event) => {
  draggable.style.transform = `translate(${event.clientX}px, ${event.clientY}px)`;
});

Keep the offset between where you pressed and where the element is, and apply only the delta. Release the capture on pointerup.

const draggable = document.querySelector('#draggable');

let dragging = false;
let startX = 0, startY = 0;   // pointer position at press time
let baseX = 0, baseY = 0;     // accumulated offset

draggable.addEventListener('pointerdown', (event) => {
  dragging = true;
  startX = event.clientX;
  startY = event.clientY;

  draggable.setPointerCapture(event.pointerId);
  draggable.style.touchAction = 'none'; // do not fight page scrolling
});

draggable.addEventListener('pointermove', (event) => {
  if (!dragging) return;

  const dx = event.clientX - startX;
  const dy = event.clientY - startY;
  draggable.style.transform = `translate(${baseX + dx}px, ${baseY + dy}px)`;
});

function endDrag(event) {
  if (!dragging) return;
  dragging = false;

  baseX += event.clientX - startX;
  baseY += event.clientY - startY;

  draggable.releasePointerCapture(event.pointerId);
}

draggable.addEventListener('pointerup', endDrag);
draggable.addEventListener('pointercancel', endDrag);
  • Move by delta, not by absolute coordinates
  • touch-action: none, or mobile page scrolling steals the gesture
  • Handle pointercancel, or an interrupted drag leaves the element stuck

Summary

  • attachShadow(), requestFullscreen(), setPointerCapture(), getAnimations() are not 2024 additions
  • The actually new ones: checkVisibility() (2024), Popover API (2025), moveBefore() (no Safari yet)
  • checkVisibility() reports CSS rendering, not whether the element is on screen
  • Popovers for supplementary UI, <dialog> when you need to block interaction
  • moveBefore() preserves state; feature-detect and fall back to insertBefore()
  • Pass Shadow DOM styles via adoptedStyleSheets
  • requestFullscreen() is a promise; track state with document.fullscreenElement
  • Drag by delta, set touch-action: none, handle pointercancel

The value of a DOM article depends heavily on when it was written. Checking the MDN compatibility table before you start saves a lot of wasted effort.