diff --git a/data/ui/add_contact.ui b/data/ui/add_contact.ui
index e93afe1b2b3cfc01c2e1b4f54824aaf98eb7f07c..5ef80c3d00d9833064c49433b3fc0860e5b7eabe 100644
--- a/data/ui/add_contact.ui
+++ b/data/ui/add_contact.ui
@@ -3,123 +3,264 @@
   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/login_flow.ui
 -->
 <interface>
-  <requires lib="gtk+" version="3.20"/>
-  <object class="GtkGrid" id="add_contact">
-    <property name="visible">True</property>
+  <requires lib="gtk+" version="3.22"/>
+  <object class="GtkStack" id="add_contact_flow_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="hhomogeneous">True</property>
+    <property name="transition_type">GTK_STACK_TRANSITION_TYPE_SLIDE_LEFT_RIGHT</property>
     <child>
-      <object class="GtkGrid" id="link_grid">
+      <object class="GtkGrid" id="links_page">
         <property name="visible">True</property>
         <property name="can_focus">False</property>
-        <property name="row_spacing">6</property>
+        <property name="halign">center</property>
+        <property name="valign">center</property>
         <property name="column_spacing">12</property>
+        <property name="row_spacing">24</property>
         <child>
-          <object class="GtkLabel" id="own_link_label">
+          <object class="GtkLabel">
             <property name="visible">True</property>
-            <property name="halign">center</property>
-            <property name="label" translatable="yes">Give this link to the contact you want to add</property>
+            <property name="use_underline">True</property>
+            <property name="can_focus">False</property>
+            <property name="label" translatable="yes" context="add contact">Give this link to the contact you want to add</property>
+            <property name="wrap">True</property>
+            <property name="wrap_mode">PANGO_WRAP_WORD_CHAR</property>
+            <property name="mnemonic_widget">own_link_entry</property>
+            <style>
+              <class name="dim-label"/>
+            </style>
           </object>
           <packing>
-            <property name="left_attach">1</property>
-            <property name="top_attach">0</property>
+            <property name="top-attach">1</property>
+            <property name="left-attach">1</property>
           </packing>
         </child>
         <child>
           <object class="GtkEntry" id="own_link_entry">
             <property name="visible">True</property>
+            <property name="max_width_chars">-1</property>
+            <property name="width_request">232</property>
+            <property name="can_focus">True</property>
             <property name="input_purpose">GTK_INPUT_PURPOSE_URL</property>
           </object>
           <packing>
-            <property name="left_attach">1</property>
-            <property name="top_attach">1</property>
+            <property name="top-attach">2</property>
+            <property name="left-attach">1</property>
           </packing>
         </child>
         <child>
-          <object class="GtkLabel" id="their_link_label">
+          <object class="GtkLabel">
             <property name="visible">True</property>
-            <property name="halign">center</property>
-            <property name="label" translatable="yes">Enter the link from your contact here</property>
+            <property name="use_underline">True</property>
+            <property name="can_focus">False</property>
+            <property name="label" translatable="yes" context="add contact">Enter the link from your contact here</property>
+            <property name="wrap">True</property>
+            <property name="wrap_mode">PANGO_WRAP_WORD_CHAR</property>
+            <property name="mnemonic_widget">their_link_entry</property>
+            <style>
+              <class name="dim-label"/>
+            </style>
           </object>
           <packing>
-            <property name="left_attach">1</property>
-            <property name="top_attach">2</property>
+            <property name="top-attach">3</property>
+            <property name="left-attach">1</property>
           </packing>
         </child>
         <child>
           <object class="GtkEntry" id="their_link_entry">
             <property name="visible">True</property>
-            <property name="placeholder_text" translatable="yes" context="link exchange: their link input field">Contact's link</property>
+            <property name="max_width_chars">-1</property>
+            <property name="width_request">232</property>
+            <property name="can_focus">True</property>
+            <property name="placeholder_text" translatable="yes" context="add contact">Contact's link</property>
             <property name="input_purpose">GTK_INPUT_PURPOSE_URL</property>
           </object>
           <packing>
-            <property name="left_attach">1</property>
-            <property name="top_attach">3</property>
+            <property name="top-attach">4</property>
+            <property name="left-attach">1</property>
           </packing>
         </child>
         <child>
-          <object class="GtkButton" id="link_continue">
-            <property name="visible">True</property>
-            <property name="halign">center</property>
-            <property name="label" translatable="yes" context="link exchange: button – initiate pairing">Continue</property>
-            <signal name="clicked" handler="on_link_button_clicked"/>
+          <object class="GtkLabel" id="link_error_label">
+            <property name="visible">False</property>
+            <property name="can_focus">False</property>
+            <property name="no_show_all">True</property>
+            <property name="label" translatable="yes" context="add contact">Please enter a link</property>
+            <property name="wrap">True</property>
+            <property name="wrap_mode">PANGO_WRAP_WORD_CHAR</property>
+            <style>
+              <class name="error-label"/>
+            </style>
           </object>
           <packing>
