diff --git a/briar-gtk/briar_gtk/actions/window.py b/briar-gtk/briar_gtk/actions/window.py
new file mode 100644
index 0000000000000000000000000000000000000000..aed217e19fd471938010d08b9b50b2e1cfe05b02
--- /dev/null
+++ b/briar-gtk/briar_gtk/actions/window.py
@@ -0,0 +1,50 @@
+# Copyright (c) 2014-2020 Cedric Bellegarde <cedric.bellegarde@adishatz.org>
+# Copyright (c) 2020 Nico Alt
+# SPDX-License-Identifier: AGPL-3.0-only
+# License-Filename: LICENSE.md
+#
+# Initial version based on GNOME Lollypop
+# https://gitlab.gnome.org/World/lollypop/blob/1.2.20/lollypop/application_actions.py
+
+from gi.repository import Gio, GLib
+
+from briar_gtk.containers.main import MainContainer
+from briar_gtk.define import APP
+
+
+# pylint: disable=too-few-public-methods
+class WindowActions:
+
+    def __init__(self):
+        self._setup_actions()
+
+    # pylint: disable=no-member
+    def _setup_actions(self):
+        back_to_sidebar_action = Gio.SimpleAction.new(
+            "back-to-sidebar", None)
+        back_to_sidebar_action.connect("activate", self._back_to_sidebar)
+        APP().set_accels_for_action("win.back-to-sidebar", ["<Ctrl>w"])
+        self.add_action(back_to_sidebar_action)
+
+        open_add_contact_action = Gio.SimpleAction.new(
+            "open-add-contact", None)
+        open_add_contact_action.connect("activate", self._open_add_contact)
+        self.add_action(open_add_contact_action)
+
+        open_private_chat_action = Gio.SimpleAction.new(
+            "open-private-chat", GLib.VariantType.new("i"))
+        open_private_chat_action.connect("activate", self._open_private_chat)
+        self.add_action(open_private_chat_action)
+
+    # pylint: disable=unused-argument
+    def _back_to_sidebar(self, action, parameter):
+        if isinstance(self.current_container, MainContainer):
+            self.current_container.show_sidebar()
+
+    # pylint: disable=unused-argument
+    def _open_add_contact(self, action, parameter):
+        self.show_add_contact_container()
+
+    # pylint: disable=unused-argument
+    def _open_private_chat(self, action, contact_id):
+        self.current_container.open_private_chat(contact_id.get_int32())
diff --git a/briar-gtk/briar_gtk/containers/chat.py b/briar-gtk/briar_gtk/containers/chat.py
deleted file mode 100644
index 11e2e0b0414c67fa6ff8a2172e7c3b14bf6b5841..0000000000000000000000000000000000000000
--- a/briar-gtk/briar_gtk/containers/chat.py
+++ /dev/null
@@ -1,60 +0,0 @@
-# Copyright (c) 2019 Nico Alt
-# SPDX-License-Identifier: AGPL-3.0-only
-# License-Filename: LICENSE.md
-
-from gi.repository import GLib, Gtk
-
-from briar_wrapper.models.private_chat import PrivateChat
-
-from briar_gtk.container import Container
-from briar_gtk.define import APP
-
-
-class ChatContainer(Container):
-
-    CONTAINER_UI = "/app/briar/gtk/ui/chat.ui"
-
-    def __init__(self, contact_id):
-        super().__init__()
-        self._api = APP().api
-        self._contact_id = contact_id
-        self._setup_view()
-        self._load_content()
-
-    def _setup_view(self):
-        self.builder.add_from_resource(self.CONTAINER_UI)
-        self.add(self.builder.get_object("chat"))
-        self.builder.connect_signals(self)
-        chat_entry = self.builder.get_object("chat_entry")
-        chat_entry.connect("key-press-event", self._key_pressed)
-
-    def _load_content(self):
-        private_chat = PrivateChat(self._api, self._contact_id)
-        messages_list = private_chat.get()
-        self._messages_list_box = self.builder.get_object("messages_list")
-        for message in messages_list:
-            self._add_message(message["text"], message["local"])
-        private_chat.watch_messages(self._add_message_async)
-
-    def _add_message(self, message, local):
-        message_label = Gtk.Label(message)
-        message_label.set_halign(Gtk.Align.START)
-        if local:
-            message_label.set_halign(Gtk.Align.END)
-        message_label.show()
-        self._messages_list_box.add(message_label)
-
-    def _add_message_async(self, message):
-        GLib.idle_add(self._add_message, message["text"], False)
-
-    # pylint: disable=unused-argument
-    def _key_pressed(self, widget, event):
-        if event.hardware_keycode != 36 and event.hardware_keycode != 104:
-            return
-        chat_entry = self.builder.get_object("chat_entry")
-        message = chat_entry.get_text()
-        private_chat = PrivateChat(self._api, self._contact_id)
-        private_chat.send(message)
-
-        self._add_message(message, True)
-        chat_entry.set_text("")
diff --git a/briar-gtk/briar_gtk/containers/login.py b/briar-gtk/briar_gtk/containers/login.py
index 7561f00bdc4d3b76623814bc6fa2ddc38286ad07..c71fc856a189c540b2b5b03dab7361a084c97989 100644
--- a/briar-gtk/briar_gtk/containers/login.py
+++ b/briar-gtk/briar_gtk/containers/login.py
@@ -64,7 +64,7 @@ class LoginContainer(Container):
     def _login_completed(self, succeeded):
         function = self._login_failed
         if succeeded:
-            function = self._window.on_startup_completed
+            function = self._window.show_main_container
         GLib.idle_add(function)
 
     def _login_failed(self):
diff --git a/briar-gtk/briar_gtk/containers/main.py b/briar-gtk/briar_gtk/containers/main.py
index 144aa969329de9195fe568bb178a53373104a4ff..7e567ec1d8dfe8cfd8890bc97e36afacfd743f73 100644
--- a/briar-gtk/briar_gtk/containers/main.py
+++ b/briar-gtk/briar_gtk/containers/main.py
@@ -1,12 +1,17 @@
 # Copyright (c) 2019 Nico Alt
 # SPDX-License-Identifier: AGPL-3.0-only
 # License-Filename: LICENSE.md
+#
+# Initial version based on GNOME Fractal
+# https://gitlab.gnome.org/GNOME/fractal/-/tags/4.2.2
 
-from gi.repository import GLib, Gtk
+from gi.repository import GLib
 
 from briar_wrapper.models.contacts import Contacts
 
 from briar_gtk.container import Container
+from briar_gtk.widgets.contact_row import ContactRowWidget
+from briar_gtk.containers.private_chat import PrivateChatContainer
 from briar_gtk.define import APP
 
 
@@ -16,32 +21,114 @@ class MainContainer(Container):
 
     def __init__(self):
         super().__init__()
