Cozy Ghost
A sleepy little sheet ghost floats above its soft navy shadow with blushy cheeks and the tiniest smile. It brought one mint prop and a sunshine sparkle, because even haunting can be cozy.
← you, in 25 minutes.
what you'll build
Here's the plan
A round-top cream ghost drawn with plain divs and CSS only: chunky navy outline, scalloped bottom bumps, sleepy half-lidded eyes, coral blush, a tiny smile, and rounded side arms. The finished doodle floats gently above a breathing shadow while a sunshine sparkle twinkles near its head and a small mint prop keeps the scene feeling fresh.
You’ll learn
- Building a CSS character from nested plain divs instead of vector paths
- Using border-radius and overlapping circles to make a simple ghost silhouette
- Drawing a tiny face with rounded rectangles, blush ovals, and a bottom-only smile arc
- Animating centered elements without dropping their translateX(-50%) alignment
- Skill level
- Beginner
- Time
- ~25 min
- Tools
- Just a browser
- Code type
- HTML + CSS
- Lines
- ~185
the steps
Build it, one change at a time
Shape the floating sheet
Start with the wrapper, a soft oval shadow, and one cream ghost body with a chunky navy outline. The body keeps a simple rounded top now, so the character reads clearly before we add the scalloped hem.
<div class="ghost-wrap"> <div class="shadow"></div> <div class="ghost"></div> </div> .ghost-wrap { position: relative; width: 200px; height: 180px; } .shadow { position: absolute; left: 50%; bottom: 8px; transform: translateX(-50%); width: 104px; height: 16px; background: #2B3A55; border-radius: 50%; opacity: 0.13; } .ghost { position: absolute; left: 50%; top: 16px; transform: translateX(-50%); width: 128px; height: 132px; background: #FFFDF7; border: 6px solid #2B3A55; border-radius: 64px 64px 18px 18px; }a very polite blank ghost sheet has entered the room.
Give it a sleepy face
Add two half-lidded eyes, tiny blush ovals, and the classic CoDoodle smile arc. The smile is just a little box with the top border removed, which is a handy trick for friendly faces.
<div class="eye eye-left"></div> <div class="eye eye-right"></div> <div class="blush blush-left"></div> <div class="blush blush-right"></div> <div class="smile"></div> .eye { position: absolute; top: 50px; width: 24px; height: 10px; background: #2B3A55; border-radius: 999px; transform-origin: center; } .eye-left { left: 30px; } .eye-right { right: 30px; } .blush { position: absolute; top: 74px; width: 18px; height: 10px; background: #FFB3AD; border-radius: 50%; opacity: 0.85; } .blush-left { left: 24px; } .blush-right { right: 24px; } .smile { position: absolute; top: 72px; left: 50%; transform: translateX(-50%); width: 22px; height: 12px; border: 3px solid #2B3A55; border-top: none; border-radius: 0 0 22px 22px; }sleepy, blushy, and already emotionally unavailable.
Add arms, scallops, and props
Now the ghost gets rounded arms and four overlapping hem bumps so the bottom turns wavy. The mint prop and sunshine sparkle sit outside the body, which keeps the character simple while the scene gets a little personality.
<div class="arm arm-left"></div> <div class="arm arm-right"></div> <div class="hem-bump hem-bump-1"></div> <div class="hem-bump hem-bump-2"></div> <div class="hem-bump hem-bump-3"></div> <div class="hem-bump hem-bump-4"></div> </div> <div class="mint-prop"></div> <div class="sparkle"></div> .arm { position: absolute; top: 80px; width: 18px; height: 28px; background: #FFFDF7; border: 5px solid #2B3A55; border-radius: 50%; z-index: -1; } .arm-left { left: -13px; transform: rotate(16deg); } .arm-right { right: -13px; transform: rotate(-16deg); } .hem-bump { position: absolute; bottom: -12px; width: 23px; height: 12px; background: #FFFDF7; border: 6px solid #2B3A55; border-top: none; border-radius: 0 0 50% 50%; } .hem-bump-1 { left: -6px; } .hem-bump-2 { left: 29px; } .hem-bump-3 { left: 64px; } .hem-bump-4 { right: -6px; } .mint-prop { position: absolute; left: 8px; bottom: 10px; width: 22px; height: 18px; background: #5ECBB4; border: 4px solid #2B3A55; border-radius: 50% 50% 7px 7px; transform: rotate(-10deg); } .sparkle { position: absolute; top: 30px; right: 16px; width: 16px; height: 16px; } .sparkle::before, .sparkle::after { content: ""; position: absolute; background: #FFD84D; border-radius: 2px; } .sparkle::before { left: 50%; top: 0; transform: translateX(-50%); width: 4px; height: 16px; } .sparkle::after { top: 50%; left: 0; transform: translateY(-50%); width: 16px; height: 4px; }now with scallops, sparkle, and one minty little side quest.
Let it float softly
The final pass adds a slow bob, an inverse shadow scale, a sleepy blink, and a twinkle. Notice how the ghost and shadow keyframes keep translateX(-50%) in every step, so the centered layout never jumps while it moves.
@media (prefers-reduced-motion: no-preference) { .ghost { animation: ghost-float 3.8s ease-in-out infinite; } .shadow { animation: ghost-shadow-breathe 3.8s ease-in-out infinite; } .eye { animation: ghost-blink 4.6s ease-in-out infinite; } .sparkle { transform-origin: center; animation: ghost-twinkle 2.8s ease-in-out infinite; } } @keyframes ghost-float { 0%, 100% { transform: translateX(-50%) translateY(0); } 50% { transform: translateX(-50%) translateY(-8px); } } @keyframes ghost-shadow-breathe { 0%, 100% { transform: translateX(-50%) scaleX(1); opacity: 0.13; } 50% { transform: translateX(-50%) scaleX(0.82); opacity: 0.08; } } @keyframes ghost-blink { 0%, 88%, 100% { transform: scaleY(1); } 92% { transform: scaleY(0.18); } } @keyframes ghost-twinkle { 0%, 100% { opacity: 0.45; transform: scale(0.82) rotate(0deg); } 45% { opacity: 1; transform: scale(1.12) rotate(8deg); } }a sleepy float loop with excellent tiny-haunt manners.
the complete code
Everything, in one place
Skipped straight to the end? Welcome. Copy the whole thing, download it, or open it in the Playground to start remixing.
<div class="ghost-wrap">
<div class="shadow"></div>
<div class="ghost">
<div class="eye eye-left"></div>
<div class="eye eye-right"></div>
<div class="blush blush-left"></div>
<div class="blush blush-right"></div>
<div class="smile"></div>
<div class="arm arm-left"></div>
<div class="arm arm-right"></div>
<div class="hem-bump hem-bump-1"></div>
<div class="hem-bump hem-bump-2"></div>
<div class="hem-bump hem-bump-3"></div>
<div class="hem-bump hem-bump-4"></div>
</div>
<div class="mint-prop"></div>
<div class="sparkle"></div>
</div>
tips & gotchas
Mistakes we actually made
I build the ghost body before the hem, because the big silhouette needs to read before the cute details arrive.
I keep the hem bumps the same cream and navy as the body so they merge into one simple sheet shape.
I use short, flat eyes for a sleepy mood; tall round eyes would make the ghost feel much more awake.
I always repeat translateX(-50%) inside centered keyframes, because transforms replace each other instead of stacking automatically.
make it yours
Remix it
- Change the moodeasy
Swap the sleepy eyes for round dots, surprised ovals, or tiny happy arcs.
- Dress the ghosteasy
Add a small bow, cap, or scarf using one extra div and the same navy outline.
- More scallopsmedium
Try five narrower hem bumps and adjust their overlap until the bottom feels wavy but not crowded.
- Night shiftmedium
Add a dark rounded backdrop with a moon dot, then lighten the shadow so the cream ghost still pops.
challenge extension
Turn this into a tiny ghost parade: duplicate the body at two smaller sizes, delay each float animation, and give every ghost a different face while keeping the same cream, navy, coral, mint, and sunshine palette.