Skip to main content

Animating titles in browser tabs with JavaScript

6 min read
Animating titles in browser tabs with JavaScript

Few days ago I came across a tweet, which I've sadly haven't been able to find after, where someone showed animated titles in Chrome's tabs. It looked interesting, and most of all fun. So I decided to test what you could actually do with the tabs, and what limitations there are to changing the values.

The browser tab title (the text that appears in the tab if you have multiple tabs open) is just the title or your website. So calling it tab title is a bit misleading. It would show up as the title of the window if you only had one tab open in your browser. With JavaScript you can of course modify the string shown in it, using the document.title property.

How document.title works

The tab title comes from the <title> element in your HTML <head>. JavaScript exposes it as a read/write property:

document.title = 'Hello'  // tab now says "Hello"
console.log(document.title)  // "Hello"

The moment you see that you realize that "oh, it's just modifying the document", not much of hack. Using that and setInterval we can then turn it into an animated title, by replacing the value with a different one to make it animated. I've setup up three examples of how you could use this. If you have any other fun ideas, let me know!

1. Typewriter animation

A block character () sweeps left to right, covering the old text, then sweeps again to reveal the new text underneath.

const from = 'Animated browser title'
const to = 'Annoying gimmick?'
const BLOCK = '\u2588' // █

const maxLen = Math.max(from.length, to.length)
let index = 0
let phase = 'block' // 'block' → 'reveal'
let source = from
let target = to

setInterval(() => {
  if (phase === 'block') {
    document.title = BLOCK.repeat(index + 1) + source.slice(index + 1)
    index++
    if (index >= maxLen) { index = 0; phase = 'reveal' }
  } else {
    document.title = (
      target.slice(0, index + 1) + BLOCK.repeat(maxLen - index - 1)
    ).trimEnd()
    index++
    if (index >= maxLen) {
      document.title = target
      index = 0
      phase = 'block'
      ;[source, target] = [target, source]
    }
  }
}, 60)

The two phases create a satisfying wipe transition. The swap at the end makes it ping-pong forever.

2. Rotating icon

Unicode has several emoji sets where the same object is shown at different orientations. For example the moon and the clock icons both have multiple alternate presentations. The moon particularly makes for a fun one to animate.

// Moon phases: 🌑🌒🌓🌔🌕🌖🌗🌘
const frames = ['🌑', '🌒', '🌓', '🌔', '🌕', '🌖', '🌗', '🌘']
let i = 0

setInterval(() => {
  document.title = `${frames[i % frames.length]} Loading...`
  i++
}, 200)

Alternatively you can make a slightly more crude animation using the three different globe emojis:

// Earth rotation — 3 views
const earth = ['🌍', '🌎', '🌏']

The previously mentioned clock works as well, but the different clock times are quite difficult to tell apart.

// Clock faces — 12 positions
const clock = ['🕛','🕐','🕑','🕒','🕓','🕔','🕕','🕖','🕗','🕘','🕙','🕚']

The trick is finding emoji that represent the same object from different angles. The moon phases work especially well because there are 8 of them, giving a smooth cycle.

3. Reading progress

Show a percentage in the title that reflects how far down the page the reader has scrolled.

const title = 'My Article'

window.addEventListener('scroll', () => {
  const scrollable = document.documentElement.scrollHeight - window.innerHeight
  const percent = Math.round((window.scrollY / scrollable) * 100)
  document.title = `(${percent}%) ${title}`
})

This one is the most practical of the three. It gives readers a quick glance at their progress without any visual chrome on the page itself. Unlike the other animations, it's driven by scroll events rather than a timer — it only updates when the user actually scrolls.

As React hooks

Each technique wraps cleanly into a hook:

// Title transition
function useOverwriteTitle(from: string, to: string, speed = 60) {
  useEffect(() => {
    const originalTitle = document.title
    // ... interval logic from above
    return () => { clearInterval(interval); document.title = originalTitle }
  }, [from, to, speed])
}

// Rotating icon
function useSpinnerTitle(frames: string[], title: string, speed = 200) {
  useEffect(() => {
    const originalTitle = document.title
    let i = 0
    const interval = setInterval(() => {
      document.title = `${frames[i++ % frames.length]} ${title}`
    }, speed)
    return () => { clearInterval(interval); document.title = originalTitle }
  }, [frames, title, speed])
}

// Reading progress
function useProgressTitle(title: string) {
  useEffect(() => {
    const originalTitle = document.title
    const onScroll = () => {
      const scrollable =
        document.documentElement.scrollHeight - window.innerHeight
      const percent = Math.round((window.scrollY / scrollable) * 100)
      document.title = `(${percent}%) ${title}`
    }
    window.addEventListener('scroll', onScroll)
    return () => {
      window.removeEventListener('scroll', onScroll)
      document.title = originalTitle
    }
  }, [title])
}

The cleanup pattern is the same in each: save the original title, do your thing, restore it on unmount.

Things to keep in mind

Browser throttling. Background tabs get setInterval throttled to once per second in Chrome. Timer-based animations slow down when the tab isn't focused — which is fine, since users can't see it anyway. The scroll-based progress indicator isn't affected since background tabs don't receive scroll events.

Tab width. Browser tabs truncate long titles. Keep text short — around 20-30 characters. The emoji spinner is nice because the icon is always visible even in a narrow tab.

Accessibility. Screen readers may announce title changes. Avoid extremely fast intervals. Consider respecting prefers-reduced-motion:

const prefersReduced = window.matchMedia(
  '(prefers-reduced-motion: reduce)'
).matches

if (prefersReduced) return

SEO is unaffected. Search engines read the <title> from your HTML source, not from runtime JavaScript changes.

When to use these

The title transition works for cycling between a page title and a tagline, or dramatic notification indicators. The rotating icon is a good fit for loading states or background processing. The reading progress indicator is genuinely useful — it's one of the few tab title animations that adds real value.

All three techniques are the same fundamental idea: document.title is a writable string, and setInterval (or scroll events) lets you update it as often as you want.


More to read

How to invoice Apple for App Store proceeds

Apple hands you a CSV, not an invoice. Drop your App Store Connect financial report below and instantly generate the self-issued invoice and itemized report your accountant needs — one PDF, entirely in your browser.

How to check if an iOS app is installed in Expo and React Native

Use Linking.canOpenURL to detect if another app is installed on iOS in Expo. Covers Expo config plugin setup, URL scheme configuration, CNG workflow, and a hook to conditionally show a deep link button.