Skip to content

Timing issue when typing after clearing sometimes causes container to be stuck with broken styles #43

@loicnico96

Description

@loicnico96

Version: 0.0.9

Description

When a TextMorph container transitions to an empty string and the user types a new character quickly (NOT immediately) the container sometimes permanently freezes at width: 0px; height: 0px.

The computed styles on the stuck element:

transition-duration: 400ms;
transition-timing-function: cubic-bezier(0.19, 1, 0.22, 1);
width: 0px;
height: 0px;

Steps to reproduce

  1. Render a TextMorph inside an input overlay (transparent <input> + morphing text overlay).
  2. Type a value, e.g. "100".
  3. Hold backspace to clear the field to "".
  4. Type a new character, slightly before fade animation ends (may need a few tries to trigger it).

Example (near the end stucked in weird position because container dimensions are stuck at 0);

demo-torph.mp4

Root cause

transitionContainerSize in animate.ts manages the width/height CSS transition on the torph-root element. When text becomes empty it animates the container shut, setting style.width and style.height to "0px", and stores a cancel callback in the module-level pendingCleanup:

pendingCleanup = () => {
  element.removeEventListener("transitionend", onEnd);
  clearTimeout(fallbackTimer);
  pendingCleanup = null;
  // ⚠️ does NOT reset style.width / style.height
};

CSS transitions are committed at frame boundaries, not synchronously during script execution. If a new keystroke arrives before the first transition frame renders, element.offsetWidth (forced reflow) returns 0 — the CSS target value — rather than the animated-from value.

The next call to transitionContainerSize then:

  1. Calls pendingCleanup() — removes the transitionend listener and clears the fallback timer, without resetting style.width or style.height.
  2. Hits the early-return guard because oldWidth === 0 || oldHeight === 0.
  3. Returns, leaving style.width = "0px" and style.height = "0px" permanently on the element.

The normal completion path (cleanup()) does reset both properties to "auto", but it is never reached because pendingCleanup() already removed the listener and cleared the timer.

Fix

Reset style.width and style.height to "auto" in the early-return branch, so the container is always unblocked when source dimensions are unknown:

export function transitionContainerSize(
  element: HTMLElement,
  oldWidth: number,
  oldHeight: number,
  duration: number,
  onComplete?: () => void,
) {
  if (pendingCleanup) {
    pendingCleanup();
    pendingCleanup = null;
  }

  if (oldWidth === 0 || oldHeight === 0) {
    element.style.width = "auto";  // ← add these two lines
    element.style.height = "auto"; // ←
    return;
  }

  // ... rest unchanged
}

This ensures that even when transitionContainerSize exits early, any previously-pinned "0px" inline styles are cleared and the container reverts to natural content sizing.

The corresponding patch-package fixes the issue.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions