Skip to main content

GL Carousel Banner

Version: 2.0.0 Package: @gift-card-market/gl-carousel-banner
Last Updated: August 3, 2026

📖 Table of Contents​


đŸŽ¯ Overview​

GL Carousel Banner is a versatile, high-performance carousel component designed for the Gift Card Marketplace. It supports both React and Vanilla JavaScript implementations, making it framework-agnostic and easy to integrate into any project.

From v2.0.0 the content is dynamic: the component fetches the frames GCM has published and renders five of them by default. You no longer define the copy, imagery or link targets yourself.

Key Features​

✅ Dynamic Content - Frames come from GCM; nothing is hard-coded in your app
✅ Dual Implementation - React component & Web Component (Vanilla JS)
✅ Links That Resolve - Published routes point at the GCM storefront that serves them
✅ Frame Control - Choose how many frames to render, hide any frame by name
✅ Offline-friendly Updates - Opt out of updates and keep the content you already received
✅ Image or Video - Each frame is an image, or a poster that opens a YouTube player
✅ Auto-rotation - Configurable automatic slide transitions
✅ Smooth Animations - Customizable transition speeds
✅ Pause on Hover - User-friendly interaction
✅ Programmatic Control - Full API for slide navigation
✅ Event System - Listen to slide changes
✅ Responsive Design - Mobile-friendly and adaptive
✅ Keyboard Accessible - Only the slide on screen takes keyboard focus
✅ TypeScript Support - Full type definitions included
✅ Customizable Styling - CSS variables for theming


đŸ“Ļ Installation​

From GitHub Packages​

First, configure npm to use GitHub Packages for @gift-card-market scope:

# Create or edit .npmrc in your project root
echo "@gift-card-market:registry=https://npm.pkg.github.com" >> .npmrc

Then install the package:

npm install @gift-card-market/gl-carousel-banner

Peer Dependencies​

This package requires React as a peer dependency:

npm install react react-dom

The Web Component build (dist/browser/gl-carousel-banner.global.js) is self-contained — nothing else to install for a Vanilla JS page.

What you need from GCM​

Partner API tokenPassed as jwt. Without it no content can be fetched and the carousel renders nothing.
EnvironmentOne of production, staging, qa, development. Selects both the Partner API and the storefront that CTA routes resolve against.

🔄 Migrating from v1.x to v2.0.0​

v2.0.0 is a breaking change. In v1.x you passed your own slides through items. In v2.0.0 the carousel fetches content from GCM, so that prop no longer exists.

What changed​

v1.xv2.0.0
items (required)Removed. Frames come from GCM.
—jwt (required) — your Partner API token
—environment — selects the API and storefront
—maxFrames — how many frames to render (default 5)
—hiddenFrames — hide frames by name
—receiveUpdates — keep the content already received
—styleConfig — override the published button colours
GlCarouselBannerItem typeRemoved. Frames are published by GCM; there is nothing to build in your app
Slide fields title_text, main_text, button_text, button_link, background_image, main_imagePublished per frame by GCM — see What a Frame Contains

autoRotate, rotationSpeed, transitionSpeed, pauseOnHover, every method on the ref and every gl: event are unchanged. No new package to install either.

Before (v1.x)​

<GlCarouselBanner items={slides} autoRotate rotationSpeed={3} />

After (v2.0.0)​

<GlCarouselBanner jwt={PARTNER_JWT} environment="production" autoRotate rotationSpeed={3} />

Migration checklist​

  • Delete your slide array and any code that built it.
  • Remove items from the component (React) or carousel.items = â€Ļ (Vanilla JS).
  • Pass jwt and environment.
  • Drop imports of GlCarouselBannerItem — there is no slide type to build any more.
  • Ask GCM to publish your frames in the Admin Portal before you deploy — an account with no frames renders an empty carousel.
  • Re-check button colours: GCM publishes one per frame. Keep your own with styleConfig.

🚀 Quick Start​

React Implementation​

1. Install Required Dependencies​

npm install react react-dom
npm install slick-carousel

2. Import Styles​

import '@gift-card-market/gl-carousel-banner/style.css';
import 'slick-carousel/slick/slick.css';
import 'slick-carousel/slick/slick-theme.css';

3. Import Component and Types​

import { 
GlCarouselBanner,
CarouselBannerEvents,
type GlCarouselBannerHandle
} from "@gift-card-market/gl-carousel-banner";

4. Basic Usage Example​

import { useRef } from 'react';
import {
GlCarouselBanner,
type GlCarouselBannerHandle
} from "@gift-card-market/gl-carousel-banner";
import '@gift-card-market/gl-carousel-banner/style.css';
import 'slick-carousel/slick/slick.css';
import 'slick-carousel/slick/slick-theme.css';

