👻 The Challenge
For Kiroween 2025, I set out to build something genuinely spooky: a Halloween-themed developer portfolio that would be memorable, polished, and actually terrifying. Not just dark colors and a pumpkin emoji — I wanted lightning flashes, ghost cursors, jump scares, and a Japanese horror girl lurking in the shadows.
The result? 17 distinct horror effects, a fully functional portfolio, and a development experience that felt like having a supernatural coding companion.
🎃 What I Built
The Haunted Portfolio features four themed sections:
- The Summoning (Landing) — Parallax graveyard, blood moon, floating spirits, glitching text
- The Séance (About) — Crystal ball bio reveal and tarot card skill showcase
- The Graveyard (Projects) — Tombstone project cards that rise from the grave
- Contact the Spirit (Contact) — An interactive Ouija board contact form
Plus global effects that haunt every page:
- Ghost cursor trails
- Random lightning
- Blood drips
- Eyes that follow your mouse
- Optional jump scares 👀
🔮 The Kiro Difference: Spec-Driven Development
Instead of diving straight into code, I started with Kiro's spec system. This changed everything.
📜 The Three Sacred Documents
1. requirements.md
Every feature got a requirement ID:
5. Global Effects
-
REQ-5.1: Custom spooky cursor (skull or ghost) -
REQ-5.2: Cursor trail with ghost particles -
REQ-5.3: Toggleable jump scares -
REQ-5.4: Flickering light effect
2. design.md
Architecture, color palette, component specs:
blood-red: #8B0000
pumpkin-orange: #FF6600
ghost-white: #F8F8FF
midnight-black: #0D0D0D
phantom-purple: #4B0082
3. tasks.md
A living checklist across 7 phases:
Phase 2: Core Effects System
- [x] Task 2.1: GhostCursor component
- [x] Task 2.2: ParticleSystem for fog/mist
- [x] Task 2.3: Lightning flash effect
Because of this system, Kiro already knew the full vision before writing code.
👻 The Most Impressive Code Generation
The GhostCursor component blew my mind. One prompt produced this:
export function GhostCursor() {
const { x, y } = useMousePosition();
const [trails, setTrails] = useState<Trail[]>([]);
useEffect(() => {
const newTrail: Trail = { id: Date.now(), x, y };
setTrails((prev) => [...prev.slice(-6), newTrail]);
}, [x, y]);
return (
<div className="pointer-events-none fixed inset-0 z-[9999]">
<AnimatePresence>
{trails.map((trail) => (
<motion.div
key={trail.id}
initial={{ opacity: 0.5, scale: 0.4 }}
animate={{ opacity: 0, scale: 0.2 }}
exit={{ opacity: 0 }}
className="absolute"
style={{ left: trail.x - 8, top: trail.y - 8 }}
>
{/* SVG ghost */}
</motion.div>
))}
</AnimatePresence>
{/* Main cursor */}
<motion.div
animate={{ x: x - 12, y: y - 12 }}
transition={{ type: 'spring', damping: 30, stiffness: 200 }}
>
{/* Glowing ghost */}
</motion.div>
</div>
);
}
Mobile detection, physics, z-indexing — all correct on the first try.
🕷️ Agent Hooks: Automation with Personality
Accessibility Guardian
{
"name": "Accessibility Guardian",
"trigger": { "type": "onSave", "pattern": "**/*.tsx" },
"action": {
"type": "prompt",
"prompt": "Review the saved file for accessibility concerns. Ensure animations respect prefers-reduced-motion…"
}
}
Automatically ensured:
- Reduced motion support
- ARIA labels
- Proper contrast
Spooky Commit Messages
{
"name": "Spooky Commit Messages",
"trigger": { "type": "manual" },
"action": {
"prompt": "Generate a Halloween-themed git commit message with emojis like 🎃👻💀🦇🕷️"
}
}
Examples:
👻 Summoned the ghost cursor from the void🦇 Lightning now strikes with double intensity
📜 Steering: No Repetition, Consistent Code
The coding-standards.md file used:
inclusion: always
Meaning every generation automatically followed:
Animation Rules
- Always use Framer Motion
- Respect
prefers-reduced-motion - Smooth transforms
- Creepy custom easing
Zero repetition. Zero mismatches. Perfect consistency.
🩸 The Creepy Details
👁️ CreepyEyes Component
Bloodshot eyes follow your cursor:
const angle = Math.atan2(mouseY - eyeRect.y, mouseX - eyeRect.x);
const distance = Math.min(eye.size * 0.2, 8);
const pupilX = Math.cos(angle) * distance;
const pupilY = Math.sin(angle) * distance;
Random blinking, random positions. Pure nightmare.
👧 HorrorGirl Component
A Sadako-style figure rises slowly from the bottom of the screen.
- Long black hair
- One glowing red eye
- Fades away after 3–5 seconds
💀 JumpScare System
const triggerScare = () => {
const delay = Math.random() * 45000 + 30000;
setTimeout(() => {
setIsActive(true);
setTimeout(() => setIsActive(false), 400);
triggerScare();
}, delay);
};
But:
- Off by default
- Respects reduced motion
- User-toggleable
The right balance between spooky and safe.
🌙 Accessibility in a Horror Site
Yes — it can be done.
const { reducedMotion, jumpScaresEnabled, soundEnabled } = useSpooky();
if (reducedMotion) return null;
if (!jumpScaresEnabled) return null;
- Full reduced-motion support
- Sound off by default
- Jump scares opt-in
- Keyboard friendly
🎯 What I Learned
- Specs > Vibes Better structure = better AI output.
- Hooks matter Accessibility checks on every save were priceless.
- Steering eliminates repetition Say your standards once. Never again.
- Context is everything The more Kiro understood, the better the generation.
🦇 Try It Yourself
Source Code:
👉 https://github.com/Ark2044/haunted-portfolio
Live Demo:
👉 https://haunted-portfolio.netlify.app/
Built with 🖤 and dark magic for Kiroween 2025.
This post is part of the Kiroween Hackathon — created with Kiro’s spec-driven development, agent hooks, and steering documents.

Top comments (0)