Docs

Loki Text Effect

Loki Text Effect

The iconic text animation from the Loki series intro.

Loki

Installation

Download all the fonts and add them to your project. Download Link

└──app
    └──fonts
        ├──fontOne.woff2
        ├──fontTwo.woff2
        ├──fontThree.woff2
        ├──fontFour.woff2
        ├──fontFive.woff2
        ├──fontSix.woff2
        └──fontSeven.woff2

Copy and paste the following code into your project.

components/atlas_ui/loki-text-effect.tsx
"use client";
 
import { useEffect, useState } from "react";
import { motion } from "motion/react";
 
import {
  fontFive,
  fontFour,
  fontOne,
  fontSeven,
  fontSix,
  fontThree,
  fontTwo,
} from "@/fonts/font";
 
const fonts = [
  fontOne.className,
  fontTwo.className,
  fontThree.className,
  fontFour.className,
  fontFive.className,
  fontSix.className,
  fontSeven.className,
];
 
interface LokiEffectProps {
  text: string;
  velocityFont?: number; // In ms
  velocityMove?: number; // In ms
  className?: string;
}
 
const getRandomFont = (current: string) => {
  let next = current;
  while (next === current) {
    next = fonts[Math.floor(Math.random() * fonts.length)];
  }
  return next;
};
 
const getRandomOffset = () => ({
  x: (Math.random() - 0.5) * 2,
  y: (Math.random() - 0.5) * 2,
});
 
const AnimatedLetter = ({
  char,
  baseFont,
  velocityFont,
  velocityMove,
}: {
  char: string;
  baseFont: string;
  velocityFont: number;
  velocityMove: number;
}) => {
  const [fontClass, setFontClass] = useState(baseFont);
  const [pos, setPos] = useState({ x: 0, y: 0 });
 
  useEffect(() => {
    const fontTimer = setInterval(() => {
      setFontClass((prev) => getRandomFont(prev));
    }, Math.max(100, velocityFont));
 
    const moveTimer = setInterval(() => {
      setPos(getRandomOffset());
    }, Math.max(100, velocityMove));
 
    return () => {
      clearInterval(fontTimer);
      clearInterval(moveTimer);
    };
  }, [velocityFont, velocityMove]);
 
  return (
    <motion.span
      className={`${fontClass} inline-block text-6xl md:text-9xl drop-shadow-[0_0_8px_black] dark:drop-shadow-[0_0_8px_white]`}
      style={{
        fontWeight: 700,
        letterSpacing: "0.15em",
        textShadow: "0 0 2px #ffffff66, 0 0 5px #ffffff66",
      }}
      animate={{ x: pos.x, y: pos.y }}
      transition={{
        x: { duration: 0.5, ease: "easeInOut" },
        y: { duration: 0.5, ease: "easeInOut" },
      }}
    >
      {char}
    </motion.span>
  );
};
 
export const LokiTextEffect = ({
  text,
  velocityFont = 800,
  velocityMove = 1800,
  className = "",
}: LokiEffectProps) => {
  return (
    <div className={`flex flex-wrap justify-center gap-[1px] ${className}`}>
      {text.split("").map((char, i) => (
        <AnimatedLetter
          key={i}
          char={char}
          baseFont={fonts[i % fonts.length]}
          velocityFont={velocityFont}
          velocityMove={velocityMove}
        />
      ))}
    </div>
  );
};

Add @/fonts/font.ts.

import localFont from "next/font/local";
 
const fontOne = localFont({
  src: "../app/fonts/fontOne.woff2",
});
const fontTwo = localFont({
  src: "../app/fonts/fontTwo.woff2",
});
const fontThree = localFont({
  src: "../app/fonts/fontThree.woff2",
});
const fontFour = localFont({
  src: "../app/fonts/fontFour.woff2",
});
const fontFive = localFont({
  src: "../app/fonts/fontFive.woff2",
});
const fontSix = localFont({
  src: "../app/fonts/fontSix.woff2",
});
const fontSeven = localFont({
  src: "../app/fonts/fontSeven.woff2",
});
 
export { fontOne, fontTwo, fontThree, fontFour, fontFive, fontSix, fontSeven };

Update the import paths to match your project setup.

Usage

import { LokiTextEffect } from "@/components/atlas_ui/(react)/loki-text-effect";
<LokiTextEffect text="Loki" />

Props / Data Attributes

PropTypeDefaultRequiredDescription
textstringtrueThe text to display and animate
velocityFontnumber800falseInterval in ms for how often the font changes
velocityMovenumber1800falseInterval in ms for how often the position jitter is updated
classNamestringfalseOptional classnames for according to match your setup