On the implementation of the dynamic
The (first) value that animate passes to the update function as argument is the frame number, if you use that as dt you can expect some strangely accelerating particles. As your dynamic does not depend on time, the frame number should not enter the computation. The time step is a global constant related to the frame rate.
You could make a few adjustments to your code:
Positioning looks inconsistent here. Make sure to set your parent element's position to relative. An absolutely positioned element is positioned relative to its parent. If there is no such parent, it is positioned relative to the window, just like in your example.
Add z-index to make ornaments and lights ...
Coordinates as a matrix
x_co = [np.random.uniform(0, 2) for i in range(n_particles)]
y_co = [np.random.uniform(0, 2) for i in range(n_particles)]
return x_co, y_co
is a little baffling. You call into Numpy to generate uniformly distributed numbers (good) but then put it in a list comprehension (not good) and separate x and y into separate lists (not ...