diff --git a/CMakeLists.txt b/CMakeLists.txt
index 6f8c167cb1c4d42fbf28b218c9250cd8834e6b2b..c15093adddc25540270e6f12340676d4c6e2b9dc 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -171,6 +171,7 @@ set(SRC_FILES
     src/ui/Badge.cc
     src/ui/LoadingIndicator.cc
     src/ui/FlatButton.cc
+    src/ui/FloatingButton.cc
     src/ui/Label.cc
     src/ui/OverlayModal.cc
     src/ui/ScrollBar.cc
@@ -224,6 +225,7 @@ qt5_wrap_cpp(MOC_HEADERS
     include/EmojiItemDelegate.h
     include/EmojiPanel.h
     include/EmojiPickButton.h
+    include/ui/FloatingButton.h
     include/ImageItem.h
     include/ImageOverlayDialog.h
     include/JoinRoomDialog.h
diff --git a/include/TimelineView.h b/include/TimelineView.h
index 400b0db0ea1d9842d879fb48fc2e7a541f2803d7..8324794824df13f1889ee716ca9ae886c1b53f71 100644
--- a/include/TimelineView.h
+++ b/include/TimelineView.h
@@ -34,6 +34,8 @@
 #include "RoomInfoListItem.h"
 #include "Text.h"
 
+class FloatingButton;
+
 namespace msgs   = matrix::events::messages;
 namespace events = matrix::events;
 
@@ -155,6 +157,8 @@ private:
         int oldPosition_;
         int oldHeight_;
 
+        FloatingButton *scrollDownBtn_;
+
         TimelineDirection lastMessageDirection_;
 
         // The events currently rendered. Used for duplicate detection.
diff --git a/include/ui/FloatingButton.h b/include/ui/FloatingButton.h
new file mode 100644
index 0000000000000000000000000000000000000000..91e99ebb3b0479b954b48b00d2b8e02425f98514
--- /dev/null
+++ b/include/ui/FloatingButton.h
@@ -0,0 +1,26 @@
+#pragma once
+
+#include "RaisedButton.h"
+
+constexpr int DIAMETER  = 40;
+constexpr int ICON_SIZE = 18;
+
+constexpr int OFFSET_X = 30;
+constexpr int OFFSET_Y = 20;
+
+class FloatingButton : public RaisedButton
+{
+        Q_OBJECT
+
+public:
+        FloatingButton(const QIcon &icon, QWidget *parent = nullptr);
+
+        QSize sizeHint() const override { return QSize(DIAMETER, DIAMETER); };
+        QRect buttonGeometry() const;
+
+protected:
+        bool event(QEvent *event) override;
+        bool eventFilter(QObject *obj, QEvent *event) override;
+
+        void paintEvent(QPaintEvent *event) override;
+};
diff --git a/resources/icons/ui/angle-arrow-down.png b/resources/icons/ui/angle-arrow-down.png
new file mode 100644
index 0000000000000000000000000000000000000000..e40ebca54e3d7c27820e67ec65d850a3bafbfd2a
--- /dev/null
+++ b/resources/icons/ui/angle-arrow-down.png
@@ -0,0 +1,13 @@
+‰PNG
+
+���
IHDR��� ��� ���szzô���sRGB�®Îé���	pHYs��ê��ê¿/G��diTXtXML:com.adobe.xmp�����<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="XMP Core 5.4.0">
+   <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
+      <rdf:Description rdf:about=""
+            xmlns:xmp="http://ns.adobe.com/xap/1.0/">
+         <xmp:CreatorTool>www.inkscape.org</xmp:CreatorTool>
+      </rdf:Description>
+   </rdf:RDF>
+</x:xmpmeta>
+ÜǦm���ïIDATX	í”±Â0D³‚øÄ�‰oçCX€‰
>î*NmRBjw²¥«“4õ»:USŠˆDú,0\õS·de±ÆìÝ¡cvÇvÂÚdEf\/Ð룲‡	ÖdmqÈ$;‹ºimâ.Ùé�±-ZT¶2Q‚“Iv[\=LŒÁÉÌÂÚÄ_p9±2Ñ—‰SŽc\&Z;ao5a
+—‰ÚãpËÄØq챉þáôáwÄgM¢dâ‰ê” Ê¦p½AÉ„ Ê.ðZ®ð_&f—LÌ
+š¸ar…̾v¯ÍKl¤"¢сæ¼&o«x´����IEND®B`‚
\ No newline at end of file
diff --git a/resources/icons/ui/angle-arrow-down@2x.png b/resources/icons/ui/angle-arrow-down@2x.png
new file mode 100644
index 0000000000000000000000000000000000000000..ed095bfea1777f62cf4d99d4832a9371f801bba4
--- /dev/null
+++ b/resources/icons/ui/angle-arrow-down@2x.png
@@ -0,0 +1,12 @@
+‰PNG
+
+���
IHDR���@���@���ªiqÞ���sRGB�®Îé���	pHYs��ê��ê¿/G��diTXtXML:com.adobe.xmp�����<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="XMP Core 5.4.0">
+   <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
+      <rdf:Description rdf:about=""
+            xmlns:xmp="http://ns.adobe.com/xap/1.0/">
+         <xmp:CreatorTool>www.inkscape.org</xmp:CreatorTool>
+      </rdf:Description>
+   </rdf:RDF>
+</x:xmpmeta>
+ÜǦm��*IDATxí˜=NÄ0FƒBB\€Šp*@Ã5(à,GàHÐPÒR±âçû£õz'ö&Ž';–;ëìÆïyâ843`Ì€0fÀ˜3`ÌÀØ
0á³Ä!â-Яñ£SúñŽø@´–ô<#~Ÿˆ+„öB²‰ldv¼"x¢Ä7Úš%pìdÖd\“‚—/i•‚¦	Ç0òâY’¥Ö&!/Ld&{óÔ/_øÂynŽ‘c•qÇj²77=OæÕž	}fÞBöfqp;bmJ¸FÔV8&Ž-6v·Ìdÿ+ø›"¡¶Û!%í)¬d^)Z%/&R%L};ä¤ýÚ̼Ô<ááÞ/±öTcê‚L{öëœLà€J•Qá¢V	Eàs%Œýt |ßMNëj/p}ëZ2¡èÌûrr$¹Yeµ÷!»ŽS%u;L’öm2JK¨
+^¤¤JÈÝ,U‘öí×”0æfiÒχm;ÎÉ‚uð1´Uð¹Úž„/¾ÉˆMëM3AåÌûÒr$\âG©ÿÉᵪ,©b¯Ù¡¾¤WÚ©%A¼HZ‚*xWBÊf)”òƒ½ÒÊ J×›f‚Ê™÷%çJ˜¼ÈH•0+øT	³„ï+aÖð]¶Þ•p‡yôÝ¢Íu¢xÙ)~Åå÷Ð<ÿ?|D½XvYË˜3`Ì€0fÀ˜3`F5ðe‚Æ¢´o����IEND®B`‚
\ No newline at end of file
diff --git a/resources/res.qrc b/resources/res.qrc
index 59d6559d974413c9c5295d2c65b922589bed2b45..55962275cf143021e9c5af4d7364eb34600837f5 100644
--- a/resources/res.qrc
+++ b/resources/res.qrc
@@ -22,6 +22,8 @@
         <file>icons/ui/paper-clip-outline@2x.png</file>
         <file>icons/ui/angle-pointing-to-left.png</file>
         <file>icons/ui/angle-pointing-to-left@2x.png</file>
+        <file>icons/ui/angle-arrow-down.png</file>
+        <file>icons/ui/angle-arrow-down@2x.png</file>
 
         <file>icons/emoji-categories/people.png</file>
         <file>icons/emoji-categories/people@2x.png</file>
diff --git a/src/TimelineView.cc b/src/TimelineView.cc
index 2142f546db85c449d051d3a5cafdc45c517ffb98..132090627108e7fcc2d084c6d9ab17066def431e 100644
--- a/src/TimelineView.cc
+++ b/src/TimelineView.cc
@@ -27,6 +27,7 @@
 #include "MessageEvent.h"
 #include "MessageEventContent.h"
 
+#include "FloatingButton.h"
 #include "ImageItem.h"
 #include "TimelineItem.h"
 #include "TimelineView.h"
@@ -140,6 +141,16 @@ TimelineView::sliderMoved(int position)
         if (!scroll_area_->verticalScrollBar()->isVisible())
                 return;
 
+        const int maxScroll     = scroll_area_->verticalScrollBar()->maximum();
+        const int currentScroll = scroll_area_->verticalScrollBar()->value();
+
+        if (maxScroll - currentScroll > SCROLL_BAR_GAP) {
+                scrollDownBtn_->show();
+                scrollDownBtn_->raise();
+        } else {
+                scrollDownBtn_->hide();
+        }
+
         // The scrollbar is high enough so we can start retrieving old events.
         if (position < SCROLL_BAR_GAP) {
                 if (isTimelineFinished)
@@ -376,6 +387,18 @@ TimelineView::init()
         QSettings settings;
         local_user_ = settings.value("auth/user_id").toString();
 
+        QIcon icon;
+        icon.addFile(":/icons/icons/ui/angle-arrow-down.png");
+        scrollDownBtn_ = new FloatingButton(icon, this);
+        scrollDownBtn_->setBackgroundColor(QColor("#F5F5F5"));
+        scrollDownBtn_->setForegroundColor(QColor("black"));
+        scrollDownBtn_->hide();
+
+        connect(scrollDownBtn_, &QPushButton::clicked, this, [=]() {
+                const int max = scroll_area_->verticalScrollBar()->maximum();
+                scroll_area_->verticalScrollBar()->setValue(max);
+        });
+
         top_layout_ = new QVBoxLayout(this);
         top_layout_->setSpacing(0);
         top_layout_->setMargin(0);
diff --git a/src/ui/FloatingButton.cc b/src/ui/FloatingButton.cc
new file mode 100644
index 0000000000000000000000000000000000000000..74dcd482ffbf4a9e9fdd8be75438424aed9867e9
--- /dev/null
+++ b/src/ui/FloatingButton.cc
@@ -0,0 +1,95 @@
+#include <QPainterPath>
+
+#include "FloatingButton.h"
+
+FloatingButton::FloatingButton(const QIcon &icon, QWidget *parent)
+  : RaisedButton(parent)
+{
+        setFixedSize(DIAMETER, DIAMETER);
+        setGeometry(buttonGeometry());
+
+        if (parentWidget())
+                parentWidget()->installEventFilter(this);
+
+        setFixedRippleRadius(50);
+        setIcon(icon);
+        raise();
+}
+
+QRect
+FloatingButton::buttonGeometry() const
+{
+        QWidget *parent = parentWidget();
+
+        if (!parent)
+                return QRect();
+
+        return QRect(parent->width() - (OFFSET_X + DIAMETER),
+                     parent->height() - (OFFSET_Y + DIAMETER),
+                     DIAMETER,
+                     DIAMETER);
+}
+
+bool
+FloatingButton::event(QEvent *event)
+{
+        if (!parent())
+                return RaisedButton::event(event);
+
+        switch (event->type()) {
+        case QEvent::ParentChange: {
+                parent()->installEventFilter(this);
+                setGeometry(buttonGeometry());
+                break;
+        }
+        case QEvent::ParentAboutToChange: {
+                parent()->installEventFilter(this);
+                break;
+        }
+        default:
+                break;
+        }
+
+        return RaisedButton::event(event);
+}
+
+bool
+FloatingButton::eventFilter(QObject *obj, QEvent *event)
+{
+        const QEvent::Type type = event->type();
+
+        if (QEvent::Move == type || QEvent::Resize == type)
+                setGeometry(buttonGeometry());
+
+        return RaisedButton::eventFilter(obj, event);
+}
+
+void
+FloatingButton::paintEvent(QPaintEvent *event)
+{
+        Q_UNUSED(event);
+
+        QRect square = QRect(0, 0, DIAMETER, DIAMETER);
+        square.moveCenter(rect().center());
+
+        QPainter p(this);
+        p.setRenderHints(QPainter::Antialiasing);
+
+        QBrush brush;
+        brush.setStyle(Qt::SolidPattern);
+        brush.setColor(backgroundColor());
+
+        p.setBrush(brush);
+        p.setPen(Qt::NoPen);
+        p.drawEllipse(square);
+
+        QRect iconGeometry(0, 0, ICON_SIZE, ICON_SIZE);
+        iconGeometry.moveCenter(square.center());
+
+        QPixmap pixmap = icon().pixmap(QSize(ICON_SIZE, ICON_SIZE));
+        QPainter icon(&pixmap);
+        icon.setCompositionMode(QPainter::CompositionMode_SourceIn);
+        icon.fillRect(pixmap.rect(), foregroundColor());
+
+        p.drawPixmap(iconGeometry, pixmap);
+}