A tiny, pure-CSS full-bleed product slider. Zero dependencies.
The first slide lines up with your page grid on the left, while the track runs all the way out to the right edge of the browser. Scroll right and the slides pass under the left gutter — bleeding off-grid on the left too.
- Why
- Install
- Quick start
- How it works
- Markup reference
- Configuration
- Modifiers
- JavaScript API
- Accessibility
- Browser support
- FAQ
- License
Most "carousel" libraries ship thousands of lines of JS to do something the browser already does natively. Bleed Slider is the opposite:
- 🧊 Core is 100% CSS — built on native
scroll-snap. Swipe, trackpad, wheel, and keyboard all work with no JavaScript. - 🪶 ~1.6 KB CSS. Optional ~1.5 KB JS only adds arrow buttons + mouse-drag.
- 📐 Aligns to your grid with a single variable (
--bs-content). - 🫥 Scrollbar hidden by default (still fully scrollable).
- ♿ Keyboard-scrollable, respects
prefers-reduced-motion. - 📦 No build step, no framework, no dependencies.
There is no build step. Copy the two files into your project:
bleed-slider.css ← required (the whole library)
bleed-slider.js ← optional (arrows + mouse drag only)
<link rel="stylesheet" href="bleed-slider.css" />
<!-- optional -->
<script src="bleed-slider.js" defer></script>Or with a package manager:
npm install bleed-sliderimport "bleed-slider/bleed-slider.css";
import "bleed-slider"; // optional enhancement; auto-inits on DOMContentLoaded<link rel="stylesheet" href="bleed-slider.css" />
<!-- normal page content goes inside .bs-container so it sits on the grid -->
<div class="bs-container">
<h2>Featured products</h2>
</div>
<!-- the slider is full-width and bleeds to the edges -->
<ul class="bleed-slider" tabindex="0" aria-label="Featured products">
<li>…your card…</li>
<li>…your card…</li>
<li>…your card…</li>
</ul>Point one variable at your grid width and you're done:
:root { --bs-content: 1180px; } /* ← your content/grid width */Two rules for correct alignment
.bleed-slidermust be full-width — a direct child of<body>or any wrapper that has no horizontal padding / max-width of its own.- Wrap your normal content (headings, text) in
.bs-containerso it lands on the same grid line as the first slide.
Three native CSS features do all the work — there is no measuring, no resize observers, no JS:
.bleed-slider {
/* distance from the viewport edge in to where the grid content starts */
--bs-edge: max(var(--bs-gutter), (100% - var(--bs-content)) / 2);
display: flex;
overflow-x: auto;
scroll-snap-type: x mandatory; /* snap each slide into place */
scroll-padding-inline-start: var(--bs-edge); /* snap to the GRID, not to 0 */
padding-inline: var(--bs-edge); /* first/last slide on the grid */
}
.bleed-slider > * {
flex: 0 0 var(--bs-slide);
scroll-snap-align: start;
}The element itself spans the full viewport width. Only its padding and scroll-padding pull the snap points in to your grid. So the ends of the track stay aligned to the grid while the middle bleeds edge-to-edge. That's the entire trick.
Because --bs-edge is expressed with % (resolved against the element's full
width) rather than vw, the slider and .bs-container land on exactly the same
line — no scrollbar-width drift.
Minimal (CSS only):
<ul class="bleed-slider" tabindex="0" aria-label="Products">
<li> … </li>
</ul>Full (with the optional arrows + drag JS):
<div class="bs-slider">
<div class="bs-container bs-slider__head">
<h2>Featured products</h2>
<div class="bs-nav">
<button data-bs-prev aria-label="Previous">‹</button>
<button data-bs-next aria-label="Next">›</button>
</div>
</div>
<ul class="bleed-slider" tabindex="0" aria-label="Featured products">
<li> … </li>
</ul>
</div>| Class / attribute | Role |
|---|---|
.bleed-slider |
The scroll container. Any direct children become slides. |
.bs-container |
Wrapper that keeps normal content on the grid. |
.bs-slider |
Optional outer wrapper that scopes the arrow buttons to one slider. |
[data-bs-prev] / [data-bs-next] |
Buttons the JS wires up to scroll one slide. |
tabindex="0" |
Makes the track keyboard-scrollable (recommended). |
Slides can be any element — <li>, <a>, <article>, a card component, etc.
Bleed Slider only sets their width and snap alignment; all visual styling is yours.
Set these custom properties globally (:root) or per-instance (inline style):
| Variable | Default | Description |
|---|---|---|
--bs-content |
1180px |
Your grid width — the alignment line. |
--bs-gutter |
1.25rem |
Minimum space from the viewport edge. |
--bs-gap |
1.25rem |
Space between slides. |
--bs-slide |
clamp(15rem, 72vw, 22rem) |
Slide width (mobile → desktop). |
--bs-radius |
18px |
Corner-radius hook for your cards. |
Per-instance override — just one slider gets narrower slides:
<ul class="bleed-slider" style="--bs-slide: clamp(13rem, 60vw, 17rem)">
…
</ul>The default --bs-slide uses clamp() so each slide is roughly 72% of the
viewport on phones (the next card peeks in, hinting it's swipeable) and caps at
22rem on desktop.
| Class | Effect |
|---|---|
| (none) | Scrollbar hidden by default — still fully scrollable. |
.bleed-slider--bar |
Opt back in to a slim, on-brand scrollbar. |
<ul class="bleed-slider bleed-slider--bar"> … </ul>The library works without any JavaScript. The optional bleed-slider.js only
adds prev/next buttons and click-and-drag (mouse / trackpad — touch
already scrolls natively).
It auto-initialises every .bleed-slider on DOMContentLoaded. To control it
manually:
// enhance everything (or pass a root element/subtree)
BleedSlider.init();
BleedSlider.init(document.querySelector("#shop"));
// enhance a single track element
BleedSlider.enhance(document.querySelector(".bleed-slider"));| Method | Description |
|---|---|
BleedSlider.init(root?) |
Enhance every .bleed-slider within root (default document). Safe to call repeatedly — already-enhanced sliders are skipped. |
BleedSlider.enhance(el) |
Enhance one specific .bleed-slider element. |
Arrow buttons ([data-bs-prev] / [data-bs-next]) are looked up inside the
nearest .bs-slider wrapper and auto-disable at the start/end of the track.
It's also available as a CommonJS module (require("bleed-slider")).
Opt a slider out of JS. Add data-bs-no-enhance to keep a specific slider
pure CSS even when the script is loaded — init() will skip it:
<ul class="bleed-slider" data-bs-no-enhance> … </ul>- The track is keyboard-scrollable when given
tabindex="0"— arrow keys, Page Up/Down, Home/End all work natively. - Add a meaningful
aria-labelto each.bleed-slider. - Arrow buttons should carry
aria-labels (e.g. "Previous" / "Next"). scroll-behavior: smoothis disabled automatically under@media (prefers-reduced-motion: reduce).
Works in all modern evergreen browsers (Chrome, Edge, Firefox, Safari).
Uses CSS scroll-snap, max() and clamp() for layout. color-mix() is used
only for the optional --bar scrollbar styling and degrades gracefully where
unsupported.
Does it need JavaScript?
No. Swiping, trackpad, wheel, and keyboard all work with CSS alone. JS only adds
arrow buttons and mouse-drag. See example-no-js.html
for a page that loads zero scripts, or add data-bs-no-enhance to opt a single
slider out of the script.
My first slide isn't aligned to the grid.
The .bleed-slider must be full-width — make sure no ancestor adds horizontal
padding or a max-width. Wrap text content in .bs-container instead, and set
--bs-content to match your grid.
Can slides be links or custom components?
Yes — any direct child of .bleed-slider becomes a slide.
How do I show the scrollbar?
Add the .bleed-slider--bar modifier.
MIT © Bullmade