diff --git a/storybook/pages/DoubleFlickablePage.qml b/storybook/pages/DoubleFlickablePage.qml
new file mode 100644
index 0000000000..002c6c489f
--- /dev/null
+++ b/storybook/pages/DoubleFlickablePage.qml
@@ -0,0 +1,238 @@
+import QtQuick 2.15
+import QtQuick.Controls 2.15
+import QtQuick.Layouts 1.15
+
+import StatusQ.Core 0.1
+import StatusQ.Core.Utils 0.1
+
+import Storybook 1.0
+
+SplitView {
+ id: root
+
+ orientation: Qt.Vertical
+
+ readonly property int headerSize: 40
+
+ function fillModel(model, count) {
+ const content = []
+
+ for (let i = 0; i < count; i++)
+ content.push({})
+
+ model.clear()
+ model.append(content)
+ }
+
+ function adjustModel(model, newCount) {
+ const countDiff = newCount - model.count
+ const randPos = () => Math.floor(Math.random() * model.count)
+
+ if (countDiff > 0) {
+ for (let i = 0; i < countDiff; i++)
+ model.insert(randPos(), {})
+ } else {
+ for (let i = 0; i < -countDiff; i++)
+ model.remove(randPos())
+ }
+ }
+
+ ListModel {
+ id: firstModel
+
+ Component.onCompleted: fillModel(this, firstSlider.value)
+ }
+
+ ListModel {
+ id: secondModel
+
+ Component.onCompleted: fillModel(this, secondSlider.value)
+ }
+
+ Item {
+ SplitView.fillWidth: true
+ SplitView.fillHeight: true
+
+ Rectangle {
+ id: frame
+
+ anchors.centerIn: parent
+
+ width: Math.round(parent.width / 2)
+ height: Math.round(parent.height / 2)
+ border.width: 1
+ color: "transparent"
+
+ DoubleFlickable {
+ id: doubleFlickable
+
+ anchors.fill: parent
+ clip: clipCheckBox.checked
+ z: -1
+
+ ScrollBar.vertical: ScrollBar { policy: ScrollBar.AlwaysOn }
+
+ flickable1: GridView {
+ width: frame.width
+ interactive: false
+ model: firstModel
+
+ cellWidth: 120
+ cellHeight: 30
+
+ header: Rectangle {
+ height: root.headerSize
+ width: GridView.view.width
+
+ color: "orange"
+
+ Label {
+ anchors.centerIn: parent
+ font.bold: true
+ text: "Community"
+ }
+ }
+
+ delegate: Rectangle {
+ width: GridView.view.cellWidth
+ height: GridView.view.cellHeight
+
+ border.color: "black"
+ color: "lightblue"
+
+ Text {
+ anchors.centerIn: parent
+ text: index
+ }
+ }
+
+ Rectangle {
+ border.color: "green"
+ border.width: 5
+ anchors.fill: parent
+ color: "transparent"
+ }
+ }
+
+ flickable2: GridView {
+ width: frame.width
+ interactive: false
+ model: secondModel
+
+ cellWidth: 100
+ cellHeight: 100
+
+ header: Rectangle {
+ height: root.headerSize
+ width: GridView.view.width
+
+ color: "red"
+
+ Label {
+ anchors.centerIn: parent
+ font.bold: true
+ text: "Others"
+ }
+ }
+
+ delegate: Rectangle {
+ width: GridView.view.cellWidth
+ height: GridView.view.cellHeight
+
+ border.color: "black"
+
+ Text {
+ anchors.centerIn: parent
+ text: index
+ }
+ }
+
+ Rectangle {
+ border.color: "blue"
+ border.width: 5
+ anchors.fill: parent
+ color: "transparent"
+ }
+ }
+ }
+ }
+ }
+
+ LogsAndControlsPanel {
+ SplitView.minimumHeight: 100
+ SplitView.preferredHeight: 200
+ SplitView.fillWidth: true
+
+ Column {
+ CheckBox {
+ id: clipCheckBox
+ text: "clip"
+ checked: true
+ }
+
+ RowLayout {
+ Label {
+ text: "first model:"
+ }
+
+ Slider {
+ id: firstSlider
+ from: 0
+ to: 200
+ stepSize: 1
+
+ value: 160
+
+ onValueChanged: adjustModel(firstModel, value)
+ }
+
+ RoundButton {
+ text: "-"
+ onClicked: firstSlider.decrease()
+ }
+
+ RoundButton {
+ text: "+"
+ onClicked: firstSlider.increase()
+ }
+
+ Label {
+ text: firstSlider.value
+ }
+ }
+
+ RowLayout {
+ Label {
+ text: "second model:"
+ }
+
+ Slider {
+ id: secondSlider
+ from: 0
+ to: 100
+ stepSize: 1
+
+ value: 90
+
+ onValueChanged: adjustModel(secondModel, value)
+ }
+
+ RoundButton {
+ text: "-"
+ onClicked: secondSlider.decrease()
+ }
+
+ RoundButton {
+ text: "+"
+ onClicked: secondSlider.increase()
+ }
+
+ Label {
+ text: secondSlider.value
+ }
+ }
+ }
+ }
+}
+
+// category: Components
diff --git a/ui/StatusQ/src/StatusQ/Core/Utils/DoubleFlickable.qml b/ui/StatusQ/src/StatusQ/Core/Utils/DoubleFlickable.qml
new file mode 100644
index 0000000000..003f3ec52e
--- /dev/null
+++ b/ui/StatusQ/src/StatusQ/Core/Utils/DoubleFlickable.qml
@@ -0,0 +1,114 @@
+import QtQuick 2.15
+import QtQml 2.15
+
+Flickable {
+ id: root
+
+ boundsBehavior: Flickable.StopAtBounds
+ maximumFlickVelocity: 2000
+ synchronousDrag: true
+
+ property Flickable flickable1: Flickable {}
+ property Flickable flickable2: Flickable {}
+
+ readonly property real flickable1ContentHeight: flickable1.contentHeight
+ readonly property real flickable2ContentHeight: flickable2.contentHeight
+
+ onWidthChanged: returnToBounds()
+ onHeightChanged: returnToBounds()
+
+ contentWidth: root.width
+ contentHeight: flickable1ContentHeight + flickable2ContentHeight
+
+ QtObject {
+ id: d
+
+ property real offsetY1
+ property real offsetY2
+
+ Binding on offsetY1 {
+ value: flickable1.originY
+ delayed: true
+ }
+
+ Binding on offsetY2 {
+ value: flickable2.originY
+ delayed: true
+ }
+ }
+
+ // First flickable
+
+ Binding {
+ target: flickable1
+ property: "parent"
+ value: contentItem
+ }
+
+ Binding {
+ target: flickable1
+ property: "interactive"
+ value: false
+ }
+
+ Binding {
+ target: flickable1
+ property: "height"
+ value: Math.min(root.height, flickable1ContentHeight)
+ delayed: true
+ }
+
+ Binding {
+ target: flickable1
+ property: "y"
+ value: Math.min(Math.max(0, root.contentY),
+ flickable1ContentHeight - flickable1.height)
+ }
+
+ Binding {
+ target: flickable1
+ property: "contentY"
+ value: Math.min(Math.max(root.contentY, 0),
+ flickable1ContentHeight - flickable1.height) + d.offsetY1
+
+ delayed: true
+ }
+
+ // Second flickable
+
+ Binding {
+ target: flickable2
+ property: "parent"
+ value: contentItem
+ }
+
+ Binding {
+ target: flickable2
+ property: "interactive"
+ value: false
+ }
+
+ Binding {
+ target: flickable2
+ property: "height"
+ value: Math.min(root.height, flickable2ContentHeight)
+
+ delayed: true
+ }
+
+ Binding {
+ target: flickable2
+ property: "y"
+ value: Math.min(Math.max(flickable1ContentHeight, root.contentY),
+ root.contentHeight - flickable2.height)
+ }
+
+ Binding {
+ target: flickable2
+ property: "contentY"
+ value: Math.min(Math.max(0, root.contentY - flickable1ContentHeight),
+ flickable2ContentHeight - flickable2.height) + d.offsetY2
+
+ delayed: true
+ }
+}
diff --git a/ui/StatusQ/src/StatusQ/Core/Utils/qmldir b/ui/StatusQ/src/StatusQ/Core/Utils/qmldir
index ede0e58c2a..1017291888 100644
--- a/ui/StatusQ/src/StatusQ/Core/Utils/qmldir
+++ b/ui/StatusQ/src/StatusQ/Core/Utils/qmldir
@@ -1,6 +1,7 @@
module StatusQ.Core.Utils
ClippingWrapper 0.1 ClippingWrapper.qml
+DoubleFlickable 0.1 DoubleFlickable.qml
EmojiJSON 1.0 emojiList.js
JSONListModel 0.1 JSONListModel.qml
ModelChangeGuard 0.1 ModelChangeGuard.qml
diff --git a/ui/StatusQ/src/statusq.qrc b/ui/StatusQ/src/statusq.qrc
index 7881b0c600..0d17a8380f 100644
--- a/ui/StatusQ/src/statusq.qrc
+++ b/ui/StatusQ/src/statusq.qrc
@@ -208,6 +208,7 @@
StatusQ/Core/Utils/ModelsComparator.qml
StatusQ/Core/Utils/ModelChangeTracker.qml
StatusQ/Core/Utils/StringUtils.qml
+ StatusQ/Core/Utils/DoubleFlickable.qml
StatusQ/Components/StatusPageIndicator.qml
StatusQ/Components/StatusQrCodeScanner.qml
StatusQ/Components/StatusOnlineBadge.qml