-            <property name="left_attach">1</property>
-            <property name="top_attach">4</property>
+            <property name="top-attach">5</property>
+            <property name="left-attach">1</property>
           </packing>
         </child>
       </object>
+      <packing>
+        <property name="name">links</property>
+      </packing>
     </child>
     <child>
-      <object class="GtkGrid" id="alias_grid">
-        <property name="visible">False</property>
+      <object class="GtkGrid" id="alias_page">
+        <property name="visible">True</property>
         <property name="can_focus">False</property>
-        <property name="row_spacing">6</property>
+        <property name="halign">center</property>
+        <property name="valign">center</property>
         <property name="column_spacing">12</property>
+        <property name="row_spacing">24</property>
         <child>
-          <object class="GtkLabel" id="alias_label">
+          <object class="GtkLabel">
             <property name="visible">True</property>
-            <property name="halign">center</property>
-            <property name="label" translatable="yes">Give your contact a nickname. Only you can see it.</property>
+            <property name="use_underline">True</property>
+            <property name="can_focus">False</property>
+            <property name="label" translatable="yes" context="add contact">Give your contact a nickname. Only you can see it.</property>
+            <property name="halign">end</property>
+            <property name="wrap">True</property>
+            <property name="wrap_mode">PANGO_WRAP_WORD_CHAR</property>
+            <property name="mnemonic_widget">own_link_entry</property>
+            <style>
+              <class name="dim-label"/>
+            </style>
           </object>
           <packing>
-            <property name="left_attach">1</property>
-            <property name="top_attach">0</property>
+            <property name="top-attach">1</property>
+            <property name="left-attach">1</property>
           </packing>
         </child>
         <child>
           <object class="GtkEntry" id="alias_entry">
             <property name="visible">True</property>
-            <property name="placeholder_text" translatable="yes" context="remote contact adding: alias input field">Enter a nickname</property>
+            <property name="max_width_chars">-1</property>
+            <property name="width_request">232</property>
+            <property name="can_focus">True</property>
+            <property name="placeholder_text" translatable="yes" context="add contact">Enter a nickname</property>
+          </object>
+          <packing>
+            <property name="top-attach">2</property>
+            <property name="left-attach">1</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkLabel" id="alias_error_label">
+            <property name="visible">False</property>
+            <property name="can_focus">False</property>
+            <property name="no_show_all">True</property>
+            <property name="xalign">0</property>
+            <property name="label" translatable="yes" context="add contact">The alias may not be empty</property>
+            <property name="wrap">True</property>
+            <property name="wrap_mode">PANGO_WRAP_WORD_CHAR</property>
+            <style>
+              <class name="error-label"/>
+            </style>
           </object>
           <packing>
-            <property name="left_attach">1</property>
-            <property name="top_attach">1</property>
+            <property name="top-attach">3</property>
+            <property name="left-attach">1</property>
           </packing>
         </child>
+      </object>
+      <packing>
+        <property name="name">alias</property>
+      </packing>
+    </child>
+  </object>
+  <object class="GtkStack" id="add_contact_flow_headers">
+    <property name="can_focus">False</property>
+    <property name="hhomogeneous">True</property>
+    <property name="visible_child_name" bind-source="add_contact_flow_stack" bind-property="visible-child-name" bind-flags="sync-create"/>
+    <property name="transition_duration" bind-source="add_contact_flow_stack" bind-property="transition-duration" bind-flags="sync-create"/>
+    <property name="transition_type" bind-source="add_contact_flow_stack" bind-property="transition-type" bind-flags="sync-create"/>
+    <child>
+      <object class="GtkHeaderBar">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="show_close_button">True</property>
+        <property name="width_request">360</property>
+        <property name="title">Add contact</property>
         <child>
-          <object class="GtkButton" id="add_contact_button">
+          <object class="GtkButton">
             <property name="visible">True</property>
-            <property name="halign">center</property>
-            <property name="label" translatable="yes" context="remote contact adding: button">Add Contact</property>
-            <signal name="clicked" handler="on_add_contact_button_clicked"/>
+            <property name="can_focus">True</property>
+            <signal name="clicked" handler="on_link_back_pressed"/>
+            <child>
+              <object class="GtkImage">
+                <property name="visible">True</property>
+                <property name="icon_name">go-previous-symbolic</property>
+              </object>
+            </child>
           </object>
           <packing>
-            <property name="left_attach">1</property>
-            <property name="top_attach">3</property>
+            <property name="pack_type">start</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkButton">
+            <property name="visible">True</property>
+            <property name="use_underline">True</property>
+            <property name="can_focus">True</property>
+            <property name="label" translatable="yes">Next</property>
+            <signal name="clicked" handler="on_links_next_pressed"/>
+            <style>
+              <class name="suggested-action"/>
+            </style>
+          </object>
+          <packing>
+            <property name="pack_type">end</property>
           </packing>
         </child>
       </object>
