Skip to content

Creating Custom Extensions

Learn how to build your own MotionRail extensions.

Extension API

Extensions are objects that implement lifecycle hooks to interact with the carousel.

ts
interface MotionRailExtension {
  name: string;
  onInit?: (motionRail: MotionRail, state: MotionRailState) => void;
  onUpdate?: (motionRail: MotionRail, state: MotionRailState) => void;
  onDestroy?: (motionRail: MotionRail, state: MotionRailState) => void;
}

See MotionRailExtension for complete type documentation.

State Object

The MotionRailState object passed to onInit, onUpdate, and onDestroy:

ts
interface MotionRailState {
  totalItems: number;              // Total number of items in carousel
  visibleItemIndexes: number[];    // Array of currently visible item indexes
  isFirstItemVisible: boolean;     // Whether the first item is visible
  isLastItemVisible: boolean;      // Whether the last item is visible
}

Lifecycle Hooks

Extensions can interact with the carousel through the MotionRail instance methods like prev(), next(), scrollToIndex(), and more.

onInit(motionRail, state)

Called once when the carousel is initialized.

Parameters:

Use for:

  • Creating DOM elements
  • Setting up event listeners
  • Initial configuration
js
onInit(motionRail, state) {
  console.log('Carousel initialized with', state.totalItems, 'items');
  // Create UI elements
  // Attach event listeners
}

onUpdate(motionRail, state)

Called whenever the carousel state changes (navigation, resize, etc).

Parameters:

Use for:

  • Updating UI based on state
  • Responding to navigation
  • Syncing external elements
js
onUpdate(motionRail, state) {
  console.log('Visible items:', state.visibleItemIndexes);
  // Update UI elements
  // Sync with other components
}

onDestroy(motionRail, state)

Called when the carousel is destroyed.

Parameters:

Use for:

  • Cleanup event listeners
  • Remove DOM elements
  • Release resources
js
onDestroy(motionRail, state) {
  // Remove event listeners
  // Clean up DOM
  // Release resources
}

Simple Example

A counter that tracks visible items:

js
function Counter() {
  let counterElement;

  return {
    name: 'counter',
    onInit(motionRail, state) {
      counterElement = document.createElement('div');
      counterElement.textContent = `Showing ${state.visibleItemIndexes.length} of ${state.totalItems}`;
      motionRail.container.appendChild(counterElement);
    },

    onUpdate(motionRail, state) {
      counterElement.textContent = `Showing ${state.visibleItemIndexes.length} of ${state.totalItems}`;
    },

    onDestroy() {
      counterElement?.remove();
    }
  };
}

// Usage
new MotionRail(element, {
  extensions: [Counter()]
});

A custom navigation extension:

js
function CustomControls() {
  let prevBtn, nextBtn;

  return {
    name: 'custom-controls',
    onInit(motionRail, state) {
      // Create buttons
      const container = document.createElement('div');
      container.className = 'custom-controls';

      prevBtn = document.createElement('button');
      prevBtn.textContent = 'Previous';
      prevBtn.onclick = () => motionRail.prev(); // See MotionRail.prev()

      nextBtn = document.createElement('button');
      nextBtn.textContent = 'Next';
      nextBtn.onclick = () => motionRail.next(); // See MotionRail.next()

      container.append(prevBtn, nextBtn);
      motionRail.container.appendChild(container);
    },

    onUpdate(motionRail, state) {
      // Disable prev when at start
      prevBtn.disabled = state.isFirstItemVisible;
      
      // Disable next when at end
      nextBtn.disabled = state.isLastItemVisible;
    },

    onDestroy() {
      prevBtn?.remove();
      nextBtn?.remove();
    }
  };
}

Progress Bar Example

A progress indicator showing first/last item visibility:

js
function ProgressBar() {
  let progressElement;

  return {
    name: 'progress-bar',
    onInit(motionRail, state) {
      progressElement = document.createElement('div');
      progressElement.className = 'carousel-progress';
      
      const bar = document.createElement('div');
      bar.className = 'carousel-progress-bar';
      progressElement.appendChild(bar);
      
      motionRail.container.appendChild(progressElement);
    },

    onUpdate(motionRail, state) {
      const bar = progressElement.querySelector('.carousel-progress-bar');
      // Calculate progress based on scroll position
      const scrollPercentage = (motionRail.container.querySelector('.motionrail-container').scrollLeft / 
        (motionRail.container.querySelector('.motionrail-container').scrollWidth - 
         motionRail.container.querySelector('.motionrail-container').clientWidth)) * 100;
      bar.style.width = `${scrollPercentage}%`;
    },

    onDestroy() {
      progressElement?.remove();
    }
  };
}

