Integrating Owl Carousel into Webflow Cloud unlocks dynamic, API-driven sliders while preserving seamless client-side performance.
Owl Carousel is a jQuery plugin, and most of the trouble people hit when they drop it into a React app comes down to timing: the plugin's source file grabs window.jQuery the moment it is evaluated, so jQuery has to exist on the window before Owl's module loads, and both have to stay off the server render entirely.
On Webflow Cloud, a second wrinkle appears. Your Next.js app lives under a mount path on the Webflow site (something like /showcase), and client-side fetch calls need that prefix. A carousel that only collapses into a stacked column after deploy is nearly always missing it.
In this build, we deploy a Next.js app to Webflow Cloud with a client component that initializes Owl Carousel over slides served by a Route Handler in the same app. The slides come from a JSON file bundled at build time, the loader reads the mount path from a public environment variable, and the whole thing mounts at /showcase on the site.
What do you need to integrate Owl Carousel in Webflow?
This build has four prerequisites. Webflow Cloud is included from the free Starter site plan up, and mounting the app to a custom domain requires Premium or higher.
Everything else is standard local tooling for a Next.js project:
- A Webflow site on any current plan: Starter, Basic, and Premium sites can all host the app, and you mount it to a path on a site you can edit.
- A supported Node.js version: Install it locally before scaffolding, since Webflow Cloud documents the general Node requirements for app builds.
- A supported Next.js version: Webflow Cloud's bring your own app page lists the supported Node and framework requirements and the npm-only requirement.
- Image assets with public URLs: A handful is enough for the slides, and stock placeholders are fine while you wire the carousel up.
Set jQuery on the window and expose the mount path before writing the slider. Each requirement shapes the client component.
6 steps to integrate Owl Carousel in Webflow Cloud
The browser needs a client component that sets window.jQuery, dynamically imports Owl Carousel, and initializes it over slides fetched from a Route Handler in the same app. The fetch URL must include the mount path Webflow Cloud injects at build time, and the Route Handler must stay off the Next.js edge runtime target.
Let’s see the steps.
1. Scaffold the Next.js app and install Owl Carousel
Use npm install from the first command and keep package-lock.json committed. Webflow Cloud accepts npm as its package manager. Its docs state: "Currently, Webflow Cloud supports only the npm package manager." Other package managers are unsupported.
The owl.carousel package is used here as a plain browser build that reads window.jQuery when it runs. Install jQuery alongside Owl so that global is available when the plugin loads. A cast around .owlCarousel() does not provide a declaration for an unresolved owl.carousel module.
You should now have a project that starts with npm run dev and serves the default Next.js page at http://localhost:3000, with jquery and owl.carousel listed under dependencies in package.json.
2. Load the Owl Carousel stylesheets in the root layout
Import Owl's base stylesheet and default theme in app/layout.tsx so they ship with every page. The base stylesheet handles the stage, item sizing, and the positioning changes applied while sliding. Its default theme styles the navigation arrows and dots.
Without the base stylesheet, the plugin still initializes and logs nothing, but the items never form a horizontal stage, which makes it hard to debug later.
Add the imports above your own global stylesheet:
// app/layout.tsx
import type { Metadata } from 'next';
import 'owl.carousel/dist/assets/owl.carousel.min.css';
import 'owl.carousel/dist/assets/owl.theme.default.min.css';
import './globals.css';
export const metadata: Metadata = {
title: 'Showcase',
description: 'Featured work carousel',
};
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
Import order matters here. Your globals.css comes last, so any overrides you write for .owl-nav or .owl-dots win the cascade without !important.
Restart the dev server and open the browser's Network tab: you should see the Owl rules bundled into the page's CSS, and the default page should look unchanged because nothing on it carries an owl-carousel class yet.
3. Build the client component that initializes Owl Carousel
Render the slides as plain markup, then set window.jQuery and import Owl inside useEffect. A static import 'owl.carousel' at the top of the file is hoisted and evaluated before any component code runs.
At that point, there is no window.jQuery in the browser and no window at all during the server prerender. Dynamic imports inside the effect run only in the browser and only after you have assigned the global.
Create the component at app/components/OwlSlider.tsx:
'use client';
import { useEffect, useRef } from 'react';
export type Slide = { title: string; imageUrl: string; alt: string };
export default function OwlSlider({ slides }: { slides: Slide[] }) {
const stageRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const el = stageRef.current;
if (!el || slides.length === 0) return;
let cancelled = false;
let $owl: any = null;
(async () => {
const jq = (await import('jquery')).default;
(window as any).jQuery = jq;
(window as any).$ = jq;
await import('owl.carousel');
if (cancelled) return;
$owl = jq(el) as any;
$owl.owlCarousel({
items: 3,
margin: 16,
loop: true,
nav: true,
dots: true,
responsive: {
0: { items: 1 },
768: { items: 2 },
1024: { items: 3 },
},
});
})();
return () => {
cancelled = true;
if ($owl) $owl.trigger('destroy.owl.carousel');
};
}, [slides]);
return (
<div ref={stageRef} className="owl-carousel owl-theme">
{slides.map((slide) => (
<div key={slide.imageUrl} className="item">
<img src={slide.imageUrl} alt={slide.alt} />
<p>{slide.title}</p>
</div>
))}
</div>
);
}
The canceled flag covers cases where the component unmounts while imports are still resolving.
For a quick check, render OwlSlider from app/page.tsx with three literal objects that match the Slide type. You should see multiple items in a row on a wide viewport and a single item on a narrow one, with arrows and dots from the default theme.
4. Serve the slides from a Route Handler
Put the slide data behind GET /api/slides and import the JSON statically. Cloudflare Workers cannot read a bundled data file through node:fs under Webflow Cloud's pinned compatibility date. The static import bundles the file at build time.
Cloudflare Workers expose node:fs only with a later compatibility date than the one Webflow Cloud pins. A readFileSync call can therefore work on your laptop and fail on the platform.
The platform does provide node:path, so the limitation is specific to file access.
Start with the data file at data/slides.json:
[
{
"title": "Harbour Lights rebrand",
"imageUrl": "https://placehold.co/800x600/1f2937/ffffff",
"alt": "Harbour Lights brand posters pinned to a studio wall"
},
{
"title": "Fielding annual report",
"imageUrl": "https://placehold.co/800x600/334155/ffffff",
"alt": "Open spread of the Fielding annual report on a desk"
},
{
"title": "Ridgeway store launch",
"imageUrl": "https://placehold.co/800x600/475569/ffffff",
"alt": "Ridgeway storefront signage photographed at dusk"
}
]
Every object must include all three keys, because the component reads imageUrl for the src and alt for accessibility.
The handler is four lines once the data is a module import:
// app/api/slides/route.ts
import { NextResponse } from 'next/server';
import slides from '@/data/slides.json';
export async function GET() {
return NextResponse.json(slides, {
headers: {
'Cache-Control': 'public, max-age=300',
},
});
}
Leave out export const runtime = 'edge'. Webflow Cloud deploys Next.js through the OpenNext Cloudflare adapter, which does not support the Next.js edge runtime target.
Webflow Cloud runs on Cloudflare Workers, and its docs say the Edge runtime provides fast, globally distributed hosting. The Next.js edge runtime target is a separate concept. Adding the directive ships a broken build.
Webflow Cloud's bring-your-own-app page still instructs you to add the directive to API routes, which is why this failure keeps recurring.
Hit http://localhost:3000/api/slides in the browser, and you should get your JSON array back with a successful status and the cache header visible in the response.
5. Fetch the slides with the mount path included
Prepend the mount path from NEXT_PUBLIC_BASE_PATH to every browser-side request. A root-relative request to /api/slides leaves the mounted app after deployment.
Webflow Cloud injects basePath into the Next.js build automatically; the docs say plainly, "You don't need to add an adapter, a base path, or an output mode." Server-side routing picks up the path, while browser-side requests require the prefix. The bring-your-own-app page states the consequence directly: "Client-side fetch calls must manually include the base path."
NEXT_PUBLIC_BASE_PATH contains non-secret public routing data, so exposing it to the browser is appropriate.
The loader itself is small, and the base path is read once at module scope:
'use client';
import { useEffect, useState } from 'react';
import OwlSlider, { type Slide } from './OwlSlider';
const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? '';
export default function SlidesLoader() {
const [slides, setSlides] = useState<Slide[]>([]);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
fetch(`${basePath}/api/slides`)
.then((res) => {
if (!res.ok) throw new Error(`Slides request failed with ${res.status}`);
return res.json() as Promise<Slide[]>;
})
.then(setSlides)
.catch((err: Error) => setError(err.message));
}, []);
if (error) return <p role="alert">{error}</p>;
return <OwlSlider slides={slides} />;
}
Render SlidesLoader from app/page.tsx in place of the hardcoded array. Locally, leave NEXT_PUBLIC_BASE_PATH unset so the fetch goes to /api/slides; on Webflow Cloud, set it in the environment's variables to the mount path. NEXT_PUBLIC_ values are inlined at build time.
That works here because Webflow Cloud exposes environment variables to the build and to the deployed app at runtime: "Both secret and non-secret environment variables are available to your application's build process and to the deployed application at runtime."
Reload the local page, and the carousel should now populate from the API route.
6. Deploy the app to Webflow Cloud and mount it at a path
The mount path and public environment variable must match. Create an environment with a mount path of /showcase and, in that environment's variables, add NEXT_PUBLIC_BASE_PATH with the value /showcase. Use the leading slash and omit a trailing slash.
Follow Webflow Cloud's current deployment and publishing flow. Mounting the app to a custom domain requires Premium or higher.
Open https://your-site-domain/showcase, and you should see the same carousel you had locally, and https://your-site-domain/showcase/api/slides should return the JSON. If the page renders but the slides never arrive, check the environment variable first.
What causes an Owl Carousel integration to fail on Webflow Cloud?
Start with the console and build log. The common signals point to a missing mount path, an incompatible runtime directive, early Owl module evaluation, or file access through node:fs.
Each category produces a specific error message, which makes the failing request or import easier to isolate.
Console shows "SyntaxError: Unexpected token '<'" and the carousel stays empty
Cause: The request reached the Webflow site's routing and returned an HTML 404 page. Calling .json() on that response throws the syntax error because the loader expects a JSON array, not the returned page markup. This usually means the browser requested /api/slides from the site root instead of requesting the route under the mounted app path.
Fix: Set NEXT_PUBLIC_BASE_PATH in the Webflow Cloud environment even if it also exists in your local .env.local, and match the mount path character for character. Then redeploy because NEXT_PUBLIC_ values are baked in at build time.
Open the failing request in the Network tab and inspect its URL, response status, and response body. A URL missing /showcase shows that the variable was empty during the build. The corrected request should use /showcase/api/slides and return the JSON array instead of HTML.
Deploy stops with a build error on the slides route
Cause: A Next.js edge runtime directive on the route conflicts with the OpenNext Cloudflare adapter configuration used by Webflow Cloud. The Next.js edge runtime target is separate from the Cloudflare Workers runtime that hosts the deployed app, so adding the directive can break the build even though the platform provides globally distributed hosting.
Fix: Search the repository for runtime = 'edge' and remove the directive from every Route Handler and page in the app. The line often comes from an older Next.js tutorial, so I search for it before every first deploy.
Keep the slides handler as a standard exported GET function that returns NextResponse.json(slides). Then rebuild and check the log for the slides route. The build should complete without trying to target the unsupported Next.js edge runtime configuration.
"ReferenceError: window is not defined" at build, or "owlCarousel is not a function" in the browser
Cause: Owl Carousel was evaluated before jQuery became a browser global. A server prerender has no window, and an early browser import leaves .owlCarousel() unavailable on jQuery's prototype.
A static import is hoisted before the component effect can assign jQuery to window.jQuery and window.$, so a client component declaration alone does not fix the evaluation order.
Fix: Use the client component import order: import jQuery inside useEffect, assign it to window.jQuery and window.$, import owl.carousel, and initialize it. A static Owl import in any component can break the build, so grep for from 'owl.carousel' and import 'owl.carousel' and move every hit into an effect.
After the change, the server build should no longer evaluate Owl, and the browser should expose .owlCarousel() after the dynamic import resolves.
Deployed route returns 500 while the same route works under npm run dev
Cause: The handler contains a node:fs import or a readFileSync call against data/slides.json. Local Node execution can hide the deployed file-access limitation because the file is available to the local process.
Under Webflow Cloud's pinned compatibility date, the deployed Worker cannot use that bundled file through node:fs, even though node:path is available.
Fix: Replace the file-system read with a static JSON module import. Import slides from @/data/slides.json at the top of the Route Handler, then return it through NextResponse.json(slides).
The import bundles the data at build time and removes the file read at runtime. If the data has to be dynamic, fetch it over HTTP from its location. Test /api/slides locally, redeploy, and confirm that the mounted route returns the JSON array with a successful response instead of a server error.
What you can build next with Owl Carousel and Webflow
The Route Handler boundary keeps a later CMS swap out of the client component. The most common next move is to replace the JSON file with items from the Webflow CMS, so editors manage case studies or products in Webflow CMS and they appear in the carousel on the next request.
The CMS REST API returns collection items you can map straight onto the Slide type, and the API token belongs in a secret environment variable read only inside the handler, where it is redacted from build logs and never shipped to the browser.
The same loader pattern can also power a testimonial rotator or a logo wall.
Explore Owl Carousel integration for more ways to connect the slider with a Webflow project.
Frequently asked questions
Why do nested carousel stages appear after slide updates?
If you see nested stages after the slides array changes, check that your effect cleanup still triggers destroy.owl.carousel. Because slides is a dependency, React runs cleanup before initializing against updated markup. You should also keep the cancellation guard so an import finishing after unmount cannot attach Owl to an element React has already removed.
Why does the carousel component use a plain image element?
You can use a plain img when you want each public imageUrl to pass directly into the slide markup without configuring Next.js image handling. You should expect the default next/image lint warning on that line. It is a warning rather than an Owl initialization failure so that you can evaluate image optimization separately from the carousel setup.
Does the slides Route Handler require authentication or rate limiting?
You do not need authentication or application-level rate limiting for the bundled, read-only marketing slides in this build. You can keep the handler public because it does not call a paid API or accept writes. If you later replace the JSON with another data source, you can handle that source within the same server-side Route Handler boundary.
How should you choose a cache policy for the slides API?
Choose a cache policy based on how often you expect slide content to change. You can keep the shown public cache for infrequently updated marketing content, or revise the header when a later data source needs fresher responses. To check the active policy, inspect the mounted API response rather than assuming your local and deployed responses match.





