GL Carousel Banner
Version: 2.0.0 Package:
@gift-card-market/gl-carousel-banner
Last Updated: August 3, 2026
A React and Web Component carousel that renders the Promotional Carousel content Gift Card Market publishes. GCM authors every frame in the Admin Portal and serves it over the Partner API, so a campaign changes without you shipping a release.â
đ Table of Contentsâ
- Overview
- Installation
- Migrating from v1.x to v2.0.0
- Quick Start
- API Reference
- What a Frame Contains
- CTA Links
- Choosing Which Frames Appear
- Controlling Content Updates
- Media: Images and YouTube Video
- Configuration Options
- Methods & Controls
- Events
- Styling & Customization
- Accessibility
- Advanced Examples
- Troubleshooting
đ¯ 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 token | Passed as jwt. Without it no content can be fetched and the carousel renders nothing. |
| Environment | One 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.x | v2.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 type | Removed. 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_image | Published 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
itemsfrom the component (React) orcarousel.items = âĻ(Vanilla JS). - Pass
jwtandenvironment. - 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
itemsproperty 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";
GlCarouselBannerItemwas 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â
| Prop | Type | Default | Description |
|---|---|---|---|
jwt | string | Required | Partner 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 |
maxFrames | number | 5 | How many frames to render. Minimum 1, no maximum â asking for more than exist renders all of them |
hiddenFrames | string[] | [] | Frame names to hide. Hidden frames still receive updates and never take one of the maxFrames slots |
receiveUpdates | boolean | true | When false, keeps the content already received instead of fetching again |
styleConfig | GlCarouselBannerStyleConfig | - | { buttonColor?, buttonTextColor? }, applied to every frame in place of the published colours |
autoRotate | boolean | true | Enable/disable automatic slide rotation |
rotationSpeed | number | 3 | Time in seconds between auto-rotations |
transitionSpeed | number | 0.5 | Transition animation duration in seconds |
pauseOnHover | boolean | true | Pause auto-rotation when hovering |
ref | GlCarouselBannerHandle | - | Reference for programmatic control |
Vanilla JS Attributesâ
| Attribute | Type | Default | Description |
|---|---|---|---|
jwt | string | Required | Partner API token |
environment | string | "development" | "production", "staging", "qa" or "development" |
max-frames | string | "5" | How many frames to render |
hidden-frames | string | "" | Comma-separated frame names to hide, e.g. "Taste of Local,Get a little help" |
receive-updates | string | "true" | "false" keeps the content already received |
button-color | string | - | Overrides the published button colour on every frame |
button-text-color | string | - | Overrides the published button text colour on every frame |
autoRotate | string | "true" | Enable/disable automatic rotation ("true" or "false") |
rotationSpeed | string | "3" | Time in seconds between rotations |
transitionSpeed | string | "0.5" | Transition duration in seconds |
pauseOnHover | string | "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:
| Name | How you refer to the frame in hiddenFrames |
| Title and body copy | Authored in the Admin Portal, rendered as HTML (bold, emphasis, or a numbered step list) |
| Background | The coloured panel behind the copy. A frame published without one is skipped |
| Image | The 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.
đ CTA Linksâ
A frame's button is built from what GCM published â you do not supply the target.
| Link mode | What is published | Where the button goes |
|---|---|---|
Route | /cards/{cardProgram}, /businesses/{handle}, /search (+ optional query) | Resolved against the GCM storefront for your environment |
FullUrl | An absolute URL | Used 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â
| Method | Parameters | Returns | Description |
|---|---|---|---|
nextSlide() | - | void | Move to the next slide |
previousSlide() | - | void | Move to the previous slide |
goToSlide(index) | index: number | void | Jump to specific slide (0-indexed) |
pause() | - | void | Pause auto-rotation |
resume() | - | void | Resume auto-rotation |
getCurrentSlide() | - | number | undefined | Get 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 Name | Event Type | Detail Object | Fired when |
|---|---|---|---|
gl:carousel-loaded | CustomEvent | { 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-changed | CustomEvent | { slideIndex: number, direction: 'next' | 'previous' } | The active slide changes â by auto-rotation, by an arrow or dot, or by goToSlide() |
gl:carousel-paused | CustomEvent | {} | Auto-rotation pauses: the visitor hovers the banner (with pauseOnHover), or you called pause() |
gl:carousel-resumed | CustomEvent | {} | Auto-rotation resumes: the visitor moves the pointer off the banner, or you called resume() |
gl:error | CustomEvent | { 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â
1. Carousel Loaded Eventâ
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>
);
}
Controlled Carousel with External Navigationâ
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 forgl:error: the detail readsFailed to fetch carousel content. HTTP 401: âĻ. - Wrong
environment. Anything other thanproduction,staging,qaordevelopmentthrowsUnknown environment: âĻ. The value is case-sensitive. - No frames published for your account.
gl:carousel-loadedfires withslideCount: 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â
maxFramesdefaults to 5. Raise it to render more.- A name in
hiddenFramesmatches 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â
receiveUpdatesisfalse, so the cached copy is being rendered. Set it back totrue, or clearlocalStoragekeygl-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
autoRotateis set totrue - Verify
rotationSpeedis 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-srcmust allow the GCM asset hosts. A CSS background raises no load event, so a blocked background fails silently â nogl:erroris 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:errornames 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â
maxFramesdefaults 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
itemsprop and theGlCarouselBannerItemtype. 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:
Routetargets resolve against the GCM storefront for the selected environment,FullUrltargets 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 inlocalStorage. - âŋ 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-linesto 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! đ