function App() {
const carouselRef = useRef<GlCarouselBannerHandle>(null);

// Control handlers
const handleNext = () => {
carouselRef.current?.nextSlide();
};

const handlePrevious = () => {
carouselRef.current?.previousSlide();
};

const handleGoToSlide = (index: number) => {
carouselRef.current?.goToSlide(index);
};

return (
<div>
<GlCarouselBanner
ref={carouselRef}
jwt={process.env.NEXT_PUBLIC_GCM_PARTNER_JWT!}
environment="production"
maxFrames={5}
autoRotate={true}
rotationSpeed={3}
transitionSpeed={0.5}
pauseOnHover={true}
/>

<div style={{ marginTop: '20px', textAlign: 'center' }}>
<button onClick={handlePrevious}>Previous</button>
<button onClick={handleNext}>Next</button>
<button onClick={() => handleGoToSlide(0)}>Go to First</button>
</div>
</div>
);
}

export default App;

That is the whole integration — no slide data. The frames, their copy, their imagery and their button targets all come from GCM.


Vanilla JavaScript Implementation​

Nothing to install beyond the package itself — the browser bundle carries everything it uses.

1. Include Scripts and Styles​

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GL Carousel Banner Demo</title>

<!-- Include CSS -->
<link rel="stylesheet"
href="./node_modules/@gift-card-market/gl-carousel-banner/dist/browser/gl-carousel-banner.css">
</head>
<body>
<div id="carousel-container"></div>

<!-- Include JS -->
<script src="./node_modules/@gift-card-market/gl-carousel-banner/dist/browser/gl-carousel-banner.global.js"></script>

<script type="module">
// Your code here
</script>
</body>
</html>

2. Basic Usage Example​

The simplest integration is markup only — the element fetches its own content:

<gl-carousel-banner
jwt="YOUR_PARTNER_JWT"
environment="production"
max-frames="5"
autoRotate="true"
rotationSpeed="3"
transitionSpeed="0.5"
pauseOnHover="true">
</gl-carousel-banner>

Or create it from script:

<script type="module">
// Create carousel element
const carousel = document.createElement('gl-carousel-banner');

// Set attributes
carousel.setAttribute('jwt', 'YOUR_PARTNER_JWT');
carousel.setAttribute('environment', 'production');
carousel.setAttribute('max-frames', '5');
carousel.setAttribute('autoRotate', 'true');
carousel.setAttribute('rotationSpeed', '3');
carousel.setAttribute('transitionSpeed', '0.5');
carousel.setAttribute('pauseOnHover', 'true');

// Append to container — content is fetched on connect
document.getElementById('carousel-container').appendChild(carousel);

customElements.whenDefined('gl-carousel-banner').then(() => {
console.log('✅ Carousel initialized successfully!');
});
</script>

Note: there is no items property in v2.0.0. Setting one has no effect.

3. Advanced Vanilla JS Example with Controls​

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>GL Carousel Banner - Full Example</title>
<link rel="stylesheet"
href="./node_modules/@gift-card-market/gl-carousel-banner/dist/browser/gl-carousel-banner.css">
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
}

.controls {
display: flex;
gap: 10px;
justify-content: center;
margin: 20px 0;
}

button {
padding: 10px 20px;
background: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-weight: 600;
transition: transform 0.2s;
}

button:hover {
transform: scale(1.05);
}

.info-box {
background: white;
padding: 20px;
border-radius: 12px;
margin-top: 20px;
text-align: center;
}
</style>
</head>
<body>
<h1 style="color: white; text-align: center;">GL Carousel Banner Demo</h1>

<div class="controls">
<button id="prevBtn">âŦ…ī¸ Previous</button>
<button id="pauseBtn">â¸ī¸ Pause</button>
<button id="resumeBtn">â–ļī¸ Resume</button>
<button id="nextBtn">Next âžĄī¸</button>
</div>

<div id="carousel-container"></div>

<div class="info-box">
<p>Current Slide: <strong id="currentSlide">1</strong></p>
</div>

<script src="./node_modules/@gift-card-market/gl-carousel-banner/dist/browser/gl-carousel-banner.global.js"></script>
<script type="module">
const carousel = document.createElement('gl-carousel-banner');
carousel.setAttribute('jwt', 'YOUR_PARTNER_JWT');
carousel.setAttribute('environment', 'production');
carousel.setAttribute('autoRotate', 'true');
carousel.setAttribute('rotationSpeed', '3');
carousel.setAttribute('transitionSpeed', '0.5');
carousel.setAttribute('pauseOnHover', 'true');

document.getElementById('carousel-container').appendChild(carousel);

customElements.whenDefined('gl-carousel-banner').then(() => {
// Setup event listeners after a small delay
setTimeout(() => {
document.getElementById('prevBtn').addEventListener('click', () => {
carousel.previousSlide();
updateCurrentSlide();
});

document.getElementById('nextBtn').addEventListener('click', () => {
carousel.nextSlide();
updateCurrentSlide();
});

document.getElementById('pauseBtn').addEventListener('click', () => {
carousel.pause();
});

document.getElementById('resumeBtn').addEventListener('click', () => {
carousel.resume();
});
}, 200);
});

function updateCurrentSlide() {
const current = carousel.getCurrentSlide();
if (current !== undefined) {
document.getElementById('currentSlide').textContent = current + 1;
}
}

