Description
- Sync List Carousel section with Google Sheets

#1. First, you need to add a List People Section

#2. Choose Carousel

#3. Create a Google Sheets file with Columns like this.

#4. Next, copy this string from Google Sheets URL

and Tab Name

#5. Use this code to Page Header Injection
<script>
class SheetsCarouselSync {
constructor(options) {
this.sheetsId = options.sheetsId || '1RN4zvG4X1JdFQQgz0gS2WwfYc5XF-gGpYqdB4T7WX70';
this.sheetName = options.sheetName || 'test data';
this.config = options;
this.carouselSection = this.config.target;
this.carouselSection.dataset.sheetsSync = "loading";
this.sheetsData = [];
this.carouselItems = this.carouselSection.querySelectorAll("li.user-items-list-carousel__slide");
this.carouselContainer = this.carouselSection.querySelector(".user-items-list-carousel__slides");
this.initialize();
}
async initialize() {
try {
this.sheetsData = await this.fetchSheetsData();
console.log("Fetched sheets data:", this.sheetsData.length);
console.log("Current carousel items:", this.carouselItems.length);
while (this.carouselItems.length < this.sheetsData.length) {
console.log("Creating new slide...");
const newSlide = this.createNewSlide();
this.carouselContainer.appendChild(newSlide);
this.carouselItems = this.carouselSection.querySelectorAll("li.user-items-list-carousel__slide");
console.log("Total slides now:", this.carouselItems.length);
}
if (this.carouselItems.length > this.sheetsData.length) {
console.warn("Too many carousel items, removing excess");
while (this.carouselItems.length > this.sheetsData.length) {
this.carouselItems[this.carouselItems.length - 1].remove();
this.carouselItems = this.carouselSection.querySelectorAll("li.user-items-list-carousel__slide");
}
}
this.createItemTemplate();
this.populateCarouselItems();
this.carouselSection.dataset.sheetsSync = "complete";
console.log("Sync completed with", this.carouselItems.length, "items");
} catch (error) {
console.error("Sheets Carousel Sync failed:", error);
this.carouselSection.dataset.sheetsSync = "error";
}
}
async fetchSheetsData() {
try {
const csvUrl = `https://docs.google.com/spreadsheets/d/${this.sheetsId}/gviz/tq?tqx=out:csv&sheet=${encodeURIComponent(this.sheetName)}`;
const response = await fetch(csvUrl);
if (!response.ok) {
throw new Error(`CSV request failed: ${response.status}`);
}
const csvText = await response.text();
return this.parseCSVData(csvText);
} catch (error) {
console.error("Error loading sheets data:", error);
throw error;
}
}
parseCSVData(csvText) {
const rows = csvText.split('\n').filter(row => row.trim());
const data = [];
if (rows.length < 2) return data;
const headers = this.parseCSVRow(rows[0]).map(header => header.replace(/"/g, '').trim());
console.log("Headers found:", headers);
console.log("Total rows:", rows.length);
for (let i = 1; i < rows.length; i++) {
const row = rows[i];
if (row.trim()) {
const cells = this.parseCSVRow(row);
const rowData = {};
headers.forEach((header, index) => {
rowData[header] = cells[index] ? cells[index].replace(/"/g, '').trim() : '';
});
console.log(`Row ${i}:`, rowData);
if (rowData.Title && rowData.Title.trim()) {
data.push({
title: rowData.Title || '',
description: rowData.Description || '',
imageUrl: rowData['Image URL'] || '',
buttonUrl: rowData['Button URL'] || '#'
});
console.log(`Added item ${data.length}:`, data[data.length - 1]);
} else {
console.log(`Skipped row ${i} - no title:`, rowData);
}
}
}
console.log("Final data array length:", data.length);
return data;
}
parseCSVRow(row) {
const cells = [];
let current = '';
let inQuotes = false;
for (let i = 0; i < row.length; i++) {
const char = row[i];
if (char === '"') {
inQuotes = !inQuotes;
} else if (char === ',' && !inQuotes) {
cells.push(current);
current = '';
} else {
current += char;
}
}
cells.push(current);
return cells;
}
createNewSlide() {
const slideTemplate = `
<li class="user-items-list-carousel__slide list-item" data-is-card-enabled="false">
<div class="user-items-list-carousel__media-container" style="margin-bottom: 4%; width: 100%;">
<div class="user-items-list-carousel__media-inner preFade fadeIn" data-media-aspect-ratio="3:2" data-animation-role="image">
<img class="user-items-list-carousel__media" data-load="false" data-mode="cover" data-use-advanced-positioning="true" style="width: 100%; height: 100%; object-fit: cover;" data-parent-ratio="1.5" loading="lazy" decoding="async" data-loader="sqs">
</div>
</div>
<div class="list-item-content">
<div class="list-item-content__text-wrapper">
<h2 class="list-item-content__title preFade" style="max-width: 100%;"></h2>
<div class="list-item-content__description" style="margin-top: 10px; max-width: 100%;"></div>
</div>
<div class="list-item-content__button-wrapper">
<div class="list-item-content__button-container" style="margin-top: 10px; max-width: 100%;" data-animation-role="button">
<a class="list-item-content__button sqs-block-button-element sqs-block-button-element--medium sqs-button-element--primary" href="#">
Xem chi tiết
</a>
</div>
</div>
</div>
</li>
`;
const tempDiv = document.createElement('div');
tempDiv.innerHTML = slideTemplate;
return tempDiv.firstElementChild;
}
createItemTemplate() {
if (this.carouselItems.length > 0) {
const templateHTML = this.carouselItems[0].innerHTML;
this.carouselItems.forEach(item => {
item.innerHTML = templateHTML;
});
}
}
populateCarouselItems() {
console.log("Populating", this.carouselItems.length, "items with", this.sheetsData.length, "sheets data");
this.carouselItems = this.carouselSection.querySelectorAll("li.user-items-list-carousel__slide");
console.log("Refreshed carousel items count:", this.carouselItems.length);
for (let index = 0; index < this.sheetsData.length; index++) {
if (!this.carouselItems[index]) {
console.log("No carousel item for index", index);
continue;
}
console.log("Processing data", index, ":", this.sheetsData[index].title);
const carouselItem = this.carouselItems[index];
const {
title: itemTitle,
description: itemDescription,
imageUrl: itemImage,
buttonUrl: itemLink
} = this.sheetsData[index];
let titleElement = carouselItem.querySelector(".list-item-content__title");
let descriptionElement = carouselItem.querySelector(".list-item-content__description");
let imageElement = carouselItem.querySelector("img");
let buttonElement = carouselItem.querySelector(".list-item-content__button");
if (titleElement) {
if (this.config.linkTitle && itemLink && itemLink !== '#') {
titleElement.innerHTML = `<a href="${itemLink}">${itemTitle}</a>`;
} else {
titleElement.innerHTML = `<span>${itemTitle}</span>`;
}
console.log("Updated title for item", index);
}
if (descriptionElement && itemDescription) {
descriptionElement.innerHTML = `<p style="white-space: pre-wrap;">${itemDescription}</p>`;
console.log("Updated description for item", index);
}
if (imageElement && itemImage) {
let newImage = imageElement.cloneNode(true);
newImage.src = itemImage;
newImage.dataset.src = itemImage;
newImage.dataset.image = itemImage;
newImage.srcset = "";
newImage.alt = itemTitle;
imageElement.parentElement.append(newImage);
imageElement.style.display = "none";
console.log("Updated image for item", index);
}
if (buttonElement) {
buttonElement.textContent = "Download";
if (itemLink && itemLink !== '#') {
buttonElement.href = itemLink;
} else {
buttonElement.href = "#";
}
console.log("Updated button for item", index);
}
}
window.dispatchEvent(new Event("resize"));
}
setupEventHandlers() {
window.addEventListener("DOMContentLoaded", () => {
this.populateCarouselItems();
});
window.addEventListener("load", () => {
this.populateCarouselItems();
});
}
handleCarouselController() {
const controllerElement = this.carouselSection.querySelector("[data-controller]");
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.attributeName === "data-controllers-bound" &&
controllerElement.dataset.controllersBound === "UserItemsListCarousel") {
controllerElement.removeAttribute("data-controller");
observer.disconnect();
}
});
});
if (controllerElement && controllerElement.dataset.controllersBound === "UserItemsListCarousel") {
controllerElement.removeAttribute("data-controller");
} else if (controllerElement) {
observer.observe(controllerElement, {
attributes: true,
attributeFilter: ["data-controllers-bound"]
});
}
}
}
(function() {
const initSheetsCarouselSync = () => {
const carouselSections = document.querySelectorAll(".user-items-list-carousel");
carouselSections.forEach(section => {
if (section.dataset.sheetsSync) return;
const config = {
target: section.closest(".page-section"),
linkTitle: true,
linkImage: false
};
if (config.target) {
config.target.SheetsCarouselSync = new SheetsCarouselSync(config);
} else {
section.dataset.sheetsSync = "no-target";
}
});
};
window.sheetsCarouselSync = {
init: initSheetsCarouselSync,
refreshAll: () => {
const carouselSections = document.querySelectorAll(".user-items-list-carousel");
carouselSections.forEach(section => {
const target = section.closest(".page-section");
if (target && target.SheetsCarouselSync) {
target.SheetsCarouselSync.refreshData();
}
});
}
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initSheetsCarouselSync);
} else {
initSheetsCarouselSync();
}
setInterval(() => {
window.sheetsCarouselSync.refreshAll();
}, 30000);
})();
</script>

