HTML slides without frameworks, just CSS
Tags: webdev, html, css
Published: 2019-02-09 03:49:54

---
title: HTML slides without frameworks, just CSS
published: true
tags: webdev, html, css
canonical_url: https://www.chenhuijing.com/blog/html-slides-without-frameworks/
---
I am a huge fan of [reveal.js](https://revealjs.com/) and have used it for almost every talk I’ve ever given. It is a full-featured presentation framework with loads of nifty features, like speaker notes, full-screen mode, code syntax highlighting etc.

But sometimes, I just need to toss up a handful of information on a screen at a local meetup or office presentation. And as a result of being a web developer, I’m way more comfortable controlling and adjusting my layouts with CSS than with a pointy-clicky interface.

I came across an implementation by [Ondřej Žára](http://ondras.zarovi.cz/) for [pure CSS slides](http://ondras.zarovi.cz/demos/nojs/#) and dug into his codebase to see what I could use for my own watered-down HTML slides. For a pure CSS implementation, it’s pretty full-featured, and you should check it out regardless.

## The` :target` pseudo-class

What I wanted was a good way to transition from slide to slide, and I noticed Ondřej’s use of the `:target` pseudo-class. It is a selector that matches an element with an id matching the current URL’s fragment, which is denoted by the `#` sign.

For example, if your site has an element with the id of `dinosaur`, you can navigate to that element from the address bar using the URL, `https://www.example.com/index.html#dinosaur`. And if that is the current URL, the following CSS rule will apply to the `#dinosaur` element:

```css
#dinosaur:target {
  background-color: green;
}
```

![:target selector invoked when current URL fragment matches](https://thepracticaldev.s3.amazonaws.com/i/blfjtfygpdj6tz857lz5.gif)

And with that, we can let the shenanigans commence.

## Markup structure

The HTML structure can be fairly simple.

```html
<main>
  <section id="slide1"></section>
  <section id="slide2"></section>
  <section id="slide3"></section>
  <section id="slide4"></section>
  <section id="slide5"></section>
</main>
```

Each slide can be a section which takes up the full height of the viewport. And we can achieve that with relative ease using viewport units. Viewport units are as their name suggests, CSS units that are relative to the size of the viewport.

We have `vw`, `vh`, `vmax` and `vmin`. For the purpose of making a HTML slidedeck, the relavant unit here is `vh` or viewport height. By setting a height of `100vh` to each section, they will always be the height of the viewport, no matter how you resize the browser.

![Slide is always full height of the viewport](https://thepracticaldev.s3.amazonaws.com/i/cer0akxglzl1y3zzq4ev.gif)

Each slide has a unique id that can be targeted, which will be the mechanism for slide controls.

![Link to previous and next slide with URL fragments](https://thepracticaldev.s3.amazonaws.com/i/9u7qugp35krc2p22en73.gif)

So where’s the `:target` pseudo-class in all this? Hang on, we’re coming to that. What we did earlier was make the browser jump to the top of each section, you can still scroll up and down, making your presentation behave like a web page instead of a slide deck.

If that’s what you want it to do, great. You’re done for the day, to be honest. But let’s say we don’t want that scroll bar activated. We want the active slide in the viewport and that’s it. Then that’s when the `:target` pseudo-class comes into play.

## Stacking your slides

But before getting into that, let’s visualise how the slide deck will behave.

<figcaption>Each slide is the same size and positioned on top of each other</figcaption> ![Stacking the sections](https://www.chenhuijing.com/assets/images/posts/html-slides/stack.svg)

We will style the sections such that only one slide is visible at any one time by stacking them over each other, then utilise the URL fragments to indicate which slide should be active.

So. Stacking along the z-index eh? We now have more than 1 option for achieving something like that. Let’s cover the tried-and-tested technique of position and z-index. The `z-index` property only applies to positioned boxes, which are boxes that have a `position` value of anything other than the default value of `static`.

There are now 4 other values for `position`, namely `relative`, `absolute`, `fixed` and `sticky`, `sticky` being a new addition. [Elad Shechter](https://twitter.com/eladsc) wrote an [excellent in-depth write-up](https://medium.com/@elad/css-position-sticky-how-it-really-works-54cd01dc2d46) of how it works, so do give it a read.

According to the [specification](https://www.w3.org/TR/CSS2/visuren.html#layers):

> The root element forms the root stacking context. Other stacking contexts are generated by any positioned element (including relatively positioned elements) having a computed value of ‘z-index’ other than ‘auto’.

So once you apply a position property to a box, you can use the `z-index` property to adjust its stack level. Setting `position: absolute` on a box removes it from normal flow and causes it to be explicitly offset with respect to its containing block.

Contents of an absolutely positioned element don’t flow around other boxes, if they have a higher stack level, they’re just going to cover whatever is below them.

![](https://thepracticaldev.s3.amazonaws.com/i/30vcb6ufrym9io8i2288.gif)

To stack them all up, apply a `position: absolute` and `width: 100%` on each slide section. Some of you might be thinking, what’s the difference between having a `width: 100%` on an absolutely positioned element versus setting all the offset values to `0`? (e.g. `top: 0; right: 0; bottom: 0; left: 0`) Good thing someone else had the exact same question.

[Keith J. Grant](https://keithjgrant.com/) wrote an analysis of the difference between these 2 approaches and found that if you use `width: 100%` with additional margins around the element, it will get shifted out of its positioned ancestor.

But since I want my slides to fill up the viewport, I’m not using margins at all, so I’d rather do a single line than 4. 🤷

The next thing is to adjust the z-index for the active slide to be higher than all the other inactive slides with our trusty `:target` selector.

```css
section {
  height: 100vh;
  width: 100%;
  position: absolute;
  z-index: 0;
}

section:target {
  z-index: 1;
}
```

Some people handle z-indexes differently, using denominations of 10 or 100, but honestly, between 2 elements, whoever has the higher integer value wins, regardless whether it’s by 1 or 1000.

![No overflow occurs](https://thepracticaldev.s3.amazonaws.com/i/idtvt68sj4v6ebkv34ky.gif)

## Sprinkle on some animation

To add some semblance of professionalism to this endeavour (who am I kidding?), I think some slide transitions are in order. CSS also offers a lovely suite of animation options for this. You can rotate, zoom, slide in and out, all with CSS transforms.

If you looked at Ondřej’s slides, he used a rotate effect, so let’s try that out. There are 3 position states for this, _before entering in viewport_, _active in the viewport_, and _left the viewport_.

<figcaption>In 'n out</figcaption> ![Stacking the sections](https://www.chenhuijing.com/assets/images/posts/html-slides/rotate.svg)

The default `transform-origin` is `50% 50%`, which means the element will rotate around its centre. To have a corner rotate like the one in the diagram, set the value to `0 0`. All slides start out rotated 90 degrees clockwise out of the viewport, while slides leaving will be rotated 90 degrees anti-clockwise up and out.

We can make use of the sibling selector to target slides that come after the active slide, and add in some transition values to make the animation appear smoother:

```css
section {
  height: 100vh;
  width: 100%;
  position: absolute;
  z-index: 0;
  transform: rotate(90deg);
  transform-origin: 0 0;
  transition: transform 1s, opacity 0.8s;
}

section:target {
  transform: rotate(0deg);
  z-index: 1;
}

section:target ~ section {
  opacity: 0;
  transform: rotate(-90deg);
}
```

When the slide is rotated out of the viewport, it somehow triggers the scrollbar. I have not figured out the exact reason for this though I do know the solution is to add an `overflow: hidden` to the `body` element. I should research this some more, because this doesn’t happen when I use translate.

## Wrapping up

One more thing to take note of is that when your slides first load, there will not be any URL fragment triggered, meaning none of your slides will be active. To workaround this issue, add a link to the first slide which will show up on first load, making sure it is also a positioned element.

![End result](https://thepracticaldev.s3.amazonaws.com/i/ohtfo5xii2rhfzjo22y6.gif)

Feel free to try out other CSS transforms for more slide transitions if rotate doesn’t suit your style. But if your slide requirements are reasonably simple, why not try something like this? You can even toss it onto some free static site hosts and have it accessible wherever you go. 😎

_For a live implementation of this, check out [the slides](https://singaporecss.github.io/talk.css) used for introducing Talk.CSS_