Motion Design for web
Hero video
The main gist is as follows:
- Create a hero container that takes up a certain height of the viewport
- Create a video container that is absolutely positioned and takes up the entire container size. Have a dark translucent overlay over the video, and make the video autoplay, loop, and muted.
<header>
<!-- centered on container -->
<div class="header-content">
<h1>Creating solutions</h1>
<p>like there's no tomorrow</p>
</div>
<!-- takes up whole container -->
<div class="video-container">
<video autoplay muted loop>
<source src="skateboard.webm" type="video/webm" />
</video>
</div>
</header>
Here is the CSS:
header {
/* set height, center content */
position: relative;
display: flex;
justify-content: center;
align-items: center;
height: 60vh;
.header-content {
/* styles for content here */
}
}
.video-container {
/* absolute position, take up full container */
position: absolute;
inset: 0;
z-index: -2;
/* take up full container */
video {
width: 100%;
height: 100%;
object-fit: cover;
}
/* create dark overlay over video container */
&::before {
content: "";
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.7);
z-index: 1;
}
}
Page loader
The page loader logic is as follows:
- Create a loader element that takes up the whole page, hides all other content with
overflow: hidden
on body. - Listen for the "load" event on the window to wait for all assets to be loaded, and once that's done, remove loader element and make
overflow: visible
again.
<body>
<div class="loader-container">
<div class="loader"></div>
</div>
<!-- rest of page ... -->
</body>
Here are the styles for the loader:
.loader-container {
/* take up whole screen */
position: fixed;
inset: 0;
background-color: #222;
display: grid;
place-content: center;
opacity: 1;
transition: opacity 1s ease-in-out;
z-index: 10;
/* fade out styles */
&.hide-loader {
opacity: 0;
z-index: -999;
}
}
.loader {
/* variables to configure */
--size: 4rem;
--color: orange;
--time: 1.5s;
/* creates spinner */
height: var(--size);
width: var(--size);
border-radius: 9999px;
border: rgba(255, 255, 255, 0.3) solid 0.25rem;
position: relative;
z-index: 1;
animation: loader var(--time) infinite linear;
&::before {
content: "";
position: absolute;
inset: 0;
border-radius: 9999px;
border-left: var(--color) solid 0.25rem;
animation: loader var(--time) infinite linear;
z-index: 2;
}
}
@keyframes loader {
0% {
transform: rotate(0deg) scale(1);
}
50% {
transform: rotate(180deg) scale(1.2);
}
to {
transform: rotate(360deg) scale(1);
}
}
Then to make sure that the body has overflow set to hidden when the page is loading, we use these styles, where we reset overflow to visible when the .hide-loader
class is applied to the loader container:
body:has(:not(.loader-container.hide-loader)) {
overflow: hidden;
height: 100vh;
}
body:has(.loader-container.hide-loader) {
overflow: visible;
height: fit-content;
}
For the typescript, we just listen for the "load"
element on the window, and when that's triggered, we add the .hide-loader
class to the loader container and shortly afterwards remove the loader container from the DOM.
class PageLoader {
static loadPage(loaderElement: Element) {
if (!loaderElement) {
throw new Error("Loader element not found");
}
window.addEventListener("load", () => {
loaderElement.classList.add("hide-loader");
setTimeout(() => {
loaderElement.remove();
}, 2000);
});
}
}
const loader = document.querySelector(".loader-container")!;
PageLoader.loadPage(loader);
Loading button animation
Check out button example for a working example.
For this animation, we create a loading animation where three dots bounce up and down.
Here are the steps:
- When the button is clicked, wait for some asynchronous operation to finish. Before that, add the
.loading
class to the button and append a loading container with three dot elements inside of it. That loading container will be overlayed on top of the button to hide the text content. - When the async operation is finished, remove the loading dots from the DOM and remove the loading class.
.btn {
--bg-color: #000000;
--text-color: #ffffff;
background-color: var(--bg-color);
border-radius: 9999px;
width: 15rem;
padding: 0.5rem;
text-align: center;
font-weight: 600;
color: var(--text-color);
text-transform: capitalize;
font-size: 1rem;
overflow: hidden;
cursor: pointer;
}
.btn.loading {
/* prevent overflow */
overflow: hidden;
position: relative;
cursor: wait;
/* */
.loader-container {
display: flex;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 10;
background-color: var(--bg-color);
gap: 0.5rem;
justify-content: center;
align-items: center;
.dot {
width: 0.5rem;
height: 0.5rem;
border-radius: 9999px;
background-color: var(--text-color);
animation: loading 1s infinite ease-in-out;
animation-delay: calc(var(--num) * 0.1s);
}
}
}
@keyframes loading {
0% {
transform: scale(1);
}
33% {
transform: scale(1.5);
}
66% {
transform: scale(0.5);
}
100% {
transform: scale(1);
}
}
And this is the script. Here's the basic flow:
- Button gets clicked
- Add loading class, append loading container with dots inside of button.
- After async operation completes, remove classes and loading container.
class LoadingButton {
constructor(private button: HTMLButtonElement) {}
onClick(cb: () => void | Promise<void>) {
const button = this.button;
button.addEventListener("click", async () => {
// add css classes and add dots
button.classList.add("loading");
const loaderContainer = document.createElement("div");
loaderContainer.classList.add("loader-container");
loaderContainer.innerHTML = `
<div class="dot" style="--num:1;"></div>
<div class="dot" style="--num:2;"></div>
<div class="dot" style="--num:3;"></div>
`;
button.appendChild(loaderContainer);
// perform callback
await cb();
// remove loading styles
button.classList.remove("loading");
loaderContainer.remove();
});
}
}
const button = new LoadingButton(document.querySelector("button")!);
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
// simulate network request for 5 seconds
button.onClick(async () => {
await sleep(5000);
});
Hamburger button
<button class="menu-button">
<span></span>
<span></span>
<span></span>
</button>
.menu-button {
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: stretch;
width: 1.25rem;
height: 1rem;
border: none;
background: none;
cursor: pointer;
span {
width: 100%;
display: block;
height: 2px;
background-color: #222;
border-radius: 0.25rem;
transition: transform 0.3s ease;
&:nth-child(2) {
transform-origin: center center;
}
}
}
.menu-button.menu-button-open {
span:nth-child(1) {
transform: translat <button class="menu-button">
<span></span>
<span></span>
<span></span>
</button>
transform: scaleX(0);
}
span:nth-child(3) {
transform: translate3d(0, -8px, 0) rotate(-45deg);
}
}
Creating a debugger element for mousemouse Events
First, create the debugger class that instantiates a .debugger
element in the DOM.
class Debugger {
private static _element: HTMLElement;
private static get element() {
if (!Debugger._element) {
return Debugger.create();
}
return Debugger._element;
}
private static create() {
try {
const debuggerElement = selectWithThrow(".debugger") as HTMLElement;
return debuggerElement;
} catch (error) {
console.log("creating new debugger element");
const debuggerElement = document.createElement("div");
debuggerElement.classList.add("debugger");
document.body.appendChild(debuggerElement);
return debuggerElement;
}
}
static hideDebugger() {
Debugger.element.style.display = "none";
}
static showDebugger() {
Debugger.element.style.display = "block";
}
static displayInfo(info: Record<string, any>) {
Debugger.element.innerHTML = "";
const list = document.createElement("ul");
for (const key in info) {
const listItem = document.createElement("li");
listItem.textContent = `${key}: ${info[key]}`;
list.appendChild(listItem);
}
Debugger.element.appendChild(list);
}
}
Then here is the CSS:
.debugger {
position: fixed;
left: 0;
bottom: 0;
background-color: rgba(34, 34, 34, 0.725);
box-shadow: 0 5px 10px rgba(0, 0, 0, 0.1);
padding: 1rem;
min-width: 15rem;
max-width: 20rem;
min-height: 10rem;
color: white;
}
And here is an example of how to use it:
Debugger.showDebugger();
const magneto = selectWithThrow(".magneto") as HTMLButtonElement;
magneto.addEventListener("mousemove", (e: MouseEvent) => {
let { bottom, height, left, right, top, width, x, y } =
magneto.getBoundingClientRect();
const buttonStrength = 40;
const textStrength = 80;
Debugger.displayInfo({
cursorX: e.clientX,
cursorY: e.clientY,
bottom,
height,
left,
right,
top,
width,
leftCornerOfElement: `(${x}, ${y})`,
});
});