// Listen to slide change events
document.addEventListener('gl:slide-changed', (event) => {
console.log('Slide changed to:', event.detail.slideIndex);
document.getElementById('currentSlide').textContent = event.detail.slideIndex + 1;
});

// Make carousel available in console for testing
window.carousel = carousel;
</script>
</body>
</html>

📚 API Reference​

Exports​

The package exports the following:

import {
GlCarouselBanner, // React Component
CarouselBannerEvents, // Event name constants
type GlCarouselBannerHandle, // Type for ref
type GlCarouselBannerPros, // Component props
type GlCarouselBannerEnvironment, // 'production' | 'staging' | 'qa' | 'development'
type GlCarouselBannerStyleConfig, // Button colour overrides
type CarouselBannerEventType
} from "@gift-card-market/gl-carousel-banner";

GlCarouselBannerItem was removed in v2.0.0.

CarouselBannerEvents Object​

Event name constants for listening to carousel events:

CarouselBannerEvents = {
CAROUSEL_LOADED: "gl:carousel-loaded",
SLIDE_CHANGED: "gl:slide-changed",
CAROUSEL_PAUSED: "gl:carousel-paused",
CAROUSEL_RESUMED: "gl:carousel-resumed",
ERROR: "gl:error"
}

See Events section for detailed usage.

React Props​

PropTypeDefaultDescription
jwtstringRequiredPartner API token. Without it no content is fetched
environment'production' | 'staging' | 'qa' | 'development''development'Selects the Partner API and the storefront that CTA routes resolve against
maxFramesnumber5How many frames to render. Minimum 1, no maximum — asking for more than exist renders all of them
hiddenFramesstring[][]Frame names to hide. Hidden frames still receive updates and never take one of the maxFrames slots
receiveUpdatesbooleantrueWhen false, keeps the content already received instead of fetching again
styleConfigGlCarouselBannerStyleConfig-{ buttonColor?, buttonTextColor? }, applied to every frame in place of the published colours
autoRotatebooleantrueEnable/disable automatic slide rotation
rotationSpeednumber3Time in seconds between auto-rotations
transitionSpeednumber0.5Transition animation duration in seconds
pauseOnHoverbooleantruePause auto-rotation when hovering
refGlCarouselBannerHandle-Reference for programmatic control

Vanilla JS Attributes​

AttributeTypeDefaultDescription
jwtstringRequiredPartner API token
environmentstring"development""production", "staging", "qa" or "development"
max-framesstring"5"How many frames to render
hidden-framesstring""Comma-separated frame names to hide, e.g. "Taste of Local,Get a little help"
receive-updatesstring"true""false" keeps the content already received
button-colorstring-Overrides the published button colour on every frame
button-text-colorstring-Overrides the published button text colour on every frame
autoRotatestring"true"Enable/disable automatic rotation ("true" or "false")
rotationSpeedstring"3"Time in seconds between rotations
transitionSpeedstring"0.5"Transition duration in seconds
pauseOnHoverstring"true"Pause on hover ("true" or "false")

Note: every attribute is observed — changing one re-renders the carousel. Any boolean attribute counts as true unless its value is exactly "false".


đŸ—‚ī¸ What a Frame Contains​

GCM publishes each frame; you cannot change its content from your page. Every frame carries:

NameHow you refer to the frame in hiddenFrames
Title and body copyAuthored in the Admin Portal, rendered as HTML (bold, emphasis, or a numbered step list)
BackgroundThe coloured panel behind the copy. A frame published without one is skipped
ImageThe picture beside the copy. Doubles as the poster when the frame carries a video
Video (optional)A YouTube link — see Media
CTA button (optional)Label, colours and target — see CTA Links

Need a change to any of it — wording, imagery, link target, ordering? That is an Admin Portal change on the GCM side, not a code change on yours.


A frame's button is built from what GCM published — you do not supply the target.

Link modeWhat is publishedWhere the button goes
Route/cards/{cardProgram}, /businesses/{handle}, /search (+ optional query)Resolved against the GCM storefront for your environment
FullUrlAn absolute URLUsed exactly as authored — this is how a button stays on your own site
environment="production" + route "/search?location=Ashburn"
→ https://www.giftcardmarket.com/search?location=Ashburn

Routes resolve against the GCM storefront because those pages belong to it, not to your site. Every button opens in a new tab (target="_blank" rel="noopener noreferrer").

No button is rendered when the CTA is disabled on the frame, or when no usable target can be built (Route with no route, FullUrl with no URL). A dead link is never rendered.


đŸŽšī¸ Choosing Which Frames Appear​

Limit the number of frames​

<GlCarouselBanner jwt={jwt} environment="production" maxFrames={3} />
<gl-carousel-banner jwt="â€Ļ" environment="production" max-frames="3"></gl-carousel-banner>

Default is 5. Minimum is 1 — a lower number is treated as 1. There is no maximum: ask for more than GCM published and you get all of them.

Hide a specific frame​

Hide by the frame's name, exactly as GCM named it (matching ignores case and surrounding spaces):

