npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@roku-sdk/slideshow

v2.19.1

Published

Horizontal slideshow engine for Roku SceneGraph TypeScript apps. Supports eager, virtual, and pooled rendering modes with animated transitions, focus integration, and composable chrome (chevrons, progress dots).

Readme

@roku-sdk/slideshow

Horizontal slideshow engine for Roku SceneGraph TypeScript apps. Supports eager, virtual, and pooled rendering modes with animated transitions, focus integration, and composable chrome (chevrons, progress dots).


Overview

@roku-sdk/slideshow provides a factory-function API (createSlideshow, createVirtualSlideshow, createPooledSlideshow) that mirrors the createFlexibleGrid pattern from @roku-sdk/flex-grid. Each factory returns an element you mount in your component tree plus imperative controls for programmatic navigation.

Key features:

  • Three rendering modes: eager (all slides upfront), virtual (windowed), pooled (template pools with imperative reparenting)
  • Smooth animation: SceneGraph Animation node via the Tween pattern — no JS animation loop
  • Focus integration: Factory creates an internal FocusBoundary — supports both non-interactive and interactive slides out of the box
  • Composable chrome: swap in your own chevrons or dots, or use the defaults
  • Tree-shakable exports: each mode and chrome component is a separate sub-path

Installation

pnpm add @roku-sdk/slideshow

Quick Start

Eager Mode (Non-Interactive Slides)

Renders all slides at mount time. Best for small, fixed-count slideshows.

import { createSlideshow, FocusableSlide, DefaultChevrons, DefaultDotIndicator } from "@roku-sdk/slideshow";

const slideshowRef = {};

const { element } = createSlideshow({
    slideCount: 5,
    slideWidth: 1920,
    slideHeight: 1080,
    focusId: slideshowRef,
    renderSlide: ({ index, focused, width, height, slideFocusId, goTo }) => (
        <FocusableSlide slideFocusId={slideFocusId} onFocused={goTo}>
            <rectangle width={width} height={height} color={focused() ? "0x0055FFFF" : "0x222222FF"} />
            <label text={`Slide ${index + 1}`} translation={[40, 40]} color="0xFFFFFFFF" />
        </FocusableSlide>
    ),
    chevrons: DefaultChevrons,
    dots: DefaultDotIndicator,
    onSlideChange: (index) => console.log("Now on slide", index),
});

// Mount inside the app-root RootFocusBoundary:
return (
    <RootFocusBoundary keyEvent={inputs.keyEvent} initialFocus={slideshowRef}>
        {element()}
    </RootFocusBoundary>
);

Eager Mode (Interactive Slides)

Slides with buttons or other focusable content use a nested FocusBoundary:

import { createSlideshow, FocusableSlide } from "@roku-sdk/slideshow";
import { FocusBoundary, useFocusable } from "@roku-sdk/navigation";

const { element } = createSlideshow({
    slideCount: 5,
    slideWidth: 1920,
    slideHeight: 1080,
    focusId: slideshowRef,
    renderSlide: ({ index, width, height, slideFocusId, goTo }) => {
        if (index === 2) {
            // Interactive slide — nested FocusBoundary with buttons
            const btnRefs = [{}, {}];
            return (
                <group>
                    <rectangle width={width} height={height} color="0x0F3460FF" />
                    <FocusBoundary focusId={slideFocusId} initialFocus={btnRefs[0]} navigation="horizontal">
                        <MyButton label="Play" focusId={btnRefs[0]} onFocused={goTo} />
                        <MyButton label="Pause" focusId={btnRefs[1]} onFocused={goTo} />
                    </FocusBoundary>
                </group>
            );
        }
        // Non-interactive slide
        return (
            <FocusableSlide slideFocusId={slideFocusId} onFocused={goTo}>
                <rectangle width={width} height={height} color="0x222222FF" />
            </FocusableSlide>
        );
    },
});

How it works: The factory's internal FocusBoundary (navigation="horizontal") navigates between slide positions on Left/Right. Interactive slides register as nested boundaries — keys dispatch into the slide first. When unhandled (e.g., Right on the last button), the key bubbles up and the factory advances to the next slide.

Virtual Mode