+      <packing>
+        <property name="name">links</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkHeaderBar">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="show_close_button">True</property>
+        <property name="title" translatable="yes">Add contact</property>
+        <child>
+          <object class="GtkButton">
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <signal name="clicked" handler="on_alias_back_pressed"/>
+            <child>
+              <object class="GtkImage">
+                <property name="visible">True</property>
+                <property name="icon_name">go-previous-symbolic</property>
+              </object>
+            </child>
+          </object>
+          <packing>
+            <property name="pack_type">start</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkButton">
+            <property name="visible">True</property>
+            <property name="use_underline">True</property>
+            <property name="can_focus">True</property>
+            <property name="label" translatable="yes">Add contact</property>
+            <signal name="clicked" handler="on_add_contact_pressed"/>
+            <style>
+              <class name="suggested-action"/>
+            </style>
+          </object>
+          <packing>
+            <property name="pack_type">end</property>
+          </packing>
+        </child>
+      </object>
+      <packing>
+        <property name="name">alias</property>
+      </packing>
     </child>
   </object>
 </interface>
 
-
diff --git a/src/briar/gtk/containers/add_contact.py b/src/briar/gtk/containers/add_contact.py
index 7d51989ad0e2951595167b1fb08425f8b4f3935a..f0a946af846b007a69da634ee4272506186db6c5 100644
--- a/src/briar/gtk/containers/add_contact.py
+++ b/src/briar/gtk/containers/add_contact.py
@@ -2,6 +2,8 @@
 # SPDX-License-Identifier: AGPL-3.0-only
 # License-Filename: LICENSE.md
 
+from gettext import gettext as _
+
 from gi.repository import GLib
 
 from briar.api.models.contacts import Contacts
@@ -11,33 +13,127 @@ from briar.gtk.define import APP
 
 class AddContactContainer(Container):
 
-    CONTAINER_UI = "/app/briar/gtk/ui/add_contact.ui"
+    ADD_CONTACT_UI = "/app/briar/gtk/ui/add_contact.ui"
+    STACK_NAME = "add_contact_flow_stack"
+    HEADERS_NAME = "add_contact_flow_headers"
 
-    def __init__(self):
+    def __init__(self, window):
         super().__init__()
-        self._api = APP().api
+        self._window = window
         self._setup_view()
         self._load_content()
 
     def _setup_view(self):
-        self.builder.add_from_resource(self.CONTAINER_UI)
-        self.add(self.builder.get_object("add_contact"))
+        self.builder.add_from_resource(self.ADD_CONTACT_UI)
         self.builder.connect_signals(self)
 
+        self._setup_add_contact_flow_stack()
+        self._setup_add_contact_flow_headers()
+        self._setup_link_keystroke_listener()
+
     def _load_content(self):
-        contacts = Contacts(self._api)
+        contacts = Contacts(APP().api)
         own_link = contacts.get_link()
         self.builder.get_object("own_link_entry").set_text(own_link)
 
+    def _setup_add_contact_flow_stack(self):
+        self.add_contact_flow_stack = self.builder.get_object(self.STACK_NAME)
+        self.add_contact_flow_stack.show_all()
+        self.add(self.add_contact_flow_stack)
+
+    def _setup_add_contact_flow_headers(self):
+        add_contact_flow_headers = self.builder.get_object(self.HEADERS_NAME)
+        add_contact_flow_headers.show_all()
+        self._window.set_titlebar(add_contact_flow_headers)
+
+    def _setup_link_keystroke_listener(self):
+        their_link_entry = self.builder.get_object("their_link_entry")
+        their_link_entry.connect("key-press-event",
+                                 self._link_keystroke)
+
+    # pylint: disable=unused-argument
+    def _link_keystroke(self, widget, event):
+        if event.hardware_keycode != 36 and event.hardware_keycode != 104:
+            return
+        self.on_links_next_pressed(None)
+
+    # pylint: disable=unused-argument
+    def on_link_back_pressed(self, button):
+        self._back_to_main_window()
+
+    # pylint: disable=unused-argument
+    def on_links_next_pressed(self, button):
+        link_error_label = self.builder.get_object("link_error_label")
+        if self._link_is_empty():
+            link_error_label.set_label(_("Please enter a link"))
+            link_error_label.show()
+            return
+        if self._their_link_is_ours():
+            link_error_label.show()
+            link_error_label.set_label(
+                _("Enter your contact's link, not your own"))
+            return
+        link_error_label.hide()
+        self._show_alias_page()
+
+    def _their_link_is_ours(self):
+        their_link = self.builder.get_object("their_link_entry").get_text()
+        own_link = self.builder.get_object("own_link_entry").get_text()
+        return their_link == own_link
+
+    def _link_is_empty(self):
+        their_link = self.builder.get_object("their_link_entry").get_text()
+        return len(their_link) == 0
+
+    def _show_alias_page(self):
+        alias_page = self.builder.get_object("alias_page")
+        self.add_contact_flow_stack.set_visible_child(alias_page)
+
+        self._focus_alias_entry()
+        self._setup_alias_keystroke_listener()
+
+    def _focus_alias_entry(self):
+        alias_entry = self.builder.get_object("alias_entry")
+        alias_entry.grab_focus()
+
+    def _setup_alias_keystroke_listener(self):
+        alias_entry = self.builder.get_object("alias_entry")
+        alias_entry.connect("key-press-event", self._alias_keystroke)
+
     # pylint: disable=unused-argument
