diff --git a/README.md b/README.md
index 4def07a..b9947cb 100644
--- a/README.md
+++ b/README.md
@@ -2,9 +2,11 @@
Snappy is yet another carousel library.
Its goal is to be as lightweight and performant as possible.
+
It does not implement every feature found in other carousel libraries. Instead, it relies on native CSS scroll snapping and adds as little on top as possible.
-Mouse-drag behavior closely follows native touch scrolling in the browser, including inertia and snapping to a slide.
+Mouse-drag behavior closely follows native touch scrolling in the browser, including inertia and snapping to a slide. Thus dragging with the mouse feels pretty identical to native scrolling on a touch device or using a trackpad.
+
| File | Size |
| ---------- | ------: |
@@ -14,7 +16,8 @@ Mouse-drag behavior closely follows native touch scrolling in the browser, inclu
## Usage
-### CSS only
+
+### CSS only for a basic scroll container
You can use Snappy without JavaScript. This gives you a simple CSS-only scroll container with snapping slides.
@@ -23,17 +26,55 @@ You can use Snappy without JavaScript. This gives you a simple CSS-only scroll c
-
-
-
-
- ...
+
```
-Snappy is mostly unstyled, leaving the appearance up to you. For inspiration, see the [example](index.html).
+Snappy is mostly unstyled, leaving the appearance up to you.
+For inspiration, see the [complete example](index.html).
-### Add JavaScript
+
+### Options
+https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Scroll_snap
+add data attribites to the carousel container to change the default behavior:
+
+```html
+
+
+
+
+### Add JavaScript for a carousel
Including the JavaScript adds the following features:
@@ -42,6 +83,18 @@ Including the JavaScript adds the following features:
- Mouse dragging
```html
+
+
```
diff --git a/index.html b/index.html
index aa84417..c3bd5ae 100644
--- a/index.html
+++ b/index.html
@@ -30,98 +30,90 @@
margin-bottom: 1em;
}
+ /* Custom snappy styling */
+ /* Colors from Open color (https://yeun.github.io/open-color) */
.snappy-carousel {
- counter-reset: snappy-slide;
- max-height: 300px;
- }
-
- .snappy-slide {
- counter-increment: snappy-slide;
- display: flex;
- justify-content: center;
- align-items: center;
- width: 33%;
- aspect-ratio: 3/2;
- max-height: 400px;
-
- background-color: #f1f3f5;
- color: #343a40;
-
- @media (prefers-color-scheme: dark) {
- background-color: #495057;
- color: #f8f9fa;
- }
- }
-
- .snappy-slide:nth-child(even) {
- background-color: #e9ecef;
-
- @media (prefers-color-scheme: dark) {
- background-color: #343a40;
- color: #f8f9fa;
- }
- }
-
- .snappy-slide:before {
- content: counter(snappy-slide);
- }
-
- .snappy-controls {
- display: flex;
- align-items: center;
- gap: 0.25rem;
- margin-top: 1rem;
- }
-
- .snappy-button {
- width: 1.5rem;
- height: 1.5rem;
- font-size: 1rem;
- border: none;
- background-color: #f1f3f5;
- color: #339af0;
-
- &:hover,
- &:active {
- background-color: #dee2e6;
+ .scroll-container {
+ max-height: 300px;
}
- @media (prefers-color-scheme: dark) {
- background-color: #495057;
+ .slide {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: 33%;
+ aspect-ratio: 3/2;
+ max-height: 400px;
+
+ background-color: #f1f3f5;
+ color: #343a40;
+
+ @media (prefers-color-scheme: dark) {
+ background-color: #495057;
+ color: #f8f9fa;
+ }
+ }
+
+ .slide:nth-child(even) {
+ background-color: #e9ecef;
+
+ @media (prefers-color-scheme: dark) {
+ background-color: #343a40;
+ color: #f8f9fa;
+ }
+ }
+
+ .prev,
+ .next {
+ width: 1.5rem;
+ height: 1.5rem;
+ font-size: 1rem;
+ border: none;
+ background-color: #f1f3f5;
+ color: #339af0;
&:hover,
&:active {
- background-color: #868e96;
- color: #1971c2;
+ background-color: #dee2e6;
+ }
+
+ @media (prefers-color-scheme: dark) {
+ background-color: #495057;
+
+ &:hover,
+ &:active {
+ background-color: #868e96;
+ color: #1971c2;
+ }
}
}
- }
- .snappy-pagination {
- display: flex;
- }
+ .pagination {
+ display: flex;
+ }
- .snappy-pagination-dot {
- width: 0.66rem !important;
- height: 0.66rem !important;
- border-radius: 0.33rem;
- background-color: #dee2e6 !important;
- margin: 0 0.25rem;
+ .pagination .dot {
+ width: 0.66rem !important;
+ height: 0.66rem !important;
+ border-radius: 0.33rem;
+ background-color: #dee2e6 !important;
+ margin: 0 0.25rem;
- transition-property: width, background;
- transition-duration: .8s;
- transition-timing-function: cubic-bezier(0.25, 1, 0.5, 1);
+ transition-property: width, background;
+ transition-duration: .8s;
+ transition-timing-function: cubic-bezier(0.25, 1, 0.5, 1);
- cursor: pointer;
- }
+ cursor: pointer;
+ }
- .snappy-pagination-dot:hover {
- background-color: #adb5bd !important;
- }
+ .pagination .dot:hover {
+ background-color: #adb5bd !important;
+ }
- .snappy-pagination-dot.is-active {
- background-color: #1971c2 !important;
- width: 2rem !important;
+ .pagination .dot.is-active {
+ background-color: #1971c2 !important;
+ width: 2rem !important;
+ }
}
@@ -151,21 +143,21 @@
data-snap-align="start"
data-snap-align="end"
-->
-
+
+
-
- ❮
- ❯
-
+ ❮
+ ❯
+
diff --git a/src/snappy.css b/src/snappy.css
index e576eed..8eed536 100644
--- a/src/snappy.css
+++ b/src/snappy.css
@@ -1,57 +1,67 @@
.snappy-carousel {
- display: flex;
- flex-direction: row;
- overflow-x: auto;
- overflow-y: hidden;
- user-select: none;
- cursor: grab;
- scroll-behavior: smooth;
- &[data-direction="y"] {
+ .scroll-container {
+ display: flex;
+ flex-direction: row;
+ overflow-x: auto;
+ overflow-y: hidden;
+ user-select: none;
+ cursor: grab;
+ scroll-behavior: smooth;
+ scroll-snap-type: x mandatory;
+
+ &.is-dragging {
+ scroll-snap-type: none;
+ scroll-behavior: unset;
+ }
+ }
+
+
+ .slide {
+ flex-shrink: 0;
+ scroll-snap-align: center;
+ scroll-snap-stop: normal;
+ }
+
+ .pagination .dot {
+ display: inline-block;
+ width: 1rem;
+ height: 1rem;
+ background-color: silver;
+ }
+
+ /* Options */
+
+ &[data-direction="y"] .scroll-container {
flex-direction: column;
overflow-x: hidden;
overflow-y: auto;
}
- &.is-dragging {
- scroll-snap-type: none;
- scroll-behavior: unset;
- }
-
- scroll-snap-type: x mandatory;
-
- &[data-direction="y"] {
+ &[data-direction="y"] .scroll-container {
flex-direction: column;
overflow-x: hidden;
overflow-y: auto;
scroll-snap-type: y mandatory;
}
- gap: var(--carousel-gap, 0);
- gap: attr(data-gap type(*), 0);
+ &[data-gap] {
+ --carousel-gap: attr(data-gap type(*), 0);
- .snappy-slide {
- flex-shrink: 0;
- scroll-snap-align: center;
- scroll-snap-stop: normal;
+ .scroll-container {
+ gap: var(--carousel-gap, 0);
+ }
}
- &[data-snap-align="start"] .snappy-slide {
+ &[data-snap-align="start"] .slide {
scroll-snap-align: start;
}
- &[data-snap-align="end"] .snappy-slide {
+ &[data-snap-align="end"] .slide {
scroll-snap-align: end;
}
- &[data-snap-stop="always"] .snappy-slide {
+ &[data-snap-stop="always"] .slide {
scroll-snap-stop: always;
}
-
-}
-.snappy-pagination-dot {
- display: inline-block;
- width: 1rem;
- height: 1rem;
- background-color: silver;
}
\ No newline at end of file
diff --git a/src/snappy.js b/src/snappy.js
index 8e16742..fdfeea7 100644
--- a/src/snappy.js
+++ b/src/snappy.js
@@ -16,7 +16,7 @@ class Snappy {
class Carousel {
// DOM references
- element = null;
+ scrollContainer = null;
prevButton = null;
nextButton = null;
pagination = null;
@@ -37,24 +37,23 @@ class Carousel {
activeSlideIndex = 0;
constructor(element) {
- this.element = element;
- this.direction = this.element.dataset.direction === 'y' ? 'y' : 'x';
+ this.scrollContainer = element.querySelector('.scroll-container');
+ this.direction = element.dataset.direction === 'y' ? 'y' : 'x';
+ const gap = element.dataset.gap;
+ if (gap) this.scrollContainer.style.setProperty('--carousel-gap', gap);
+ this.slides = this.scrollContainer.querySelectorAll('.slide');
- const gap = this.element.dataset.gap;
- if (gap) this.element.style.setProperty('--carousel-gap', gap);
+ this.prevButton = document.querySelector(`.prev`);
+ this.nextButton = document.querySelector(`.next`);
+ this.pagination = document.querySelector(`.pagination`);
- this.slides = this.element.querySelectorAll('.snappy-slide');
- this.prevButton = document.querySelector(`.snappy-prev[data-carousel="${this.element.id}"]`);
- this.nextButton = document.querySelector(`.snappy-next[data-carousel="${this.element.id}"]`);
- this.pagination = document.querySelector(`.snappy-pagination[data-carousel="${this.element.id}"]`);
-
- this.element.addEventListener('mousedown', this.startDrag.bind(this));
- this.element.addEventListener('mousemove', this.drag.bind(this));
- this.element.addEventListener('mouseup', this.endDrag.bind(this));
- this.element.addEventListener('mouseleave', this.endDrag.bind(this));
- this.element.addEventListener('touchstart', this.reset.bind(this));
- this.element.addEventListener('wheel', this.reset.bind(this));
- this.element.addEventListener('scroll', this.updateActiveSlide.bind(this));
+ this.scrollContainer.addEventListener('mousedown', this.startDrag.bind(this));
+ this.scrollContainer.addEventListener('mousemove', this.drag.bind(this));
+ this.scrollContainer.addEventListener('mouseup', this.endDrag.bind(this));
+ this.scrollContainer.addEventListener('mouseleave', this.endDrag.bind(this));
+ this.scrollContainer.addEventListener('touchstart', this.reset.bind(this));
+ this.scrollContainer.addEventListener('wheel', this.reset.bind(this));
+ this.scrollContainer.addEventListener('scroll', this.updateActiveSlide.bind(this));
if (this.prevButton) {
this.prevButton.addEventListener('click', () => this.prev());
@@ -73,7 +72,7 @@ class Carousel {
startDrag(e) {
this.isDragging = true;
- this.element.classList.add('is-dragging'); // Disable scroll snapping
+ this.scrollContainer.classList.add('is-dragging'); // Disable scroll snapping
this.velocity = 0;
this.lastPosition = undefined;
this.lastTime = undefined;
@@ -113,7 +112,6 @@ class Carousel {
}
const target = this.calculateDragTarget();
- const element = this.element;
const distance = target - this.getScrollPosition();
this.velocity = -distance * (1 - this.inertia) / 10;
@@ -159,19 +157,19 @@ class Carousel {
}
getScrollPosition() {
- return this.direction === 'y' ? this.element.scrollTop : this.element.scrollLeft;
+ return this.direction === 'y' ? this.scrollContainer.scrollTop : this.scrollContainer.scrollLeft;
}
setScrollPosition(position) {
if (this.direction === 'y') {
- this.element.scrollTop = position;
+ this.scrollContainer.scrollTop = position;
} else {
- this.element.scrollLeft = position;
+ this.scrollContainer.scrollLeft = position;
}
}
calculateDragTarget() {
- const element = this.element;
+ const element = this.scrollContainer;
const isVertical = this.direction === 'y';
const maxScrollPosition = isVertical
? element.scrollHeight - element.clientHeight
@@ -237,7 +235,7 @@ class Carousel {
reset() {
this.isDragging = false;
- this.element.classList.remove('is-dragging');
+ this.scrollContainer.classList.remove('is-dragging');
this.velocity = 0
}
@@ -245,7 +243,7 @@ class Carousel {
this.slides.forEach((slide, index) => {
// Populate the pagination container with dots for each slide
let div = document.createElement('div');
- div.classList.add('snappy-pagination-dot');
+ div.classList.add('dot');
div.addEventListener('click', () => this.goToSlide(index));
this.pagination.appendChild(div);
@@ -267,11 +265,11 @@ class Carousel {
const marginStart = isVertical ? 'scrollMarginTop' : 'scrollMarginLeft';
const marginEnd = isVertical ? 'scrollMarginBottom' : 'scrollMarginRight';
const cssPixels = value => Number.parseFloat(value) || 0;
- const containerRect = this.element.getBoundingClientRect();
- const containerStyle = getComputedStyle(this.element);
+ const containerRect = this.scrollContainer.getBoundingClientRect();
+ const containerStyle = getComputedStyle(this.scrollContainer);
const snapportStartEdge = containerRect[startEdge] +
- (isVertical ? this.element.clientTop : this.element.clientLeft);
- const clientExtent = isVertical ? this.element.clientHeight : this.element.clientWidth;
+ (isVertical ? this.scrollContainer.clientTop : this.scrollContainer.clientLeft);
+ const clientExtent = isVertical ? this.scrollContainer.clientHeight : this.scrollContainer.clientWidth;
const snapportStart = snapportStartEdge + cssPixels(containerStyle[paddingStart]);
const snapportEnd = snapportStartEdge + clientExtent - cssPixels(containerStyle[paddingEnd]);
const snapportCenter = (snapportStart + snapportEnd) / 2;