CSS Only Typewriter Effect
Tags: css, nojs, a11y
Published: 2020-10-18 18:39:17

After reading Stokry's [post](https://dev.to/stokry/typing-effect-without-javascript-54ol) on his typing animation with CSS I took the opportunity to use this effect and improve it.

## The Idea
At [Mr. Henry](https://www.mrhenry.be/) we're currently working on the full website of our client [theAddresses](https://www.theaddresses.com/). Our designers came up with the idea to show some Portuguese words in sentences, when you click it the explanation appears.

![before](https://dev-to-uploads.s3.amazonaws.com/i/klye5w1bpchji7zgjk07.png)
<figcaption>Before the user interacts with component</figcaption>

![after](https://dev-to-uploads.s3.amazonaws.com/i/9w9cymcg5zsba6pvje4q.png)
<figcaption>After the user interacts with component</figcaption>

## The Goal
We try our best at respecting the a11y rules as much as possible so for this component we do make no exceptions.

The component should:
- be understood by screen readers;
- respect users their preferences on reduced motion;
- is usable for keyboard users.

This component should be flexible with our CMS editor and it would be great to make it work without any JavaScript.

### The Code: HTML
First of all the html, note that we're working with twig.
```twig
{% set explainer_id = function('uniqid', 'explainer-', false) %}

<span
	aria-label="'{{ text }}' which means '{{ explanation }}'"
	class="explainer"
>
	<input
		aria-hidden="true"
		class="explainer__checkbox"
		id="{{ explainer_id }}"
		name="{{ explainer_id }}"
		type="checkbox"
	>

	<label
		aria-hidden="true"
		class="explainer__label"
		data-text="{{ text }}"
		data-explanation="{{ explanation }}"
		for="{{ explainer_id }}"
	>
		<span class="explainer__text">
			{{ text }}
		</span>

		<span class="explainer__explanation">
			{{ explanation }}
		</span>
	</label>
</span>
```
**HTML — Interactivity**
A very simple yet powerful hack in HTML/CSS is to make use of a checkbox and listen to it `:checked` state in CSS to make your components interactive without a single line of JS.

**HTML — A11y**
We've made this clear and simple for screen readers by adding an aria-label with the copy:
> 'a Portuguese word' which means 'the explanation'

Our child elements have an `aria-hidden` attribute so the user can't be bothered or confused with other content.
Because we're using a checkbox, this element will be accessible for keyboard users.

**HTML — IDs**
IDs always should be unique in a HTML document, this is why we're using the PHP function [uniqid](https://www.php.net/manual/en/function.uniqid.php).
Later on, this will come in handy to select the specific component with css.

### The Code: Inline Style
Secondly we're going to add the inline style to our twig code:
```twig
{% set msPerChar = 32 %}
{% set msExtraDelay = 320 %}

<style>
	@media (prefers-reduced-motion: no-preference) {
		#{{ explainer_id }} ~ .explainer__label::after,
		#{{ explainer_id }} ~ .explainer__label::before {
			transition: max-width 0s linear {{ msPerChar * explanation|length }}ms;
		}

		#{{ explainer_id }}:checked ~ .explainer__label::after,
		#{{ explainer_id }}:checked ~ .explainer__label::before {
			transition: max-width 0s linear {{ msPerChar * text|length }}ms;
		}

		#{{ explainer_id }} ~ .explainer__label .explainer__text {
			transition: width {{ msPerChar * text|length }}ms steps({{ text|length }});
			transition-delay: {{ (msPerChar * explanation|length) + msExtraDelay }}ms;
		}

		#{{ explainer_id }} ~ .explainer__label .explainer__explanation {
			transition: width {{ msPerChar * explanation|length }}ms steps({{ explanation|length }});
			transition-delay: 0s;
		}

		#{{ explainer_id }}:checked ~ .explainer__label .explainer__text {
			transition-delay: 0s;
		}

		#{{ explainer_id }}:checked ~ .explainer__label .explainer__explanation {
			transition-delay: {{ (msPerChar * text|length) + msExtraDelay }}ms;
		}
	}
</style>
```
**TWIG — `msPerChar` & `msExtraDelay`**
To make the animation as consistent as possible, we're using twig variables and calculate our delays together with the string lengths.

**CSS — Animation**
As you can see we're making use of a before and after element, more on that further in this post.
The text and explanation width is animated in steps corresponding with the string length.

### The Code: CSS
```css
.explainer {
	border-bottom: 2px dashed;
	display: inline-block;
	white-space: nowrap;

	&:hover {
		border-bottom-style: solid;
	}
}

.explainer__checkbox {
	height: 0;
	left: 0;
	outline: none;
	position: absolute;
	top: 0;
	visibility: hidden;
	width: 0;
}

.explainer__label {
	cursor: help;
}

.explainer__text,
.explainer__explanation {
	display: inline-block;
	overflow: hidden;
	position: relative;
}

.explainer__checkbox:checked ~ .explainer__label .explainer__text,
.explainer__checkbox:not(:checked) ~ .explainer__label .explainer__explanation {
	/* Visually hide elements on older browsers or for users who prefer no animations. */
	width: 0;
}

@media (prefers-reduced-motion: no-preference) {
	.explainer__label {
		position: relative;
	}

	/* Before and After pseudo element will be used as placeholders of the text */
	.explainer__label::after,
	.explainer__label::before {
		display: inline-block;
		opacity: 0;
		overflow: hidden;
		width: auto;
	}

	.explainer__label::before {
		content: attr(data-text);
		max-width: 420px;

		.explainer__checkbox:checked ~ & {
			/* Hide original placeholder text */
			max-width: 0;
		}
	}

	.explainer__label::after {
		content: attr(data-explanation);
		max-width: 0;

		.explainer__checkbox:checked ~ & {
			/* Show explanation placeholder text */
			max-width: 420px;
		}
	}

	.explainer__text,
	.explainer__explanation {
		/* Position the actual text absolute */
		left: 0;
		position: absolute;
		top: 50%;
		transform: translateY(-50%) translateZ(0);
		width: 100%;
	}

	.explainer__text::after,
	.explainer__explanation::after {
		/* To add a more typing look we're adding a cursor here */
		background-color: transparent;
		content: "";
		height: 100%;
		position: absolute;
		right: 0;
		top: 0;
		width: 2px;

		.explainer:focus &,
		.explainer:hover &,
		.explainer__checkbox:focus ~ .explainer__label & {
			animation: BLINKING 320ms step-end infinite alternate;
		}
	}
}

@keyframes BLINKING {
	50% {
		background-color: currentColor;
	}
}
```

**CSS — `prefers-reduced-motion: no-preference`**
To respect users who do prefer reduced motion we're going to show and hide the text and explanation without any transition.
Only if the preferences are set to `no-preference` we're going to add some animations and transitions.

**CSS — Pseudo Elements with data attributes**
To make the animation snappy and smooth we're going to add pseudo elements with the text and explanation. This so we can toggle the width and be able to smoothly animate the width of the text and explanation spans from 0 to 100%.

**CSS — :hover, :focus & `:checked` animations**
As a hover state we change the border-style from `dashed` to `solid`. Bonus: we've added a blinking animation to give it a more typewriter look and feel.
When the keyboard users are focussed on the checkbox, the blinking. When they hit space, the checkbox sets itself to checked and the animation initiates.
When the checkbox is `:checked` we will change the width of the text and explanation span.

**CSS — pitfalls**
The element needs `white-space: nowrap` and a max-width. This means the element can only be used for single line short sentences.

## Codepen Example
{% codepen https://codepen.io/thomas-verleye/pen/pobbEyZ %}

## The animation
![the animation](https://dev-to-uploads.s3.amazonaws.com/i/s3wqbkhuxfycnz4fkmqt.gif)

##### Sidenote
I also want to point out this animation could've been more accurate when using a mono typeface and use ch units like [Lea Verou did come up with](https://lea.verou.me/2012/02/simpler-css-typing-animation-with-the-ch-unit/).