Skip to main content

Alert Box

A Twitch follower alert that plays an animated notification when a new follower arrives. Demonstrates useLumioEvent, CSS keyframe animations via defineKeyframes, and auto-dismiss timers.

Project structure

alert-box/
├── lumio.config.json
├── src/
│ └── layer.tsx
└── package.json

lumio.config.json

{
"extensionId": "ext_placeholder",
"name": "Alert Box",
"version": "1.0.0",
"targets": ["layer"],
"permissions": ["events:read"]
}

src/layer.tsx

import { Lumio, Box, Text, useLumioEvent, defineKeyframes } from "@zaflun/lumio-sdk";
import { useState, useEffect, useRef } from "react";

// Define CSS keyframe animations
const slideIn = defineKeyframes({
from: { opacity: 0, transform: "translateY(-40px)" },
to: { opacity: 1, transform: "translateY(0)" },
});

const fadeOut = defineKeyframes({
from: { opacity: 1, transform: "translateY(0)" },
to: { opacity: 0, transform: "translateY(-20px)" },
});

interface AlertItem {
id: string;
name: string;
phase: "in" | "visible" | "out";
}

const DISPLAY_DURATION_MS = 4_000;
const OUT_DURATION_MS = 500;

function AlertBox() {
const event = useLumioEvent("twitch:follower");
const [alerts, setAlerts] = useState<AlertItem[]>([]);
const timersRef = useRef<Map<string, ReturnType<typeof setTimeout>>>(new Map());

useEffect(() => {
if (!event) return;

const id = `${event.timestamp}-${event.data.userId}`;
const newAlert: AlertItem = { id, name: event.data.userName, phase: "in" };

setAlerts((prev) => [...prev, newAlert]);

// Transition to "visible" after animation completes (~600ms)
const inTimer = setTimeout(() => {
setAlerts((prev) =>
prev.map((a) => (a.id === id ? { ...a, phase: "visible" } : a))
);
}, 600);

// Transition to "out" after display duration
const outTimer = setTimeout(() => {
setAlerts((prev) =>
prev.map((a) => (a.id === id ? { ...a, phase: "out" } : a))
);

// Remove after out animation
const removeTimer = setTimeout(() => {
setAlerts((prev) => prev.filter((a) => a.id !== id));
timersRef.current.delete(id);
}, OUT_DURATION_MS);

timersRef.current.set(`${id}-remove`, removeTimer);
}, DISPLAY_DURATION_MS);

timersRef.current.set(`${id}-in`, inTimer);
timersRef.current.set(`${id}-out`, outTimer);

return () => {
// Cleanup on unmount
timersRef.current.forEach(clearTimeout);
};
}, [event]);

if (alerts.length === 0) return null;

return (
<Box
style={{
position: "absolute",
top: 60,
left: "50%",
transform: "translateX(-50%)",
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: 12,
pointerEvents: "none",
}}
>
{alerts.map((alert) => (
<Box
key={alert.id}
style={{
background: "linear-gradient(135deg, #6366f1, #8b5cf6)",
borderRadius: 12,
padding: "16px 28px",
minWidth: 280,
textAlign: "center",
boxShadow: "0 8px 32px rgba(99,102,241,0.4)",
animation:
alert.phase === "in"
? `${slideIn} 0.6s cubic-bezier(0.34, 1.56, 0.64, 1) forwards`
: alert.phase === "out"
? `${fadeOut} ${OUT_DURATION_MS}ms ease forwards`
: undefined,
}}
>
<Text
content="New Follower!"
variant="muted"
style={{ fontSize: 11, letterSpacing: 2, textTransform: "uppercase", marginBottom: 4 }}
/>
<Text
content={alert.name}
variant="heading"
style={{ fontSize: 22, fontWeight: 700 }}
/>
<Text
content="Thanks for following!"
variant="muted"
style={{ fontSize: 13, marginTop: 4 }}
/>
</Box>
))}
</Box>
);
}

Lumio.render(<AlertBox />, { target: "layer" });

How it works

  1. useLumioEvent("twitch:follower") subscribes to Twitch follow events. The hook returns the latest event — the reference changes every time a new event arrives.

  2. useEffect runs when the event reference changes. Each event gets a unique id derived from its timestamp and user ID to handle rapid follows without duplicating alerts.

  3. The alert goes through three phases:

    • in — the slide-in animation plays for ~600ms
    • visible — the alert holds on screen for DISPLAY_DURATION_MS
    • out — the fade-out animation plays, then the alert is removed from state
  4. defineKeyframes() generates a unique class name for each keyframe block, avoiding conflicts with other extensions.

Extending the example

Add subscriber alerts:

const subEvent = useLumioEvent("twitch:subscribe");

useEffect(() => {
if (!subEvent) return;
// Push a different alert type into the queue
}, [subEvent]);

Add a sound:

const handleNewFollower = () => {
const audio = new Audio("/alert.mp3"); // bundled static asset
audio.play().catch(() => {}); // catch autoplay policy errors
};

Queue multiple alert types by maintaining a typed queue array in state and dequeuing one alert at a time.