How to Build Dynamic Atmosphere Bubbles with React‑Transition‑Group vs Swiper
This article explains how to implement real‑time atmosphere bubbles in live‑streaming pages, compares Swiper and react‑transition‑group for animation, provides detailed code examples for Swiper, Transition, CSSTransition, SwitchTransition, and shows a complete React solution using a queue‑based component.
1. Introduction
We recently received a requirement to create an “atmosphere bubble” component that appears in live‑streaming rooms (e.g., Taobao, Douyin, Bilibili) to convey other users’ behavior in real time.
The visual effect is transient, but it relies on a push‑message center and a backend service that are already well‑built.
Task: set up a listener for push callbacks and render the corresponding component.
1.1. Atmosphere Bubble Requirement
The bubble looks like a flashing carousel, similar to a Marquee component built on
swiper/react. Swiper is a powerful, ready‑to‑use carousel library that supports React.
1.2. Development Assessment
1.2.1. Reusing Existing Component
We found a
Marqueecomponent based on
swiper/react. Basic usage:
<code>import SwiperCore, { Autoplay } from "swiper";
import { Swiper, SwiperSlide } from "swiper/react";
import "swiper/css";
import "swiper/css/navigation";
import "swiper/css/pagination";
import "swiper/css/scrollbar";
import "./styles.css";
SwiperCore.use([Autoplay]);
const renderBubble = (bubble) => {
const { name, wording, btnText } = bubble;
return (
<SwiperSlide>
<p>
<span className="live-bubble-username">{name}</span>
<span className="live-bubble-wording">{wording}</span>
<span className="live-bubble-action">{btnText}</span>
</p>
</SwiperSlide>
);
};
export default () => {
const dataList = [
{ name: "李*", wording: "咨询了课程", btnText: "去咨询" },
{ name: "黄*", wording: "领取了优惠券", btnText: "去领取" },
{ name: "高*", wording: "分享了直播间给好友", btnText: "去分享" },
{ name: "刘*", wording: "领取了直播间资料", btnText: "去领取" },
{ name: "朱*", wording: "购买了直播间课程《xxx》", btnText: "去领取" }
];
return (
<div style={{ background: "#000" }}>
<Swiper slidesPerView={1} direction="horizontal" onSlideChange={() => console.log("slide change")} autoplay>
{dataList.map((item) => renderBubble(item))}
</Swiper>
</div>
);
};
</code>We set
autoplayto enable automatic playback.
1.2.2. Swiper Customisation
Desired effects:
Slide direction: left to right
Enter effect: slide in from left while fading in
Exit effect: stay in place while fading out
We modified Swiper parameters, for example:
<code>autoplay={{
delay: 3000,
reverseDirection: true
}}</code>We also used the
creativeEffectAPI to customise the transition:
<code><Swiper
slidesPerView={1}
direction="horizontal"
onSlideChange={() => console.log("slide change")}
autoplay={{ delay: 3000, reverseDirection: true }}
loop
speed={3000}
effect="creative"
creativeEffect={{
prev: { opacity: 0, translate: ["-300%", 0, 0] },
next: { opacity: 0 }
}}
>
…
</Swiper>
</code>However, Swiper shows both the previous and next slides simultaneously, which makes the fade‑in effect imperfect and limits fine‑grained control.
1.3. Other Solutions?
Because Swiper does not fit the requirement well, we turned to a React animation library.
2. react‑transition‑group
react-transition-groupis the official React library for handling component transitions. It provides four main APIs:
Transition,
CSSTransition,
SwitchTransition, and
TransitionGroup.
2.1. Transition Component
Basic usage with a boolean
inprop to control mounting and unmounting:
<code>import React, { useState } from "react";
import { Transition } from "react-transition-group";
const duration = 500;
const defaultStyle = { transition: `opacity ${duration}ms ease-in-out`, opacity: 0 };
const transitionStyles = {
entering: { opacity: 1 },
entered: { opacity: 1 },
exiting: { opacity: 0 },
exited: { opacity: 0 }
};
export default function App() {
const [inProp, setInProp] = useState(false);
return (
<div>
<Transition in={inProp} timeout={duration}>
{state => (
<div style={{ ...defaultStyle, ...transitionStyles[state] }}>
I'm a fade Transition!
</div>
)}
</Transition>
<button onClick={() => setInProp(prev => !prev)}>Click to Enter</button>
</div>
);
}
</code>2.2. CSSTransition Component
Wraps
Transitionand automatically adds class names based on the transition state. Example:
<code>import React, { useState } from "react";
import { CSSTransition } from "react-transition-group";
import "./styles.scss";
export default function App() {
const [inProp, setInProp] = useState(false);
return (
<div>
<CSSTransition in={inProp} timeout={300} classNames="my-fade" unmountOnExit>
<p className="my-fade">I'm a fade Transition!</p>
</CSSTransition>
<button onClick={() => setInProp(prev => !prev)}>Click to Enter</button>
</div>
);
}
</code>The accompanying SCSS defines
.my-fade-enter,
.my-fade-enter-active,
.my-fade-exit, and
.my-fade-exit-activewith opacity and transform transitions.
2.3. SwitchTransition
SwitchTransitioncontrols how two components replace each other. It supports
out‑in(default) and
in‑outmodes.
<code>import React, { useState } from "react";
import { CSSTransition, SwitchTransition } from "react-transition-group";
export default function App() {
const [state, setState] = useState(true);
return (
<SwitchTransition mode="out-in">
<CSSTransition
key={state}
timeout={500}
classNames="fade"
addEndListener={(node, done) => node.addEventListener("transitionend", done, false)}
>
<div>{state ? "Hello, world!" : "Goodbye, world!"}</div>
</CSSTransition>
</SwitchTransition>
);
}
</code>In our bubble scenario we need the
out‑inbehaviour.
2.4. TransitionGroup
TransitionGroupmanages a list of transition components, useful for dynamic lists.
<code><TransitionGroup>
{list.map(({ id }) => (
<CSSTransition key={id} timeout={300} classNames="my">
<MyComponent key={id} />
</CSSTransition>
))}
</TransitionGroup>
</code>3. Implement Atmosphere Bubbles with react‑transition‑group
We use
CSSTransition+
SwitchTransition(out‑in mode) to display a queue of bubble components.
3.1. Data Structure
The bubbles are stored in a JSX array; an index determines which bubble is currently visible. New bubbles are appended to the array, and a timer advances the index.
3.2. Queue Implementation
<code>import React, { useEffect, useCallback } from "react";
import { CSSTransition, SwitchTransition } from "react-transition-group";
const AtmosphereBubbleSequence = ({ bubbleList, index, setCurIndex, nextBubble, next, resetList }) => {
const bubbleListLength = bubbleList.length;
let curIndex = 0;
if (index > -1 && index < bubbleListLength) {
curIndex = index;
} else {
curIndex = bubbleListLength - 1;
setCurIndex(curIndex);
}
useEffect(() => {
const interval = setInterval(() => {
nextBubble();
}, 3000);
return () => {
clearInterval(interval);
};
}, []);
const onExited = useCallback(
(node) => {
if (+node.dataset.bubbleKey === bubbleListLength - 1) {
next(); // switch to fallback component
resetList(); // clear bubble list
}
},
[bubbleListLength, next, resetList]
);
return (
<SwitchTransition>
<CSSTransition
key={bubbleList[index]?.type?.name + index || Math.random()}
timeout={{ enter: 300, exit: 300 }}
classNames="atmosphere-bubble-cnt"
unmountOnExit
onExited={onExited}
>
<div data-bubble-key={index} className="atmosphere-bubble-cnt">
{bubbleList[index]}
</div>
</CSSTransition>
</SwitchTransition>
);
};
export default AtmosphereBubbleSequence;
</code>The accompanying CSS defines the enter and exit animations with opacity and
translate3dtransforms.
3.3. Effect Simulation
A mock demo continuously pushes new bubble data into the list and advances the index every three seconds.
<code>import React, { useEffect, useState } from "react";
import Bubble from "./Bubble";
import AtmosphereBubbleSequence from "./Sequence";
import "./styles.css";
export default function App() {
const [index, setIndex] = useState(0);
const [bubbleDataList, setBubbleDataList] = useState([
{ nick: "李*", content: "咨询了课程", eventMsg: "去咨询" },
{ nick: "黄*", content: "领取了优惠券", eventMsg: "去领取" },
{ nick: "高*", content: "分享了直播间给好友", eventMsg: "去分享" },
{ nick: "刘*", content: "领取了直播间资料", eventMsg: "去领取" },
{ nick: "朱*", content: "购买了直播间课程《xxx》", eventMsg: "去领取" }
]);
useEffect(() => {
const interval = setInterval(() => {
setBubbleDataList(prev => [
...prev,
{ nick: "朱*", content: "购买了直播间课程《xxx》", eventMsg: "去领取" }
]);
}, 3000);
return () => {
clearInterval(interval);
};
}, []);
if (!bubbleDataList.length) return null;
const nextBubble = (newIndex) => {
setIndex(prev => typeof newIndex === "undefined" ? prev + 1 : newIndex);
};
return (
<div style={{ background: "#000", height: "56px", display: "flex", alignItems: "center" }}>
<AtmosphereBubbleSequence
bubbleList={bubbleDataList.map(data => <Bubble data={data} />)}
index={index}
setCurIndex={setIndex}
nextBubble={nextBubble}
/>
</div>
);
}
</code>The demo shows bubbles appearing one after another with smooth fade‑in/out transitions.
4. Summary
4.1. Animation‑Effect Comparison
react‑transition‑group is more flexible and suitable for component‑level animations.
Swiper is a carousel component; its simultaneous rendering of previous and next slides limits precise control for the bubble use case.
4.2. State‑Management Comparison
react‑transition‑group lets us design data structures that drive UI directly, fitting React’s declarative model.
Swiper relies on instance methods to modify slides, which can cause state‑UI mismatch when slides are added or removed dynamically.
4.3. Solution Selection
When the data list is static, a carousel like Swiper works well. When the list changes dynamically, a React‑centric solution such as react‑transition‑group is preferable.
5. CodeSandbox Demo
Swiper atmosphere bubble
Transition demo
CSSTransition demo
SwitchTransition demo
Atmosphere bubble based on react‑transition‑group
Tencent IMWeb Frontend Team
IMWeb Frontend Community gathering frontend development enthusiasts. Follow us for refined live courses by top experts, cutting‑edge technical posts, and to sharpen your frontend skills.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.