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
-
useLumioEvent("twitch:follower")subscribes to Twitch follow events. The hook returns the latest event — the reference changes every time a new event arrives. -
useEffectruns when the event reference changes. Each event gets a uniqueidderived from its timestamp and user ID to handle rapid follows without duplicating alerts. -
The alert goes through three phases:
in— the slide-in animation plays for ~600msvisible— the alert holds on screen forDISPLAY_DURATION_MSout— the fade-out animation plays, then the alert is removed from state
-
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.