-        self._api = APP().api
         self._setup_view()
         self._load_content()
 
+    @property
+    def main_window_leaflet(self):
+        return self.builder.get_object("main_window_leaflet")
+
+    @property
+    def room_name_label(self):
+        return self.builder.get_object("room_name")
+
+    @property
+    def contacts_list_box(self):
+        return self.builder.get_object("contacts_list_box")
+
+    @property
+    def main_content_stack(self):
+        return self.builder.get_object("main_content_stack")
+
+    @property
+    def main_content_container(self):
+        return self.builder.get_object("main_content_container")
+
+    @property
+    def chat_placeholder(self):
+        return self.main_content_stack.get_child_by_name("chat_placeholder")
+
+    @property
+    def chat_view(self):
+        return self.main_content_stack.get_child_by_name("chat_view")
+
+    @property
+    def history_container(self):
+        return self.builder.get_object("history_container")
+
+    @property
+    def chat_entry(self):
+        return self.builder.get_object("chat_entry")
+
+    def open_private_chat(self, contact_id):
+        contact_name = self._get_contact_name(contact_id)
+        self._prepare_chat_view(contact_name)
+        self._setup_private_chat_widget(contact_name, contact_id)
+
+    def _prepare_chat_view(self, contact_name):
+        if self._no_chat_opened():
+            self.chat_placeholder.hide()
+        else:
+            self._clear_history_container()
+
+        self.chat_view.show()
+        self.main_window_leaflet.set_visible_child(
+            self.main_content_container)
+        self.room_name_label.set_text(contact_name)
+
+    def _setup_private_chat_widget(self, contact_name, contact_id):
+        private_chat_widget = PrivateChatContainer(contact_name, contact_id)
+        self.history_container.add(private_chat_widget)
+        self.history_container.show_all()
+        self.chat_entry.connect("activate", private_chat_widget.send_message)
+
+    def _no_chat_opened(self):
+        return self.chat_placeholder.get_visible()
+
+    def _get_contact_name(self, contact_id):
+        name = ""
+        for contact in self.contacts_list:
+            if contact["contactId"] is contact_id:
+                name = contact["author"]["name"]
+                if "alias" in contact:
+                    name = contact["alias"]
+                break
+        return name
+
+    def _clear_history_container(self):
+        children = self.history_container.get_children()
+        for child in children:
+            self.history_container.remove(child)
+
     def _setup_view(self):
         self.builder.add_from_resource(self.CONTAINER_UI)
-        self.add(self.builder.get_object("contacts_list"))
         self.builder.connect_signals(self)
 
+        self._setup_main_window_stack()
+        self._setup_headerbar_stack_holder()
+        self.room_name_label.set_text("")
+
+    def _setup_main_window_stack(self):
+        main_window_stack = self.builder.get_object("main_window_stack")
+        main_window_stack.show_all()
+        self.add(main_window_stack)
+
+    def _setup_headerbar_stack_holder(self):
+        headerbar_stack_holder = self.builder.get_object(
+            "headerbar_stack_holder")
+        headerbar_stack_holder.show_all()
+        APP().window.set_titlebar(headerbar_stack_holder)
+
     def _load_content(self):
-        self._contacts = Contacts(self._api)
+        self._contacts = Contacts(APP().api)
         self._load_contacts()
         self._contacts.watch_contacts(self._refresh_contacts_async)
 
     def _load_contacts(self):
-        contacts_list = self._contacts.get()
-        contacts_list_box = self.builder.get_object("contacts_list")
-        for contact in contacts_list:
-            name = contact["author"]["name"]
-            if "alias" in contact:
-                name = contact["alias"]
-            contact_button = Gtk.Button(name)
-            contact_button.connect("clicked", MainContainer._contact_clicked,
-                                   contact["contactId"])
-            contact_button.show()
-            contacts_list_box.add(contact_button)
+        self.contacts_list = self._contacts.get()
+        for contact in self.contacts_list:
+            contact_row = ContactRowWidget(contact)
+            self.contacts_list_box.add(contact_row)
 
     def _refresh_contacts_async(self):
         GLib.idle_add(self._refresh_contacts)
@@ -51,13 +138,16 @@ class MainContainer(Container):
         self._load_contacts()
 
     def _clear_contact_list(self):
-        contacts_list_box = self.builder.get_object("contacts_list")
-        contacts_list_box_children = contacts_list_box.get_children()
+        contacts_list_box_children = self.contacts_list_box.get_children()
         for child in contacts_list_box_children:
-            contacts_list_box.remove(child)
+            self.contacts_list_box.remove(child)
 
     # pylint: disable=unused-argument
-    @staticmethod
-    def _contact_clicked(widget, contact_id):
-        GLib.idle_add(APP().get_property("active_window").
-                      open_private_chat, contact_id)
+    def show_sidebar(self):
+        self.main_window_leaflet.set_visible_child(
+            self.builder.get_object("sidebar_box"))
+        self.chat_view.hide()
+        self.chat_placeholder.show()
+        self._clear_history_container()
+        self.contacts_list_box.unselect_all()
+        self.room_name_label.set_text("")
diff --git a/briar-gtk/briar_gtk/containers/private_chat.py b/briar-gtk/briar_gtk/containers/private_chat.py
new file mode 100644
index 0000000000000000000000000000000000000000..54c63d996a5580c214c952426b6d78b1f1eddfb3
--- /dev/null
+++ b/briar-gtk/briar_gtk/containers/private_chat.py
@@ -0,0 +1,83 @@
+# Copyright (c) 2019 Nico Alt
+# SPDX-License-Identifier: AGPL-3.0-only
+# License-Filename: LICENSE.md
+#
+# Initial version based on GNOME Fractal
+# https://gitlab.gnome.org/GNOME/fractal/-/tags/4.2.2
+
+import time
+
+from gi.repository import GLib, Gtk, Handy
+
+from briar_wrapper.models.private_chat import PrivateChat
+
+from briar_gtk.container import Container
+from briar_gtk.define import APP
+from briar_gtk.widgets.private_message import PrivateMessageWidget
+
+
+# pylint: disable=too-few-public-methods
+class PrivateChatContainer(Container):
+
+    CONTAINER_UI = "/app/briar/gtk/ui/private_chat.ui"
+
+    def __init__(self, contact_name, contact_id):
+        super().__init__()
+
+        self._contact_name = contact_name
+        self._contact_id = contact_id
+
+        self._setup_view()
+        self._load_content()
+
+    def _setup_view(self):
+        self.builder.add_from_resource(self.CONTAINER_UI)
+
+        self._messages_box = Gtk.ListBox()
+        self._messages_box.get_style_context().add_class("messages-history")
+        self._messages_box.show()
+
+        column = Handy.Column()
+        column.set_maximum_width(800)
+        column.set_linear_growth_width(600)
+        column.set_hexpand(True)
+        column.set_vexpand(True)
+        column.add(self._messages_box)
+        column.show()
+
+        messages_column = self.builder.get_object("messages_column")
+        messages_column.get_style_context().add_class("messages-box")
+        messages_column.add(column)
+        messages_column.show()
+
+        self.add(self.builder.get_object("messages_scroll"))
+
+        self.builder.connect_signals(self)
+
+    def _load_content(self):
+        private_chat = PrivateChat(APP().api, self._contact_id)
+        messages_list = private_chat.get()
+        for message in messages_list:
+            self._add_message(message)
+        private_chat.watch_messages(self._add_message_async)
+
+    def _add_message(self, message):
+        message = PrivateMessageWidget(self._contact_name, message)
+        self._messages_box.add(message)
+
+    def _add_message_async(self, message):
+        GLib.idle_add(self._add_message, message)
+
+    # pylint: disable=unused-argument
+    def send_message(self, widget):
+        message = widget.get_text()
+        private_chat = PrivateChat(APP().api, self._contact_id)
+        private_chat.send(message)
+
+        self._add_message(
+            {
+                "text": message,
+                "local": True,
+                "timestamp": int(round(time.time() * 1000))
+            })
+        widget.set_text("")
diff --git a/briar-gtk/briar_gtk/containers/registration.py b/briar-gtk/briar_gtk/containers/registration.py
index d98b0b5c6c0ec10f3214ac550a6c7f989778291f..e4ba235c7c623e2ae560a2a691bd2fd6c50b4009 100644
--- a/briar-gtk/briar_gtk/containers/registration.py
+++ b/briar-gtk/briar_gtk/containers/registration.py
@@ -124,7 +124,7 @@ class RegistrationContainer(Container):
     def _registration_completed(self, succeeded):
         function = self._registration_failed
         if succeeded:
