diff --git a/qml.qrc b/qml.qrc
index b80096d9a076c7f550e0b672610bfc36459bf66d..5b974ba7e5de68a4f0d5f8855f3f95fc2fc2e0b4 100644
--- a/qml.qrc
+++ b/qml.qrc
@@ -136,5 +136,6 @@
         <file>src/mainview/components/ConversationListView.qml</file>
         <file>src/mainview/components/SmartListItemDelegate.qml</file>
         <file>src/mainview/components/BadgeNotifier.qml</file>
+        <file>src/mainview/components/ParticipantsLayer.qml</file>
     </qresource>
 </RCC>
diff --git a/src/calloverlaymodel.cpp b/src/calloverlaymodel.cpp
index aae7c75207d74dedef9c47fbe57a7871b9355872..fb130de346dd4016665c1a2a1fd8b029e237f95b 100644
--- a/src/calloverlaymodel.cpp
+++ b/src/calloverlaymodel.cpp
@@ -18,6 +18,10 @@
 
 #include "calloverlaymodel.h"
 
+#include <QEvent>
+#include <QMouseEvent>
+#include <QQuickWindow>
+
 CallControlListModel::CallControlListModel(QObject* parent)
     : QAbstractListModel(parent)
 {}
@@ -183,6 +187,42 @@ CallOverlayModel::overflowHiddenModel()
     return QVariant::fromValue(overflowHiddenModel_);
 }
 
+void
+CallOverlayModel::registerFilter(QQuickWindow* object, QQuickItem* item)
+{
+    if (!object || !item || watchedItems_.contains(item))
+        return;
+    watchedItems_.push_back(item);
+    if (watchedItems_.size() == 1)
+        object->installEventFilter(this);
+}
+
+void
+CallOverlayModel::unregisterFilter(QQuickWindow* object, QQuickItem* item)
+{
+    if (!object || !item || !watchedItems_.contains(item))
+        return;
+    watchedItems_.removeOne(item);
+    if (watchedItems_.size() == 0)
+        object->removeEventFilter(this);
+}
+
+bool
+CallOverlayModel::eventFilter(QObject* object, QEvent* event)
+{
+    if (event->type() == QEvent::MouseMove) {
+        auto mouseEvent = static_cast<QMouseEvent*>(event);
+        QPoint eventPos(mouseEvent->x(), mouseEvent->y());
+        auto windowItem = static_cast<QQuickWindow*>(object)->contentItem();
+        Q_FOREACH (const auto& item, watchedItems_) {
+            if (item->contains(windowItem->mapToItem(item, eventPos))) {
+                Q_EMIT mouseMoved(item);
+            }
+        }
+    }
+    return QObject::eventFilter(object, event);
+}
+
 void
 CallOverlayModel::setControlRanges()
 {
diff --git a/src/calloverlaymodel.h b/src/calloverlaymodel.h
index f3f70fb1f77c4eb3e3fab22a62c25c3ec5a012cf..856a71a3dd91d78aaf0d97b668aff37ca199f36c 100644
--- a/src/calloverlaymodel.h
+++ b/src/calloverlaymodel.h
@@ -25,7 +25,7 @@
 #include <QObject>
 #include <QQmlEngine>
 #include <QSortFilterProxyModel>
-#include <QDebug>
+#include <QQuickItem>
 
 #define CC_ROLES \
     X(QObject*, ItemAction) \
@@ -103,6 +103,13 @@ public:
     Q_INVOKABLE QVariant overflowVisibleModel();
     Q_INVOKABLE QVariant overflowHiddenModel();
 
+    Q_INVOKABLE void registerFilter(QQuickWindow* object, QQuickItem* item);
+    Q_INVOKABLE void unregisterFilter(QQuickWindow* object, QQuickItem* item);
+    bool eventFilter(QObject* object, QEvent* event) override;
+
+Q_SIGNALS:
+    void mouseMoved(QQuickItem* item);
+
 private Q_SLOTS:
     void setControlRanges();
 
@@ -114,4 +121,6 @@ private:
     IndexRangeFilterProxyModel* overflowModel_;
     IndexRangeFilterProxyModel* overflowVisibleModel_;
     IndexRangeFilterProxyModel* overflowHiddenModel_;
+
+    QList<QQuickItem*> watchedItems_;
 };
diff --git a/src/commoncomponents/ModalPopup.qml b/src/commoncomponents/ModalPopup.qml
index 83e7d8201ca0f2819fdd0f2c45af963c2a905bd9..53d4753b3d70e380c4dc46700acb0cef2d83ef98 100644
--- a/src/commoncomponents/ModalPopup.qml
+++ b/src/commoncomponents/ModalPopup.qml
@@ -25,8 +25,6 @@ import net.jami.Constants 1.0
 Popup {
     id: root
 
-    property int fadeDuration: 100
-
     // convient access to closePolicy
     property bool autoClose: true
 
@@ -76,13 +74,13 @@ Popup {
     enter: Transition {
         NumberAnimation {
             properties: "opacity"; from: 0.0; to: 1.0
-            duration: fadeDuration
+            duration: JamiTheme.shortFadeDuration
         }
     }
     exit: Transition {
         NumberAnimation {
             properties: "opacity"; from: 1.0; to: 0.0
-            duration: fadeDuration
+            duration: JamiTheme.shortFadeDuration
         }
     }
 }
diff --git a/src/commoncomponents/PushButton.qml b/src/commoncomponents/PushButton.qml
index 314d5df1b4c53b89e56f60e0827bff6e80564a00..255fb853a20acc99f1f12a2fb107f76b31a9ba33 100644
--- a/src/commoncomponents/PushButton.qml
+++ b/src/commoncomponents/PushButton.qml
@@ -58,7 +58,7 @@ AbstractButton {
     property string checkedColor: pressedColor
 
     // State transition duration
-    property int duration: JamiTheme.fadeDuration
+    property int duration: JamiTheme.shortFadeDuration
 
     // Image properties
     property alias source: image.source
diff --git a/src/constant/JamiTheme.qml b/src/constant/JamiTheme.qml
index 55f256c9220b0d9eaeecde5dd7e8e2cd8c4ee604..f70caac3eea636a2c25b84493a514f7695c30916 100644
--- a/src/constant/JamiTheme.qml
+++ b/src/constant/JamiTheme.qml
@@ -163,7 +163,10 @@ Item {
     property color bgSideBarDarkMode_: rgba256(24, 24, 24, 100)
     property color bgDarkMode_: rgba256(32, 32, 32, 100)
 
-    property int fadeDuration: 150
+    property int shortFadeDuration: 150
+    property int overlayFadeDelay: 2000
+    property int overlayFadeDuration: 500
+    property int smartListTransitionDuration: 120
 
     // Sizes
     property real splitViewHandlePreferredWidth: 4
@@ -185,7 +188,6 @@ Item {
     property real accountListAvatarSize: 40
     property real smartListItemHeight: 64
     property real smartListAvatarSize: 52
-    property real smartListTransitionDuration: 120
     property real avatarSizeInCall: 130
     property real callButtonPreferredSize: 50
 
diff --git a/src/mainview/components/AccountComboBox.qml b/src/mainview/components/AccountComboBox.qml
index 28152bb7a4da09f1af5f910ca44d9f2aad433066..36a4eb2a11fca77f2e0b3b5f9901592f237d651f 100644
--- a/src/mainview/components/AccountComboBox.qml
+++ b/src/mainview/components/AccountComboBox.qml
@@ -76,7 +76,7 @@ Label {
                        Qt.lighter(JamiTheme.hoverColor, 1.05) :
                        JamiTheme.backgroundColor
         Behavior on color {
-            ColorAnimation { duration: JamiTheme.fadeDuration }
+            ColorAnimation { duration: JamiTheme.shortFadeDuration }
         }
 
         // TODO: this can be removed when frameless window is implemented
diff --git a/src/mainview/components/CallOverlay.qml b/src/mainview/components/CallOverlay.qml
index 1c375c737137c5ac6fc33ff1cc8e0bc720f02de3..ea5b150fc8b492052de4e77ec5e28268e945294a 100644
--- a/src/mainview/components/CallOverlay.qml
+++ b/src/mainview/components/CallOverlay.qml
@@ -33,22 +33,25 @@ import "../js/pluginhandlerpickercreation.js" as PluginHandlerPickerCreation
 
 import "../../commoncomponents"
 
-Rectangle {
+Item {
     id: root
 
+    property alias participantsLayer: __participantsLayer
     property string timeText: "00:00"
     property string remoteRecordingLabel: ""
     property bool isVideoMuted: true
     property bool isAudioOnly: false
     property string bestName: ""
 
-    property var participantOverlays: []
-    property var participantComponent: Qt.createComponent("ParticipantOverlay.qml")
-
     signal overlayChatButtonClicked
 
     onVisibleChanged: if (!visible) callViewContextMenu.close()
 
+    ParticipantsLayer {
+        id: __participantsLayer
+        anchors.fill: parent
+    }
+
     function setRecording(localIsRecording) {
         callViewContextMenu.localIsRecording = localIsRecording
         recordingRect.visible = localIsRecording
@@ -57,6 +60,7 @@ Rectangle {
 
     function updateButtonStatus(isPaused, isAudioOnly, isAudioMuted, isVideoMuted,
                                 isRecording, isSIP, isConferenceCall) {
+        root.isVideoMuted = isVideoMuted
         callViewContextMenu.isSIP = isSIP
         callViewContextMenu.isPaused = isPaused
         callViewContextMenu.isAudioOnly = isAudioOnly
@@ -83,112 +87,6 @@ Rectangle {
         PluginHandlerPickerCreation.closePluginHandlerPicker()
     }
 
-    // returns true if participant is not fully maximized
-    function showMaximize(pX, pY, pW, pH) {
-        // Hack: -1 offset added to avoid problems with odd sizes
-        return (pX - distantRenderer.getXOffset() !== 0
-                || pY - distantRenderer.getYOffset() !== 0
-                || pW < (distantRenderer.width - distantRenderer.getXOffset() * 2 - 1)
-                || pH < (distantRenderer.height - distantRenderer.getYOffset() * 2 - 1))
-    }
-
-    function handleParticipantsInfo(infos) {
-        if (root.isAudioOnly)
-            return;
-        // TODO: in the future the conference layout should be entirely managed by the client
-        // Hack: truncate and ceil participant's overlay position and size to correct
-        // when they are not exacts
-        callOverlay.updateMenu()
-        var showMax = false
-        var showMin = false
-
-        var deletedUris = []
-        var currentUris = []
-        for (var p in participantOverlays) {
-            if (participantOverlays[p]) {
-                var participant = infos.find(e => e.uri === participantOverlays[p].uri);
-                if (participant) {
-                    // Update participant's information
-                    var newX = Math.trunc(distantRenderer.getXOffset()
-                            + participant.x * distantRenderer.getScaledWidth())
-                    var newY = Math.trunc(distantRenderer.getYOffset()
-                            + participant.y * distantRenderer.getScaledHeight())
-
-                    var newWidth = Math.ceil(participant.w * distantRenderer.getScaledWidth())
-                    var newHeight = Math.ceil(participant.h * distantRenderer.getScaledHeight())
-
-                    var newVisible = participant.w !== 0 && participant.h !== 0
-                    if (participantOverlays[p].x !== newX)
-                        participantOverlays[p].x = newX
-                    if (participantOverlays[p].y !== newY)
-                        participantOverlays[p].y = newY
-                    if (participantOverlays[p].width !== newWidth)
-                        participantOverlays[p].width = newWidth
-                    if (participantOverlays[p].height !== newHeight)
-                        participantOverlays[p].height = newHeight
-                    if (participantOverlays[p].visible !== newVisible)
-                        participantOverlays[p].visible = newVisible
-
-                    showMax = showMaximize(participantOverlays[p].x,
-                                           participantOverlays[p].y,
-                                           participantOverlays[p].width,
-                                           participantOverlays[p].height)
-
-                    participantOverlays[p].setMenu(participant.uri, participant.bestName,
-                                                   participant.isLocal, participant.active, showMax)
-                    if (participant.videoMuted)
-                        participantOverlays[p].setAvatar(true, participant.avatar, participant.uri, participant.isLocal, participant.isContact)
-                    else
-                        participantOverlays[p].setAvatar(false)
-                    currentUris.push(participantOverlays[p].uri)
-                } else {
-                    // Participant is no longer in conference
-                    deletedUris.push(participantOverlays[p].uri)
-                    participantOverlays[p].destroy()
-                }
-            }
-        }
-        participantOverlays = participantOverlays.filter(part => !deletedUris.includes(part.uri))
-
-        if (infos.length === 0) { // Return to normal call
-            previewRenderer.visible = true
-            for (var part in participantOverlays) {
-                if (participantOverlays[part]) {
-                        participantOverlays[part].destroy()
-                }
-            }
-            participantOverlays = []
-        } else {
-            previewRenderer.visible = false
-            for (var infoVariant in infos) {
-                // Only create overlay for new participants
-                if (!currentUris.includes(infos[infoVariant].uri)) {
-                    var hover = participantComponent.createObject(callOverlayRectMouseArea, {
-                        x: Math.trunc(distantRenderer.getXOffset() + infos[infoVariant].x * distantRenderer.getScaledWidth()),
-                        y: Math.trunc(distantRenderer.getYOffset() + infos[infoVariant].y * distantRenderer.getScaledHeight()),
-                        width: Math.ceil(infos[infoVariant].w * distantRenderer.getScaledWidth()),
-                        height: Math.ceil(infos[infoVariant].h * distantRenderer.getScaledHeight()),
-                        visible: infos[infoVariant].w !== 0 && infos[infoVariant].h !== 0
-                    })
-                    if (!hover) {
-                        console.log("Error when creating the hover")
-                        return
-                    }
-
-                    showMax = showMaximize(hover.x, hover.y, hover.width, hover.height)
-
-                    hover.setMenu(infos[infoVariant].uri, infos[infoVariant].bestName,
-                                  infos[infoVariant].isLocal, infos[infoVariant].active, showMax)
-                    if (infos[infoVariant].videoMuted)
-                        hover.setAvatar(true, infos[infoVariant].avatar, infos[infoVariant].uri, infos[infoVariant].isLocal, infos[infoVariant].isContact)
-                    else
-                        hover.setAvatar(false)
-                    participantOverlays.push(hover)
-                }
-            }
-        }
-    }
-
     // x, y position does not need to be translated
     // since they all fill the call page
     function openCallViewContextMenuInPos(x, y) {
@@ -224,8 +122,6 @@ Rectangle {
         recordingRect.visible = callViewContextMenu.localIsRecording
     }
 
-    anchors.fill: parent
-
     SipInputPanel {
         id: sipInputPanel
 
@@ -233,158 +129,6 @@ Rectangle {
         y: root.height / 2 - sipInputPanel.height / 2
     }
 
-    // Timer to decide when overlay fade out.
-    Timer {
-        id: callOverlayTimer
-        interval: 5000
-        onTriggered: {
-            if (overlayUpperPartRect.state !== 'freezed') {
-                overlayUpperPartRect.state = 'freezed'
-                resetLabelsTimer.restart()
-            }
-            if (callOverlayButtonGroup.state !== 'freezed') {
-                callOverlayButtonGroup.state = 'freezed'
-                resetLabelsTimer.restart()
-            }
-        }
-    }
-
-    // Timer to reset recording label and call duration time
-    Timer {
-        id: resetLabelsTimer
-
-        interval: 1000
-        running: root.visible
-        repeat: true
-        onTriggered: {
-            timeText = CallAdapter.getCallDurationTime(LRCInstance.currentAccountId,
-                                                       LRCInstance.selectedConvUid)
-            if (callOverlayButtonGroup.state === 'freezed'
-                    && !callViewContextMenu.peerIsRecording)
-                remoteRecordingLabel = ""
-        }
-    }
-
-    Rectangle {
-        id: overlayUpperPartRect
-
-        anchors.top: root.top
-
-        width: root.width
-        height: 50
-        opacity: 0
-
-        RowLayout {
-            id: overlayUpperPartRectRowLayout
-
-            anchors.fill: parent
-
-            Text {
-                id: jamiBestNameText
-
-                Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft
-                Layout.preferredWidth: overlayUpperPartRect.width / 3
-                Layout.preferredHeight: 50
-                leftPadding: 16
-
-                font.pointSize: JamiTheme.textFontSize
-
-                horizontalAlignment: Text.AlignLeft
-                verticalAlignment: Text.AlignVCenter
-
-                text: textMetricsjamiBestNameText.elidedText
-                color: "white"
-
-                TextMetrics {
-                    id: textMetricsjamiBestNameText
-                    font: jamiBestNameText.font
-                    text: {
-                        if (!root.isAudioOnly) {
-                            if (remoteRecordingLabel === "") {
-                                return root.bestName
-                            } else {
-                                return remoteRecordingLabel
-                            }
-                        }
-                        return ""
-                    }
-                    elideWidth: overlayUpperPartRect.width / 3
-                    elide: Qt.ElideRight
-                }
-            }
-
-            Text {
-                id: callTimerText
-                Layout.alignment: Qt.AlignVCenter | Qt.AlignRight
-                Layout.preferredWidth: 64
-                Layout.minimumWidth: 64
-                Layout.preferredHeight: 48
-                Layout.rightMargin: recordingRect.visible?
-                                        0 : JamiTheme.preferredMarginSize
-                font.pointSize: JamiTheme.textFontSize
-                horizontalAlignment: Text.AlignRight
-                verticalAlignment: Text.AlignVCenter
-                text: textMetricscallTimerText.elidedText
-                color: "white"
-                TextMetrics {
-                    id: textMetricscallTimerText
-                    font: callTimerText.font
-                    text: timeText
-                    elideWidth: overlayUpperPartRect.width / 4
-                    elide: Qt.ElideRight
-                }
-            }
-
-            Rectangle {
-                id: recordingRect
-                Layout.alignment: Qt.AlignVCenter | Qt.AlignRight
-                Layout.rightMargin: JamiTheme.preferredMarginSize
-                height: 16
-                width: 16
-                radius: height / 2
-                color: "red"
-
-                SequentialAnimation on color {
-                    loops: Animation.Infinite
-                    running: true
-                    ColorAnimation { from: "red"; to: "transparent";  duration: 500 }
-                    ColorAnimation { from: "transparent"; to: "red"; duration: 500 }
-                }
-            }
-        }
-
-        color: "transparent"
-
-
-        // Rect states: "entered" state should make overlay fade in,
-        //              "freezed" state should make overlay fade out.
-        // Combine with PropertyAnimation of opacity.
-        states: [
-            State {
-                name: "entered"
-                PropertyChanges {
-                    target: overlayUpperPartRect
-                    opacity: 1
-                }
-            },
-            State {
-                name: "freezed"
-                PropertyChanges {
-                    target: overlayUpperPartRect
-                    opacity: 0
-                }
-            }
-        ]
-
-        transitions: Transition {
-            PropertyAnimation {
-                target: overlayUpperPartRect
-                property: "opacity"
-                duration: 1000
-            }
-        }
-    }
-
     ResponsiveImage {
         id: onHoldImage
 
@@ -399,136 +143,167 @@ Rectangle {
         source: "qrc:/images/icons/ic_pause_white_100px.svg"
     }
 
-    CallOverlayButtonGroup {
-        id: callOverlayButtonGroup
-
-        anchors.bottom: root.bottom
-        anchors.bottomMargin: 10
-        anchors.horizontalCenter: root.horizontalCenter
+    Item {
+        id: mainOverlay
 
-        height: 56
-        width: root.width
+        anchors.fill: parent
         opacity: 0
 
-        onChatButtonClicked: {
-            root.overlayChatButtonClicked()
-        }
+        // (un)subscribe to an app-wide mouse move event trap filtered
+        // for the overlay's geometry
+        onVisibleChanged: visible ?
+                              CallOverlayModel.registerFilter(appWindow, this) :
+                              CallOverlayModel.unregisterFilter(appWindow, this)
 
-        onAddToConferenceButtonClicked: {
-            // Create contact picker - conference.
-            ContactPickerCreation.createContactPickerObjects(
-                        ContactList.CONFERENCE,
-                        root)
-            ContactPickerCreation.openContactPicker()
-        }
+        Connections {
+            target: CallOverlayModel
 
-        states: [
-            State {
-                name: "entered"
-                PropertyChanges {
-                    target: callOverlayButtonGroup
-                    opacity: 1
-                }
-            },
-            State {
-                name: "freezed"
-                PropertyChanges {
-                    target: callOverlayButtonGroup
-                    opacity: 0
+            function onMouseMoved(item) {
+                if (item === mainOverlay) {
+                    mainOverlay.opacity = 1
+                    fadeOutTimer.restart()
                 }
             }
-        ]
+        }
 
-        transitions: Transition {
-            PropertyAnimation {
-                target: callOverlayButtonGroup
-                property: "opacity"
-                duration: 1000
+        // control overlay fade out.
+        Timer {
+            id: fadeOutTimer
+            interval: JamiTheme.overlayFadeDelay
+            onTriggered: {
+                if (callOverlayButtonGroup.hovered)
+                    return
+                mainOverlay.opacity = 0
+                resetLabelsTimer.restart()
             }
         }
-    }
 
-    // MouseAreas to make sure that overlay states are correctly set.
-    MouseArea {
-        id: callOverlayButtonGroupLeftSideMouseArea
+        // Timer to reset recording label and call duration time
+        Timer {
+            id: resetLabelsTimer
+            interval: 1000
+            running: root.visible
+            repeat: true
+            onTriggered: {
+                timeText = CallAdapter.getCallDurationTime(LRCInstance.currentAccountId,
+                                                           LRCInstance.selectedConvUid)
+                if (mainOverlay.opacity === 0 && !callViewContextMenu.peerIsRecording)
+                    remoteRecordingLabel = ""
+            }
+        }
 
-        anchors.bottom: root.bottom
-        anchors.left: root.left
+        Item {
+            id: overlayUpperPartRect
 
-        width: root.width / 6
-        height: 60
+            anchors.top: parent.top
 
-        hoverEnabled: true
-        propagateComposedEvents: true
-        acceptedButtons: Qt.NoButton
+            width: parent.width
+            height: 50
 
-        onEntered: {
-            callOverlayRectMouseArea.entered()
-        }
+            RowLayout {
+                anchors.fill: parent
 
-        onMouseXChanged: {
-            callOverlayRectMouseArea.entered()
-        }
-    }
+                Text {
+                    id: jamiBestNameText
 
-    MouseArea {
-        id: callOverlayButtonGroupRightSideMouseArea
+                    Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft
+                    Layout.preferredWidth: overlayUpperPartRect.width / 3
+                    Layout.preferredHeight: 50
+                    leftPadding: 16
 
-        anchors.bottom: root.bottom
-        anchors.right: root.right
+                    font.pointSize: JamiTheme.textFontSize
 
-        width: root.width / 6
-        height: 60
+                    horizontalAlignment: Text.AlignLeft
+                    verticalAlignment: Text.AlignVCenter
 
-        hoverEnabled: true
-        propagateComposedEvents: true
-        acceptedButtons: Qt.NoButton
+                    text: textMetricsjamiBestNameText.elidedText
+                    color: "white"
 
-        onEntered: {
-            callOverlayRectMouseArea.entered()
-        }
+                    TextMetrics {
+                        id: textMetricsjamiBestNameText
+                        font: jamiBestNameText.font
+                        text: {
+                            if (!root.isAudioOnly) {
+                                if (remoteRecordingLabel === "") {
+                                    return root.bestName
+                                } else {
+                                    return remoteRecordingLabel
+                                }
+                            }
+                            return ""
+                        }
+                        elideWidth: overlayUpperPartRect.width / 3
+                        elide: Qt.ElideRight
+                    }
+                }
 
-        onMouseXChanged: {
-            callOverlayRectMouseArea.entered()
-        }
-    }
+                Text {
+                    id: callTimerText
+                    Layout.alignment: Qt.AlignVCenter | Qt.AlignRight
+                    Layout.preferredWidth: 64
+                    Layout.minimumWidth: 64
+                    Layout.preferredHeight: 48
+                    Layout.rightMargin: recordingRect.visible?
+                                            0 : JamiTheme.preferredMarginSize
+                    font.pointSize: JamiTheme.textFontSize
+                    horizontalAlignment: Text.AlignRight
+                    verticalAlignment: Text.AlignVCenter
+                    text: textMetricscallTimerText.elidedText
+                    color: "white"
+                    TextMetrics {
+                        id: textMetricscallTimerText
+                        font: callTimerText.font
+                        text: timeText
+                        elideWidth: overlayUpperPartRect.width / 4
+                        elide: Qt.ElideRight
+                    }
+                }
 
-    MouseArea {
-        id: callOverlayRectMouseArea
+                Rectangle {
+                    id: recordingRect
+                    Layout.alignment: Qt.AlignVCenter | Qt.AlignRight
+                    Layout.rightMargin: JamiTheme.preferredMarginSize
+                    height: 16
+                    width: 16
+                    radius: height / 2
+                    color: "red"
+
+                    SequentialAnimation on color {
+                        loops: Animation.Infinite
+                        running: true
+                        ColorAnimation { from: "red"; to: "transparent";  duration: 500 }
+                        ColorAnimation { from: "transparent"; to: "red"; duration: 500 }
+                    }
+                }
+            }
+        }
 
-        anchors.top: root.top
+        CallOverlayButtonGroup {
+            id: callOverlayButtonGroup
 
-        width: root.width
-        height: root.height
+            anchors.bottom: parent.bottom
+            anchors.bottomMargin: 10
+            anchors.horizontalCenter: parent.horizontalCenter
 
-        hoverEnabled: true
-        propagateComposedEvents: true
-        acceptedButtons: Qt.LeftButton
+            height: 56
+            width: root.width
 
-        function resetStates() {
-            if (overlayUpperPartRect.state !== 'entered') {
-                overlayUpperPartRect.state = 'entered'
+            onChatButtonClicked: {
+                root.overlayChatButtonClicked()
             }
-            if (callOverlayButtonGroup.state !== 'entered') {
-                callOverlayButtonGroup.state = 'entered'
-            }
-            callOverlayTimer.restart()
-        }
 
-        onReleased: {
-            resetStates()
-        }
-        onEntered: {
-            resetStates()
+            onAddToConferenceButtonClicked: {
+                // Create contact picker - conference.
+                ContactPickerCreation.createContactPickerObjects(
+                            ContactList.CONFERENCE,
+                            root)
+                ContactPickerCreation.openContactPicker()
+            }
         }
 
-        onMouseXChanged: {
-            resetStates()
-        }
+        Behavior on opacity { NumberAnimation { duration: JamiTheme.overlayFadeDuration }}
     }
 
-    color: "transparent"
-
     CallViewContextMenu {
         id: callViewContextMenu
 
diff --git a/src/mainview/components/CallOverlayButtonGroup.qml b/src/mainview/components/CallOverlayButtonGroup.qml
index 1e6eee8e3612f36f3fd2581f6f63eef64a029736..4783fb1d40aab295478cdb769fd87f8c68838204 100644
--- a/src/mainview/components/CallOverlayButtonGroup.qml
+++ b/src/mainview/components/CallOverlayButtonGroup.qml
@@ -29,7 +29,7 @@ import net.jami.Constants 1.0
 
 import "../../commoncomponents"
 
-Rectangle {
+Control {
     id: root
 
     // ButtonCounts here is to make sure that flow layout margin is calculated correctly,
@@ -56,7 +56,6 @@ Rectangle {
         noVideoButton.checked = isVideoMuted
     }
 
-    color: "transparent"
     z: 2
 
     RowLayout {
diff --git a/src/mainview/components/OngoingCallPage.qml b/src/mainview/components/OngoingCallPage.qml
index a549db802ee5ab11e9f05963bc1811e0d48299c0..45bfbc0ef617e834e9bf9243c6315d75880e16ac 100644
--- a/src/mainview/components/OngoingCallPage.qml
+++ b/src/mainview/components/OngoingCallPage.qml
@@ -30,7 +30,7 @@ import net.jami.Constants 1.0
 
 import "../../commoncomponents"
 
-Rectangle {
+Rectangle  {
     id: root
 
     property var accountPeerPair: ["", ""]
@@ -45,11 +45,13 @@ Rectangle {
     property alias callId: distantRenderer.rendererId
     property var linkedWebview: null
 
+    color: "black"
+
     onAccountPeerPairChanged: {
         if (accountPeerPair[0] === "" || accountPeerPair[1] === "")
             return;
         contactImage.updateImage(accountPeerPair[1])
-        callOverlay.handleParticipantsInfo(CallAdapter.getConferencesInfos())
+        callOverlay.participantsLayer.update(CallAdapter.getConferencesInfos())
 
         bestName = UtilsAdapter.getBestName(accountPeerPair[0], accountPeerPair[1])
         var id = UtilsAdapter.getBestId(accountPeerPair[0], accountPeerPair[1])
@@ -95,7 +97,7 @@ Rectangle {
         } else {
             bestName = ""
         }
-        callOverlay.handleParticipantsInfo(infos)
+        callOverlay.participantsLayer.update(infos)
     }
 
     function previewMagneticSnap() {
@@ -178,49 +180,6 @@ Rectangle {
                         callOverlay.openCallViewContextMenuInPos(mouse.x, mouse.y)
                 }
 
-                CallOverlay {
-                    id: callOverlay
-
-                    anchors.fill: parent
-
-                    Connections {
-                        target: CallAdapter
-
-                        function onUpdateOverlay(isPaused, isAudioOnly, isAudioMuted, isVideoMuted,
-                                                 isRecording, isSIP, isConferenceCall, bestName) {
-                            callOverlay.showOnHoldImage(isPaused)
-                            audioCallPageRectCentralRect.visible = !isPaused && root.isAudioOnly
-                            callOverlay.updateButtonStatus(isPaused,
-                                                                isAudioOnly,
-                                                                isAudioMuted,
-                                                                isVideoMuted,
-                                                                isRecording, isSIP,
-                                                                isConferenceCall)
-                            root.bestName = bestName
-                            callOverlay.handleParticipantsInfo(CallAdapter.getConferencesInfos())
-                        }
-
-                        function onShowOnHoldLabel(isPaused) {
-                            callOverlay.showOnHoldImage(isPaused)
-                            audioCallPageRectCentralRect.visible = !isPaused && root.isAudioOnly
-                        }
-
-                        function onRemoteRecordingChanged(label, state) {
-                            callOverlay.showRemoteRecording(label, state)
-                        }
-
-                        function onEraseRemoteRecording() {
-                            callOverlay.resetRemoteRecording()
-                        }
-                    }
-
-                    onOverlayChatButtonClicked: {
-                        inCallMessageWebViewStack.visible ?
-                                    closeInCallConversation() :
-                                    openInCallConversation()
-                    }
-                }
-
                 DistantRenderer {
                     id: distantRenderer
 
@@ -232,7 +191,7 @@ Rectangle {
                     visible: !root.isAudioOnly
 
                     onOffsetChanged: {
-                        callOverlay.handleParticipantsInfo(CallAdapter.getConferencesInfos())
+                        callOverlay.participantsLayer.update(CallAdapter.getConferencesInfos())
                     }
                 }
 
@@ -334,6 +293,49 @@ Rectangle {
                     }
                 }
 
+                CallOverlay {
+                    id: callOverlay
+
+                    anchors.fill: parent
+
+                    Connections {
+                        target: CallAdapter
+
+                        function onUpdateOverlay(isPaused, isAudioOnly, isAudioMuted, isVideoMuted,
+                                                 isRecording, isSIP, isConferenceCall, bestName) {
+                            callOverlay.showOnHoldImage(isPaused)
+                            audioCallPageRectCentralRect.visible = !isPaused && root.isAudioOnly
+                            callOverlay.updateButtonStatus(isPaused,
+                                                                isAudioOnly,
+                                                                isAudioMuted,
+                                                                isVideoMuted,
+                                                                isRecording, isSIP,
+                                                                isConferenceCall)
+                            root.bestName = bestName
+                            callOverlay.participantsLayer.update(CallAdapter.getConferencesInfos())
+                        }
+
+                        function onShowOnHoldLabel(isPaused) {
+                            callOverlay.showOnHoldImage(isPaused)
+                            audioCallPageRectCentralRect.visible = !isPaused && root.isAudioOnly
+                        }
+
+                        function onRemoteRecordingChanged(label, state) {
+                            callOverlay.showRemoteRecording(label, state)
+                        }
+
+                        function onEraseRemoteRecording() {
+                            callOverlay.resetRemoteRecording()
+                        }
+                    }
+
+                    onOverlayChatButtonClicked: {
+                        inCallMessageWebViewStack.visible ?
+                                    closeInCallConversation() :
+                                    openInCallConversation()
+                    }
+                }
+
                 ColumnLayout {
                     id: audioCallPageRectCentralRect
                     anchors.centerIn: parent
@@ -400,6 +402,4 @@ Rectangle {
             clip: true
         }
     }
-
-    color: "black"
 }
diff --git a/src/mainview/components/ParticipantOverlay.qml b/src/mainview/components/ParticipantOverlay.qml
index cee174d776d837819fbf64f9601b9806339a80d8..7d166ae171db9b35174b36077a2253763bf26123 100644
--- a/src/mainview/components/ParticipantOverlay.qml
+++ b/src/mainview/components/ParticipantOverlay.qml
@@ -30,7 +30,7 @@ import net.jami.Constants 1.0
 
 import "../../commoncomponents"
 
-Rectangle {
+Item {
     id: root
 
     // svg path for the participant indicators background shape
@@ -38,8 +38,10 @@ Rectangle {
     property int shapeHeight: 16
     property int shapeRadius: 6
     property string pathShape: "M0,0 h%1 q%2,0 %2,%2 v%3 h-%4 z"
-    .arg(shapeWidth-shapeRadius).arg(shapeRadius).arg(shapeHeight-shapeRadius).
-    arg(shapeWidth)
+        .arg(shapeWidth - shapeRadius)
+        .arg(shapeRadius)
+        .arg(shapeHeight - shapeRadius)
+        .arg(shapeWidth)
 
     property string uri: overlayMenu.uri
     property bool participantIsActive: false
@@ -49,6 +51,8 @@ Rectangle {
     property bool participantIsModeratorMuted: false
     property bool participantMenuActive: false
 
+    z: -1
+
     function setAvatar(show, avatar, uri, local, isContact) {
         if (!show)
             contactImage.visible = false
@@ -98,9 +102,6 @@ Rectangle {
         overlayMenu.showHangup = isModerator && !isLocal && !participantIsHost
     }
 
-    color: "transparent"
-    z: 1
-
     // Participant header with host, moderator and mute indicators
     Rectangle {
         id: participantIndicators
@@ -213,90 +214,40 @@ Rectangle {
         layer.smooth: true
     }
 
-    // Participant background, mousearea, hover and buttons for moderation
-    Rectangle {
+    // Participant background and buttons for moderation
+    MouseArea {
         id: participantRect
 
         anchors.fill: parent
         opacity: 0
-        color: "transparent"
         z: 1
 
-        MouseArea {
-            id: mouseAreaHover
-
-            anchors.fill: parent
-            hoverEnabled: true
-            propagateComposedEvents: true
-            acceptedButtons: Qt.LeftButton
-
-            ParticipantOverlayMenu {
-                id: overlayMenu
-                visible: participantRect.opacity !== 0
-
-                onMouseAreaExited: {
-                    root.z = 1
-                    participantRect.state = "exited"
-                }
-                onMouseChanged: {
-                    participantRect.state = "entered"
-                    fadeOutTimer.restart()
-                    participantMenuActive = true
-                }
-            }
-
-            onEntered: {
-                root.z = 2
-                participantRect.state = "entered"
-            }
-
-            onExited: {
-                root.z = 1
-                participantRect.state = "exited"
-            }
-
-            onMouseXChanged: {
-                // Hack: avoid listening mouseXChanged emitted when
-                // ParticipantOverlayMenu is exited
-                if (participantMenuActive) {
-                    participantMenuActive = false
-                } else {
-                    participantRect.state = "entered"
-                    fadeOutTimer.restart()
-                }
-            }
+        propagateComposedEvents: true
+        hoverEnabled: true
+        onPositionChanged: {
+            participantRect.opacity = 1
+            fadeOutTimer.restart()
+            // Here we could call: root.parent.positionChanged(mouse)
+            // to relay the event to a main overlay mouse area, either
+            // as a parent object or some property passed in. But, this
+            // will still fail when hovering over menus, etc.
         }
-
-        states: [
-            State {
-                name: "entered"
-                PropertyChanges {
-                    target: participantRect
-                    opacity: 1
-                }
-            },
-            State {
-                name: "exited"
-                PropertyChanges {
-                    target: participantRect
-                    opacity: 0
-                }
-            }
-        ]
-
-        transitions: Transition {
-            PropertyAnimation {
-                target: participantRect
-                property: "opacity"
-                duration: 50
+        onExited: participantRect.opacity = 0
+        onEntered: participantRect.opacity = 1
+
+        // Timer to decide when ParticipantOverlay fade out
+        Timer {
+            id: fadeOutTimer
+            interval: JamiTheme.overlayFadeDelay
+            onTriggered: {
+                if (overlayMenu.hovered)
+                    return
+                participantRect.opacity = 0
             }
         }
-    }
 
-    // Timer to decide when ParticipantOverlay fade out
-    Timer {
-        id: fadeOutTimer
-        interval: 5000
-        onTriggered: participantRect.state = "exited"
+        ParticipantOverlayMenu { id: overlayMenu }
+
+        Behavior on opacity { NumberAnimation { duration: JamiTheme.shortFadeDuration }}
     }
 }
diff --git a/src/mainview/components/ParticipantOverlayMenu.qml b/src/mainview/components/ParticipantOverlayMenu.qml
index 5f8169414b811c0de870a87fbd5b87e1aea2a820..2535724a9c56ff6137e6e9808a5ced3e8475ce12 100644
--- a/src/mainview/components/ParticipantOverlayMenu.qml
+++ b/src/mainview/components/ParticipantOverlayMenu.qml
@@ -29,7 +29,7 @@ import net.jami.Constants 1.0
 import "../../commoncomponents"
 
 // Overlay menu for conference moderation
-Rectangle {
+Control {
     id: root
 
     property string uri: ""
@@ -71,9 +71,6 @@ Rectangle {
 
     property int isSmall: !isBarLayout && (height < 100 || width < 160)
 
-    signal mouseAreaExited
-    signal mouseChanged
-
     width: isBarLayout? bestNameLabel.contentWidth + buttonsSize + 32
                       : (isOverlayRect? buttonsSize + 32 : parent.width)
     height: isBarLayout? shapeHeight : (isOverlayRect? 80 : parent.height)
@@ -82,20 +79,13 @@ Rectangle {
     anchors.left: isBarLayout? parent.left : undefined
     anchors.centerIn: isBarLayout? undefined : parent
 
-    color: isBarLayout? "transparent" : JamiTheme.darkGreyColorOpacity
-
-    radius: (isBarLayout || !isOverlayRect)? 0 : 10
-
-    MouseArea {
-        id: mouseAreaHover
+    background: Rectangle {
+        color: isBarLayout? "transparent" : JamiTheme.darkGreyColorOpacity
+        radius: (isBarLayout || !isOverlayRect)? 0 : 10
+    }
 
+    Item {
         anchors.fill: parent
-        hoverEnabled: true
-        propagateComposedEvents: true
-        acceptedButtons: Qt.LeftButton
-
-        onExited: mouseAreaExited()
-        onMouseXChanged: mouseChanged()
 
         Shape {
             id: myShape
diff --git a/src/mainview/components/ParticipantsLayer.qml b/src/mainview/components/ParticipantsLayer.qml
new file mode 100644
index 0000000000000000000000000000000000000000..5a15616e702865eb7129f1ae575a81701d28ba48
--- /dev/null
+++ b/src/mainview/components/ParticipantsLayer.qml
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2020 by Savoir-faire Linux
+ * Author: Sébastien Blin <sebastien.blin@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+import QtQuick 2.14
+import QtQml 2.14
+
+Item {
+    id: root
+
+    property var participantOverlays: []
+    property var participantComponent: Qt.createComponent("ParticipantOverlay.qml")
+
+    // returns true if participant is not fully maximized
+    function showMaximize(pX, pY, pW, pH) {
+        // Hack: -1 offset added to avoid problems with odd sizes
+        return (pX - distantRenderer.getXOffset() !== 0
+                || pY - distantRenderer.getYOffset() !== 0
+                || pW < (distantRenderer.width - distantRenderer.getXOffset() * 2 - 1)
+                || pH < (distantRenderer.height - distantRenderer.getYOffset() * 2 - 1))
+    }
+
+    function update(infos) {
+        if (isAudioOnly)
+            return;
+        // TODO: in the future the conference layout should be entirely managed by the client
+        // Hack: truncate and ceil participant's overlay position and size to correct
+        // when they are not exacts
+        callOverlay.updateMenu()
+        var showMax = false
+        var showMin = false
+
+        var deletedUris = []
+        var currentUris = []
+        for (var p in participantOverlays) {
+            if (participantOverlays[p]) {
+                var participant = infos.find(e => e.uri === participantOverlays[p].uri);
+                if (participant) {
+                    // Update participant's information
+                    var newX = Math.trunc(distantRenderer.getXOffset()
+                                          + participant.x * distantRenderer.getScaledWidth())
+                    var newY = Math.trunc(distantRenderer.getYOffset()
+                                          + participant.y * distantRenderer.getScaledHeight())
+
+                    var newWidth = Math.ceil(participant.w * distantRenderer.getScaledWidth())
+                    var newHeight = Math.ceil(participant.h * distantRenderer.getScaledHeight())
+
+                    var newVisible = participant.w !== 0 && participant.h !== 0
+                    if (participantOverlays[p].x !== newX)
+                        participantOverlays[p].x = newX
+                    if (participantOverlays[p].y !== newY)
+                        participantOverlays[p].y = newY
+                    if (participantOverlays[p].width !== newWidth)
+                        participantOverlays[p].width = newWidth
+                    if (participantOverlays[p].height !== newHeight)
+                        participantOverlays[p].height = newHeight
+                    if (participantOverlays[p].visible !== newVisible)
+                        participantOverlays[p].visible = newVisible
+
+                    showMax = showMaximize(participantOverlays[p].x,
+                                           participantOverlays[p].y,
+                                           participantOverlays[p].width,
+                                           participantOverlays[p].height)
+
+                    participantOverlays[p].setMenu(participant.uri, participant.bestName,
+                                                   participant.isLocal, participant.active, showMax)
+                    if (participant.videoMuted)
+                        participantOverlays[p].setAvatar(true, participant.avatar, participant.uri, participant.isLocal, participant.isContact)
+                    else
+                        participantOverlays[p].setAvatar(false)
+                    currentUris.push(participantOverlays[p].uri)
+                } else {
+                    // Participant is no longer in conference
+                    deletedUris.push(participantOverlays[p].uri)
+                    participantOverlays[p].destroy()
+                }
+            }
+        }
+        participantOverlays = participantOverlays.filter(part => !deletedUris.includes(part.uri))
+
+        if (infos.length === 0) { // Return to normal call
+            previewRenderer.visible = !isVideoMuted
+            for (var part in participantOverlays) {
+                if (participantOverlays[part]) {
+                    participantOverlays[part].destroy()
+                }
+            }
+            participantOverlays = []
+        } else {
+            previewRenderer.visible = false
+            for (var infoVariant in infos) {
+                // Only create overlay for new participants
+                if (!currentUris.includes(infos[infoVariant].uri)) {
+                    var hover = participantComponent.createObject(root, {
+                                                                      x: Math.trunc(distantRenderer.getXOffset() + infos[infoVariant].x * distantRenderer.getScaledWidth()),
+                                                                      y: Math.trunc(distantRenderer.getYOffset() + infos[infoVariant].y * distantRenderer.getScaledHeight()),
+                                                                      width: Math.ceil(infos[infoVariant].w * distantRenderer.getScaledWidth()),
+                                                                      height: Math.ceil(infos[infoVariant].h * distantRenderer.getScaledHeight()),
+                                                                      visible: infos[infoVariant].w !== 0 && infos[infoVariant].h !== 0
+                                                                  })
+                    if (!hover) {
+                        console.log("Error when creating the hover")
+                        return
+                    }
+
+                    showMax = showMaximize(hover.x, hover.y, hover.width, hover.height)
+
+                    hover.setMenu(infos[infoVariant].uri, infos[infoVariant].bestName,
+                                  infos[infoVariant].isLocal, infos[infoVariant].active, showMax)
+                    if (infos[infoVariant].videoMuted)
+                        hover.setAvatar(true, infos[infoVariant].avatar, infos[infoVariant].uri, infos[infoVariant].isLocal, infos[infoVariant].isContact)
+                    else
+                        hover.setAvatar(false)
+                    participantOverlays.push(hover)
+                }
+            }
+        }
+    }
+}