#6. Update string and tab name you have in step #4

#7. If you want to do this, but on another platform, or coding new Carousel, you can edit page > Add a Code Block

Then use these syntax into Code Block (or HTML field with other platforms)
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css">
<div class="carousel-container" id="carouselContainer">
<div class="carousel-wrapper">
<button class="arrow-button arrow-left" onclick="moveSlide(-1)" aria-label="Previous">
<div class="arrow-background"></div>
<i class="fas fa-chevron-left"></i>
</button>
<div class="carousel-track" id="carouselTrack">
<div class="carousel-item">
<div class="report-card">
<img src="https://images.squarespace-cdn.com/content/v1/6846edf34bfaf3462330bfc2/ad178611-96a5-4a7b-898b-48962b5cd139/2025Report.png?format=750w" alt="Loading...">
<h3>Loading...</h3>
<p>Please wait while we load data from Google Sheets...</p>
<button class="download-btn" onclick="window.open('#', '_blank')">DOWNLOAD</button>
</div>
</div>
</div>
<button class="arrow-button arrow-right" onclick="moveSlide(1)" aria-label="Next">
<div class="arrow-background"></div>
<i class="fa-solid fa-arrow-right"></i>
</button>
</div>
<div class="loading-indicator" id="loadingIndicator">
<div class="loading-spinner"></div>
<p>Loading data from Google Sheets...</p>
</div>
</div>
<style>
.carousel-container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
position: relative;
}
.carousel-wrapper {
position: relative;
overflow: hidden;
border-radius: 12px;
}
.carousel-track {
display: flex;
transition: transform 0.3s ease;
cursor: grab;
user-select: none;
}
.carousel-track:active {
cursor: grabbing;
}
.carousel-item {
flex: 0 0 25%;
padding: 0 10px;
box-sizing: border-box;
}
.report-card {
background: #e6e4e9;
border-radius: 12px;
transition: transform 0.3s ease, box-shadow 0.3s ease;
height: 100%;
display: flex;
flex-direction: column;
text-align: center;
}
.report-card:hover {
transform: translateY(-5px);
}
.report-card img {
width: 100%;
height: 250px;
object-fit: cover;
display: block;
}
.report-card h3 {
font-size: 20px;
font-weight: bold;
margin: 20px 20px 10px;
color: #333;
line-height: 1.3;
}
.report-card p {
font-size: 14px;
color: #666;
margin: 0 20px 20px;
line-height: 1.4;
flex-grow: 1;
}
.download-btn {
margin: 0 20px 20px;
padding: 12px 24px;
background: transparent;
border: 2px solid #333;
border-radius: 25px;
font-weight: bold;
font-size: 12px;
letter-spacing: 1px;
cursor: pointer;
transition: all 0.3s ease;
color: #333;
}
.download-btn:hover {
background: #333;
color: white;
transform: translateY(-2px);
}
.arrow-button {
position: absolute;
top: 50%;
transform: translateY(-50%);
width: 60px;
height: 60px;
background: rgba(255,255,255,0.9);
border: none;
border-radius: 50%;
cursor: pointer;
z-index: 10;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
}
.arrow-button:hover {
background: white;
box-shadow: 0 6px 20px rgba(0,0,0,0.15);
transform: translateY(-50%) scale(1.1);
}
.arrow-button i {
font-size: 18px;
color: #333;
}
.arrow-left {
left: 0px;
}
.arrow-right {
right: 0px;
}
.arrow-background {
position: absolute;
width: 100%;
height: 100%;
border-radius: 50%;
background: rgba(0,0,0,0.05);
opacity: 0;
transition: opacity 0.3s ease;
}
.arrow-button:hover .arrow-background {
opacity: 1;
}
.loading-indicator {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
background: rgba(255,255,255,0.95);
padding: 30px;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
z-index: 20;
}
.loading-spinner {
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
border-top: 4px solid #8BC34A;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 15px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loading-indicator p {
margin: 0;
color: #666;
font-size: 14px;
}
.carousel-container[data-sheets-sync="complete"] .loading-indicator {
display: none;
}
.carousel-container[data-sheets-sync="error"] .loading-indicator p {
color: #d32f2f;
}
.carousel-container[data-sheets-sync="error"] .loading-indicator p:after {
content: " Error occurred while loading data!";
}
@media (max-width: 768px) {
.carousel-item {
flex: 0 0 50%;
}
.arrow-button {
width: 50px;
height: 50px;
}
.arrow-button i {
font-size: 16px;
}
.arrow-left {
left: -25px;
}
.arrow-right {
right: -25px;
}
}
@media (max-width: 480px) {
.carousel-container {
padding: 10px;
}
.carousel-item {
padding: 0 5px;
}
.report-card h3 {
font-size: 18px;
margin: 15px 15px 8px;
}
.report-card p {
font-size: 13px;
margin: 0 15px 15px;
}
.download-btn {
margin: 0 15px 15px;
padding: 10px 20px;
font-size: 11px;
}
}
</style>
<script>
class SheetsCarouselSync {
constructor(options) {
this.sheetsId = options.sheetsId || '1RN4zvG4X1JdFQQgz0gS2WwfYc5XF-gGpYqdB4T7WX70';
this.sheetName = options.sheetName || 'test data';
this.config = options;
this.carouselContainer = this.config.target;
this.carouselContainer.dataset.sheetsSync = "loading";
this.sheetsData = [];
this.carouselTrack = this.carouselContainer.querySelector("#carouselTrack");
this.initialize();
}
async initialize() {
try {
console.log("Starting to load data from Google Sheets...");
this.sheetsData = await this.fetchSheetsData();
console.log("Loaded", this.sheetsData.length, "items from Google Sheets");
if (this.sheetsData.length > 0) {
this.createCarouselItems();
this.carouselContainer.dataset.sheetsSync = "complete";
console.log("Sync completed!");
setTimeout(() => {
window.updateCarousel && window.updateCarousel();
}, 100);
} else {
throw new Error("No data from Google Sheets");
}
} catch (error) {
console.error("Error syncing Google Sheets:", error);
this.carouselContainer.dataset.sheetsSync = "error";
}
}
async fetchSheetsData() {
try {
const csvUrl = `https://docs.google.com/spreadsheets/d/${this.sheetsId}/gviz/tq?tqx=out:csv&sheet=${encodeURIComponent(this.sheetName)}`;
console.log("Loading from:", csvUrl);
const response = await fetch(csvUrl);
if (!response.ok) {
throw new Error(`CSV request failed: ${response.status}`);
}
const csvText = await response.text();
return this.parseCSVData(csvText);
} catch (error) {
console.error("Error loading sheets data:", error);
throw error;
}
}
parseCSVData(csvText) {
const rows = csvText.split('\n').filter(row => row.trim());
const data = [];
if (rows.length < 2) return data;
const headers = this.parseCSVRow(rows[0]).map(header => header.replace(/"/g, '').trim());
console.log("Headers found:", headers);
for (let i = 1; i < rows.length; i++) {
const row = rows[i];
if (row.trim()) {
const cells = this.parseCSVRow(row);
const rowData = {};
headers.forEach((header, index) => {
rowData[header] = cells[index] ? cells[index].replace(/"/g, '').trim() : '';
});
if (rowData.Title && rowData.Title.trim()) {
data.push({
title: rowData.Title || '',
description: rowData.Description || '',
imageUrl: rowData['Image URL'] || 'https://via.placeholder.com/300x400?text=No+Image',
buttonUrl: rowData['Button URL'] || '#'
});
}
}
}
return data;
}
parseCSVRow(row) {
const cells = [];
let current = '';
let inQuotes = false;
for (let i = 0; i < row.length; i++) {
const char = row[i];
if (char === '"') {
inQuotes = !inQuotes;
} else if (char === ',' && !inQuotes) {
cells.push(current);
current = '';
} else {
current += char;
}
}
cells.push(current);
return cells;
}
createCarouselItems() {
this.carouselTrack.innerHTML = '';
this.sheetsData.forEach((item, index) => {
const carouselItem = document.createElement('div');
carouselItem.className = 'carousel-item';
carouselItem.innerHTML = `
<div class="report-card">
<img src="${item.imageUrl}" alt="${item.title}" onerror="this.src='https://via.placeholder.com/300x400?text=Image+Error'">
<h3>${item.title}</h3>
<p>${item.description}</p>
<button class="download-btn" onclick="window.open('${item.buttonUrl}', '_blank')">DOWNLOAD</button>
</div>
`;
this.carouselTrack.appendChild(carouselItem);
console.log(`Created item ${index + 1}: ${item.title}`);
});
}
}
let currentSlide = 0;
let isDragging = false;
let startX = 0;
let currentX = 0;
let threshold = 50;
const track = document.getElementById('carouselTrack');
function getItemsPerView() {
return window.innerWidth <= 768 ? 2 : 4;
}
function updateCarousel() {
const items = document.querySelectorAll('.carousel-item');
const totalItems = items.length;
if (totalItems === 0) return;
const itemsPerView = getItemsPerView();
const maxSlide = Math.max(0, totalItems - itemsPerView);
if (currentSlide > maxSlide) {
currentSlide = maxSlide;
}
const translateX = -currentSlide * (100 / itemsPerView);
track.style.transform = `translateX(${translateX}%)`;
}
function moveSlide(direction) {
const items = document.querySelectorAll('.carousel-item');
const itemsPerView = getItemsPerView();
const maxSlide = Math.max(0, items.length - itemsPerView);
currentSlide += direction;
if (currentSlide < 0) {
currentSlide = 0;
} else if (currentSlide > maxSlide) {
currentSlide = maxSlide;
}
updateCarousel();
}
track.addEventListener('mousedown', (e) => {
isDragging = true;
startX = e.clientX;
track.style.cursor = 'grabbing';
track.style.transition = 'none';
});
track.addEventListener('mousemove', (e) => {
if (!isDragging) return;
e.preventDefault();
currentX = e.clientX - startX;
});
track.addEventListener('mouseup', () => {
if (!isDragging) return;
isDragging = false;
track.style.cursor = 'grab';
track.style.transition = 'transform 0.3s ease';
if (Math.abs(currentX) > threshold) {
if (currentX > 0) {
moveSlide(-1);
} else {
moveSlide(1);
}
}
currentX = 0;
});
track.addEventListener('mouseleave', () => {
if (isDragging) {
isDragging = false;
track.style.cursor = 'grab';
track.style.transition = 'transform 0.3s ease';
currentX = 0;
}
});
track.addEventListener('touchstart', (e) => {
isDragging = true;
startX = e.touches[0].clientX;
track.style.transition = 'none';
});
track.addEventListener('touchmove', (e) => {
if (!isDragging) return;
currentX = e.touches[0].clientX - startX;
});
track.addEventListener('touchend', () => {
if (!isDragging) return;
isDragging = false;
track.style.transition = 'transform 0.3s ease';
if (Math.abs(currentX) > threshold) {
if (currentX > 0) {
moveSlide(-1);
} else {
moveSlide(1);
}
}
currentX = 0;
});
window.addEventListener('resize', updateCarousel);
window.updateCarousel = updateCarousel;
(function() {
const initSheetsCarouselSync = () => {
const carouselContainer = document.getElementById('carouselContainer');
if (carouselContainer && !carouselContainer.dataset.sheetsSync) {
const config = {
target: carouselContainer,
linkTitle: true,
linkImage: false
};
new SheetsCarouselSync(config);
}
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initSheetsCarouselSync);
} else {
initSheetsCarouselSync();
}
})();
</script>

Remember to update Sheets string & tab name

Result like this
