Creating a RevealJS clone with CSS Scroll Snap Points

Nicolas Hoffmann
4 min readJun 8, 2018


I wanted to test this CSS module for a long time (and some other things), I had the opportunity to do it with my slides for Sud Web 2018. Here is a return of experience.

Warning, important note: some parts that I present here are NOT widely usable in production. I was in a particular context where I could do it… and you will notice that I really did not miss it. To sum up, it is a simple RevealJS clone, coded quick and dirty in less than 2 hours.

My slides (in french)

CSS Scroll Snap Points

The scroll-snap-type CSS property defines how strictly snap points are enforced on the scroll container in case there is one. Not clear? This is normal!

An example worth a thousand words, have a look (with Firefox, better test: use touch) to an example.

As you may see, the idea of slides in HTML is quite simple. Each element will have a width of 100% and a height of 100vh. Once we will move, CSS Scroll Snap Points will allow to move slide by slide, in a “native way ”.

The cool feature: touch is very well managed (try on a Surface with Firefox or Edge). Once focus is in the page… left and right arrows also allow moving slide by slide. Do you see the interest for a carousel for example? :)

Well, this CSS module has a lot of advantages. At this moment, touch is very well handled by Edge and Firefox. Chrome and other browsers seems not (yet?) to manage it.

In my case, I only had to define a container with scroll-snap-type: mandatory and scroll-snap-points-x: repeat(100%). And to define each slide to be sized to the full width/height of the viewport. Yes, as simple as that.

The doc : CSS Scroll Snap Points

Intersection Observer

This API provides a way to observe changes in the intersection of a target element with an ancestor element or with viewport, and then to call a function when it is the case, this avoids doing it with self-made functions that can be not fantastic from performance point of view.

To be honest, I wanted to use it at the beginning only… to use it (it became useful later), see Intersection Observer on MDN.

The idea is to add a class to know which slide is the active one. I fact, I observe all slides.

if ('IntersectionObserver' in window) { 
// IntersectionObserver Supported
let config = {
root: null,
rootMargin: '0px',
threshold: 0.5
let observer = new IntersectionObserver(onChange, config);
slides.forEach(slide => observer.observe(slide));
} else {
// IntersectionObserver NOT Supported
console.log('Whomp Whomp Whomp');

And when one becomes visible in viewport, I add the class is-active and I add the fragment in the URL at the same time.

function onChange(changes) { 
changes.forEach(change => {
if (change.intersectionRatio > 0.5) {'is-active');
history.pushState(null, null, location.pathname + + '#' +'id'));
} else {'is-active'); }

It was a whim at first, but it will be useful later.

Add contents that appear/move

In the slideshow, I needed that some contents that appear in the same slide. I added some buttons like this.

<button class="js-next" data-next="next_2">Ce qui change…</button>

When this button is activated, it will get element with id next_2 and it displays it.

Yes, not perfect for accessibility, I could do better, I know! :)

Managing the remote control

When testing the presentation, I found that having to be near my Surface to activate buttons/change slides was annoying for the rhythm and my freedom on stage (and it was a really good idea, as the presentation will need it). At this point, I coded some little sophistication… which will be really essential for the presentation.

The remote control sends a keyboard event (see, as if you press Page Down (34) or Page Up (33). I already coded that if this event happened, I call a function that performed a next/previous slide. For the animation, I did it simply by using scrollIntoView, and as I did not have any compatibility issue, I usedbehavior: "smooth" option (be careful, support is not optimal, base version is ok, but scrollIntoViewOptions is not supported everywhere).

As I wanted to activate buttons, the idea is quite simple: if I have a not yet activated button in the active slide, I send a click on it.

if (eventName === 'keydown' && 34 === e.keyCode) { 
let activeSlide = document.querySelector('');
let buttonNext = activeSlide.querySelector('.js-next:not(.has-been-pressed)');
if (buttonNext) {;
} else {
let buttonAnim = activeSlide.querySelector('.js-anim:not(.has-been-played)');
if (buttonAnim) {;
} else {
let slides = []'.slide'));
selectSlideInList(slides, 'next');

In the function that goes back into the presentation (I could need it during the presentation), I reinitialised “animation” and “next” buttons. This is not perfect, but I will be able to play them again if needed.

Job was done, I was controlling all the slideshow with the remote control. Fortunately, because I will need it on stage.

To conclude

Yes, it is done quick and dirty, but with some APIs, CSS, a few JavaScript and some efforts, I was able to test some things and recreate a light revealJS-like very quickly.

It would be easy to add a print version, to enhance the system, I did also some tests to have horizontal and vertical slides, to enhance later, because I did not have the time to do it.

Which I find really interesting is for a potential carousel : We would not need to manage touch using some JavaScript, but everything will be done thanks to CSS Scroll Snap Points… when this CSS module will work correctly on Webkit.

Here we are, if you have questions or comments, do not hesitate!

Aucun commentaire pour le moment.

Originally published at



Nicolas Hoffmann

Front-end dev in ❤ with CSS, CSP/security, #a11y, webperfs & Web quality. Made Röcssti/@Van11y . Works for @ProtonMail . Homme de ménage. Humour fourni avec.