-            function = self._window.on_startup_completed
+            function = self._window.show_main_container
         GLib.idle_add(function)
 
     def _registration_failed(self):
diff --git a/briar-gtk/briar_gtk/containers/startup.py b/briar-gtk/briar_gtk/containers/startup.py
index d379f0075841b4011d3347905ac681ff2a47421d..8da951c337d35c474d483541c169a720ded10120 100644
--- a/briar-gtk/briar_gtk/containers/startup.py
+++ b/briar-gtk/briar_gtk/containers/startup.py
@@ -19,5 +19,4 @@ class StartupContainer(Container):
         if APP().api.has_account():
             container = LoginContainer(window)
 
-        container.show()
         self.add(container)
diff --git a/briar-gtk/briar_gtk/toolbar.py b/briar-gtk/briar_gtk/toolbar.py
deleted file mode 100644
index 3c456f6a28e0f59d804a1e629fb0d331db9a26a5..0000000000000000000000000000000000000000
--- a/briar-gtk/briar_gtk/toolbar.py
+++ /dev/null
@@ -1,51 +0,0 @@
-# Copyright (c) 2019 Nico Alt
-# Copyright (c) 2014-2019 Cedric Bellegarde <cedric.bellegarde@adishatz.org>
-# SPDX-License-Identifier: AGPL-3.0-only
-# License-Filename: LICENSE.md
-#
-# Initial version based on GNOME Lollypop
-# https://gitlab.gnome.org/World/lollypop/blob/1.0.12/lollypop/toolbar.py
-
-from gi.repository import Gtk
-
-from briar_gtk.define import APPLICATION_NAME
-
-
-class Toolbar(Gtk.HeaderBar):
-
-    TOOLBAR_UI = "/app/briar/gtk/ui/toolbar_start.ui"
-
-    def __init__(self):
-        super().__init__()
-        self._setup_builder()
-        self._setup_toolbar()
-
-    def show_add_contact_button(self, show, callback=None):
-        add_contact_button = self._builder.get_object("add_contact_button")
-        if not show:
-            add_contact_button.hide()
-            return
-        if callback is None:
-            raise Exception("Callback needed when showing add contact button")
-        add_contact_button.show()
-        add_contact_button.connect("clicked", callback)
-
-    def show_back_button(self, show, callback=None):
-        back_button = self._builder.get_object("back_button")
-        if not show:
-            back_button.hide()
-            return
-        if callback is None:
-            raise Exception("Callback needed when showing back button")
-        back_button.show()
-        back_button.connect("clicked", callback)
-
-    def _setup_builder(self):
-        self._builder = Gtk.Builder()
-
-    def _setup_toolbar(self):
-        self.set_title(APPLICATION_NAME)
-
-        self._builder.add_from_resource(self.TOOLBAR_UI)
-        toolbar_start = self._builder.get_object("toolbar_start")
-        self.pack_start(toolbar_start)
diff --git a/briar-gtk/briar_gtk/widgets/contact_row.py b/briar-gtk/briar_gtk/widgets/contact_row.py
new file mode 100644
index 0000000000000000000000000000000000000000..09aaa5195df98e7e71771fe13496222dc432ec50
--- /dev/null
+++ b/briar-gtk/briar_gtk/widgets/contact_row.py
@@ -0,0 +1,54 @@
+# Copyright (c) 2020 Nico Alt
+# SPDX-License-Identifier: AGPL-3.0-only
+# License-Filename: LICENSE.md
+#
+# Initial version based on GNOME Fractal
+# https://gitlab.gnome.org/GNOME/fractal/-/tags/4.2.2
+
+from gi.repository import GLib, Gtk, Pango
+
+
+class ContactRowWidget(Gtk.ListBoxRow):
+
+    def __init__(self, contact):
+        super().__init__()
+        self._setup_view(contact)
+
+    def _setup_view(self, contact):
+        name = ContactRowWidget._get_contact_name(contact)
+        contact_label = ContactRowWidget._create_contact_label(name)
+        contact_box = ContactRowWidget._create_contact_box()
+        contact_event_box = Gtk.EventBox()
+
+        contact_box.pack_start(contact_label, True, True, 0)
+        contact_event_box.add(contact_box)
+        self.add(contact_event_box)
+
+        self.show_all()
+        self._set_action(contact["contactId"])
+
+    @staticmethod
+    def _get_contact_name(contact):
+        name = contact["author"]["name"]
+        if "alias" in contact:
+            name = contact["alias"]
+        return name
+
+    @staticmethod
+    def _create_contact_label(name):
+        contact_label = Gtk.Label(name)
+        contact_label.set_valign(Gtk.Align.CENTER)
+        contact_label.set_halign(Gtk.Align.START)
+        contact_label.set_ellipsize(Pango.EllipsizeMode.END)
+        return contact_label
+
+    @staticmethod
+    def _create_contact_box():
+        contact_box = Gtk.Box(Gtk.Orientation.HORIZONTAL, 5)
+        contact_box.get_style_context().add_class("room-row")
+        return contact_box
+
+    def _set_action(self, contact_id):
+        data = GLib.Variant.new_int32(contact_id)
+        self.set_action_target_value(data)
+        self.set_action_name("win.open-private-chat")
diff --git a/briar-gtk/briar_gtk/widgets/private_message.py b/briar-gtk/briar_gtk/widgets/private_message.py
new file mode 100644
index 0000000000000000000000000000000000000000..9c665280b41d77ca4775aab4d106549a1c78e66e
--- /dev/null
+++ b/briar-gtk/briar_gtk/widgets/private_message.py
@@ -0,0 +1,97 @@
+# Copyright (c) 2020 Nico Alt
+# SPDX-License-Identifier: AGPL-3.0-only
+# License-Filename: LICENSE.md
+#
+# Initial version based on GNOME Fractal
+# https://gitlab.gnome.org/GNOME/fractal/-/tags/4.2.2
+
+import datetime
+from gettext import gettext as _
+
+from gi.repository import Gtk
+
+
+class PrivateMessageWidget(Gtk.ListBoxRow):
+
+    def __init__(self, contact_name, message):
+        super().__init__()
+        self._setup_view(contact_name, message)
+
+    def _setup_view(self, contact_name, message):
+        self.set_selectable(False)
+        self.set_margin_top(12)
+
+        username = contact_name
+        if message["local"]:
+            username = _("Myself")
+
+        username_info = PrivateMessageWidget._create_username_info(username)
+        date_info = PrivateMessageWidget._create_date_info(
+            message["timestamp"] / 1000)
+        info = PrivateMessageWidget._create_info(username_info, date_info)
+
+        body_content = PrivateMessageWidget._create_body_content(
+            message["text"])
+        body = PrivateMessageWidget._create_body(body_content)
+
+        content = PrivateMessageWidget._create_content(info, body)
+        message_box = PrivateMessageWidget._create_message_box(content)
+
+        event_box = Gtk.EventBox()
+        event_box.add(message_box)
+
+        self.add(event_box)
+        self.show_all()
+
+    @staticmethod
+    def _create_username_info(username):
+        username_label = Gtk.Label.new(username)
+        username_label.set_justify(Gtk.Justification.LEFT)
+        username_label.set_halign(Gtk.Align.START)
+        username_label.get_style_context().add_class("username")
+
+        username_event_box = Gtk.EventBox()
+        username_event_box.add(username_label)
+        return username_event_box
+
+    @staticmethod
+    def _create_date_info(time):
+        date_label = Gtk.Label.new(
+            datetime.datetime.fromtimestamp(time).strftime("%I:%M"))
+        date_label.set_justify(Gtk.Justification.RIGHT)
+        date_label.set_valign(Gtk.Align.START)
+        date_label.set_halign(Gtk.Align.END)
+        date_label.get_style_context().add_class("timestamp")
+        return date_label
+
+    @staticmethod
+    def _create_info(username_info, date_info):
+        info = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 0)
+        info.pack_start(username_info, True, True, 0)
+        info.pack_end(date_info, False, False, 0)
+        return info
+
+    @staticmethod
+    def _create_body_content(text):
+        body_content = Gtk.Label.new(text)
+        body_content.set_halign(Gtk.Align.START)
+        return body_content
+
+    @staticmethod
+    def _create_body(body_content):
+        body = Gtk.Box.new(Gtk.Orientation.VERTICAL, 0)
+        body.add(body_content)
+        return body
+
+    @staticmethod
+    def _create_content(info, body):
+        content = Gtk.Box.new(Gtk.Orientation.VERTICAL, 0)
+        content.pack_start(info, False, False, 0)
+        content.pack_start(body, True, True, 0)
+        return content
+
+    @staticmethod
+    def _create_message_box(content):
+        message_box = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 10)
+        message_box.pack_start(content, True, True, 0)
+        return message_box
diff --git a/briar-gtk/briar_gtk/window.py b/briar-gtk/briar_gtk/window.py
index bd107602bd36a545211652c63c9af1f92b07b461..d54cf4552af16435b0afb21272a985b93d1e3c9f 100644
--- a/briar-gtk/briar_gtk/window.py
+++ b/briar-gtk/briar_gtk/window.py
@@ -2,44 +2,45 @@
 # SPDX-License-Identifier: AGPL-3.0-only
 # License-Filename: LICENSE.md
 
