Ever wondered how those app icons on your iPhone or MacBook wiggle so charmingly? Or what makes that playful animation pop up when you type a wrong word in Wordle? Well, it's very simple and it's all about a minimal animation. This article shows how to create a simple animation using plain CSS and then proceeds with Angular animations.
Before we start, you might wonder why would you need such a functionality? Right of the top of my head, you can include those into the validation process of some fields, or you might use them as a mean to gather users' attention.
Simple CSS Solution
Let's start by creating a new angular component using ng g c wiggle-css
. In the template file we will add a simple
label and a button that will be further animated. The button will have two classes - button for general styling and
wiggle that will be responsible for the animation.
<!-- wiggle-css.component.html -->
<div>Pure CSS</div>
<button class="button wiggle">Hey!</button>
With the template in place, let's move on to the styling and update the component css file like this:
/* wiggle-css.component.scss */
:host {
width: 50vw;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
gap: 1rem;
}
First of all, let's add some styles to the component host. The combination of classes used above will make the content of the component go 50% width, all items will be centered and will be displayed vertically with some space between them.
Moving on to the button - let's apply some general styles to it, so it will look prettier than the default html version. The button class will look like this:
/* wiggle-css.component.scss */
.button {
color: rgb(255, 255, 255);
background-color: rgb(79 70 229);
border: none;
border-radius: 0.375rem;
font-weight: 600;
font-size: 0.875rem;
line-height: 1.25rem;
padding: 0.5rem 0.75rem;
text-decoration: none;
cursor: pointer;
}
Finally, the most important piece is the wiggle class. We will make use of css animations and will create a new animation with name wiggle, which will last for 1 second and will have an ease-in-out timing. For the defined animation we need to attach the keyframes and apply the necessary transforms so that the elements will "shake". The desired effect will be achieved by setting the rotation to 0 degrees at start (0%) and end (100%) of the animation. The in-between states will have either 10 degree (17%, 50%, 84%) or -10 degree (35%, 67%) rotation.
/* wiggle-css.component.scss */
.wiggle {
animation: wiggle;
animation-duration: 1s;
animation-timing-function: ease-in-out;
}
@keyframes wiggle {
0%, 100% {
transform: rotate(0deg);
}
17%, 50%, 84% {
transform: rotate(10deg);
}
35%, 67% {
transform: rotate(-10deg);
}
}
This is it, a simple wiggle animation on our button using just an animation class with some keyframes attached to it.
The simple solution above is fine, but it can be improved. First of all, it runs only once and second, it runs immediately. What we want to achieve is to have the animation run multiple times and have a pause between each run. For that we can use another animation property and that is animation-iteration-count alongside with updated duration to 4 seconds and modified keyframe timestamps. So, we will set the iteration count to infinite and make all the rotations between the 80% keyframe and 100% keyframe. The updated css should look like this:
/* wiggle-css.component.scss */
.wiggle {
animation: wiggle;
animation-duration: 4s;
animation-iteration-count: infinite;
animation-timing-function: ease-in-out;
}
@keyframes wiggle {
80%, 100% {
transform: rotate(0deg);
}
83%, 89%, 97% {
transform: rotate(10deg);
}
86%, 93% {
transform: rotate(-10deg);
}
}
Triggering the animation on event
Moving on, you probably would like to have that animation only on some custom event triggered by the user, for example you want to animate some inputs after the user clicks a submit button and those fields fail the validation.
For the demo purpose, let's create a new component using ng g c wiggle-css-click
command. In the new component we will keep
the structure of the html file, but we need to handle the custom (click in our case) event and trigger the animation. So we
will attach the onWiggleClick()
method and will create a template reference for the element to be animated #wiggleButton
.
<!-- wiggle-css-click.component.html -->
<div>On Click</div>
<button
class="button"
(click)="onWiggleClick()"
#wiggleButton
>
Click me!
</button>
The css file will have the same structure as the initial option (with 1 second duration and running only once). In the typescript
component file we need to implement the logic for click handler method. What we need to do in order to trigger the animation
each time is to remove it's class from the element and add it back. However, since this happens too fast, we need to adjust it
with a little trick, which is not the best one since you rely on a side effect. That side effect is to call the offset width on
the element itself between removing and adding the animation class. In order to reference the element we will use the ViewChild
decorator and manage the classes on its' native element property. The method will look like this:
// wiggle-css-click.component.ts
export class WiggleCssClickComponent {
@ViewChild("wiggleButton") wiggleButton! : ElementRef<HTMLButtonElement>;
onWiggleClick() {
this.wiggleButton.nativeElement.classList.remove("wiggle");
this.wiggleButton.nativeElement.offsetWidth;
this.wiggleButton.nativeElement.classList.add("wiggle");
}
}
CSS and Angular Directives
In Angular world, we can use another option to trigger the behaviour of our element. In this case we are limited only to the
element itself. We can use a directive to attach the animation behaviour on button click. To generate a new directive we
should run the ng g d wiggle
command. Inside the directive we need to get the element ref and then listen for the click event
using the HostListener decorator. The method implementation remains the same as in the option above: we remove the animation
class, call the offsetWidth and then attach the class back. The directive class should look like this:
// wiggle.directive.ts
export class WiggleDirective {
private el = inject(ElementRef);
@HostListener('click') onClick() {
this.el.nativeElement.classList.remove("wiggle");
this.el.nativeElement.offsetWidth;
this.el.nativeElement.classList.add("wiggle");
}
}
And then, in a new component, we have to use the directive on our button:
<div>Directive</div>
<button class="button" appWiggle>Click me!</button>
Angular Animations - Two states
One great thing about angular is that it has its own animation package. We need to enable the animations first. In Angular 17
standalone mode we need to update the app.config.ts
file and add provideAnimations() (also import it at the top)
to the providers array.
import { provideAnimations } from '@angular/platform-browser/animations';
export const appConfig: ApplicationConfig = {
providers: [provideAnimations()],
};
That is it, now we can move on and configure our animations. We will create a new file and add a new property to the component decorator - animations.
@Component({
...,
animations: [],
})
Then we need to add a trigger that will be attached to an element in the template, we'll call it wiggle.
@Component({
...,
animations: [
trigger('wiggle', []),
],
})
The trigger should define a set of states and transitions. Initially we will create two states named start and end. These states will correspond to the 0% and 100% keyframes from the previous options, respectively. So, each of the states will have a style with rotate: 0deg.
// wiggle-ng-animations-1.component.ts
@Component({
...,
animations: [
trigger('wiggle', [
state(
'start',
style({
rotate: '0deg',
})
),
state(
'end',
style({
rotate: '0deg',
})
),
]),
],
})
Now that we have the states, we need to define an animation that will run whenever a state is changed between these two, thus the state change expression will look like this: 'start <=> end' (notice the <=> symbol). For the transition we also have to declare the animation, which will run for 1 second in our case and will have an array of three keyframes, which will alternate styles between -10 degree rotation and 10 degree rotation at offset 0.25, 0.5 and 0.75. These offsets are an equivalent to keyframe percents in css and correspond to the point in time from the whole animation duration when the corresponding styles are applied.
// wiggle-ng-animations-1.component.ts
@Component({
...,
animations: [
trigger('wiggle', [
// [states],
transition(
'start <=> end',
animate(
'1s',
keyframes([
style({ rotate: '-10deg', offset: 0.25 }),
style({ rotate: '10deg', offset: 0.5 }),
style({ rotate: '-10deg', offset: 0.75 }),
])
)
),
]),
],
})
Finally, we need to control the states (start and end) in our component so that the animation take place. We will declare a new component property and assign it one the two states and then create a toggleWiggle() method to switch the state to the other one.
// wiggle-ng-animations-1.component.ts
@Component({
selector: 'app-wiggle-ng-animations-1',
standalone: true,
imports: [],
templateUrl: './wiggle-ng-animations-1.component.html',
styleUrl: './wiggle-ng-animations-1.component.scss',
animations: [
// all triggers go here
],
})
export class WiggleNgAnimations1Component {
wiggle = 'start';
toggleWiggle() {
this.wiggle = this.wiggle == 'start' ? 'end' : 'start';
}
}
Then in the template we need to attach the animation to the button using @ANIMATION_TRIGGER
syntax and also react to
click event and toggle the state on the animation:
<!-- wiggle-ng-animations-1.component.html -->
<div>Angular Animations Two States</div>
<button
class="button"
[@wiggle]="wiggle"
(click)="toggleWiggle()"
>Click me!</button>
Angular Animations - Single state
Instead of using two states that store the same styles we can use only one and name it default. Afterward, when declaring the transition, for the state change expression we will use the wildcard (void) state as the initial and the default as we final state of the transition. We will keep the animate method and its keyframes.
// wiggle-ng-animations-2.component.ts
@Component({
...,
animations: [
trigger('wiggle', [
state(
'default',
style({
rotate: '0deg',
})
),
transition(
'* => default',
animate(
'1s',
keyframes([
style({ rotate: '-10deg', offset: 0.25 }),
style({ rotate: '10deg', offset: 0.5 }),
style({ rotate: '-10deg', offset: 0.75 }),
])
)
),
]),
],
})
In this case let's implement the 'toggle' logic directly in the template and reset the wiggle property to default. Since the animation transition is only one way, from void to default, we need to make sure that after the animation ends we reset the state back to void. This is why we need to listen for @wiggle.done and attach a method that will be triggered when the event happens.
<!-- wiggle-ng-animations-2.component.html -->
<div>Angular Animations Single State</div>
<button
class="button"
[@wiggle]="wiggle"
(@wiggle.done)="onAnimationDone($event)"
(click)="wiggle = 'default'"
>Click me!</button>
The methods itself will check the 'toState' property on the AnimationEvent and will reset it to an empty string if the state is default - and this happens when the button is clicked.
// wiggle-ng-animations-2.component.ts
export class WiggleNgAnimations2Component {
wiggle = '';
onAnimationDone(event: AnimationEvent): void {
if (event.toState === 'default') {
this.wiggle = ''; // Reset the state to trigger the animation again
}
}
}
That's pretty much it, we have a wiggle animation implemented in two ways (using plain CSS and Angular Animations).
Source Code
Wrapping everything together - you can check the source code on Gitlab or GitHub. For a live demo you can check the Stackblitz project below.