<GlCarouselBanner jwt={jwt} hiddenFrames={['Taste of Local', 'Get a little help']} />
<gl-carousel-banner jwt="â€Ļ" hidden-frames="Taste of Local,Get a little help"></gl-carousel-banner>

A hidden frame keeps receiving content updates and does not consume one of the maxFrames slots — hiding one of five while asking for five gives you the next frame instead.


🔁 Controlling Content Updates​

By default the carousel fetches the current content on every mount. Set receiveUpdates to false to freeze what you already have:

<GlCarouselBanner jwt={jwt} receiveUpdates={false} />
<gl-carousel-banner jwt="â€Ļ" receive-updates="false"></gl-carousel-banner>

The most recent content is kept in localStorage under gl-carousel-banner:content. With updates off, the carousel renders that copy and makes no request. If there is nothing cached yet — a first visit — it fetches once so the visitor is not shown an empty carousel.


đŸ–ŧī¸ Media: Images and YouTube Video​

Each frame carries one image (mediaUrl). When GCM also publishes mediaVideoUrl:

  • the image becomes the poster, with a play control over it;
  • the YouTube player is only loaded once a visitor clicks — a page with several video frames pulls in no player on first paint;
  • the video opens in a modal over the carousel and unloads when closed (Escape, the close button, or a click on the backdrop).

Only YouTube links are supported. Video files are never uploaded or hosted.

Aspect ratio​

GCM publishes the ratio each image must match (backgroundImageAspectRatio, mediaAspectRatio) and a tolerance. A frame whose raster image breaks that ratio is dropped rather than rendered distorted, and a gl:error event is raised naming the frame. SVG images are exempt — they scale to any box. If GCM leaves a ratio unset, no check is made.


âš™ī¸ Configuration Options​

Auto-Rotation Settings​

// React
<GlCarouselBanner
autoRotate={true} // Enable auto-rotation
rotationSpeed={5} // Rotate every 5 seconds
pauseOnHover={true} // Pause when user hovers
jwt={jwt}
/>
// Vanilla JS
carousel.setAttribute('autoRotate', 'true');
carousel.setAttribute('rotationSpeed', '5');
carousel.setAttribute('pauseOnHover', 'true');

Transition Settings​

// React
<GlCarouselBanner
transitionSpeed={0.8} // Slower transitions (0.8 seconds)
jwt={jwt}
/>
// Vanilla JS
carousel.setAttribute('transitionSpeed', '0.8');

Disable Auto-Rotation​

// React
<GlCarouselBanner
autoRotate={false} // Manual control only
jwt={jwt}
/>
// Vanilla JS
carousel.setAttribute('autoRotate', 'false');

🎮 Methods & Controls​

React (via ref)​

const carouselRef = useRef<GlCarouselBannerHandle>(null);

// Navigate to next slide
carouselRef.current?.nextSlide();

// Navigate to previous slide
carouselRef.current?.previousSlide();

// Go to specific slide (0-indexed)
carouselRef.current?.goToSlide(2);

// Pause auto-rotation
carouselRef.current?.pause();

// Resume auto-rotation
carouselRef.current?.resume();

// Get current slide index
const currentIndex = carouselRef.current?.getCurrentSlide();

Vanilla JavaScript​

// Navigate to next slide
carousel.nextSlide();

// Navigate to previous slide
carousel.previousSlide();

// Go to specific slide (0-indexed)
carousel.goToSlide(2);

// Pause auto-rotation
carousel.pause();

// Resume auto-rotation
carousel.resume();

// Get current slide index
const currentIndex = carousel.getCurrentSlide();

Method Reference​

MethodParametersReturnsDescription
nextSlide()-voidMove to the next slide
previousSlide()-voidMove to the previous slide
goToSlide(index)index: numbervoidJump to specific slide (0-indexed)
pause()-voidPause auto-rotation
resume()-voidResume auto-rotation
getCurrentSlide()-number | undefinedGet current slide index

📡 Events​

The carousel component emits custom events that you can listen to for various lifecycle events and state changes.

CarouselBannerEvents Constants​

The package exports a CarouselBannerEvents object containing all available event names:

import { CarouselBannerEvents } from "@gift-card-market/gl-carousel-banner";

// Event names:
CarouselBannerEvents.CAROUSEL_LOADED // "gl:carousel-loaded"
CarouselBannerEvents.SLIDE_CHANGED // "gl:slide-changed"
CarouselBannerEvents.CAROUSEL_PAUSED // "gl:carousel-paused"
CarouselBannerEvents.CAROUSEL_RESUMED // "gl:carousel-resumed"
CarouselBannerEvents.ERROR // "gl:error"

Available Events​

