DEV Community

Art
Art

Posted on • Originally published at blog.dailysandbox.pro on

Adding a "Shaking" Effect to Login Popovers for a Visual Error Cue

Adding a

Sometimes, subtle design elements can make a huge difference in user experience. Instead of displaying traditional error messages, a "shaking" effect on a login popover provides a clear and immediate indication that something went wrong. This tutorial will guide you through implementing this functionality using vanilla JavaScript , CSS animations , and the open-source library Tippy.js.

Our objective is to:

  1. Create a login popover with Tippy.js.
  2. Add the "shaking" effect when errors occur.
  3. Automatically reset the animation once the shake ends.

Let’s dive in!


Part 1: Setting Up the Shaking Animation with CSS

We’ll start by defining a reusable CSS animation for the shake effect. The @keyframes rule below mimics a side-to-side shake:

@keyframes shaking {
    10%, 90% { transform: translate3d(-1px, 0, 0); }
    20%, 80% { transform: translate3d(2px, 0, 0); }
    30%, 50%, 70% { transform: translate3d(-4px, 0, 0); }
    40%, 60% { transform: translate3d(4px, 0, 0); }
}
.shake {
    animation: shaking 0.82s cubic-bezier(.36,.07,.19,.97) both;
}

Enter fullscreen mode Exit fullscreen mode
  • Keyframe Details : The translate3d property moves the popover slightly to the left and right to create the shaking effect.
  • Reusable Class : Applying the shake class to any element triggers the animation.

Part 2: Creating the Login Popover with Tippy.js

We’ll use Tippy.js to create a clickable login popover. The popover will include:

  • An email input field.
  • A "Sign In" button.
  • Event listeners to handle sign-in logic and the shake effect.

Here’s the core JavaScript for setting up the popover:

Step 1: The Sign-In Class

APP.Signin = class {
    constructor($target) {
        this.values = {}; // Store any required state
        if ($target) this.$target = $target; // The DOM element triggering the popover
        this.init(); // Initialize the popover
        return this;
    }

    // Trigger the shake animation
    shake() {
        this.$tippy.classList.add('shake');
        return this;
    }

    // Handle Sign-In button clicks
    onSigninClicked(event) {
        event.preventDefault();

        // Retrieve the entered email
        let email = document.querySelector('.app-signin-email').value;

        // Error and success handlers
        let _onError = () => this.shake();
        let _onSuccess = (response) => {
            if (response.errors.length) {
                this.shake(); // Shake on error
            } else {
                // Handle successful login
                console.log('Login successful!');
            }
        };

        // Simulate a backend login request
        let form_data = new FormData();
        form_data.append('method', 'quickSignIn');
        form_data.append('email', email);

        axios({
            method: 'POST',
            url: 'path/to/server',
            data: form_data,
            headers: { 'content-type': 'application/x-www-form-urlencoded;charset=UTF-8' },
        })
            .then((response) => (typeof response.data === 'string' ? JSON.parse(response.data) : response.data))
            .then(_onSuccess)
            .catch(_onError);
    }

    // Define the HTML content of the popover
    getContent() {
        return `
            <div class="app-signin">
                <label class="form-label app-signin-form-elm app-signin-label">Account Email</label>
                <input type="text" class="app-signin-form-elm app-signin-input app-signin-email" value="" placeholder="e.g., jdoe@email.com">
                <a href="#" class="app-signin-form-elm app-signin-btn">Sign In</a>                                      
            </div>
        `;
    }

    // Initialize the popover
    init() {
        let $content = this.getContent();

        if (this.$target) {
            tippy(this.$target, {
                content: $content,
                allowHTML: true,
                trigger: 'click',
                hideOnClick: true,
                interactive: true,
                arrow: true,
                animation: 'scale',
                placement: 'bottom',
                theme: 'white',
                offset: [0, 15],
                onMount: () => {
                    this.$tippy = document.querySelector('.tippy-box'); // The popover box
                    this.$content = document.querySelector('.app-signin');
                    this.$signin_btn = document.querySelector('.app-signin-btn');
                    this.$email = document.querySelector('.app-signin-email');

                    // Add event listeners
                    this.$signin_btn.addEventListener('click', this.onSigninClicked.bind(this));
                    this.$email.addEventListener('keypress', (event) => {
                        if (event.key === 'Enter') {
                            event.preventDefault();
                            this.$signin_btn.click();
                        }
                    });

                    // Remove the shake class after animation ends
                    this.$tippy.addEventListener('animationend', () => {
                        this.$tippy.classList.remove('shake');
                    });

                    this.$email.focus(); // Auto-focus on the email input
                },
            });
        }
    }
};

// Initialize the Sign-In popover on page load
document.addEventListener('DOMContentLoaded', () => {
    new APP.Signin(document.querySelector('.app-header-control-signin'));
});

Enter fullscreen mode Exit fullscreen mode

2228+ FREE RESOURCES FOR DEVELOPERS!! ❤️ 😍🥳 (updated daily)

1400+ Free HTML Templates

359+ Free News Articles

69+ Free AI Prompts

323+ Free Code Libraries

52+ Free Code Snippets & Boilerplates for Node, Nuxt, Vue, and more!

25+ Free Open Source Icon Libraries

Visit dailysandbox.pro for free access to a treasure trove of resources!


Part 3: Wiring It All Together

Simulated Backend : If you don’t have a real server, mock responses with a promise:

const mockServer = (email) =>
    new Promise((resolve, reject) => {
        setTimeout(() => {
            if (email === 'test@email.com') resolve({ errors: [] });
            else reject({ errors: ['Invalid email'] });
        }, 500);
    });

Enter fullscreen mode Exit fullscreen mode

CSS for the Popover : Ensure the popover matches your design. Here’s a simple setup:

.app-signin {
    display: flex;
    flex-direction: column;
    gap: 10px;
}

.app-signin-email {
    width: 100%;
    padding: 10px;
    font-size: 1rem;
    border: 1px solid #ccc;
    border-radius: 4px;
}

.app-signin-btn {
    background-color: #007bff;
    color: white;
    text-align: center;
    padding: 10px;
    border-radius: 4px;
    cursor: pointer;
}

.app-signin-btn:hover {
    background-color: #0056b3;
}

Enter fullscreen mode Exit fullscreen mode

HTML Trigger Element : Add a trigger element to your HTML:

<button class="app-header-control-signin">Sign In</button>

Enter fullscreen mode Exit fullscreen mode

Part 4: Test the Shaking Effect

  1. Click the Sign In button to open the popover.
  2. Enter an invalid email and submit. Watch the popover shake, indicating an error.
  3. Enter a valid email and see no shaking—success!

Conclusion: A User-Friendly Error Cue

The shaking effect enhances user experience by offering a clear, intuitive error indicator without cluttering the UI with extra messages. Combined with Tippy.js for a sleek popover and vanilla JS for interactivity, this setup is clean, functional, and visually appealing.

Keep experimenting and tweaking—because great UX is all about the details!

For more tips on web development, check out DailySandbox and sign up for our free newsletter to stay ahead of the curve!

Top comments (1)

Collapse
 
devluc profile image
Devluc

Great tutorial to improve forms UX