Only renders slides within windowSize of the current index. Best for large slide counts.

import { createVirtualSlideshow } from "@roku-sdk/slideshow/virtual";
import { FocusableSlide } from "@roku-sdk/slideshow";

const { element, currentIndex, goTo } = createVirtualSlideshow({
    slideCount: 100,
    slideWidth: 1920,
    slideHeight: 1080,
    focusId: slideshowRef,
    windowSize: 2,
    renderSlide: ({ index, width, height, slideFocusId, goTo }) => (
        <FocusableSlide slideFocusId={slideFocusId} onFocused={goTo}>
            <MySlideComponent index={index} width={width} height={height} />
        </FocusableSlide>
    ),
});

Pooled Mode

Uses a fixed pool of template nodes, reparented imperatively. Best for high-slide-count carousels with repeated content types.

import { createPooledSlideshow } from "@roku-sdk/slideshow/pooled";

const { element } = createPooledSlideshow({
    slideCount: 200,
    slideWidth: 400,
    slideHeight: 300,
    focusId: slideshowRef,
    templates: {
        standard: StandardSlideComponent,
        featured: FeaturedSlideComponent,
    },
    getTemplateId: (index, item) => item?.featured ? "featured" : "standard",
    poolSize: { standard: 5, featured: 2 },
    items: myDataArray,
});

Note: Pooled mode uses a leaf useFocusable with onKey handlers (rather than per-slide boundaries) because pool templates render inside createRoot outside FocusBoundary context. Navigation still works — Left/Right scrolls between slides.

Chrome Sub-Path Imports

import { DefaultChevrons } from "@roku-sdk/slideshow/chrome/chevrons";
import { DefaultDotIndicator } from "@roku-sdk/slideshow/chrome/dots";

// Pass as props:
createSlideshow({ ..., chevrons: DefaultChevrons, dots: DefaultDotIndicator });

// Or disable chrome:
createSlideshow({ ..., chevrons: false, dots: false });

JSX Wrapper

For purely declarative usage:

import { defineSlideshow, DefaultChevrons, DefaultDotIndicator } from "@roku-sdk/slideshow";

const MySlideshow = defineSlideshow<MyItemType>();

return (
    <MySlideshow
        slideCount={items.length}
        slideWidth={1920}
        slideHeight={1080}
        items={items}
        renderSlide={props => <MySlide {...props} />}
        chevrons={DefaultChevrons}
        dots={DefaultDotIndicator}
    />
);

Focus Integration

The three factories differ here, and the difference decides how focus leaves the slideshow:

| Factory | Boundary it renders | Movement | At the first or last slide | |---|---|---|---| | createSlideshow | FocusBoundary with navigation="horizontal" | The boundary walks between slides | Fires onEdgeReached and keeps the key — call setFocus yourself | | createVirtualSlideshow | FocusBoundary with no navigation | The factory calls prev() / next() and moves focus itself | Fires onEdgeReached and keeps the key — call setFocus yourself | | createPooledSlideshow | none — a single leaf useFocusable | Its onKey handlers advance the index | Returns bubble, so the key bubbles and focus leaves on its own |

In eager and virtual modes each slide registers in the boundary via the slideFocusId prop passed to renderSlide. Pooled templates receive PooledSlideRenderProps, which has no slideFocusId, and render outside any boundary's context — they can never be focused. Put a slide that needs focus in uniqueSlides.

renderSlide Props

Every renderSlide callback receives:

interface SlideRenderProps<T> {
    index: number;                // Slide position
    item: T | undefined;          // Data item (if items array provided)
    focused: Accessor<boolean>;   // True when this is the current slide
    width: number;                // Slide width
    height: number;               // Slide height
    slideFocusId: object;             // Focus ref — attach to your focusable
    goTo: () => void;             // Scrolls slideshow to THIS slide
}

Two Patterns

Non-interactive slides: Use the FocusableSlide helper (calls useFocusable internally):

<FocusableSlide slideFocusId={slideFocusId} onFocused={goTo}>
    {/* your slide content */}
</FocusableSlide>

Interactive slides: Use slideFocusId as the focusId on your own <FocusBoundary>. Wire goTo into each button's onFocusGained so the strip scrolls when focus enters the slide:

