React Keen Slider: The Only Setup & Customization Guide You Need
Why keen-slider Deserves Your Attention
The React ecosystem has no shortage of slider and carousel libraries — Swiper, Slick, Embla, Glider, the list goes on. Most of them share a familiar problem: they were designed before modern React patterns existed, and it shows. You wrestle with `componentDidMount` compatibility, bloated CSS overrides, and a dependency footprint that would make a mid-2010s jQuery plugin blush.
keen-slider takes a different approach. It’s a framework-agnostic, dependency-free touch slider that ships a first-class React binding through the useKeenSlider hook. The entire library weighs around 6KB gzipped. It handles touch, mouse drag, keyboard, and wheel interactions natively — without a plugin for each one. If you’ve ever spent 40 minutes debugging why Slick breaks on resize, this will feel like a breath of mountain air.
This guide covers everything: keen-slider installation, the useKeenSlider hook API, responsive breakpoints, autoplay, custom navigation, and the plugin architecture that makes advanced use cases possible. By the end, you’ll have a production-grade React slider component that you actually understand — not just copied from StackOverflow.
keen-slider Installation and Project Setup
Getting keen-slider into a React project takes about two minutes, and that’s including the time to open your terminal. The package ships everything you need: the core engine, the React hook, TypeScript types, and the base stylesheet. No peer dependencies, no PostCSS plugins required, no peer-pressure to install Lodash.
Install the package via npm or yarn:
# npm
npm install keen-slider
# yarn
yarn add keen-slider
# pnpm
pnpm add keen-slider
Once installed, you’ll need to import the base CSS. This stylesheet provides the flex layout and overflow handling that makes the slider actually work. You can import it globally in your index.js / main.tsx, or at the component level — both approaches are valid:
// In your entry file or directly in the component
import 'keen-slider/keen-slider.min.css'
That’s genuinely it for setup. No Webpack aliases, no babel plugins, no reading a MIGRATION.md that’s three major versions behind. The keen-slider setup is intentionally minimal because the library’s philosophy is to get out of your way as fast as possible.
Your First keen-slider React Component
The heart of React keen-slider integration is the useKeenSlider hook. It follows a clean, idiomatic React pattern: you pass your configuration object as an argument, and the hook returns a tuple — a ref for the DOM container and an instance ref for programmatic control. There’s no class instantiation, no document.querySelector, no lifecycle gymnastics.
Here’s the minimum viable keen-slider example in React:
import { useKeenSlider } from 'keen-slider/react'
import 'keen-slider/keen-slider.min.css'
export function BasicSlider() {
const [sliderRef] = useKeenSlider({
loop: true,
slides: { perView: 1 },
})
return (
<div ref={sliderRef} className="keen-slider">
<div className="keen-slider__slide">Slide 1</div>
<div className="keen-slider__slide">Slide 2</div>
<div className="keen-slider__slide">Slide 3</div>
</div>
)
}
Two class names do the heavy lifting here: keen-slider on the wrapper and keen-slider__slide on each slide. These aren’t styling suggestions — they’re functional selectors that the library uses to calculate dimensions and apply transforms. You can absolutely extend them with your own styles, but don’t remove them unless you plan to rewrite the core layout logic yourself (and you really don’t want to do that).
The loop: true option enables infinite looping. The slides: { perView: 1 } object tells keen-slider how many slides to show at once. Both are optional — the library defaults to a single, non-looping slide. But in practice, you’ll almost always want to configure at least these two.
keen-slider CSS class sets display: flex and overflow: hidden on the container. If your slides aren’t visible, the most common culprit is a parent element with overflow: hidden interfering, or a missing height on the slide content.
Deep Dive into the useKeenSlider Hook
The useKeenSlider hook is where the real power of React slider hooks architecture shows itself. The second element of the returned tuple — instanceRef — is a React ref that holds the live slider instance. This gives you full programmatic access: navigate to a specific slide, read the current index, subscribe to events, and even destroy and re-initialize the slider if needed.
import { useKeenSlider } from 'keen-slider/react'
import { useState } from 'react'
export function ControlledSlider() {
const [currentSlide, setCurrentSlide] = useState(0)
const [loaded, setLoaded] = useState(false)
const [sliderRef, instanceRef] = useKeenSlider({
initial: 0,
loop: true,
slideChanged(slider) {
setCurrentSlide(slider.track.details.rel)
},
created() {
setLoaded(true)
},
})
return (
<div style={{ position: 'relative' }}>
<div ref={sliderRef} className="keen-slider">
<div className="keen-slider__slide">Slide 1</div>
<div className="keen-slider__slide">Slide 2</div>
<div className="keen-slider__slide">Slide 3</div>
</div>
{loaded && (
<>
<button onClick={() => instanceRef.current?.prev()}>←</button>
<button onClick={() => instanceRef.current?.next()}>→</button>
<span>Slide {currentSlide + 1}</span>
</>
)}
</div>
)
}
Notice the loaded state guard. The created callback fires when the slider has finished initializing and attached to the DOM. It’s a good practice to gate your navigation controls behind this flag — otherwise you risk calling instanceRef.current?.next() before the instance exists, which produces a silent no-op at best and a cryptic error at worst.
The slideChanged event callback receives the full slider instance as its argument, giving you access to slider.track.details — a rich object containing rel (relative position), abs (absolute position), position (decimal scroll position), and more. This is how you build dot indicators, progress bars, or any UI that needs to reflect the current slider state. No custom state management boilerplate required.
Responsive Breakpoints and slides Configuration
A React carousel slider that looks great on desktop and terrible on mobile isn’t a slider — it’s a liability. keen-slider handles responsive behavior through a breakpoints configuration object that mirrors CSS media query logic. Each key is a media query string, and the value is a partial options object that overrides the defaults when the query matches.
const [sliderRef] = useKeenSlider({
loop: true,
slides: { perView: 3, spacing: 16 },
breakpoints: {
'(max-width: 768px)': {
slides: { perView: 1.25, spacing: 10 },
},
'(max-width: 1024px)': {
slides: { perView: 2, spacing: 12 },
},
},
})
The perView: 1.25 value is worth calling out — this is a deliberate design pattern in keen-slider that shows a “peek” of the next slide, visually communicating to mobile users that there’s more content to scroll. It’s a tiny detail that significantly improves discoverability on touch devices without requiring any extra markup or JavaScript.
The spacing property controls the gap between slides in pixels. Unlike CSS gap, keen-slider’s spacing is layout-aware — it factors into the perView calculation so that three slides with 16px gaps always fill exactly 100% of the container width. This predictability is one of the most underrated advantages of keen-slider over purely CSS-based approaches.
Adding Autoplay Without a Plugin
Unlike some slider libraries that lock autoplay behind a premium tier or a separate package, keen-slider gives you all the tools to build autoplay yourself in about 15 lines of code. This might sound like extra work, but it means you have complete control over the behavior — pause on hover, pause when the tab is hidden, resume on focus, custom intervals per slide. You’re not fighting a black box.
import { useKeenSlider } from 'keen-slider/react'
import { useEffect, useRef } from 'react'
export function AutoplaySlider() {
const timer = useRef<ReturnType<typeof setInterval> | null>(null)
const [sliderRef, instanceRef] = useKeenSlider({
loop: true,
slides: { perView: 1 },
})
useEffect(() => {
timer.current = setInterval(() => {
instanceRef.current?.next()
}, 3000)
return () => {
if (timer.current) clearInterval(timer.current)
}
}, [instanceRef])
return (
<div ref={sliderRef} className="keen-slider">
<div className="keen-slider__slide">Slide 1</div>
<div className="keen-slider__slide">Slide 2</div>
<div className="keen-slider__slide">Slide 3</div>
</div>
)
}
The useEffect cleanup function is critical here. Without it, your interval survives component unmounting and attempts to call next() on a destroyed instance — a classic source of React memory leak warnings. Always clean up your timers, especially in components that mount and unmount frequently (modals, tabs, virtualized lists).
For a pause-on-hover variant, attach onMouseEnter and onMouseLeave handlers to the slider wrapper that call clearInterval and restart the timer respectively. This is standard React event handling — no keen-slider-specific API required. The library stays out of your logic and lets React do what React does best.
keen-slider Plugins: Extending the Core
keen-slider ships with a plugin architecture that allows you to extend the slider’s behavior without modifying its internals. The second argument to useKeenSlider is a plugins array. The library’s documentation provides three ready-made plugins — ResizePlugin, MutationPlugin, and a WheelControls recipe — and the plugin interface is simple enough that writing your own is a realistic option, not a theoretical one.
import { useKeenSlider } from 'keen-slider/react'
// WheelControls plugin — enables mouse wheel navigation
function WheelControls(slider: any) {
let touchTimeout: ReturnType<typeof setTimeout>
let position: { x: number; y: number }
let wheelActive: boolean
function dispatch(e: WheelEvent, name: string) {
position.x -= e.deltaX
position.y -= e.deltaY
slider.container.dispatchEvent(
new CustomEvent(name, {
detail: { x: position.x, y: position.y },
})
)
}
function wheelStart(e: WheelEvent) {
position = { x: e.pageX, y: e.pageY }
dispatch(e, 'ksDragStart')
}
function wheel(e: WheelEvent) {
dispatch(e, 'ksDrag')
}
function wheelEnd(e: WheelEvent) {
dispatch(e, 'ksDragEnd')
}
function eventWheel(e: WheelEvent) {
e.preventDefault()
if (!wheelActive) {
wheelStart(e)
wheelActive = true
}
wheel(e)
clearTimeout(touchTimeout)
touchTimeout = setTimeout(() => {
wheelActive = false
wheelEnd(e)
}, 50)
}
slider.on('created', () => {
slider.container.addEventListener('wheel', eventWheel, { passive: false })
})
}
export function WheelSlider() {
const [sliderRef] = useKeenSlider(
{ loop: true },
[WheelControls]
)
return (
<div ref={sliderRef} className="keen-slider">
<div className="keen-slider__slide">Slide 1</div>
<div className="keen-slider__slide">Slide 2</div>
<div className="keen-slider__slide">Slide 3</div>
</div>
)
}
The plugin pattern is a function that receives the slider instance and attaches behavior through the event system. The slider exposes a clean lifecycle: created, updated, destroyed, slideChanged, animationStarted, animationEnded. You listen to lifecycle events via slider.on() and dispatch custom drag events to hook into the core animation pipeline. It’s a tight, well-designed API.
The MutationPlugin is particularly useful in React applications where slides are rendered dynamically — for example, from an API call or a paginated list. It uses a MutationObserver under the hood to detect when children are added or removed from the slider container and automatically re-measures and re-initializes. Without it, adding slides after initial render leaves the slider in a broken state.
Advanced keen-slider Customization: Dots, Arrows, and Thumbnails
Navigation UI — dots, arrows, thumbnails — is intentionally outside keen-slider’s scope. This is a deliberate philosophy: the library provides the motion engine, you provide the UI. In practice, this means your navigation components are just React state and instanceRef method calls, which is exactly where they should be. No fighting with a library’s built-in arrow styles, no !important CSS wars.
import { useKeenSlider } from 'keen-slider/react'
import { useState } from 'react'
import 'keen-slider/keen-slider.min.css'
export function SliderWithDots() {
const [currentSlide, setCurrentSlide] = useState(0)
const [loaded, setLoaded] = useState(false)
const slides = ['Slide 1', 'Slide 2', 'Slide 3', 'Slide 4']
const [sliderRef, instanceRef] = useKeenSlider({
loop: true,
slideChanged(s) {
setCurrentSlide(s.track.details.rel)
},
created() {
setLoaded(true)
},
})
return (
<div style={{ position: 'relative' }}>
{/* Slider */}
<div ref={sliderRef} className="keen-slider">
{slides.map((slide, i) => (
<div key={i} className="keen-slider__slide">
{slide}
</div>
))}
</div>
{/* Dot Navigation */}
{loaded && (
<div style={{ display: 'flex', justifyContent: 'center', gap: 8, marginTop: 16 }}>
{slides.map((_, i) => (
<button
key={i}
onClick={() => instanceRef.current?.moveToIdx(i)}
style={{
width: 10,
height: 10,
borderRadius: '50%',
border: 'none',
background: currentSlide === i ? '#6c63ff' : '#ccc',
cursor: 'pointer',
padding: 0,
}}
aria-label={`Go to slide ${i + 1}`}
/>
))}
</div>
)}
</div>
)
}
The moveToIdx(i) method is the key here — it navigates directly to a zero-indexed slide. It accepts an optional second argument for the animation duration override if you want a faster or slower transition for programmatic navigation versus drag. The aria-label on each dot isn’t optional if you care about accessibility — and you should, because a slider component that screen readers can’t navigate is a lawsuit waiting to happen in certain jurisdictions.
For a thumbnail slider — a pattern common in e-commerce product galleries — you can instantiate two separate useKeenSlider hooks and sync them via the slideChanged event. The main slider’s instanceRef calls moveToIdx on the thumbnail slider and vice versa. This is a pure React state coordination problem, not a keen-slider feature request. The library’s composable design makes it trivially solvable.
Why keen-slider Is a Performant React Slider
Performance in slider libraries comes down to two things: how animations are executed and how much JavaScript runs per frame. keen-slider uses CSS transforms exclusively — no left, no margin, no scrollLeft manipulation. CSS transforms are composited on the GPU by the browser’s rendering engine, meaning they don’t trigger layout or paint phases. The result is smooth 60fps animation even on mid-range mobile hardware.
The library also uses pointer events instead of separate touch and mouse event listeners, reducing the event handler surface significantly. Velocity calculations for inertia-based dragging are done in a tight loop with no DOM reads inside the animation frame, avoiding the classic layout thrashing pattern that tanks performance in naive implementations. These aren’t marketing claims — they’re visible in the source code, which you can and should read given its modest size.
From a React perspective, keen-slider integrates without causing extra re-renders. The hook doesn’t store slider state in React state — instanceRef is a plain ref, and the slider operates entirely outside the React rendering cycle. Your component only re-renders when you explicitly call setState inside event callbacks (like slideChanged). This is the correct model for DOM-heavy UI: React manages what React should manage, the browser manages the rest.
Complete Production-Ready keen-slider React Example
Let’s put everything together: responsive breakpoints, loop, dot navigation, arrow buttons, autoplay with pause-on-hover, and the MutationPlugin for dynamic content. This is the component you’d actually ship.
import { useEffect, useRef, useState } from 'react'
import { useKeenSlider } from 'keen-slider/react'
import 'keen-slider/keen-slider.min.css'
// MutationPlugin from keen-slider docs
function MutationPlugin(slider: any) {
const observer = new MutationObserver(mutations => {
mutations.forEach(() => slider.update())
})
const config = { childList: true }
slider.on('created', () => observer.observe(slider.container, config))
slider.on('destroyed', () => observer.disconnect())
}
const SLIDES = [
{ id: 1, label: 'Mountain View', bg: '#6c63ff' },
{ id: 2, label: 'Ocean Breeze', bg: '#ff6584' },
{ id: 3, label: 'City Lights', bg: '#43d9ad' },
{ id: 4, label: 'Forest Trail', bg: '#f9a825' },
]
export function ProductionSlider() {
const [currentSlide, setCurrentSlide] = useState(0)
const [loaded, setLoaded] = useState(false)
const [paused, setPaused] = useState(false)
const timer = useRef<ReturnType<typeof setInterval> | null>(null)
const [sliderRef, instanceRef] = useKeenSlider(
{
loop: true,
initial: 0,
slides: { perView: 1 },
breakpoints: {
'(min-width: 640px)': {
slides: { perView: 2, spacing: 16 },
},
'(min-width: 1024px)': {
slides: { perView: 3, spacing: 20 },
},
},
slideChanged(s) {
setCurrentSlide(s.track.details.rel)
},
created() {
setLoaded(true)
},
},
[MutationPlugin]
)
useEffect(() => {
if (paused) {
if (timer.current) clearInterval(timer.current)
return
}
timer.current = setInterval(() => {
instanceRef.current?.next()
}, 3500)
return () => {
if (timer.current) clearInterval(timer.current)
}
}, [paused, instanceRef])
return (
<section aria-label="Featured content slider">
<div
ref={sliderRef}
className="keen-slider"
onMouseEnter={() => setPaused(true)}
onMouseLeave={() => setPaused(false)}
style={{ borderRadius: 12, overflow: 'hidden' }}
>
{SLIDES.map(slide => (
<div
key={slide.id}
className="keen-slider__slide"
style={{
background: slide.bg,
minHeight: 220,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: '#fff',
fontSize: '1.25rem',
fontWeight: 600,
borderRadius: 8,
}}
>
{slide.label}
</div>
))}
</div>
{loaded && (
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: 12,
marginTop: 20,
}}
>
{/* Prev Arrow */}
<button
onClick={() => instanceRef.current?.prev()}
aria-label="Previous slide"
style={{
background: 'none',
border: '2px solid #6c63ff',
borderRadius: '50%',
width: 36,
height: 36,
cursor: 'pointer',
color: '#6c63ff',
fontWeight: 700,
fontSize: '1rem',
}}
>
‹
</button>
{/* Dots */}
{SLIDES.map((_, i) => (
<button
key={i}
onClick={() => instanceRef.current?.moveToIdx(i)}
aria-label={`Go to slide ${i + 1}`}
aria-current={currentSlide === i ? 'true' : 'false'}
style={{
width: currentSlide === i ? 24 : 10,
height: 10,
borderRadius: 5,
border: 'none',
background: currentSlide === i ? '#6c63ff' : '#ddd',
cursor: 'pointer',
padding: 0,
transition: 'width 0.3s ease, background 0.3s ease',
}}
/>
))}
{/* Next Arrow */}
<button
onClick={() => instanceRef.current?.next()}
aria-label="Next slide"
style={{
background: 'none',
border: '2px solid #6c63ff',
borderRadius: '50%',
width: 36,
height: 36,
cursor: 'pointer',
color: '#6c63ff',
fontWeight: 700,
fontSize: '1rem',
}}
>
›
</button>
</div>
)}
</section>
)
}
This component covers the 95% use case for a React carousel slider in a real application. The animated dots (width transition from 10px to 24px) provide a visual progress cue without requiring an external animation library. The aria-current attribute on the active dot ensures screen readers report the current slide position correctly. The autoplay pauses precisely on mouse enter and resumes on mouse leave, which is the expected behavior for accessible carousels per WCAG 2.1 guidelines.
Notice that the entire navigation section is conditionally rendered behind the loaded flag. This prevents a flash of non-functional buttons during server-side rendering or hydration, which is a subtle but real UX problem in Next.js applications. If you’re using useState initial values from SSR props, combine this with a suppressHydrationWarning on the container div for full compatibility.
keen-slider vs Swiper vs Embla: Choosing the Right React Slider Library
Choosing a React slider library in 2025 is less about features and more about fit. Every serious option — keen-slider, Swiper.js, Embla Carousel, and Splide — can build a functional touch slider. The differences emerge at the edges: bundle size, API design, TypeScript support, and how much the library trusts you to handle your own UI.
- keen-slider — ~6KB gzipped, hooks-first React API, plugin architecture, framework-agnostic core. Best for: developers who want control, small bundles, and a clean hooks integration.
- Swiper.js — ~35KB gzipped, massive feature set (3D cube, coverflow, thumbs, zoom), dedicated React component. Best for: feature-heavy marketing pages where configuration beats code.
- Embla Carousel — ~7KB gzipped, lower-level API than keen-slider, excellent performance. Best for: developers comfortable writing more boilerplate for a slightly more barebones engine.
- Splide — ~28KB gzipped, accessible by default, rich built-in UI. Best for: projects where accessibility compliance is a hard requirement and custom UI work is undesirable.
For most React applications — SPAs, e-commerce product galleries, marketing landing pages — keen-slider hits the sweet spot. It’s small enough that bundle size paranoia is unwarranted, flexible enough that you’ll never hit a wall, and its React API is genuinely idiomatic rather than a thin wrapper over a jQuery-era constructor. The only scenario where it falls short is when you need deeply specialized effects like coverflow or 3D card flipping, where Swiper’s built-in implementations save meaningful development time.
The TypeScript support deserves a specific mention. keen-slider ships complete type definitions and the KeenSliderOptions and KeenSliderInstance types are well-documented and accurate. Writing a typed plugin or a typed wrapper component is straightforward. This isn’t universally true across the slider library ecosystem — Slick’s types, for example, have historically been a community maintenance effort of varying quality.
Frequently Asked Questions
How do I install and set up keen-slider in a React project?
Run npm install keen-slider in your project root. Import useKeenSlider from 'keen-slider/react' and the base stylesheet from 'keen-slider/keen-slider.min.css'. In your component, call the hook with your options object and attach the returned ref to your container div with the class keen-slider. Each slide needs the class keen-slider__slide. That’s the complete minimum setup — no additional configuration is required to get a functional drag-and-touch slider running.
How does the useKeenSlider hook work in React?
useKeenSlider returns a tuple of two values: [sliderRef, instanceRef]. The sliderRef is a React callback ref — attach it directly to your container DOM element. The instanceRef is a React.MutableRefObject holding the live keen-slider instance, which exposes methods like .prev(), .next(), .moveToIdx(n), and .update(). Configuration options (loop, breakpoints, plugins, event callbacks) are passed as arguments to the hook and are reactive — changing them will update the slider instance accordingly.
Is keen-slider better than Swiper.js for React?
keen-slider is the better choice when bundle size, performance, and a clean hooks API are priorities. At ~6KB gzipped versus Swiper’s ~35KB, it loads significantly faster, especially on mobile. Its React integration is hooks-native rather than a wrapper component, which means cleaner code and no extra abstraction layer. Swiper.js wins when you need a large catalogue of built-in effects (3D cube, coverflow, fade) or prefer configuration-heavy setup over code. For most production React applications — product carousels, testimonials, image galleries — keen-slider is the pragmatic choice.

Recente reacties