SlideUp JavaScript: A Vanilla JS Guide

Sep 18, 2026·20 min read

Imagine a timeout-based disclosure panel where four quick clicks leave it half-sized and still present in the layout. A delayed cleanup from the first click can hide the panel after the final click reopens it.

A vanilla JavaScript slideUp effect animates a panel from its current rendered height to zero, clips the collapsing content, and applies hidden only when the closing animation successfully finishes.

What SlideUp Means in JavaScript

A slideUp effect collapses an element vertically until it occupies no height, then removes it from the page layout. It does not use translateY() to move the element toward the top of the screen.

That distinction changes the surrounding page. A translated element moves visually but keeps its original layout space. A collapsed element gives that space back, so the content below it moves upward.

Move pixels or reclaim space?BeforeAftertranslateYpanelcontentbelowspaceremainspanelmovescontentbelowcollapsepanelcontentbelowcontentrisesspace returned
Moving a panel and collapsing it have different effects on page layout.

The familiar slideUp() name comes from jQuery’s effects API. Browsers do not give DOM elements a same-named method. In vanilla JavaScript, you build the behavior from measurement, animation, and final state changes.

The finished sequence has three steps:

  • Measure the panel’s current rendered height.
  • Animate that height to 0 while clipping overflow.
  • Apply hidden after the closing animation finishes.

Opening reverses the height change, but its cleanup differs. The panel loses hidden before measurement, and its temporary height is removed after the animation so the content can return to natural layout.

That is slideUp. The JavaScript animations guide covers the larger timing loop around animated values.

A Minimal Vanilla JavaScript SlideUp

Start with an unpadded outer panel containing a padded inner wrapper. The outer panel owns the animated height and overflow: hidden; the inner wrapper owns spacing, borders, and content.

Two boxes, two jobsheightanimatesouter shell: height + clippinginner wrapper: padding +bordercontent staysundistortedthe shell clips at its changing edge
The outer shell animates and clips; the inner wrapper owns decoration and spacing.

This standalone version assumes a button whose aria-controls value is details-panel, an outer panel with that ID, and one inner wrapper:

if (typeof document !== "undefined") {
  const button = document.querySelector(
    '[aria-controls="details-panel"]'
  );
  const panel = document.querySelector("#details-panel");

  async function minimalSlideUp(element, duration = 300) {
    const startHeight = element.getBoundingClientRect().height; // measure

    element.style.overflow = "hidden";
    element.style.height = `${startHeight}px`;

    const animation = element.animate( // animate
      [
        { height: `${startHeight}px` },
        { height: "0px" },
      ],
      {
        duration,
        easing: "ease",
      }
    );

    await animation.finished;

    element.hidden = true; // finalize
    element.style.height = "";
  }

  button?.addEventListener("click", async () => {
    await minimalSlideUp(panel);
    button.setAttribute("aria-expanded", "false");
  });
}

getBoundingClientRect().height reads the height currently painted on screen. animate() starts the effect and returns an Animation, while finished is that controller’s completion promise.

The panel keeps clipping its children as the outer height shrinks. Once finished resolves, hidden removes the completed panel from rendering and normal layout.

This version demonstrates one uninterrupted close. It does not reopen the panel or protect its finalization from cancellation. The canonical implementation below adds those parts.

Build slideUp, slideDown, and slideToggle

The three public functions can share one state-changing function. slideUp requests closed, slideDown requests open, and slideToggle requests the opposite of the last requested state.

Open and closed mean more than a height value:

  • Open means hidden is absent, aria-expanded is true, and no fixed height remains after completion.
  • Closed means aria-expanded is false, the height has reached zero, and hidden is present.
  • Animating means the panel remains rendered while one Animation moves between those states.

For an opening panel, remove hidden, set its height to auto, and read scrollHeight. That property supplies a content-sized target rather than a guessed value such as 400px.

Unknown height becomes measurablecurrentscrollHeightheight:autostart hereanimate hereclear heightnew content fits
Measure the real content height, animate to it, then return the panel to natural sizing.

After measurement, restore the explicit starting height and animate toward the target. When opening finishes, clear the inline height. Responsive text wrapping, images, fonts, and content added later can then determine the panel’s height.

For a closing panel, keep hidden off during the animation. Applying it at the beginning would remove the box immediately, leaving no visible height to animate.

The timing of hidden is asymmetricOPENINGclosedhidden presenthidden removedheight growspanel visibleopennatural heightCLOSINGopenhidden absenthidden still offheight shrinkspanel visibleclosedhidden added
Opening removes hidden before motion; closing adds it only after motion finishes.

Each function returns the promise created from animation.finished. Code that needs to wait can use await:

async function closePanel(panel, disclosureButton) {
  const completed = await slideUp(panel, disclosureButton);

  if (completed) {
    console.log("panel closed");
  }
}

The resolved boolean distinguishes a completed request from one replaced by a newer request. Cancellation rejects the original finished promise, so the implementation handles that rejection and resolves the public result as false.

