Because subtle notifications are for quitters
The Problem: Downloads That Disappear Into the Void
Picture this: Youâre building a web app with non-blocking downloads (fancy!), and users click that shiny download button expecting⌠well, something. Instead, they get the digital equivalent of shouting into the void. The download starts silently in the background while your users sit there wondering if they accidentally clicked on a dead pixel.
The solution? Make that download icon literally fly across the screen like a caffeinated hummingbird on a mission. Because if you canât be subtle, be spectacular.
UI
The Magic: A Flying Download Icon That Actually Works
This Angular 20 component creates a delightful animation where clicking the download button launches a mini download icon that gracefully soars through the digital stratosphere to land in your navigation bar. Itâs like watching a paper airplane, but with more TypeScript and fewer disappointed coworkers.
The Component Structure
Letâs start with the foundation â our standalone Angular component thatâs cleaner than your code after a three-day refactoring binge:
import { Component } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
@Component({
selector: 'app-root',
standalone: true,
template: `
<!-- Add FontAwesome CSS -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<nav style="height:60px; background:#f5f5f5; display:flex; align-items:center; justify-content:flex-end; padding:0 40px;">
<i id="navbar-download" class="fa fa-download" style="font-size:2rem; color:#0f204b; cursor:pointer;"></i>
</nav>
<main style="padding:40px;">
<button id="download-btn" style="font-size:1.2rem; padding:10px 24px; cursor:pointer; background:#007bff; color:white; border:none; border-radius:4px;">
<i class="fa fa-download" style="margin-right:8px;"></i>Download
</button>
</main>
`,
styles: [
`
.fly-download-anim {
position: fixed;
z-index: 9999;
pointer-events: none;
opacity: 1;
box-shadow: 0 4px 16px 0 rgba(0,0,0,0.18);
border-radius: 50%;
background: #e0e0e0;
transition: transform 0.9s cubic-bezier(0.4,0,0.2,1), opacity 0.9s cubic-bezier(0.4,0,0.2,1);
}
`,
],
})
Whatâs happening here?
- Weâve got FontAwesome doing the heavy lifting for our icons (because drawing icons is for masochists)
- A nav bar with our target download icon sitting pretty in the top-right corner
- A download button thatâs about to become the launch pad for our flying icon adventure
- CSS that makes our flying icon look like it escaped from a modern design system
The Event Listener: Where the Magic Begins
ngAfterViewInit() {
document.getElementById('download-btn')!.addEventListener('click', () => {
this.animateFlyToDownload(document.getElementById('download-btn')!);
});
}
This is where we tell Angular âHey, when someone clicks that download button, donât just sit there looking pretty â make something fly!â The ngAfterViewInit
lifecycle hook ensures the DOM is ready for our shenanigans.
The Animation Method: Where Physics Goes to Have Fun
Now for the pièce de rĂŠsistance â the method that makes icons defy gravity:
private animateFlyToDownload(sourceEl: HTMLElement) {
const targetIcon = document.getElementById('navbar-download') as HTMLElement;
let targetRect: DOMRect | null = null;
let iconFontSize = 15;
let iconColor = '#0f204b';
if (targetIcon) {
targetRect = targetIcon.getBoundingClientRect();
const computed = window.getComputedStyle(targetIcon);
iconFontSize = parseInt(computed.fontSize, 10) || 15;
iconColor = computed.color || '#0f204b';
} else {
// Fallback positioning if target not found
targetRect = {
left: window.innerWidth - 120,
top: 20,
width: 24,
height: 24,
right: window.innerWidth - 96,
bottom: 44,
x: window.innerWidth - 120,
y: 20,
toJSON: () => {},
} as DOMRect;
}
The Detective Work: First, we play detective and figure out where everything is. We grab the target icon and interrogate it for its position, size, and color. If the target icon is hiding (or doesnât exist), we create a fake DOMRect
like a professional con artist – because the show must go on!
Creating the Flying Icon
const sourceRect = sourceEl.getBoundingClientRect();
const startX = sourceRect.left + sourceRect.width / 2 - iconFontSize / 2 + 20;
const startY = sourceRect.top + sourceRect.height / 2 - iconFontSize / 2;
const endX = targetRect.left + targetRect.width / 2 - iconFontSize / 2 + 25;
const endY = targetRect.top + targetRect.height / 2 - iconFontSize / 2;
const flyIcon = document.createElement('i');
flyIcon.className = 'fa fa-download fly-download-anim';
The Math Behind the Magic: We calculate the exact center points of both the source button and target icon. Itâs like planning a flight path, but instead of avoiding mountains, weâre avoiding other DOM elements. The slight offsets (+20 and +25) are because even flying icons need a little personal space.
Styling the Flying Icon
flyIcon.style.cssText = `
position: fixed;
left: ${startX}px;
top: ${startY}px;
font-size: ${iconFontSize}px;
color: ${iconColor};
z-index: 9999;
pointer-events: none;
opacity: 1;
box-shadow: 0 4px 16px 0 rgba(0,0,0,0.18);
border-radius: 50%;
background: #e0e0e0;
transform: scale(1) translate(0, 0);
transition: transform 0.9s cubic-bezier(0.4,0,0.2,1), opacity 0.9s cubic-bezier(0.4,0,0.2,1);
`;
document.body.appendChild(flyIcon);
We dress up our flying icon like itâs going to a job interview: perfect positioning, nice shadows, and a z-index
so high it could give you altitude sickness. The pointer-events: none
ensures it won’t interfere with other elements during its journey.
The Flight Sequence
setTimeout(() => {
const deltaX = endX - startX;
const deltaY = endY - startY;
flyIcon.style.transform = `translate(${deltaX}px, ${deltaY}px) scale(1.2)`;
flyIcon.style.opacity = '1';
}, 50);
After a brief pause (because even digital icons need a moment to stretch before takeoff), we calculate the flight path and launch our icon with a subtle scale-up effect. Itâs like giving it a tiny confidence boost before the big journey.
The Landing and Cleanup
const cleanup = () => {
setTimeout(() => {
flyIcon.style.transition =
'transform 0.35s cubic-bezier(0.4,2,0.2,1), opacity 0.35s';
flyIcon.style.transform = `translate(${endX - startX}px, ${endY - startY}px) scale(0.2)`;
flyIcon.style.opacity = '0';
setTimeout(() => {
if (flyIcon.parentNode) {
flyIcon.parentNode.removeChild(flyIcon);
}
}, 350);
}, 50);
};
flyIcon.addEventListener('transitionend', cleanup, { once: true });
setTimeout(cleanup, 1200);
The Grand Finale: Our cleanup function is like a responsible party host â it makes sure everything gets cleaned up afterward. The icon shrinks down and fades out at its destination, then politely removes itself from the DOM. We have both an event listener and a timeout because, like a good backup plan, redundancy is key.
Bootstrap the Application
bootstrapApplication(AppComponent);
And finally, we launch this masterpiece into the digital world!
Why This Actually Matters
This isnât just eye candy (though itâs definitely that). This animation solves a real UX problem by:
- Drawing attention to where download notifications will appear
- Providing immediate feedback that something actually happened
- Creating a delightful micro-interaction that makes users smile
- Establishing a visual connection between action and result
The Technical Brilliance
- Smooth animations using CSS transitions with cubic-bezier timing functions
- Fallback positioning for when things go wrong (because they always do)
- Dynamic styling that adapts to the target iconâs appearance
- Proper cleanup to avoid memory leaks and DOM pollution
- Pixel-perfect positioning using
getBoundingClientRect()
Complete code
import { Component } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
@Component({
selector: 'app-root',
standalone: true,
template: `
<!-- Add FontAwesome CSS -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<nav style="height:60px; background:#f5f5f5; display:flex; align-items:center; justify-content:flex-end; padding:0 40px;">
<i id="navbar-download" class="fa fa-download" style="font-size:2rem; color:#0f204b; cursor:pointer;"></i>
</nav>
<main style="padding:40px;">
<button id="download-btn" style="font-size:1.2rem; padding:10px 24px; cursor:pointer; background:#007bff; color:white; border:none; border-radius:4px;">
<i class="fa fa-download" style="margin-right:8px;"></i>Download
</button>
</main>
`,
styles: [
`
.fly-download-anim {
position: fixed;
z-index: 9999;
pointer-events: none;
opacity: 1;
box-shadow: 0 4px 16px 0 rgba(0,0,0,0.18);
border-radius: 50%;
background: #e0e0e0;
transition: transform 0.9s cubic-bezier(0.4,0,0.2,1), opacity 0.9s cubic-bezier(0.4,0,0.2,1);
}
`,
],
})
export class AppComponent {
ngAfterViewInit() {
document.getElementById('download-btn')!.addEventListener('click', () => {
this.animateFlyToDownload(document.getElementById('download-btn')!);
});
}
private animateFlyToDownload(sourceEl: HTMLElement) {
const targetIcon = document.getElementById('navbar-download') as HTMLElement;
let targetRect: DOMRect | null = null;
let iconFontSize = 15;
let iconColor = '#0f204b';
if (targetIcon) {
targetRect = targetIcon.getBoundingClientRect();
const computed = window.getComputedStyle(targetIcon);
iconFontSize = parseInt(computed.fontSize, 10) || 15;
iconColor = computed.color || '#0f204b';
} else {
// Fallback positioning if target not found
targetRect = {
left: window.innerWidth - 120,
top: 20,
width: 24,
height: 24,
right: window.innerWidth - 96,
bottom: 44,
x: window.innerWidth - 120,
y: 20,
toJSON: () => {},
} as DOMRect;
}
if (!targetRect) {
return;
}
const sourceRect = sourceEl.getBoundingClientRect();
const startX = sourceRect.left + sourceRect.width / 2 - iconFontSize / 2 + 20;
const startY = sourceRect.top + sourceRect.height / 2 - iconFontSize / 2;
const endX = targetRect.left + targetRect.width / 2 - iconFontSize / 2 + 25;
const endY = targetRect.top + targetRect.height / 2 - iconFontSize / 2;
const flyIcon = document.createElement('i');
flyIcon.className = 'fa fa-download fly-download-anim';
flyIcon.style.cssText = `
position: fixed;
left: ${startX}px;
top: ${startY}px;
font-size: ${iconFontSize}px;
color: ${iconColor};
z-index: 9999;
pointer-events: none;
opacity: 1;
box-shadow: 0 4px 16px 0 rgba(0,0,0,0.18);
border-radius: 50%;
background: #e0e0e0;
transform: scale(1) translate(0, 0);
transition: transform 0.9s cubic-bezier(0.4,0,0.2,1), opacity 0.9s cubic-bezier(0.4,0,0.2,1);
`;
document.body.appendChild(flyIcon);
setTimeout(() => {
const deltaX = endX - startX;
const deltaY = endY - startY;
flyIcon.style.transform = `translate(${deltaX}px, ${deltaY}px) scale(1.2)`;
flyIcon.style.opacity = '1';
}, 50);
const cleanup = () => {
setTimeout(() => {
flyIcon.style.transition =
'transform 0.35s cubic-bezier(0.4,2,0.2,1), opacity 0.35s';
flyIcon.style.transform = `translate(${endX - startX}px, ${endY - startY}px) scale(0.2)`;
flyIcon.style.opacity = '0';
setTimeout(() => {
if (flyIcon.parentNode) {
flyIcon.parentNode.removeChild(flyIcon);
}
}, 350);
}, 50);
};
flyIcon.addEventListener('transitionend', cleanup, { once: true });
setTimeout(cleanup, 1200);
}
}
bootstrapApplication(AppComponent);
Conclusion
Sometimes the best solutions are the ones that make users go âOoh, thatâs neat!â This flying download icon animation transforms a mundane download experience into something memorable. Itâs proof that with a little creativity and some well-crafted code, you can turn functional into delightful.
Now go forth and make your downloads fly! đ
P.S. â If your users start clicking the download button just to watch the animation, consider it a feature, not a bug.
Learn more đ Making Downloads Fly: The Angular Icon Animation That Actually Draws Attention