A carousel selects one slide while keeping Previous and Next available. This three-slide example shows how selection changes visibility without moving focus away from the controls. It is manual; optional rotation uses the same state but requires a separate timer.
Start with a manual walkthrough
paused defaults true. This example has no timer and therefore no misleading rotation control:
Previous and Next are sufficient. Each slide stays mounted; selection changes its hidden state.
import { html, component } from "@typed/template";
import * as Carousel from "@typed/ui/Carousel";
export const ProjectWalkthrough = component(function* () {
const state = yield* Carousel.makeState({ activeId: "walkthrough-create", paused: true });
const collection = yield* Carousel.makeCollection();
return Carousel.Root({ state, label: "Project walkthrough", content: [
html`<div>
${Carousel.Previous({ state, collection, content: "Previous step" })}
${Carousel.Next({ state, collection, content: "Next step" })}
</div>`,
Carousel.Slide({ state, collection, id: "walkthrough-create", label: "1 of 3: Create",
content: html`<h3>Create a project</h3><p>Give the project a name and owner.</p>` }),
Carousel.Slide({ state, collection, id: "walkthrough-review", label: "2 of 3: Review",
content: html`<h3>Review the draft</h3><p>Invite teammates to review the details.</p>` }),
Carousel.Slide({ state, collection, id: "walkthrough-share", label: "3 of 3: Share",
content: html`<h3>Share the result</h3><p>Publish once the review is complete.</p>` }),
] });
});
The root renders a labeled region with carousel role description. Slides render labeled groups
with slide role description. The controls are native type=button elements, so normal Tab,
Enter, and Space work. A click changes activeId without transferring focus to the newly revealed
slide; keeping focus on the control allows repeated navigation.
A slide registry is not a timer
Carousel.move(state, collection, "next" | "previous") selects the adjacent registered slide in
DOM order and wraps. The controls need the collection; without it their internal action is inert.
Carousel.select(state, id) changes slide identity directly. Neither operation creates animation,
preloading, or automatic rotation. The collection includes hidden slides because those are the
next available destinations.
paused is a policy flag. RotationControl toggles it, but no built-in scheduler observes it to
advance slides. Adding that control to the manual example alone would promise motion that never
occurs. If your product needs rotation, create a scoped timer that checks paused and advances using
move; dispose it with the rendered component and avoid starting a new untracked timer on every
state emission. See Fx lifetime before adding recurring work.
Pause behavior must cooperate with the scheduler
The root pauses on focus entering. Focus-triggered pause remains paused until an explicit restart. Mouse entry temporarily pauses and records whether rotation had been running; mouse leave resumes only that pointer-paused state. Focus entry clears the pending pointer resume. These transitions are already supplied, but they only affect motion if the application’s scheduler honors the flag.
For automatic rotation, put a labeled RotationControl before the slides and derive its label from
paused. The APG carousel pattern covers stop/start
controls and announcements. The primitive supplies neither a live region nor reduced-motion policy.
Visibility is not lifetime
A slide’s hidden attribute removes it from normal presentation while keeping its DOM and subscriptions mounted. Videos, network work, or local timers can remain alive in hidden slides. Pause that work explicitly if the slide should stop consuming resources. Avoid CSS that overrides hidden rendering, and keep the navigation controls outside slide content so selecting a slide cannot hide the focused control.
If slide data changes, preserve the current ID when possible and choose a successor when it is removed. Selecting a nonexistent ID hides every slide; the state constructor does not validate membership. A keyed dynamic list preserves retained DOM identity, but still needs this application reconciliation policy.
Verify that exactly one slide is visible and focus stays on Previous/Next when navigation wraps. An automatic variant also needs to verify that focus pause and pointer resume actually stop and restart its timer. Public parts: Carousel.