cswork / libraryOpen practice workspace

Articles

Image Carousel Coding Interview: Autoplay, Focus, and Reduced Motion

Prepare for image carousel coding interviews with autoplay state, focus stops, reduced motion, timer cleanup, hidden slides, and deterministic test traces.

An image carousel coding interview becomes interesting when automatic movement meets user control. A component can advance slides on schedule yet still restart after the user stopped it, hide a focused link, announce every automatic change, or keep an old timer alive after unmounting.

The central design is persistent rotation intent plus temporary blockers, not one overloaded paused boolean. Start with Implement image carousel with comments and upvotes. The event ledger and timer model below are original exercises, not employer-process claims. W3C and MDN sources were checked October 1, 2026; a current W3C pattern/example disagreement is called out rather than hidden.

Which carousel contract are you implementing?

First establish whether autoplay is required. A manual carousel avoids an entire class of timing and interruption problems. If autoplay is required, clarify the initial state, rotation control, hover behavior, keyboard focus, reduced motion, and what happens when the page is hidden. A swipe gesture is an additional input, not a substitute for discoverable buttons.

Use a three-slide landscape gallery for this exercise. Previous and Next wrap, inactive slides are removed from interaction, and a rotation control appears before the other controls in keyboard order. The synthetic interval is five seconds; it is a test parameter, not a universal recommendation for readable content. Slides have stable IDs because their order may change.

The W3C Carousel pattern says focus entering an automatically rotating carousel stops rotation, and moving focus out does not restart it without explicit user action. Hover also stops rotation. Its guidance for previous/next controls preserves focus on the control, rather than moving focus with the slide.

There is a current evidence wrinkle: the W3C previous/next carousel example describes automatic resumption when focus leaves, and an explicit start mode that overrides focus/hover pauses. That is not the same policy as the pattern text. This exercise follows the pattern's explicit-restart rule and uses a conservative hover policy. State this choice if borrowing example code.

How do you separate intent from conditions?

Store requested, meaning the user or initial policy currently requests rotation. Store temporary conditions separately: hovered, hidden, and reducedMotion. Derive whether a timer may run:

function canRotate(s, slideCount) {
  return s.requested && !s.hovered && !s.hidden &&
    !s.reducedMotion && slideCount > 1;
}

On focus entry, set requested = false. On blur, do not turn it back on. On Stop, also set it false. On explicit Start, set it true if the reduced-motion policy allows it. Hover and visibility changes update their own fields without rewriting user intent.

Why not keep focused as a permanent blocker? The Start button is inside the carousel. If any focused descendant makes rotation impossible, pressing Start with a keyboard can never work. In our policy, a focus-entry event cancels intent; a deliberate Start action can establish new intent while that button already owns focus. Moving focus to another carousel control stops it again.

This is a chosen implementation contract. Start while hovering arms rotation but waits until the pointer leaves; it does not override hover. The UI should explain that waiting condition if it would be confusing. Do not label every temporary blockage as “stopped by you.” User Stop, hidden-page pause, and motion preference are distinct reasons with distinct recovery paths.

Maintain a clear invariant: only the explicit Start action can undo a focus-induced or user-requested stop. Removing a temporary blocker can resume only an already-requested rotation. For pointer activation, capture the control's intended action before focus entry changes its label: clicking Stop must not become Start because focus stopped rotation first. That is easier to test than scattered setPaused(false) calls.

Can you walk through the event ledger?

Start with three slides, rotation requested, no blockers, and one pending timer. The following ledger is original exercise data. Each row starts from the state described, rather than pretending all events are interchangeable.

Event sequenceIntent / blocker resultExpected timer behavior
Hover enters, then leavesRequest remains true; hover clearsResume with one fresh interval
Focus enters, then leavesRequest becomes false and stays falseNo restart on blur
User presses Stop, then pointer leavesRequest remains falseNo restart on mouseleave
User presses Start while already focusedRequest becomes trueRun if no temporary blocker
Page hides, then becomes visibleRequest preserved; hidden clearsResume only if still requested
Reduced motion becomes activeMotion blocker becomes trueCancel rotation immediately

Now combine conditions. The page becomes hidden while the pointer is over the gallery. Returning to the page does not clear hover, so it cannot resume yet. If the user had pressed Stop before either event, clearing both conditions still cannot restart rotation. A single boolean loses that history unless every handler reconstructs it perfectly.

Test a queued callback too. The timer reaches its due time, but before it can apply the slide change, a focus event stops rotation. Clearing the timer alone is not the full assertion: a stale callback must also fail a current-generation and eligibility check. This separates “we asked the scheduler to cancel” from “obsolete work cannot mutate the current gallery.”

What timer design is easiest to defend?

A single-shot timeout is a useful interview choice because its lifecycle is explicit. Keep a generation counter and one timer handle. Whenever eligibility or the interval changes, invalidate the old generation, clear the handle, and schedule at most one new timeout. A simplified lifecycle sketch is:

let epoch = 0;
let timer = null;
function reconcileTimer() {
  const ticket = ++epoch;
  clearTimeout(timer);
  timer = null;
  if (!canRotate(state, slides.length)) return;
  timer = setTimeout(() => {
    if (ticket !== epoch || !canRotate(state, slides.length)) return;
    showNextSlide();
    reconcileTimer();
  }, intervalMs);
}
function dispose() {
  ++epoch;
  clearTimeout(timer);
  timer = null;
}

This sketch assumes state and slides refer to the current model. A framework implementation must avoid capturing an obsolete snapshot and must connect cleanup to its actual lifecycle. It is not a complete drop-in component. Test repeated reconciliation to prove that only the latest ticket advances a slide.

Use document visibility changes to update the hidden-page condition. Our exercise does not catch up on missed rotations after a hidden period. It starts a fresh full interval, leaving the reader on a predictable slide. That is an editorial product choice, not a browser promise about timer precision.

Manual Next can reset the timer interval so the newly selected slide receives a full viewing period. State this policy and implement it in the same reconciliation path. Do not add another interval in the Next handler. Cleanup also removes media-query and visibility listeners; a canceled timer with a leaking listener is still an incomplete lifecycle.

What should reduced motion change?

The prefers-reduced-motion media feature exposes a user preference about motion. It is not a JavaScript timer cancellation API. Removing a CSS transition while slides continue to switch every five seconds does not implement our intended manual-only mode.

For this exercise, reduced motion disables autoplay and animated transitions while preserving Previous, Next, and direct selection. The Start control explains why automatic rotation is unavailable. Listen for preference changes at runtime as well as reading the initial preference. We choose a stricter manual-only policy than merely starting paused; that distinction belongs in the answer.

When the preference turns off, our model does not silently manufacture a new Start action after a focus or user stop. If initial requested intent remained true only behind the motion blocker, decide whether clearing that blocker may resume. To make the exercise unambiguous, activating reduced motion also clears requested intent, so leaving the mode requires explicit restart.

Do not change the user's operating-system setting to test this case. A controlled media-query stub or test input can exercise the state transition. Then separately verify CSS in the target environment. A model assertion proves the chosen decision logic; it does not prove every browser/assistive-technology combination renders the transition correctly.

How do slides, focus, and announcements stay consistent?

Use a visible gallery label and meaningful image alternatives. Keep control names stable and descriptive. A button labeled Next is clearer than an unlabeled chevron. The rotation control should describe the action it performs, such as Start rotation or Stop rotation, rather than requiring the user to infer the current state from an icon.

The WAI carousel functionality tutorial illustrates native previous/next buttons and user-controlled focus, including a polite announcement for manual slide changes. Our fixture uses a short manual status message, not a stream of automatic announcements. Automatic cycling should not repeatedly interrupt unrelated reading.

Inactive slides must not leave hidden links in the Tab sequence. For this small exercise, the hidden attribute is simpler than off-screen positioning plus accessibility adjustments. aria-hidden alone does not stop a descendant from receiving keyboard focus. If a manual change would hide a currently focused element inside the old slide, first decide where focus should move and why; do not simply conceal it.

Retain the active slide by stable ID when data reorders. If that slide is removed, choose a neighboring survivor and reconcile the timer. Zero slides produce an empty state; one slide needs no rotation. Both cases must avoid modulo by zero and useless scheduling. A failed image should preserve controls and a stable layout instead of collapsing the gallery and moving the surrounding page unexpectedly.

What should your test report actually prove?

Test the event ledger with a deterministic scheduler. Assert timer count, generation validity, requested intent, and active slide ID, not just the label of the pause button. Include a stale callback fired after Stop, two reconciliations before a timeout, removal of the active slide, and destruction of the component.

Then exercise real browser focus. Tab into the carousel, leave it, and confirm no restart. Press Start while the control is focused. Use Next repeatedly and verify focus stays on Next. Check that inactive slide links are absent from sequential navigation. Our local fixture tests these bounded behaviors; it is not an end-to-end accessibility certification or evidence of a particular interview's grading system.

Explain limitations honestly. A pure reducer test does not inspect image loading, touch gestures, viewport geometry, or screen-reader announcements. A browser screenshot does not establish timer correctness. Combining a state ledger, actual focus observations, and scoped rendering checks gives the interviewer a much stronger proof than saying “I tested the carousel.”

Five exercises for the same engineering habits

QuestionCarousel-related practice
Implement image carousel with comments and upvotesKeep active-item state separate from per-item state
Build and optimize a video playlist componentDistinguish playback intent from data and loading
Implement a traffic-light UI with JSTrace timer-driven transitions and cleanup
Implement a React flag loader with typewriter effectBound animation work and reset behavior
Plan Testing and Deployment for a React FeatureSeparate model tests from browser evidence

If that distinction is clear, the timer code becomes much easier to reason about.

Sources and Further Reading