Event NameEvent TypeDetail ObjectFired when
gl:carousel-loadedCustomEvent{ slideCount: number }The carousel is initialised and ready. slideCount is how many frames it actually rendered — 0 means GCM published none for your account, or every frame was dropped
gl:slide-changedCustomEvent{ slideIndex: number, direction: 'next' | 'previous' }The active slide changes — by auto-rotation, by an arrow or dot, or by goToSlide()
gl:carousel-pausedCustomEvent{}Auto-rotation pauses: the visitor hovers the banner (with pauseOnHover), or you called pause()
gl:carousel-resumedCustomEvent{}Auto-rotation resumes: the visitor moves the pointer off the banner, or you called resume()
gl:errorCustomEvent{ error: string }One of three things went wrong: the content request failed (Failed to fetch carousel content. HTTP â€Ļ), a frame was published without a background image, or a frame's image broke the required aspect ratio. The message names the frame

This is the only channel for reporting a problem — the component never throws into your render tree, so listen for gl:error if you want to know when the carousel is empty and why.

Event Usage Examples​

Listen for when the carousel finishes initialization:

document.addEventListener('gl:carousel-loaded', (event) => {
console.log('✅ Carousel loaded with', event.detail.slideCount, 'slides');
// Initialize custom UI or analytics
});

// Or use the constant:
import { CarouselBannerEvents } from "@gift-card-market/gl-carousel-banner";

document.addEventListener(CarouselBannerEvents.CAROUSEL_LOADED, (event) => {
console.log('Carousel ready!', event.detail);
});

2. Slide Changed Event​

Track slide changes with direction information:

document.addEventListener('gl:slide-changed', (event) => {
const { slideIndex, direction } = event.detail;
console.log(`Moved to slide ${slideIndex} (${direction})`);

// Update custom UI
document.getElementById('currentSlide').textContent = slideIndex + 1;

// Track analytics
analytics.track('Carousel Slide Changed', {
slide: slideIndex,
direction: direction
});
});

3. Pause/Resume Events​

Monitor auto-rotation state:

document.addEventListener('gl:carousel-paused', () => {
console.log('â¸ī¸ Carousel paused');
document.getElementById('status').textContent = 'Paused';
});

document.addEventListener('gl:carousel-resumed', () => {
console.log('â–ļī¸ Carousel resumed');
document.getElementById('status').textContent = 'Playing';
});

4. Error Handling​

Handle errors gracefully:

document.addEventListener('gl:error', (event) => {
console.error('❌ Carousel error:', event.detail.error);

// Show error to user or log to error tracking service
showNotification('Carousel failed to load', 'error');
});

React Event Listeners​

In React, you can listen to these events using useEffect:

import { useEffect, useState } from 'react';
import { CarouselBannerEvents } from "@gift-card-market/gl-carousel-banner";

function CarouselWithEvents() {
const [slideInfo, setSlideInfo] = useState({ index: 0, total: 0 });
const [isPaused, setIsPaused] = useState(false);

useEffect(() => {
// Carousel loaded handler
const handleLoaded = (event: CustomEvent) => {
setSlideInfo(prev => ({ ...prev, total: event.detail.slideCount }));
console.log('Carousel loaded');
};

// Slide changed handler
const handleSlideChange = (event: CustomEvent) => {
setSlideInfo(prev => ({
...prev,
index: event.detail.slideIndex
}));
console.log('Slide changed to', event.detail.slideIndex);
};

// Pause handler
const handlePause = () => {
setIsPaused(true);
};

// Resume handler
const handleResume = () => {
setIsPaused(false);
};

// Error handler
const handleError = (event: CustomEvent) => {
console.error('Carousel error:', event.detail.error);
};

// Add event listeners
document.addEventListener(CarouselBannerEvents.CAROUSEL_LOADED, handleLoaded as EventListener);
document.addEventListener(CarouselBannerEvents.SLIDE_CHANGED, handleSlideChange as EventListener);
document.addEventListener(CarouselBannerEvents.CAROUSEL_PAUSED, handlePause);
document.addEventListener(CarouselBannerEvents.CAROUSEL_RESUMED, handleResume);
document.addEventListener(CarouselBannerEvents.ERROR, handleError as EventListener);

// Cleanup
return () => {
document.removeEventListener(CarouselBannerEvents.CAROUSEL_LOADED, handleLoaded as EventListener);
document.removeEventListener(CarouselBannerEvents.SLIDE_CHANGED, handleSlideChange as EventListener);
document.removeEventListener(CarouselBannerEvents.CAROUSEL_PAUSED, handlePause);
document.removeEventListener(CarouselBannerEvents.CAROUSEL_RESUMED, handleResume);
document.removeEventListener(CarouselBannerEvents.ERROR, handleError as EventListener);
};
}, []);

return (
<div>
<GlCarouselBanner jwt={jwt} />
<div>
Slide {slideInfo.index + 1} of {slideInfo.total}
{isPaused ? ' (Paused)' : ' (Playing)'}
</div>
</div>
);
}

Where to Listen​

Every event is dispatched on document (with bubbles: true and composed: true, so it crosses the shadow DOM boundary and reaches window too).

// ✅ Works
document.addEventListener('gl:slide-changed', handler);
window.addEventListener('gl:slide-changed', handler);

// ❌ Never fires — the event does not originate from the element
document.querySelector('gl-carousel-banner').addEventListener('gl:slide-changed', handler);