The Web Animations API uses the same Animation controller for other effects. The useful split is small: animate() starts and returns the controller; finished reports completion.

Handle Rapid Toggles and Changing Content

A timeout-based cleanup assumes that the first animation still owns the panel when its duration expires. Rapid toggles break that assumption.

Say a close begins, an open replaces it after 80 milliseconds, and the old close callback later applies hidden. The new animation may be correct, but stale cleanup wins.

TimeEventAnimation stored in the WeakMap
0 msClose animation A beginsA
80 msThe current height is measured and A is canceledA
80 msReplacement open animation B beginsB
LaterA rejects or reports stale completion, so its cleanup is ignoredB
FinishB completes and applies the open stateB

Interruption safety starts the replacement at the panel’s current rendered height. A WeakMap stores the active Animation for each panel. Before replacement, the function measures that visible height, cancels the old controller, and starts the new controller from the measurement.

Cancellation rejects finished. That rejection must be handled, even though cancellation is deliberate. More importantly, every completion handler checks whether its controller is still the one stored for the panel. An obsolete close can no longer hide a reopened panel.

Who still owns cleanup?WeakMap sayspanel → Bold close Acancelednew open Bcurrentam I stillstored?NOYESignorecleanupapply openstate
Only the animation still named by the WeakMap may finalize the panel.

The outer shell stays free of vertical padding and borders. scrollHeight includes padding but excludes borders and margins, so putting those details on the animated box makes the height arithmetic harder to match. The inner wrapper owns them instead.

Nested margins can also protrude or collapse in surprising ways. Padding on the inner wrapper gives its children a stable boundary, and the outer shell clips the result.

Changing content while open works because successful expansion removes the fixed height. Content added during an opening animation may remain clipped until that animation completes, but the cleanup returns the panel to natural height afterward.

Here is the canonical implementation and console demo. Open a new about:blank tab, open its developer tools, select the Console, and paste the code; it creates a toggle button, a rapid-toggle control, an add-content control, and a simulated reduced-motion control:

if (typeof document !== "undefined") {
  const animations = new WeakMap();
  const requestedStates = new WeakMap();
  let simulatedReducedMotion = false;

  function reducedMotionRequested(options = {}) {
    return options.reduceMotion === true ||
      matchMedia("(prefers-reduced-motion: reduce)").matches;
  }

  function setFinalState(panel, button, open) {
    panel.hidden = !open;
    panel.style.height = "";
    button.setAttribute("aria-expanded", String(open));
  }

  function changePanel(panel, button, open, options = {}) {
    requestedStates.set(panel, open);

    const previous = animations.get(panel);
    const startHeight = panel.hidden
      ? 0
      : panel.getBoundingClientRect().height;

    if (previous) {
      previous.cancel();
      animations.delete(panel);
    }

    if (reducedMotionRequested(options)) {
      setFinalState(panel, button, open);
      return Promise.resolve(true);
    }

    panel.hidden = false;
    if (open) {
      button.setAttribute("aria-expanded", "true");
    }
    panel.style.overflow = "hidden";
    panel.style.height = "auto";

    const endHeight = open ? panel.scrollHeight : 0;
    panel.style.height = `${startHeight}px`;

    const animation = panel.animate(
      [
        { height: `${startHeight}px` },
        { height: `${endHeight}px` },
      ],
      {
        duration: options.duration ?? 300,
        easing: options.easing ?? "ease",
      }
    );

    animations.set(panel, animation);

    return animation.finished.then(
      () => {
        if (animations.get(panel) !== animation) {
          return false;
        }

        animations.delete(panel);
        setFinalState(panel, button, open);
        return true;
      },
      (error) => {
        if (error.name === "AbortError") {
          return false;
        }
        throw error;
      }
    );
  }

  function slideUp(panel, button, options) {
    return changePanel(panel, button, false, options);
  }

  function slideDown(panel, button, options) {
    return changePanel(panel, button, true, options);
  }

  function slideToggle(panel, button, options) {
    const current = requestedStates.has(panel)
      ? requestedStates.get(panel)
      : !panel.hidden;

    return changePanel(panel, button, !current, options);
  }

  function makeButton(text) {
    const button = document.createElement("button");
    button.type = "button";
    button.textContent = text;
    document.body.append(button);
    return button;
  }

  const toggle = makeButton("Toggle panel");
  const rapid = makeButton("Toggle three times");
  const add = makeButton("Add content");
  const motion = makeButton("Simulated reduced motion: off");

  const panel = document.createElement("section");
  const inner = document.createElement("div");
  panel.id = "shipping-details";
  panel.style.overflow = "hidden";
  inner.style.padding = "16px";
  inner.style.border = "1px solid";
  inner.append("Delivery takes two working days.");
  panel.append(inner);
  document.body.append(panel);

  toggle.setAttribute("aria-controls", panel.id);
  toggle.setAttribute("aria-expanded", "true");

  toggle.addEventListener("click", () => {
    slideToggle(panel, toggle, {
      reduceMotion: simulatedReducedMotion,
    });
  });

  rapid.addEventListener("click", () => {
    const toggleWithDemoSettings = () => slideToggle(panel, toggle, {
      reduceMotion: simulatedReducedMotion,
    });

    toggleWithDemoSettings();
    setTimeout(toggleWithDemoSettings, 60);
    setTimeout(toggleWithDemoSettings, 120);
  });

  add.addEventListener("click", () => {
    const paragraph = document.createElement("p");
    paragraph.textContent = "A signature is required at the door.";
    inner.append(paragraph);
  });

  motion.addEventListener("click", () => {
    simulatedReducedMotion = !simulatedReducedMotion;
    motion.textContent =
      `Simulated reduced motion: ${
        simulatedReducedMotion ? "on" : "off"
      }`;

    const open = requestedStates.has(panel)
      ? requestedStates.get(panel)
      : !panel.hidden;

    changePanel(panel, toggle, open, {
      reduceMotion: simulatedReducedMotion,
    });
  });

  window.slideUp = slideUp;
  window.slideDown = slideDown;
  window.slideToggle = slideToggle;
}

