DEV Community

Cover image for CSS Only Typewriter Effect
Thomas Verleye
Thomas Verleye

Posted on

CSS Only Typewriter Effect

After reading Stokry's post on his typing animation with CSS I took the opportunity to use this effect and improve it.

The Idea

At Mr. Henry we're currently working on the full website of our client theAddresses. Our designers came up with the idea to show some Portuguese words in sentences, when you click it the explanation appears.

before

Before the user interacts with component

after

After the user interacts with component

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.

{% 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>
Enter fullscreen mode Exit fullscreen mode

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.
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:

{% 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>
Enter fullscreen mode Exit fullscreen mode

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

.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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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

The animation

the animation

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.

Discussion (0)