-from gi.repository.Gtk import ApplicationWindow, Grid
+from gi.repository import Gtk
 
+from briar_gtk.actions.window import WindowActions
 from briar_gtk.containers.add_contact import AddContactContainer
-from briar_gtk.containers.chat import ChatContainer
 from briar_gtk.containers.main import MainContainer
 from briar_gtk.containers.startup import StartupContainer
 from briar_gtk.define import APP, APPLICATION_ID, APPLICATION_NAME
-from briar_gtk.toolbar import Toolbar
 
 
-class Window(ApplicationWindow):
+class Window(Gtk.ApplicationWindow, WindowActions):
 
-    DEFAULT_WINDOW_SIZE = (600, 400)
+    DEFAULT_WINDOW_SIZE = (900, 600)
 
     def __init__(self):
         self._initialize_gtk_application_window()
+        WindowActions.__init__(self)
         self._setup_content()
 
-    @property
-    def container(self):
-        return self._container
+    def show_main_container(self):
+        self.current_container.destroy()
+        self._setup_main_container()
 
-    @property
-    def toolbar(self):
-        return self._toolbar
+    def show_add_contact_container(self):
+        self.current_container.destroy()
+        self._setup_add_contact_container()
 
     def _initialize_gtk_application_window(self):
-        ApplicationWindow.__init__(self, application=APP(),
-                                   title=APPLICATION_NAME,
-                                   icon_name=APPLICATION_ID)
+        Gtk.ApplicationWindow.__init__(self, application=APP(),
+                                       title=APPLICATION_NAME,
+                                       icon_name=APPLICATION_ID)
 
     def _setup_content(self):
-        self._setup_size(self.DEFAULT_WINDOW_SIZE)
+        self._resize_window(self.DEFAULT_WINDOW_SIZE)
         self._setup_startup_container()
 
-    def _setup_size(self, size):
-        if Window._size_is_valid(size):
-            self.resize(size[0], size[1])
+    def _resize_window(self, size):
+        if not Window._size_is_valid(size):
+            raise Exception("Couldn't resize window; invalid size parameter")
+        self.resize(size[0], size[1])
 
     @staticmethod
     def _size_is_valid(size):
@@ -47,63 +48,16 @@ class Window(ApplicationWindow):
                isinstance(size[0], int) and\
                isinstance(size[1], int)
 