Because the target is document, two carousels on one page raise indistinguishable events. To drive them separately, use the ref (React) or the element's own methods (Vanilla JS) instead of events.

🎨 Styling & Customization​

CSS Variables​

You can customize the carousel appearance using CSS variables. The values below are what the promotional carousel is designed around — the panel is a coloured gradient, so the copy and controls are white.

:root {
/* Dot Navigation */
--gl-carousel-dot-color: rgba(255, 255, 255, 0.5);
--gl-carousel-dot-active-color: #ffffff;

/* Arrow Controls */
--gl-carousel-arrow-color: #ffffff;
--gl-carousel-arrow-bg: transparent;

/* Slide Title */
--gl-slide-title-font: 'Poppins', helvetica, arial, sans-serif;
--gl-slide-title-size: 55px;
--gl-slide-title-color: #ffffff;
--gl-slide-title-max-lines: 3;

/* Slide Text */
--gl-slide-text-font: 'Poppins', helvetica, arial, sans-serif;
--gl-slide-text-size: 18px;
--gl-slide-text-color: #ffffff;

/* Button */
--gl-button-border-radius: 1000px;

/* Layout — reserved; not read yet, the slide padding is fixed */
--gl-slide-padding: 40px;
}

--gl-slide-title-max-lines (new in 2.0.0) caps how many lines a heading may occupy. Every frame is rendered at the tallest frame's height, so one long title would otherwise stretch the whole carousel; the heading is clamped at three lines and anything past that is cut. Raise it if your frames need more room.

Typefaces: a variable only selects a family, it cannot load one. Your page stays responsible for making Poppins (or your own face) available. Without it the stack falls through to helvetica/arial.

Buttons: GCM publishes a colour per frame. To use your own on every frame, pass styleConfig (React) or button-color / button-text-color (Web Component) rather than fighting the inline style with CSS.

Example Customization​

:root {
--gl-carousel-dot-active-color: #EE3D84;
--gl-slide-title-size: 40px;
--gl-slide-title-color: #1a1a1a;
--gl-button-border-radius: 24px;
}

Override Specific Styles​

/* Custom arrow styling */
gl-carousel-banner .slick-arrow {
background: rgba(0, 82, 204, 0.9) !important;
border-radius: 50%;
}

/* Custom dot styling */
gl-carousel-banner .slick-dots li button:before {
font-size: 14px;
}

â™ŋ Accessibility​

What the component handles for you:

  • Tab order follows the slide on screen — only the visible frame's button is reachable, so tabbing never jumps to a frame the visitor cannot see.
  • The carousel is reachable by keyboard and announces the current slide to screen readers ("Slide 2 of 5"). Arrows, dots and the CTA all carry labels, and the CTA announces that it opens a new tab.
  • Focus rings are drawn on every control and thicken under prefers-contrast: high.
  • Slide transitions are dropped under prefers-reduced-motion: reduce.

What you may still need to act on:

  • Contrast. White copy over the mid-tone gradients sits below the WCAG 1.4.3 ratio. If your site is held to AA, raise it with --gl-slide-title-color / --gl-slide-text-color, or ask GCM for darker artwork.

🚀 Advanced Examples​

Rendering a subset of what GCM published​

function CampaignCarousel() {
return (
<GlCarouselBanner
jwt={PARTNER_JWT}
environment="production"
maxFrames={3} // Only the first three
hiddenFrames={['Taste of Local']} // â€Ļskipping this one
styleConfig={{ buttonColor: '#1a1a1a', buttonTextColor: '#ffffff' }}
/>
);
}

Reacting to loaded content​

import { useEffect, useState } from 'react';
import { GlCarouselBanner, CarouselBannerEvents } from '@gift-card-market/gl-carousel-banner';

function CarouselWithStatus() {
const [slideCount, setSlideCount] = useState<number | null>(null);

useEffect(() => {
const onLoaded = (event: Event) => {
setSlideCount((event as CustomEvent).detail.slideCount);
};
const onError = (event: Event) => {
console.error('Carousel error:', (event as CustomEvent).detail.error);
};

document.addEventListener(CarouselBannerEvents.CAROUSEL_LOADED, onLoaded);
document.addEventListener(CarouselBannerEvents.ERROR, onError);
return () => {
document.removeEventListener(CarouselBannerEvents.CAROUSEL_LOADED, onLoaded);
document.removeEventListener(CarouselBannerEvents.ERROR, onError);
};
}, []);

return (
<>
<GlCarouselBanner jwt={PARTNER_JWT} environment="production" />
{slideCount === 0 && <p>No campaign is running right now.</p>}
</>
);
}

Freezing content for a campaign period​

// Keeps whatever was received last, and stops calling the Partner API on every mount.
<GlCarouselBanner jwt={PARTNER_JWT} environment="production" receiveUpdates={false} />

Multiple Carousels on Same Page​

Both instances render the same published content — vary what each shows with maxFrames and hiddenFrames:

