Inspiration

Every student has a to-do list full of tasks that died weeks ago. Mine had "do laundry" on it for three weeks straight. Normal to-do apps just let tasks sit there quietly, and it's way too easy to ignore a checkbox. So I thought: what if ignored tasks didn't stay quiet? What if they came back to haunt you? It's spooky season, so it felt right.

What it does

Procrastinghost is a to-do list where every task has a timer. If the timer runs out before you finish, the task dies and comes back as a ghost that floats around your screen and nags you in speech bubbles.

The longer you ignore it, the angrier it gets:

Level Name Behavior
1 Mildly spooky Small ghost, polite reminders
2 Per my last haunting Bigger, talks like a work email
3 Not mad, just disappointed Angry eyebrows, guilt trips
4 Full poltergeist Huge, red, shaking, yells in all caps, tints the whole screen

When you click a ghost you get three choices:

  • Exorcise it by actually doing the task. The ghost bursts into sparkles and rests in the Graveyard.
  • Make an excuse. The ghost rebuts it ("Mercury is fine. It grows.") and levels up.
  • Snooze. It leaves for a while but comes back one level angrier.

The only way to win is to do your tasks. That's the joke, and also the point.

How I built it

Plain HTML, CSS and vanilla JavaScript, no frameworks.

  • The ghosts are SVGs. Their eyes and mouth change shape at each level, going from round eyes to angry eyebrows to glowing red pupils.
  • Movement uses requestAnimationFrame. Each ghost has a position and direction and bounces off the screen edges, and higher-level ghosts move faster.
  • Tasks are saved in localStorage, so your ghosts are still waiting when you come back.

The haunt level is calculated from how overdue a task is, plus a penalty for every excuse or snooze:

$$ \text{level} = \min\left(4,\ 1 + \left\lfloor \frac{t_{\text{now}} - t_{\text{haunt}}}{\Delta t} \right\rfloor + E\right) $$

where \( \Delta t \) is the level-up interval and \( E \) is the number of excuses you've made.

Challenges I ran into

  • Clicks breaking: the task list re-renders every second to update timers, which kept destroying my button event listeners. I fixed it with event delegation, using one listener on the document that checks data- attributes.
  • Ghosts teleporting: if you switched tabs and came back, the animation frame gap was huge and ghosts jumped across the screen. Capping the frame time fixed it.
  • Clicking through ghosts: the ghost layer covers the whole screen, so at first nothing underneath was clickable. Setting pointer-events: none on the layer and auto on just the ghosts solved it.
  • Writing the messages: making the ghosts sound passive-aggressive without being actually mean took more rewrites than the code did.

What I learned

  • How to build a simple physics/animation loop from scratch
  • Drawing and changing SVGs dynamically with JavaScript
  • Event delegation, and why it matters when the DOM keeps changing
  • That comedy is surprisingly hard to get right

What's next

  • Spooky sound effects (chains rattling at level 4)
  • Ghosts that physically block your other buttons
  • Browser notifications when a new ghost spawns
  • A leaderboard for most exorcisms

Built With

Share this project:

Updates

Submission history