-    def _setup_startup_container(self):
-        self._container = StartupContainer(self)
-        self._container.show()
-        self.add(self._container)
-
-    def on_startup_completed(self):
-        self._container.destroy()
-        self._setup_grid()
-        self._setup_toolbar()
-        self._setup_main_container()
-
-    def _setup_grid(self):
-        self._grid = Grid()
-        self._grid.show()
-        self.add(self._grid)
+    def _setup_container(self, container):
+        self.current_container = container
+        self.current_container.show_all()
+        self.add(self.current_container)
 
-    def _reset_window(self):
-        self._container.destroy()
-        self._grid.destroy()
-        self._setup_grid()
-        self._setup_toolbar()
-
-    def _setup_toolbar(self):
-        self._toolbar = Toolbar()
-        self._toolbar.show()
-        self._toolbar.set_show_close_button(True)
-        self.set_titlebar(self._toolbar)
+    def _setup_startup_container(self):
+        self._setup_container(StartupContainer(self))
 
     def _setup_main_container(self):
-        self._container = MainContainer()
-        self._container.show()
-        self._grid.add(self._container)
-        self._toolbar.show_add_contact_button(True, self.show_add_contact)
-
-    # pylint: disable=unused-argument
-    def show_add_contact(self, widget):
-        self._setup_add_contact()
-
-    def _setup_add_contact(self):
-        self._grid.destroy()
-        self._container = AddContactContainer(self)
-        self._container.show()
-        self.add(self._container)
-
-    def open_private_chat(self, contact_id):
-        self._reset_window()
-        self._setup_private_chat(contact_id)
-
-    def _setup_private_chat(self, contact_id):
-        self._container = ChatContainer(contact_id)
-        self._container.show()
-        self._grid.add(self._container)
-        self._toolbar.show_back_button(True, self.back_to_main)
-        self._toolbar.show_add_contact_button(False)
-
-    # pylint: disable=unused-argument
-    def back_to_main(self, widget):
-        self._reset_window()
-        self._toolbar.show_back_button(False)
-        self._setup_main_container()
+        self._setup_container(MainContainer())
+
+    def _setup_add_contact_container(self):
+        self._setup_container(AddContactContainer())
diff --git a/briar-gtk/data/ui/app.briar.gtk.gresource.xml b/briar-gtk/data/ui/app.briar.gtk.gresource.xml
index 8cd71a21eba4b0f8b5ea6ae09d30a4773a1e2a9d..5203c52d90d8787a3f725e45575c0d505ad1848b 100644
--- a/briar-gtk/data/ui/app.briar.gtk.gresource.xml
+++ b/briar-gtk/data/ui/app.briar.gtk.gresource.xml
@@ -3,10 +3,9 @@
   <gresource prefix="/app/briar/gtk">
     <file compressed="true">ui/application.css</file>
     <file compressed="true" preprocess="xml-stripblanks">ui/add_contact.ui</file>
-    <file compressed="true" preprocess="xml-stripblanks">ui/chat.ui</file>
     <file compressed="true" preprocess="xml-stripblanks">ui/login.ui</file>
     <file compressed="true" preprocess="xml-stripblanks">ui/main.ui</file>
+    <file compressed="true" preprocess="xml-stripblanks">ui/private_chat.ui</file>
     <file compressed="true" preprocess="xml-stripblanks">ui/registration.ui</file>
-    <file compressed="true" preprocess="xml-stripblanks">ui/toolbar_start.ui</file>
   </gresource>
 </gresources>
diff --git a/briar-gtk/data/ui/application.css b/briar-gtk/data/ui/application.css
index 430e31831668894f888ddc72520079eb3a4a67dc..27852f26d76561e1c2d13ea051aadd348db28a56 100644
--- a/briar-gtk/data/ui/application.css
+++ b/briar-gtk/data/ui/application.css
@@ -1,4 +1,87 @@
+/**
+  Copyright (c) 2019 - 2020 Nico Alt
+  SPDX-License-Identifier: AGPL-3.0-only
+  License-Filename: LICENSE.md
+
+  Based on parts of GNOME Fractal
+  https://gitlab.gnome.org/GNOME/fractal/blob/4.2.2/fractal-gtk/res/app.css
+**/
+
+/** Dialog like windows **/
+
 .error-label {
   color: @error_color;
 }
 
+/** Main window **/
+
+.messages-box {
+  background-color: @theme_base_color;
+}
+
+.messages-history {
+  background-color: @theme_base_color;
+  padding: 0 18px 18px;
+}
+
+.history-view {
+  background-color: @theme_base_color;
+}
+
+.messages-history > row:not(.msg-mention) {
+  padding: 6px 9px;
+}
+
+.scroll_button {
+  border-radius: 9999px;
+  -gtk-outline-radius: 9999px;
+}
+
+.message-input-area {
+  padding: 6px;
+}
+
+.message-input-focused {
+  border: 2px solid @theme_selected_bg_color;
+  padding: 5px;
+}
+
+.messages-scroll {
+  background-color: @theme_base_color;
+  border-bottom: 1px solid @borders;
+}
+
+.chat-placeholder-title {
+  font-size: larger;
+  opacity: 0.5;
+}
+
+.chat-placeholder-description {
+  font-size: smaller;
+  opacity: 0.5;
+}
+
+row .username {
+  font-weight: bold;
+  font-size: small;
+}
+
+row .username,
+row.msg-emote,
+.divider {
+  color: @theme_selected_bg_color;
+}
+
+row .timestamp {
+  font-size: small;
+}
+
+/** Sidebar **/
+.room-row {
+  padding: 6px 6px;
+}
+
+.sidebar {
+  border: none;
+}
+
diff --git a/briar-gtk/data/ui/chat.ui b/briar-gtk/data/ui/chat.ui
deleted file mode 100644
index e779caf6b367c92139824e8d06b8276cfdf32be4..0000000000000000000000000000000000000000
--- a/briar-gtk/data/ui/chat.ui
+++ /dev/null
@@ -1,42 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-  Copyright (c) 2019 Nico Alt
-  SPDX-License-Identifier: AGPL-3.0-only
-  License-Filename: LICENSE.md
--->
-<interface>
-  <requires lib="gtk+" version="3.20"/>
-  <object class="GtkGrid" id="chat">
-    <property name="visible">True</property>
-    <property name="can_focus">False</property>
-    <property name="halign">center</property>
-    <property name="valign">center</property>
-    <property name="margin_left">18</property>
-    <property name="margin_right">18</property>
-    <property name="margin_top">18</property>
-    <property name="margin_bottom">18</property>
-    <child>
-      <object class="GtkListBox" id="messages_list">
-        <property name="visible">True</property>
-        <property name="can_focus">False</property>
-        <property name="hexpand">True</property>
-      </object>
-      <packing>
-        <property name="left_attach">0</property>
-        <property name="top_attach">0</property>
-      </packing>
-    </child>
-    <child>
-      <object class="GtkEntry" id="chat_entry">
-        <property name="visible">True</property>
-        <property name="can_focus">True</property>
-        <property name="placeholder_text" translatable="yes" context="chat page: input field">Type Message</property>
-        <property name="show_emoji_icon">True</property>
-      </object>
-      <packing>
-        <property name="left_attach">0</property>
-        <property name="top_attach">1</property>
-      </packing>
-    </child>
-  </object>
-</interface>
diff --git a/briar-gtk/data/ui/main.ui b/briar-gtk/data/ui/main.ui
index ab14dc861ee6e6121025f89f0eb7c2d9e0c4f6dc..2d1bf36736f5f29e9f9db291535ba853be418693 100644
--- a/briar-gtk/data/ui/main.ui
+++ b/briar-gtk/data/ui/main.ui
@@ -1,22 +1,452 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <!--
-  Copyright (c) 2019 Nico Alt
+  Copyright (c) 2019 - 2020 Nico Alt
   SPDX-License-Identifier: AGPL-3.0-only
   License-Filename: LICENSE.md