function MultiCarouselPage() {
return (
<div>
<section>
<h2>This week</h2>
<GlCarouselBanner jwt={PARTNER_JWT} maxFrames={2} autoRotate rotationSpeed={3} />
</section>

<section style={{ marginTop: '40px' }}>
<h2>Everything else</h2>
<GlCarouselBanner
jwt={PARTNER_JWT}
maxFrames={10}
hiddenFrames={['Instant Gift Cards']}
autoRotate
rotationSpeed={5}
/>
</section>
</div>
);
}
function ControlledCarousel() {
const carouselRef = useRef<GlCarouselBannerHandle>(null);
const [currentSlide, setCurrentSlide] = useState(0);
const [slideCount, setSlideCount] = useState(0);

useEffect(() => {
const onLoaded = (event: Event) => setSlideCount((event as CustomEvent).detail.slideCount);
const onChanged = (event: Event) => setCurrentSlide((event as CustomEvent).detail.slideIndex);

document.addEventListener(CarouselBannerEvents.CAROUSEL_LOADED, onLoaded);
document.addEventListener(CarouselBannerEvents.SLIDE_CHANGED, onChanged);
return () => {
document.removeEventListener(CarouselBannerEvents.CAROUSEL_LOADED, onLoaded);
document.removeEventListener(CarouselBannerEvents.SLIDE_CHANGED, onChanged);
};
}, []);

return (
<div>
<GlCarouselBanner ref={carouselRef} jwt={PARTNER_JWT} autoRotate={false} />

<div className="pager">
{Array.from({ length: slideCount }, (_, index) => (
<button
key={index}
onClick={() => carouselRef.current?.goToSlide(index)}
className={currentSlide === index ? 'active' : ''}
>
{index + 1}
</button>
))}
</div>
</div>
);
}

Responsive Configuration​