-    def on_link_button_clicked(self, button):
-        self.builder.get_object("link_grid").set_visible(False)
-        self.builder.get_object("alias_grid").set_visible(True)
+    def _alias_keystroke(self, widget, event):
+        if event.hardware_keycode != 36 and event.hardware_keycode != 104:
+            return
+        self.on_add_contact_pressed(None)
 
     # pylint: disable=unused-argument
-    def on_add_contact_button_clicked(self, button):
+    def on_alias_back_pressed(self, button):
+        self._show_links_page()
+
+    def _show_links_page(self):
+        links_page = self.builder.get_object("links_page")
+        self.add_contact_flow_stack.set_visible_child(links_page)
+
+    # pylint: disable=unused-argument
+    def on_add_contact_pressed(self, button):
+        alias_error_label = self.builder.get_object(
+            "alias_error_label")
+        if self._alias_is_empty():
+            alias_error_label.show()
+            return
+        alias_error_label.hide()
+        self._add_contact()
+        self._back_to_main_window()
+
+    def _alias_is_empty(self):
+        alias = self.builder.get_object("alias_entry").get_text()
+        return len(alias) == 0
+
+    def _add_contact(self):
+        contacts = Contacts(APP().api)
         their_link = self.builder.get_object("their_link_entry").get_text()
         alias = self.builder.get_object("alias_entry").get_text()
-        contacts = Contacts(self._api)
         contacts.add_pending(their_link, alias)
-        GLib.idle_add(APP().window.back_to_main, None)
+
+    def _back_to_main_window(self):
+        GLib.idle_add(self._window.back_to_main, None)
diff --git a/src/briar/gtk/containers/main.py b/src/briar/gtk/containers/main.py
index 32ad26e5a5f1c2e6b2d5f67b82e8de37e6ee64dd..5cdadcca928eb2df87b619c08d3e3c13494d6ab6 100644
--- a/src/briar/gtk/containers/main.py
+++ b/src/briar/gtk/containers/main.py
@@ -38,4 +38,5 @@ class MainContainer(Container):
     # pylint: disable=unused-argument
     @staticmethod
     def _contact_clicked(widget, contact_id):
-        GLib.idle_add(APP().get_property("active_window").open_private_chat, contact_id)
+        GLib.idle_add(APP().get_property("active_window").
+                      open_private_chat, contact_id)
diff --git a/src/briar/gtk/window.py b/src/briar/gtk/window.py
index dad4ff2b536e785780de915a38093a71b0c7e392..665d0063a15b9ebe9314c3aa57ed2a0cd935f9cd 100644
--- a/src/briar/gtk/window.py
+++ b/src/briar/gtk/window.py
@@ -63,9 +63,11 @@ class Window(ApplicationWindow):
         self._grid.show()
         self.add(self._grid)
 
-    def _reset_grid(self):
+    def _reset_window(self):
+        self._container.destroy()
         self._grid.destroy()
         self._setup_grid()
+        self._setup_toolbar()
 
     def _setup_toolbar(self):
         self._toolbar = Toolbar()
@@ -81,18 +83,16 @@ class Window(ApplicationWindow):
 
     # pylint: disable=unused-argument
     def show_add_contact(self, widget):
-        self._reset_grid()
         self._setup_add_contact()
 
     def _setup_add_contact(self):
-        self._container = AddContactContainer()
+        self._grid.destroy()
+        self._container = AddContactContainer(self)
         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)
+        self.add(self._container)
 
     def open_private_chat(self, contact_id):
-        self._reset_grid()
+        self._reset_window()
         self._setup_private_chat(contact_id)
 
     def _setup_private_chat(self, contact_id):
@@ -104,6 +104,6 @@ class Window(ApplicationWindow):
 
     # pylint: disable=unused-argument
     def back_to_main(self, widget):
-        self._reset_grid()
+        self._reset_window()
         self._toolbar.show_back_button(False)
         self._setup_main_container()