+
+  Based on parts of GNOME Fractal
+  https://gitlab.gnome.org/GNOME/fractal/blob/4.2.2/fractal-gtk/res/ui/main_window.ui
 -->
 <interface>
-  <requires lib="gtk+" version="3.20"/>
-  <object class="GtkListBox" id="contacts_list">
-    <property name="visible">True</property>
+  <requires lib="gtk+" version="3.22"/>
+  <object class="GtkStack" id="main_window_stack">
     <property name="can_focus">False</property>
-    <property name="halign">center</property>
-    <property name="valign">center</property>
-    <property name="margin_left">18</property>
-    <property name="margin_right">18</property>
-    <property name="margin_top">18</property>
-    <property name="margin_bottom">18</property>
-    <property name="hexpand">True</property>
+    <property name="hhomogeneous">False</property>
+    <child>
+      <object class="HdyLeaflet" id="main_window_leaflet">
+        <property name="child-transition-duration" bind-source="header_leaflet" bind-property="child-transition-duration" bind-flags="bidirectional|sync-create"/>
+        <property name="transition-type" bind-source="header_leaflet" bind-property="transition-type" bind-flags="bidirectional|sync-create"/>
+        <property name="mode-transition-duration" bind-source="header_leaflet" bind-property="mode-transition-duration" bind-flags="bidirectional|sync-create"/>
+        <property name="visible-child-name" bind-source="header_leaflet" bind-property="visible-child-name" bind-flags="bidirectional|sync-create"/>
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="hhomogeneous-folded">True</property>
+        <child>
+          <object class="GtkBox" id="sidebar_box">
+            <property name="width_request">200</property>
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="halign">fill</property>
+            <property name="hexpand">False</property>
+            <property name="orientation">vertical</property>
+            <child>
+              <object class="GtkScrolledWindow">
+                <property name="width_request">200</property>
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+                <property name="hscrollbar_policy">never</property>
+                <child>
+                  <object class="GtkViewport">
+                    <property name="width_request">200</property>
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <property name="shadow_type">none</property>
+                    <child>
+                      <object class="GtkBox">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="orientation">vertical</property>
+                        <child>
+                          <object class="GtkListBox" id="contacts_list_box">
+                            <property name="visible">True</property>
+                            <property name="can_focus">False</property>
+                            <property name="halign">fill</property>
+                            <property name="valign">center</property>
+                          </object>
+                        </child>
+                        <style>
+                          <class name="sidebar"/>
+                        </style>
+                      </object>
+                    </child>
+                  </object>
+                </child>
+              </object>
+              <packing>
+                <property name="expand">True</property>
+                <property name="fill">True</property>
+                <property name="position">1</property>
+              </packing>
+            </child>
+          </object>
+          <packing>
+            <property name="name">sidebar</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkSeparator" id="content_separator">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <style>
+                <class name="sidebar"/>
+            </style>
+          </object>
+        </child>
+        <child>
+          <object class="GtkOverlay" id="main_content_container">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <child>
+              <object class="GtkStack" id="main_content_stack">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <child>
+                  <object class="GtkBox">
+                    <property name="name">room</property>
+                    <property name="can_focus">False</property>
+                    <child>
+                      <object class="GtkBox">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="orientation">vertical</property>
+                        <child>
+                          <object class="GtkBox" id="history_container">
+                            <property name="visible">True</property>
+                            <property name="can_focus">False</property>
+                            <property name="orientation">vertical</property>
+                          </object>
+                          <packing>
+                            <property name="expand">True</property>
+                            <property name="fill">True</property>
+                            <property name="position">1</property>
+                          </packing>
+                        </child>
+                        <child>
+                          <object class="GtkBox">
+                            <property name="visible">True</property>
+                            <child>
+                              <object class="GtkEntry" id="chat_entry">
+                                <property name="visible">True</property>
+                                <property name="can_focus">True</property>
+                                <property name="placeholder_text" translatable="yes" context="chat page: input field">Type Message</property>
+                                <property name="show_emoji_icon">True</property>
+                              </object>
+                              <packing>
+                                <property name="expand">True</property>
+                                <property name="fill">True</property>
+                              </packing>
+                            </child>
+                            <style>
+                              <class name="message-input-area" />
+                            </style>
+                          </object>
+                        </child>
+                      </object>
+                      <packing>
+                        <property name="expand">True</property>
+                        <property name="fill">True</property>
+                        <property name="position">0</property>
+                      </packing>
+                    </child>
+                  </object>
+                  <packing>
+                    <property name="name">chat_view</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkBox">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <property name="orientation">vertical</property>
+                    <property name="margin_bottom">30</property>
+                    <property name="halign">center</property>
+                    <property name="valign">center</property>
+                    <child>
+                      <object class="GtkImage">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="margin_bottom">16</property>
+                        <property name="pixel_size">128</property>
+                        <property name="icon_name">app.briar.gtk</property>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkLabel">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="label" translatable="yes">No contact selected</property>
+                        <property name="margin_bottom">3</property>
+                        <property name="justify">center</property>
+                        <attributes>
+                          <attribute name="weight" value="bold"/>
+                        </attributes>
+                        <style>
+                          <class name="chat-placeholder-title"/>
+                        </style>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkLabel">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="label" translatable="yes">Select a contact to start chatting</property>
+                        <property name="justify">center</property>
+                        <style>
+                          <class name="chat-placeholder-description"/>
+                        </style>
+                      </object>
+                    </child>
+                  </object>
+                  <packing>
+                    <property name="name">chat_placeholder</property>
+                    <property name="position">2</property>
+                  </packing>
+                </child>
+              </object>
+              <packing>
+                <property name="index">-1</property>
+              </packing>
+            </child>
+          </object>
+          <packing>
+            <property name="name">main_content</property>
+          </packing>
+        </child>
+      </object>
+      <packing>
+        <property name="name">chat</property>
+      </packing>
+    </child>
+  </object>
+  <object class="HdyTitleBar" id="headerbar_stack_holder">
+    <property name="visible">True</property>
+    <child>
+      <object class="GtkStack" id="headerbar_stack">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="hexpand">True</property>
+        <property name="hhomogeneous">False</property>
+        <child>
+          <object class="HdyLeaflet" id="header_leaflet"> <!--message view-->
+            <property name="visible">True</property>
+            <property name="transition-type">slide</property>
+            <property name="hhomogeneous-folded">True</property>
+            <child>
+              <object class="GtkHeaderBar" id="left-header"> <!--left titlebar-->
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="halign">fill</property>
+                <property name="show-close-button">True</property>
+                <child>
+                  <object class="GtkMenuButton" id="add_room_menu">
+                    <property name="visible">True</property>
+                    <property name="can_focus">True</property>
+                    <property name="receives_default">True</property>
+                    <property name="action_name">win.open-add-contact</property>
+                    <child>
+                      <object class="GtkImage">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="icon_name">list-add-symbolic</property>
+                      </object>
+                    </child>
+                    <accessibility>
+
+                    </accessibility>
+                    <child internal-child="accessible">
+                      <object class="AtkObject" id="a11y-add_room_menu">
+                        <property name="AtkObject::accessible_name" translatable="yes">Add</property>
+                      </object>
+                    </child>
+                  </object>
+                  <packing>
+                    <property name="pack_type">start</property>
+                    <property name="position">1</property>
+                  </packing>
+                </child>
+              </object>
+              <packing>
+                <property name="name">sidebar</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkSeparator" id="header_separator">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="orientation">vertical</property>
+                <style>
+                  <class name="sidebar"/>
+                </style>
+              </object>
+            </child>
+            <child>
+              <object class="GtkHeaderBar" id="room_header_bar"> <!--right titlebar-->
+                <property name="show-close-button">True</property>
+                <property name="has-subtitle">False</property>
+                <property name="hexpand">true</property>
+                <property name="width-request">360</property>
+                <child>
+                  <object class="GtkRevealer">
+                    <property name="reveal-child" bind-source="header_leaflet" bind-property="folded" bind-flags="sync-create"/>
+                    <property name="transition-duration" bind-source="header_leaflet" bind-property="mode-transition-duration" bind-flags="bidirectional|sync-create"/>
+                    <property name="transition-type">crossfade</property>
+                    <property name="visible">True</property>
+                    <child>
+                      <object class="GtkButton" id="leaflet_back_button">
+                        <property name="action_name">win.back-to-sidebar</property>
+                        <property name="visible">True</property>
+                        <property name="can_focus">True</property>
+                        <property name="receives_default">True</property>
+                        <child>
+                          <object class="GtkImage">
+                            <property name="visible">True</property>
+                            <property name="can_focus">False</property>
+                            <property name="icon_name">go-previous-symbolic</property>
+                          </object>
+                        </child>
+                        <accessibility>
+
+                        </accessibility>
+                        <child internal-child="accessible">
+                          <object class="AtkObject" id="a11y-leaflet_back_button">
+                            <property name="AtkObject::accessible_name" translatable="yes">Back</property>
+                          </object>
+                        </child>
+                      </object>
+                    </child>
+                  </object>
+                </child>
+                <child type="title">
+                  <object class="GtkScrolledWindow">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <property name="hexpand">True</property>
+                    <property name="hscrollbar_policy">never</property>
+                    <property name="vscrollbar_policy">never</property>
+                    <property name="propagate_natural_height">True</property>
+                    <property name="propagate_natural_width">False</property>
+                    <child>
+                      <object class="GtkBox" id="room_info">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="orientation">vertical</property>
+                        <child>
+                          <object class="GtkLabel" id="room_name">
+                            <property name="can_focus">False</property>
+                            <!-- Translators: This string is replaced not user-visible -->
+                            <property name="label">Room name</property>
+                            <property name="ellipsize">end</property>
+                            <style>
+                              <class name="title"/>
+                            </style>
+                          </object>
+                          <packing>
+                            <property name="expand">False</property>
+                            <property name="fill">True</property>
+                            <property name="position">0</property>
+                          </packing>
+                        </child>
+                      </object>
+                    </child>
+                  </object>
+                </child>
+              </object>
+              <packing>
+                <property name="name">main_content</property>
+              </packing>
+            </child>
+          </object>
+          <packing>
+            <property name="name">normal</property>
+            <property name="title">normal</property>
+            <property name="position">1</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkBox">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="orientation">vertical</property>
+            <child>
+              <object class="GtkHeaderBar">
+                <property name="can_focus">False</property>
+                <property name="show-close-button">True</property>
+                <property name="title">Briar</property>
+              </object>
+            </child>
+          </object>
+          <packing>
+            <property name="name">loading</property>
+            <property name="title">loading</property>
+            <property name="position">1</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkBox">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="orientation">vertical</property>
+            <child>
+              <object class="HdyHeaderBar">
+                <property name="can_focus">False</property>
+                <property name="show_close_button">True</property>
+                <property name="width_request">360</property>
+                <property name="centering_policy">HDY_CENTERING_POLICY_STRICT</property>
+                <child>
+                  <object class="GtkButton" id="back_button">
+                    <property name="visible">True</property>
+                    <property name="can_focus">True</property>
+                    <property name="receives_default">True</property>
+                    <property name="action_name">app.back</property>
+                    <child>
+                      <object class="GtkImage">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="icon_name">go-previous-symbolic</property>
+                      </object>
+                    </child>
+                    <child internal-child="accessible">
+                      <object class="AtkObject" id="back_button-atkobject">
+                        <property name="AtkObject::accessible-name" translatable="yes">Back</property>
+                      </object>
+                    </child>
+                  </object>
+                </child>
+              </object>
+              <packing>
+                <property name="expand">False</property>
+                <property name="fill">True</property>
+                <property name="position">0</property>
+              </packing>
+            </child>
+          </object>
+          <packing>
+            <property name="name">back</property>
+            <property name="title" translatable="yes">Back</property>
+            <property name="position">2</property>
+          </packing>
+        </child>
+      </object>
+    </child>
   </object>