The two short timers simulate repeated input; they do not decide when an animation has finished. Completion comes from finished, and the identity check decides whether that completion still owns the panel.

Make the Collapsible Panel Accessible

The controlling element is a native button. Its aria-controls value points to the panel’s ID, while aria-expanded reports whether the panel is currently available.

One control, one panel, one statenative buttonaria-controlspanelmatching IDcontrols by IDOPENaria-expanded: truehidden: absentCLOSEDaria-expanded: falsehidden: present
The button identifies the panel and reports a state consistent with the panel’s visibility.

When opening, remove hidden and set aria-expanded to true together before the animation. When closing, leave aria-expanded as true while the content remains available, then set it to false alongside hidden after successful completion.

The identity guard prevents a canceled close from applying that final state.

The reduced-motion branch checks matchMedia("(prefers-reduced-motion: reduce)"). When it matches, the function cancels existing work and applies the requested state immediately. The simulated control in the demo exercises the same branch without changing an operating-system setting.

Different motion, same resultrequestedstatemotionpreferenceanimate heightthen finalizecancel motionapply nowhidden + ariasynchronized
Both motion preferences converge on the same synchronized final state.

These attributes describe state; they do not replace keyboard behavior. A native button already handles activation through the expected button interactions, which is why a clickable generic container is the wrong starting point. Introduction to browser events explains how those activations reach the listener.

The panel’s visual rules still belong with its other styles and classes. Keep the outer shell unpadded, place decorative spacing on the inner wrapper, and let JavaScript own the temporary height and state attributes as well as the panel’s inline overflow.

jQuery slideUp Versus the Vanilla Version

jQuery supplies slideUp(), slideDown(), and slideToggle() as library methods. Its slideUp() default duration is 400 milliseconds, and the method accepts a duration, an easing name, and a completion callback:

if (typeof window !== "undefined" && window.jQuery) {
  jQuery("#shipping-details").slideUp(
    400,
    "swing",
    function () {
      console.log("panel closed");
    }
  );
}
panel closed

jQuery effects normally participate in the library’s fx queue. Calling another queued effect can place it behind the current effect, and jQuery also provides queue and stopping controls. Existing code that depends on those semantics should retain the official slideUp() method.

The vanilla functions here take milliseconds, accept a Web Animations easing value, and return a promise-compatible result. They cancel and replace the current animation rather than reproducing jQuery’s effects queue.

Queue or replace?jQueryqueueA runs fullyB waits, thenrunsvanillareplaceAcanceledB startsimmediatelycurrent heighttime →
jQuery’s queued effects and this utility’s replacement behavior produce different timelines.

They also do not emulate named speeds, per-element callbacks across a collection, or every interaction involving margins and min-height. This is a practical disclosure-panel utility, not a drop-in jQuery replacement.

For new code without jQuery, the native version keeps the behavior local and makes interruption ownership explicit. JavaScript Fundamentals develops the functions, promises, DOM state, and event handling used to reason about that implementation.

Frequently asked questions

What does slideUp do in JavaScript?
A slideUp effect vertically collapses an element and removes it from the page layout when the animation finishes. The familiar slideUp() method belongs to jQuery, so vanilla JavaScript needs its own implementation.
How do you animate an element with an unknown height?
Remove hidden, read the element's scrollHeight, and animate from its current rendered height to that measured value. Clear the temporary height after opening so later content changes can resize the element.
How do you stop rapid toggles from breaking a slide animation?
Store the current Animation for each element, measure its rendered height, and cancel it before starting the replacement. Only the Animation still stored for that element may apply the final hidden state.
How should slideUp handle reduced motion?
Check prefers-reduced-motion before starting the animation. When it matches reduce, apply the requested open or closed state immediately while keeping hidden and aria-expanded synchronized.