Question: With Plotly, I'd like to display two imshow on the same page, at the same place, with opacity. This nearly works: import plotly.express as px, numpy as np from skimage import io img = io.imread('https://upload.wikimedia.org/wikipedia/commons/thumb/0/00/Crab_Nebula.jpg/240px-Crab_Nebula.jpg') fig = px.imshow(img) x = np.random.random((100, 200)) fig2 = px.imshow(x) fig.show() fig2.show()  but it displays the two imshow images in two different tabs. How to display the two "imshow" on the same plot, with an opacity slider for both layers? For reference, here is the matplotlib equivalent: import numpy as np, matplotlib.pyplot as plt, matplotlib.widgets as mpwidgets, scipy.misc x = scipy.misc.face(gray=False) # shape (768, 1024, 3) y = np.random.random((100, 133)) # shape (100, 133) fig, (ax0, ax1) = plt.subplots(2, 1, gridspec_kw={'height_ratios': [5, 1]}) OPACITY = 0.5 img0 = ax0.imshow(x, cmap="jet") img1 = ax0.imshow(y, cmap="jet", alpha=OPACITY, extent=img0.get_extent()) slider0 = mpwidgets.Slider(ax=ax1, label='opacity', valmin=0, valmax=1, valinit=OPACITY) slider0.on_changed(lambda value: img1.set_alpha(value)) plt.show() 

Answer: As pointed out by the OP, since opacity is a style attribute applied by the client regardless of the trace (data) which is associated to a given image, there is no need to precompute one trace for each image variation, nor to redraw anything when the slider moves. Using simple slider controls, we should be able to apply such (minor) changes in real-time.  Option 1 : The first option is to let Plotly.js handle the changes by specifying the proper method and args in the slider' steps configuration (ie. using the restyle method, Plotly is smart enough to not redraw the whole plot) : from PIL import Image import plotly.graph_objects as go import numpy as np import scipy.misc imgA = scipy.misc.face() imgB = Image.fromarray(np.random.random(imgA.shape[:2])*255).convert('RGB') fig = go.Figure([ go.Image(name='raccoon', z=imgA, opacity=1), # trace 0 go.Image(name='noise', z=imgB, opacity=0.5) # trace 1 ]) slider = { 'active': 50, 'currentvalue': {'prefix': 'Noise: '}, 'steps': [{ 'value': step/100, 'label': f'{step}%', 'visible': True, 'execute': True, 'method': 'restyle', 'args': [{'opacity': step/100}, [1]] # apply to trace [1] only } for step in range(101)] } fig.update_layout(sliders=[slider]) fig.show(renderer='browser')   Option 2 : The second option demonstrates a more general case where one wants to bypass Plotly API on slider events and trigger his own code instead. For example, what if the restyle method were not efficient enough for this task ? In this situation, one can hook into the plotly_sliderchange event and apply the changes manually in order to obtain smooth transitions. The browser (default) renderer makes it possible by using the post_script parameter, which you can pass to the show() method. It allows to add javascript snippets which are executed just after plot creation. Once one knows that (documentation should emphasize this part!), it is just a matter of binding the appropriate handler. For example (JS syntax highlighting) : // {plot_id} is a placeholder for the graphDiv id. const gd = document.getElementById('{plot_id}'); // Retrieve the image (easier with d3, cf. 1st revision, see comments) const trace = gd.calcdata.find(data => data[0].trace.name === 'noise')[0]; const group = Object.keys(trace).find(prop => (/node[0-4]/).test(prop)); const img = trace[group][0][0].firstChild; // Listen for slider events and apply changes on the fly gd.on('plotly_sliderchange', event => img.style.opacity = event.step.value);  Now for the slider configuration, the important thing is to set execute=False and method='skip' to bypass API calls and prevent redrawing the plot when the slider changes : slider = { 'active': 50, 'currentvalue': {'prefix': 'Noise: '}, 'steps': [{ 'value': step/100, 'label': f'{step}%', 'visible': True, 'execute': False, 'method': 'skip', } for step in range(101)] } js = ''' const gd = document.getElementById('{plot_id}'); const trace = gd.calcdata.find(data => data[0].trace.name === 'noise')[0]; const group = Object.keys(trace).find(prop => (/node[0-4]/).test(prop)); const img = trace[group][0][0].firstChild; gd.on('plotly_sliderchange', event => img.style.opacity = event.step.value); ''' fig.update_layout(sliders=[slider]) fig.show(renderer='browser', post_script=[js])