function ResponsiveCarousel() {
const [isMobile, setIsMobile] = useState(window.innerWidth < 768);

useEffect(() => {
const handleResize = () => {
setIsMobile(window.innerWidth < 768);
};

window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);

return (
<GlCarouselBanner
jwt={PARTNER_JWT}
maxFrames={isMobile ? 3 : 5} // Fewer frames on a small screen
autoRotate={!isMobile} // Disable auto-rotate on mobile
rotationSpeed={isMobile ? 5 : 3} // Slower on mobile
pauseOnHover={!isMobile} // Only on desktop
/>
);
}

🐛 Troubleshooting​

Common Issues​

1. Nothing Renders​

Problem: The carousel area is empty.

Solution: work down this list — each one produces an empty carousel:

  • No jwt, or a rejected one. Listen for gl:error: the detail reads Failed to fetch carousel content. HTTP 401: â€Ļ.
  • Wrong environment. Anything other than production, staging, qa or development throws Unknown environment: â€Ļ. The value is case-sensitive.
  • No frames published for your account. gl:carousel-loaded fires with slideCount: 0. Ask GCM to publish content in the Admin Portal.
  • Every frame was dropped. A frame with no background image, or a raster image that breaks the required ratio, is skipped and reported through gl:error.
document.addEventListener('gl:error', (event) => console.error(event.detail.error));
document.addEventListener('gl:carousel-loaded', (event) => console.log(event.detail.slideCount, 'frames'));

2. Methods Not Available (Vanilla JS)​

Problem: carousel.nextSlide() throws "not a function" error.

Solution: Wait for component initialization with a small delay:

customElements.whenDefined('gl-carousel-banner').then(() => {
// Wait for React to mount internally
setTimeout(() => {
carousel.nextSlide(); // Now available!
}, 200);
});

2b. Fewer Frames Than Expected​

  • maxFrames defaults to 5. Raise it to render more.
  • A name in hiddenFrames matches a published frame — hidden frames never appear, though they keep receiving updates.
  • A frame breaking the required aspect ratio is dropped; check gl:error.

2c. Content Does Not Update After a Change in the Admin Portal​

  • receiveUpdates is false, so the cached copy is being rendered. Set it back to true, or clear localStorage key gl-carousel-banner:content.
  • The change was published to a different environment than the one you pass.

3. Styles Not Applied​

Problem: Carousel has no styling or looks broken.

Solution: Ensure all CSS files are imported:

// React - Import ALL required styles
import '@gift-card-market/gl-carousel-banner/style.css';
import 'slick-carousel/slick/slick.css';
import 'slick-carousel/slick/slick-theme.css';
<!-- Vanilla JS - Include stylesheet -->
<link rel="stylesheet"
href="./node_modules/@gift-card-market/gl-carousel-banner/dist/browser/gl-carousel-banner.css">

4. TypeScript Errors (React)​

Problem: Property 'items' does not exist or Cannot find name 'GlCarouselBannerItem'.

Solution: both were removed in v2.0.0 — see Migrating from v1.x to v2.0.0. Pass jwt instead; there is no slide type to declare any more:

import { 
GlCarouselBanner,
type GlCarouselBannerHandle
} from "@gift-card-market/gl-carousel-banner";

// Use the types
const carouselRef = useRef<GlCarouselBannerHandle>(null);

5. Auto-Rotation Not Working​

Problem: Carousel doesn't auto-advance.

Solution:

  • Check autoRotate is set to true
  • Verify rotationSpeed is a positive number
  • Ensure carousel is not paused
// React
<GlCarouselBanner
autoRotate={true} // Must be boolean true
rotationSpeed={3} // Must be number > 0
jwt={jwt}
/>

6. Images Not Loading​

Problem: A frame renders with no background or no image.

Solution: the URLs are published by GCM, so this is nearly always the embedding page rather than the content:

  • Content-Security-Policy. Your img-src must allow the GCM asset hosts. A CSS background raises no load event, so a blocked background fails silently — no gl:error is reported.
  • Network/CORS. Open the URL from the failing page and check the response.
  • A frame vanished entirely. It was dropped for a missing background or a wrong aspect ratio; gl:error names it.

7. CTA Buttons Point to the Wrong Place​

Route links resolve against the GCM storefront for your environment, not your own domain — /search becomes https://www.giftcardmarket.com/search in production. A button that must stay on your site has to be published as a full URL by GCM. Check environment first: development sends visitors to the dev storefront.

8. A Video Frame Shows Nothing​

Only YouTube links are supported, and the player is only loaded after a visitor clicks the poster. If the modal opens empty, your CSP is blocking www.youtube.com in frame-src.


📊 Performance Tips​

1. Images​

Imagery is published by GCM, which serves it in WebP where it can. On your side: the component already loads the first frame eagerly and the rest lazily, and a video frame pulls in no YouTube player until a visitor clicks the poster.

2. Limit Frame Count​

  • maxFrames defaults to 5, which is the intended size. Raising it renders more DOM and more images.
  • Frames you never show should go in hiddenFrames — a hidden frame is dropped before rendering and does not consume a slot.

3. Adjust Animation Speed​

  • Balance between smooth transitions and performance
  • Slower devices may benefit from faster transitions (lower transitionSpeed)
<GlCarouselBanner
transitionSpeed={0.3} // Faster = better performance
rotationSpeed={4} // Longer view time = less frequent transitions
jwt={jwt}
/>

4. Disable Auto-Rotation When Not Visible​

useEffect(() => {
const handleVisibilityChange = () => {
if (document.hidden) {
carouselRef.current?.pause();
} else {
carouselRef.current?.resume();
}
};

document.addEventListener('visibilitychange', handleVisibilityChange);
return () => {
document.removeEventListener('visibilitychange', handleVisibilityChange);
};
}, []);

📝 Changelog​

Version 2.0.0 (Current)​

âš ī¸ BREAKING CHANGE — see Migrating from v1.x to v2.0.0.

  • đŸ’Ĩ Removed the items prop and the GlCarouselBannerItem type. Content is now published by GCM and fetched over the Partner API.
  • ✨ New props: jwt (required), environment, maxFrames, hiddenFrames, receiveUpdates, styleConfig; matching Web Component attributes.
  • ✨ CTA links: Route targets resolve against the GCM storefront for the selected environment, FullUrl targets are used as authored. A CTA with no usable target renders no button.
  • ✨ Frames may carry a YouTube video: the image becomes a poster and the player is only loaded on click.
  • ✨ Frames whose raster image breaks the ratio GCM published are dropped and reported through gl:error; SVG is exempt.
  • ✨ receiveUpdates={false} keeps the last content received, cached in localStorage.
  • â™ŋ Only the slide on screen takes keyboard focus. Tabbing onto a button in an off-screen frame used to scroll the slide viewport sideways and leave the carousel showing half of two frames.
  • 🐛 Navigation arrows stay clear of the pagination dots at any frame count — at seven frames or more the dot strip used to run straight through them.
  • 🐛 Long headings no longer stretch every frame: the title is clamped, with --gl-slide-title-max-lines to adjust it.
  • 📚 Documentation rewritten for the dynamic content model, with a migration guide.
  • â„šī¸ No new dependency: the peer requirements are unchanged from 1.0.9.

Version 1.0.9​

  • 🔒 Encapsulated CSS bleeding fixes (scoped variables and nested selectors).
  • âš™ī¸ Added Tailwind utility prefix and configured important option.

🔗 Shadow DOM​

The Web Component wrapper (gl-carousel-banner) uses Shadow DOM (mode: 'open') to isolate styles. Component styles do not bleed out into your page, and your CSS does not disrupt the carousel layout. Every stylesheet it needs — including slick-carousel's — is injected into the shadow root, which is why the Vanilla JS build needs no extra CSS beyond gl-carousel-banner.css.

Two consequences worth knowing:

  • Your global CSS does not reach inside. Theme the carousel through the CSS variables, which do cross the boundary, rather than through selectors.
  • Events are dispatched on document, not on the element — see Events.

📞 Support​

For questions, issues, or feature requests, please contact the Gift Card Marketplace development team.


📄 License​

Š 2025 Gift Card Marketplace. All rights reserved.


Happy Coding! 🎉