Why I Started Animating My Sleep
I track my sleep aggressively. Oura, Apple Health, occasional Whoop data exports when I borrow a strap from a friend. The problem is always the same. Nice charts in the app, useless CSV once you export.
I wanted something I could embed on my own site. A visual that actually feels alive. Not another static stacked bar chart screenshot.
So I built a small CSS animation system that turns sleep stages into a moving timeline. Light, deep, REM, awake. All mapped to colors, durations, and subtle motion. The goal was simple: you hit the page and your brain instantly knows whether the night was trash or decent.
The Data I Started With
I am not piping real-time Bluetooth data into the browser. This is not that kind of setup. I work with nightly exports.
For one night the data usually looks like this after I normalize it:
const sleepStages = [
{ stage: 'light', start: '23:10', end: '23:45' },
{ stage: 'deep', start: '23:45', end: '00:20' },
{ stage: 'rem', start: '00:20', end: '00:45' },
{ stage: 'light', start: '00:45', end: '01:30' },
// ...
];
I convert these into relative minutes from sleep start so the browser does not care about clock time. Just total duration in minutes, and the offset for each stage.
On the frontend I end up with something closer to:
const normalized = [
{ stage: 'light', startMin: 0, durationMin: 35 },
{ stage: 'deep', startMin: 35, durationMin: 35 },
{ stage: 'rem', startMin: 70, durationMin: 25 },
// ...
];
That is all I need for the animation. A start, a duration, and a label.
The Visual Metaphor: A Night As A Line
I tried fancy radial graphs and layered blobs. They looked cool for about 10 seconds, then my brain stopped caring.
The simplest metaphor actually worked best. A horizontal bar that represents one night. Split into segments. Each segment gets a color and a subtle animation that matches the stage.
- Light sleep: soft, slow pulse
- Deep sleep: very stable, barely moving
- REM: more active shimmer, hint of motion
- Awake: sharp, noticeable flicker
I want to be able to glance at a week of these bars and see patterns. Too much REM at the wrong time. Chopped deep sleep. Extended wake between 3 and 4 AM after a late-night coding session.
Building The Timeline Layout With Plain CSS
The layout is just flexbox. Nothing exotic. I like boring layout code for things that should not break.
<div class="sleep-night" data-total-min="430">
<div class="sleep-stage stage-light" style="--start:0; --duration:35"></div>
<div class="sleep-stage stage-deep" style="--start:35; --duration:35"></div>
<div class="sleep-stage stage-rem" style="--start:70; --duration:25"></div>
<!-- etc. -->
</div>
I use CSS custom properties for --start and --duration. That keeps JS stupid simple, and lets CSS handle positioning and animation timing without inline calc spam.
The container defines the total minutes as a property. That gives me a clean way to convert minutes to percentage width.
.sleep-night {
--total: 430; /* total sleep in minutes */
position: relative;
display: flex;
height: 18px;
border-radius: 999px;
overflow: hidden;
background: #050712;
}
.sleep-stage {
position: absolute;
top: 0;
bottom: 0;
left: calc(var(--start) / var(--total) * 100%);
width: calc(var(--duration) / var(--total) * 100%);
}
I prefer absolutely positioned segments over flex children for this. Flex introduces rounding issues across many segments. Absolute keeps control tight and visually clean.
Colors That Actually Read In The Dark
I usually look at this data late at night or early morning. Dark mode first. So color choices matter.
- Light sleep: muted blue
- Deep sleep: dark teal
- REM: violet accent
- Awake: sharp orange
.stage-light { background: #2f6fff; }
.stage-deep { background: #0b8b6d; }
.stage-rem { background: #9b5bff; }
.stage-awake { background: #ff8a3c; }
None of these are perfect from a scientific standpoint. I am not trying to publish a paper. I want a quick emotional read. Blue-ish calm, violet activity, orange interruption.
Matching Motion To Sleep Stages
The real fun starts when you give each stage its own animation language. This is where CSS actually becomes useful, not just decorative.
I kept one constraint. Animations should communicate state. Not distract from it.
Light Sleep: Gentle Breathing Pulse
Light sleep is where I spend most of the night. It should feel soft, stable, slightly alive.
@keyframes lightBreath {
0% { opacity: 0.85; transform: scaleY(1); }
50% { opacity: 1; transform: scaleY(1.04); }
100% { opacity: 0.85; transform: scaleY(1); }
}
.stage-light {
animation: lightBreath 6s ease-in-out infinite;
transform-origin: center;
}
The subtle vertical scale is barely visible, but your eyes register it as a calm breathing rhythm. If it distracts you, you tone it down to 1.02. Tiny tweaks matter with sleep visuals.
Deep Sleep: Almost No Motion
Deep sleep is the prize. When I hit decent deep sleep blocks, I want the bar to look like a solid block of calm.
I keep animation here minimal. Just enough shimmer to avoid dead pixels.
@keyframes deepStill {
0%, 100% { filter: brightness(0.9); }
50% { filter: brightness(1); }
}
.stage-deep {
animation: deepStill 10s ease-in-out infinite;
}
Ten second cycles feel right. Long, lazy, almost not there. If I speed it up, deep sleep starts to look nervous, which is the opposite of what I want.
REM: Lighter, More Active Motion
REM is where my dreams go weird, and my tracker usually shows more variability. So I let CSS be a bit more expressive here.
@keyframes remShimmer {
0% { opacity: 0.7; filter: blur(0); }
50% { opacity: 1; filter: blur(1px); }
100% { opacity: 0.7; filter: blur(0); }
}
.stage-rem {
animation: remShimmer 4s ease-in-out infinite;
}
A tiny blur pulse gives REM a dreamy quality without turning the whole bar into a neon mess. If your GPU is crying, you can drop the blur and just modulate opacity.
Awake: Make The Interruptions Loud
Awakenings are the parts I actually want to avoid. So they get the most obvious motion. Short, sharp flicker, not a gentle pulse.
@keyframes awakeFlicker {
0%, 100% { opacity: 0.4; }
20% { opacity: 1; }
40% { opacity: 0.4; }
60% { opacity: 1; }
80% { opacity: 0.4; }
}
.stage-awake {
animation: awakeFlicker 1.5s steps(4, end) infinite;
}
The steps() timing function gives it that digital heartbeat feel. Not smooth, almost jittery. Which is basically what a 3 AM bathroom trip feels like.
Time-Based Animation, Not Just Decoration
Flat animations are cute. What I actually wanted was to tie animation to time. The bar should tell me when in the night something happened, not just that it existed.
For that I sync animation delays to the position of the stage within the night. Early stages animate earlier, later segments lag behind. It feels like the night plays out on repeat.
.sleep-night {
--total: 430;
--night-speed: 60s; /* one loop represents the entire night */
}
.sleep-stage {
animation-duration: var(--night-speed);
animation-iteration-count: infinite;
animation-timing-function: linear;
animation-delay: calc(var(--start) / var(--total) * var(--night-speed) * -1);
}
I use a negative delay trick. Each stage looks like it started in the past, so the loop always shows the full night flowing. You can scrub the whole thing by changing --night-speed on the container.
This is where CSS custom properties pull their weight. JS only sets minutes. CSS translates that into both position and timing.
Adding Context: Axes Without A Chart Library
A pure animated bar looks cool but slightly abstract. I needed just enough context to make it usable.
So I added a light time axis below each night. Not a full chart component. Just flex children with 1, 2, 3, 4, 5, 6, 7 written under subtle ticks.
<div class="sleep-row">
<div class="sleep-night">...</div>
<div class="sleep-axis">
<span>23:00</span>
<span>01:00</span>
<span>03:00</span>
<span>05:00</span>
<span>07:00</span>
</div>
</div>
.sleep-axis {
display: flex;
justify-content: space-between;
font-size: 11px;
color: #85879c;
margin-top: 6px;
}
It is not pixel-perfect to the minute, and I do not care. It gives my brain a quick sense of when the REM cluster hit. That is enough.
Handling Multiple Nights Without Melting The GPU
One night looks fine. A month of nights with four stages each, all animated, will start to feel heavy on weaker machines.
I did not want to switch to static images for history, so I added a very simple rule. Only animate the current week by default. Everything else falls back to static bars until you hover.
.sleep-night[data-period="archive"] .sleep-stage {
animation: none;
}
.sleep-night[data-period="archive"]:hover .sleep-stage {
animation-play-state: running;
}
.sleep-night .sleep-stage {
animation-play-state: running;
}
If you really want to go nerd-level on performance, you can prefer-reduced-motion this entire thing.
@media (prefers-reduced-motion: reduce) {
.sleep-stage {
animation: none !important;
}
}
I think any motion-heavy data viz should respect that. People do not expect their sleep page to be the most visually aggressive thing in their browser.
What I Actually Learned From The Animation
The point of all this is not to flex CSS chops. I wanted better feedback loops on my rest experiments.
Once I had a week of animated nights on screen, patterns hit me fast.
- Late caffeine shows up as bright orange awake blocks exactly between 2 and 4 AM.
- Heavy evening strength work shifts my first deep sleep block later, which I do not like.
- Low-stress days produce absurdly clean deep segments with barely any REM jitter early on.
Some of that is visible in standard tracker apps, but the motion layer makes it more visceral. You do not just see that deep sleep was 18 percent. You watch that fragile block get chopped up across the night.
Why CSS And Not A Chart Library
I could have thrown this into D3 or a React charting library. I did not, on purpose.
CSS gives me a few things I like here.
- Animation is declarative. No requestAnimationFrame loops.
- Styling and behavior live in the same layer, which makes iterating on metaphors fast.
- I can drop it into any static page without bundling headaches.
For numeric overlays and complex tooltips a chart library wins. For this kind of high-level, pattern-first, almost ambient visualization, CSS felt cleaner.
Where I Want To Take This Next
This is version one. It already replaced three separate views in my personal dashboard.
Next I want to tie this to HRV and resting heart rate trends. Same bar, second subtle overlay. Maybe a barely visible gradient that creeps upward when my heart rate stays elevated all night.
I also want to auto-generate small embed snippets. That way I can drop my weekly sleep bar into a private note or a client check-in doc when I explain why their Slack messages at midnight are a bad habit.
If you build anything similar, I suggest you skip the fancy visualizations at first. Start with a single bar, four colors, and one simple animation per stage. Wire it straight to your own data. Your nervous system will tell you very quickly if the motion helps or just decorates.
Subscribe to my newsletter to get the latest updates and news
Member discussion