<FocusBoundary focusId={slideFocusId} navigation="horizontal" initialFocus={btnRefs[0]}>
    <button focusId={btnRefs[0]} onFocusGained={goTo} />
    <button focusId={btnRefs[1]} onFocusGained={goTo} />
</FocusBoundary>

focusId Prop

Pass a focusId object to the factory config. This registers the slideshow's internal boundary in whatever parent FocusBoundary you have at the app root:

const slideshowRef = {};
const { element } = createSlideshow({ ..., focusId: slideshowRef });

// App-root boundary uses slideshowRef as initialFocus:
<RootFocusBoundary keyEvent={inputs.keyEvent} initialFocus={slideshowRef}>
    {element()}
</RootFocusBoundary>

Imperative Handle

All factory functions return a SlideshowHandle<T>:

interface SlideshowHandle<T> {
    element: () => JSX.Element;         // Mount this (call it: {element()})
    currentIndex: Accessor<number>;     // Reactive current index
    currentItem: Accessor<T | undefined>; // Reactive current item
    goTo(index: number, animated?: boolean): void;
    next(): void;
    prev(): void;
    getCurrentIndex(): number;
    updateSlides(startIndex: number, items: (T | undefined)[]): void;
    setSlideCount(n: number): void;
}

Package Exports

| Import path | What you get | |---|---| | @roku-sdk/slideshow | createSlideshow, FocusableSlide, defineSlideshow, Slideshow, DefaultChevrons, DefaultDotIndicator, types | | @roku-sdk/slideshow/virtual | createVirtualSlideshow | | @roku-sdk/slideshow/pooled | createPooledSlideshow, createSlideshowPool, pool types | | @roku-sdk/slideshow/chrome/chevrons | DefaultChevrons | | @roku-sdk/slideshow/chrome/dots | DefaultDotIndicator |


Configuration

Common Options (all modes)

| Option | Type | Default | Description | |---|---|---|---| | slideCount | number | required | Total number of slides | | slideWidth | number | required | Width of each slide (px) | | slideHeight | number | required | Height of each slide (px) | | margin | number | 0 | Gap between slides (px) | | translation | [number, number] | [0, 0] | Position offset of the slideshow container | | focusId | object | undefined | Registers the slideshow in a parent FocusBoundary | | initialIndex | number | 0 | Starting slide index | | items | T[] | undefined | Data array (passed to renderSlide as item) | | renderSlide | (props: SlideRenderProps<T>) => JSX.Element | undefined | Slide renderer | | chevrons | Component \| false | undefined | Chevron chrome component (or false to disable) | | dots | Component \| false | undefined | Dot indicator component (or false to disable) | | animation | { durationSec?, easeFunction? } | { durationSec: 0.333, easeFunction: "outCubic" } | Tween animation config | | onSlideChange | (index, item?) => void | undefined | Fires when current slide changes | | onEdgeReached | (edge: "first" \| "last") => void | undefined | Fires when navigating past the first/last slide | | onSelect | (index, item?) => void | undefined | Fires on OK press |

createVirtualSlideshow Options

All common options, plus:

| Option | Type | Default | Description | |---|---|---|---| | windowSize | number | 1 | Number of slides to render on each side of current |

createPooledSlideshow Options

All common options, plus:

| Option | Type | Description | |---|---|---| | templates | Record<string, Component> | Template components keyed by template ID | | getTemplateId | (index, item?) => string | Returns template ID for a given slide | | poolSize | Record<string, number> | Initial pool size per template | | uniqueSlides | number[] \| ((index) => boolean) | Indices that bypass pooling entirely and render eagerly through renderSlide. The only slides a pooled slideshow can make focusable |


Examples

See examples/slideshow/ in the workspace for runnable examples:

  • eager — Basic non-interactive slideshow with chevrons and dots
  • virtual — Large slide count with windowed rendering
  • pooled — Template pool with multiple slide types
  • focus — Mixed interactive + non-interactive slides using the factory's focus integration

Run any example:

cd examples/slideshow
npx rk dev --config roku-config-eager.ts --host <device-ip> --password <pwd> --no-typecheck