-</interface>
 
+<!-- Synchronize left header and sidebar -->
+  <object class="GtkSizeGroup">
+    <property name="mode">horizontal</property>
+    <widgets>
+      <widget name="left-header"/>
+      <widget name="sidebar_box"/>
+    </widgets>
+  </object>
+  <object class="GtkSizeGroup">
+    <property name="mode">horizontal</property>
+    <widgets>
+      <widget name="main_content_container"/>
+      <widget name="room_header_bar"/>
+    </widgets>
+  </object>
+  <object class="GtkSizeGroup">
+    <property name="mode">horizontal</property>
+    <widgets>
+      <widget name="header_separator"/>
+      <widget name="content_separator"/>
+    </widgets>
+  </object>
+  <object class="HdyHeaderGroup">
+    <headerbars>
+      <headerbar name="left-header"/>
+      <headerbar name="room_header_bar"/>
+    </headerbars>
+  </object>
+</interface>
 
diff --git a/briar-gtk/data/ui/private_chat.ui b/briar-gtk/data/ui/private_chat.ui
new file mode 100644
index 0000000000000000000000000000000000000000..b874d1a75eb86cf31d073c97de0280c0a125d20f
--- /dev/null
+++ b/briar-gtk/data/ui/private_chat.ui
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Copyright (c) 2019 Nico Alt
+  SPDX-License-Identifier: AGPL-3.0-only
+  License-Filename: LICENSE.md
+
+  Based on parts of GNOME Fractal
+  https://gitlab.gnome.org/GNOME/fractal/blob/4.2.2/fractal-gtk/res/ui/scroll_widget.ui
+-->
+<interface>
+  <requires lib="gtk+" version="3.20"/>
+    <object class="GtkScrolledWindow" id="messages_scroll">
+      <property name="visible">True</property>
+      <property name="can_focus">True</property>
+      <property name="hscrollbar_policy">never</property>
+      <property name="vscrollbar_policy">always</property>
+      <property name="window_placement">bottom-left</property>
+      <property name="min_content_width">300</property>
+      <property name="min_content_height">300</property>
+      <style>
+        <class name="messages-scroll" />
+      </style>
+      <child>
+        <object class="GtkViewport">
+          <property name="visible">True</property>
+          <property name="can_focus">False</property>
+          <property name="valign">end</property>
+          <property name="vscroll_policy">natural</property>
+          <property name="shadow_type">none</property>
+          <child>
+            <object class="GtkBox" id="messages_column">
+              <property name="visible">True</property>
+              <property name="can_focus">False</property>
+              <property name="expand">True</property>
+            </object>
+          </child>
+        </object>
+      </child>
+    </object>
+</interface>
diff --git a/briar-gtk/data/ui/toolbar_start.ui b/briar-gtk/data/ui/toolbar_start.ui
deleted file mode 100644
index 31a939280b91719dace4365b13ed95fa7a30167b..0000000000000000000000000000000000000000
--- a/briar-gtk/data/ui/toolbar_start.ui
+++ /dev/null
@@ -1,52 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-  Copyright (c) 2019 Nico Alt
-  SPDX-License-Identifier: AGPL-3.0-only
-  License-Filename: LICENSE.md
-
-  Based on parts of GNOME Lollypop
-  https://gitlab.gnome.org/World/lollypop/blob/1.0.12/data/ToolbarPlayback.ui
--->
-<interface>
-  <requires lib="gtk+" version="3.10"/>
-  <object class="GtkImage" id="back_image">
-    <property name="visible">True</property>
-    <property name="can_focus">False</property>
-    <property name="icon_name">go-previous-symbolic</property>
-  </object>
-  <object class="GtkImage" id="add_contact_image">
-    <property name="visible">True</property>
-    <property name="can_focus">False</property>
-    <property name="icon_name">list-add-symbolic</property>
-  </object>
-  <object class="GtkBox" id="toolbar_start">
-    <property name="visible">True</property>
-    <property name="can_focus">False</property>
-    <property name="spacing">5</property>
-    <child>
-      <object class="GtkButton" id="back_button">
-        <property name="visible">False</property>
-        <property name="valign">center</property>
-        <property name="image">back_image</property>
-      </object>
-      <packing>
-        <property name="expand">False</property>
-        <property name="fill">True</property>
-        <property name="position">0</property>
-      </packing>
-    </child>
-    <child>
-      <object class="GtkButton" id="add_contact_button">
-        <property name="visible">False</property>
-        <property name="valign">center</property>
-        <property name="image">add_contact_image</property>
-      </object>
-      <packing>
-        <property name="expand">False</property>
-        <property name="fill">True</property>
-        <property name="position">0</property>
-      </packing>
-    </child>
-  </object>
-</interface>
-
diff --git a/briar-gtk/tests/briar_gtk/test_toolbar.py b/briar-gtk/tests/briar_gtk/test_toolbar.py
deleted file mode 100644
index d5215eccb2256c9c0cc1b28d3e5ceb57d9f41d98..0000000000000000000000000000000000000000
--- a/briar-gtk/tests/briar_gtk/test_toolbar.py
+++ /dev/null
@@ -1,50 +0,0 @@
-# Copyright (c) 2019 Nico Alt
-# SPDX-License-Identifier: AGPL-3.0-only
-# License-Filename: LICENSE.md
-
-import pytest
-from unittest.mock import Mock
-
-from briar_gtk.toolbar import Toolbar
-
-
-def test_show_back_button(mocker):
-    get_object_mock = mocker.patch("gi.repository.Gtk.Builder.get_object")
-    back_button_mock = get_object_mock.return_value
-    callback = Mock()
-    mocker.patch("briar_gtk.toolbar.Toolbar._setup_toolbar")
-
-    Toolbar().show_back_button(True, callback)
-
-    get_object_mock.assert_called_once_with("back_button")
-    back_button_mock.hide.assert_not_called()
-    back_button_mock.show.assert_called_once()
-    back_button_mock.connect.assert_called_once()
-
-
-def test_show_back_button_without_callback(mocker):
-    get_object_mock = mocker.patch("gi.repository.Gtk.Builder.get_object")
-    back_button_mock = get_object_mock.return_value
-    mocker.patch("briar_gtk.toolbar.Toolbar._setup_toolbar")
-
-    with pytest.raises(Exception,
-                       match="Callback needed when showing back button"):
-        Toolbar().show_back_button(True)
-
-    get_object_mock.assert_called_once_with("back_button")
-    back_button_mock.hide.assert_not_called()
-    back_button_mock.show.assert_not_called()
-    back_button_mock.connect.assert_not_called()
-
-
-def test_hide_back_button(mocker):
-    get_object_mock = mocker.patch("gi.repository.Gtk.Builder.get_object")
-    back_button_mock = get_object_mock.return_value
-    mocker.patch("briar_gtk.toolbar.Toolbar._setup_toolbar")
-
-    Toolbar().show_back_button(False)
-
-    get_object_mock.assert_called_once_with("back_button")
-    back_button_mock.hide.assert_called_once()
-    back_button_mock.show.assert_not_called()
-    back_button_mock.connect.assert_not_called()