CSS:

css
.carousel-progress {
  width: 100%;
  height: 4px;
  background: #e0e0e0;
  margin-top: 16px;
}

.carousel-progress-bar {
  height: 100%;
  background: #007bff;
  transition: width 0.3s ease;
}

Keyboard Navigation Example

Add keyboard controls to the carousel:

js
function KeyboardControls() {
  let handleKeydown;

  return {
    name: 'keyboard-controls',
    onInit(motionRail, state) {
      handleKeydown = (e) => {
        if (e.key === 'ArrowLeft') {
          motionRail.prev();
        } else if (e.key === 'ArrowRight') {
          motionRail.next();
        }
      };

      document.addEventListener('keydown', handleKeydown);
    },

    onDestroy() {
      if (handleKeydown) {
        document.removeEventListener('keydown', handleKeydown);
      }
    }
  };
}

External Sync Example

Sync carousel with external UI elements:

js
function ExternalSync(config) {
  const { thumbnailContainer } = config;

  return {
    name: 'external-sync',
    onInit(motionRail, state) {
      // Create thumbnail buttons for each item
      for (let i = 0; i < state.totalItems; i++) {
        const btn = document.createElement('button');
        btn.textContent = i + 1;
        btn.onclick = () => motionRail.scrollToIndex(i); // See MotionRail.scrollToIndex()
        thumbnailContainer.appendChild(btn);
      }
    },

    onUpdate(motionRail, state) {
      // Update active thumbnails based on visible items
      const buttons = thumbnailContainer.querySelectorAll('button');
      buttons.forEach((btn, i) => {
        btn.classList.toggle('active', state.visibleItemIndexes.includes(i));
      });
    },

    onDestroy() {
      thumbnailContainer.innerHTML = '';
    }
  };
}

// Usage
const thumbnailContainer = document.getElementById('thumbnails');

new MotionRail(element, {
  extensions: [ExternalSync({ thumbnailContainer })]
});

TypeScript Support

Full type definitions:

ts
import { MotionRail } from 'motionrail';
import type { MotionRailState, MotionRailExtension } from 'motionrail';

function MyExtension(): MotionRailExtension {
  let element: HTMLElement | null = null;

  return {
    name: 'my-extension',
    onInit(motionRail: MotionRail, state: MotionRailState) {
      element = document.createElement('div');
      element.textContent = `Total items: ${state.totalItems}`;
      motionRail.container.appendChild(element);
    },

    onUpdate(motionRail: MotionRail, state: MotionRailState) {
      if (element) {
        element.textContent = `Visible: ${state.visibleItemIndexes.join(', ')}`;
      }
    },

    onDestroy() {
      element?.remove();
    }
  };
}

Best Practices

Cleanup Resources

Always clean up in onDestroy:

js
onDestroy(motionRail, state) {
  // Remove event listeners
  document.removeEventListener('keydown', handleKeydown);
  
  // Remove DOM elements
  element?.remove();
  
  // Clear references
  element = null;
}

Handle Missing Container

Check if elements exist before manipulating:

js
onUpdate(motionRail, state) {
  if (!element) return;
  element.textContent = `${state.visibleItemIndexes.length} visible`;
}

Use Configuration

Make extensions configurable:

js
function MyExtension(config = {}) {
  const { enabled = true, className = 'default' } = config;
  
  return {
    name: 'my-extension',
    onInit(motionRail, state) {
      if (!enabled) return;
      // Use config...
    }
  };
}

Prevent Memory Leaks

Store references properly:

js
function MyExtension() {
  let listeners = [];

  return {
    name: 'my-extension',
    onInit(motionRail, state) {
      const handler = () => { /* ... */ };
      listeners.push(handler);
      document.addEventListener('click', handler);
    },

    onDestroy() {
      listeners.forEach(handler => {
        document.removeEventListener('click', handler);
      });
      listeners = [];
    }
  };
}

Next Steps