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]);

Play a sound with the alert: add sounds:play to permissions and call playSound(soundId, volume) from useLumioSound when the alert enters. The sound plays in the overlay or widget that shows the alert.

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