diff --git a/gajim/data/gui/assistant.ui b/gajim/data/gui/assistant.ui
new file mode 100644
index 0000000000000000000000000000000000000000..9844e5bddebccc9538304c05a8b472c4d47a2c3e
--- /dev/null
+++ b/gajim/data/gui/assistant.ui
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.22.1 -->
+<interface>
+  <requires lib="gtk+" version="3.20"/>
+  <object class="GtkGrid" id="main_grid">
+    <property name="visible">True</property>
+    <property name="can_focus">False</property>
+    <property name="row_spacing">6</property>
+    <child>
+      <object class="GtkBox" id="content_area">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="hexpand">True</property>
+        <property name="vexpand">True</property>
+        <property name="orientation">vertical</property>
+        <child>
+          <object class="GtkStack" id="stack">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="hexpand">True</property>
+            <property name="vexpand">True</property>
+            <signal name="notify::visible-child-name" handler="_on_visible_child_name" swapped="no"/>
+            <child>
+              <placeholder/>
+            </child>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">True</property>
+            <property name="position">0</property>
+          </packing>
+        </child>
+      </object>
+      <packing>
+        <property name="left_attach">0</property>
+        <property name="top_attach">0</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkBox" id="action_area">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="spacing">12</property>
+        <child>
+          <placeholder/>
+        </child>
+      </object>
+      <packing>
+        <property name="left_attach">0</property>
+        <property name="top_attach">1</property>
+      </packing>
+    </child>
+    <style>
+      <class name="assistant-grid"/>
+    </style>
+  </object>
+</interface>
diff --git a/gajim/data/style/gajim.css b/gajim/data/style/gajim.css
index ebe47f77933fd1cf58b65bc4f013beb1e2fbfef2..8d7b2864e627e0e5e70f6af3f92750c630171ad6 100644
--- a/gajim/data/style/gajim.css
+++ b/gajim/data/style/gajim.css
@@ -273,4 +273,7 @@ .gajim-treeview check { padding: 0px; }
 .gajim-scrolled { border: 1px solid; border-color:@unfocused_borders; }
 
 
-#GroupchatJoin > box {padding: 18px; }
\ No newline at end of file
+#GroupchatJoin > box {padding: 18px; }
+
+.assistant-grid { padding: 18px; margin: 6px;}
+.assistant-grid > box stack  { padding: 18px; }
diff --git a/gajim/gtk/assistant.py b/gajim/gtk/assistant.py
new file mode 100644
index 0000000000000000000000000000000000000000..43b579bfd535816bb8433755951c31a67667e2ec
--- /dev/null
+++ b/gajim/gtk/assistant.py
@@ -0,0 +1,197 @@
+# This file is part of Gajim.
+#
+# Gajim is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published
+# by the Free Software Foundation; version 3 only.
+#
+# Gajim is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
+
+from gi.repository import Gdk
+from gi.repository import Gtk
+from gi.repository import Gio
+from gi.repository import GObject
+
+from gajim.gtk.util import get_builder
+from gajim.gtk.util import EventHelper
+
+
+class Assistant(Gtk.ApplicationWindow, EventHelper):
+
+    __gsignals__ = dict(
+        button_clicked=(
+            GObject.SignalFlags.RUN_LAST | GObject.SignalFlags.ACTION,
+            None,
+            (str, )
+        ),
+        page_changed=(
+            GObject.SignalFlags.RUN_LAST | GObject.SignalFlags.ACTION,
+            None,
+            (str, )
+        ))
+
+    def __init__(self, transient_for=None,
+                       width=550,
+                       height=400,
+                       transition_duration=200):
+        Gtk.ApplicationWindow.__init__(self)
+        EventHelper.__init__(self)
+        self.set_application(Gio.Application.get_default())
+        self.set_position(Gtk.WindowPosition.CENTER)
+        self.set_show_menubar(False)
+        self.set_name('Assistant')
+        self.set_default_size(width, height)
+        self.set_resizable(True)
+        self.set_transient_for(transient_for)
+
+        self._pages = {}
+        self._buttons = {}
+        self._button_visible_func = None
+
+        self._ui = get_builder('assistant.ui')
+        self.add(self._ui.main_grid)
+
+        self._ui.stack.set_transition_duration(transition_duration)
+
+        self.connect('key-press-event', self._on_key_press_event)
+        self.connect('destroy', self.__on_destroy)
+        self._ui.connect_signals(self)
+
+    def show_all(self):
+        page_name = self._ui.stack.get_visible_child_name()
+        buttons = self._button_visible_func(self, page_name)
+        self._set_buttons_visible(buttons)
+        self.emit('page-changed', page_name)
+        Gtk.ApplicationWindow.show_all(self)
+
+    def _on_key_press_event(self, _widget, event):
+        if event.keyval == Gdk.KEY_Escape:
+            self.destroy()
+
+    def set_button_visible_func(self, func):
+        self._button_visible_func = func
+
+    def set_default_button(self, button_name):
+        self._buttons[button_name].grab_default()
+
+    def add_button(self, name, label, css_class=None):
+        button = Gtk.Button(label=label,
+                            can_default=True,
+                            no_show_all=True)
+        button.connect('clicked', self.__on_button_clicked)
+        if css_class is not None:
+            button.get_style_context().add_class(css_class)
+        self._buttons[name] = button
+        self._ui.action_area.pack_end(button, False, False, 0)
+
+    def add_pages(self, pages):
+        self._pages = pages
+        for name, widget in pages.items():
+            self._ui.stack.add_named(widget, name)
+
+    def add_default_page(self, name):
+        if name == 'success':
+            page = SuccessPage()
+        elif name == 'error':
+            page = ErrorPage()
+        else:
+            raise ValueError('Unknown page: %s' % name)
+
+        self._pages[name] = page
+        self._ui.stack.add_named(page, name)
+
+    def get_current_page(self):
+        return self._ui.stack.get_visible_child_name()
+
+    def show_page(self, name, transition=Gtk.StackTransitionType.NONE):
+        buttons = self._button_visible_func(self, name)
+        self._set_buttons_visible(buttons)
+        self._ui.stack.set_visible_child_full(name, transition)
+
+    def get_error_page(self):
+        return self._pages['error']
+
+    def get_success_page(self):
+        return self._pages['success']
+
+    def _set_buttons_visible(self, buttons):
+        for button in self._buttons.values():
+            button.hide()
+
+        if buttons is None:
+            return
+
+        for button_name in buttons:
+            self._buttons[button_name].show()
+
+    def _on_visible_child_name(self, stack, _param):
+        self.set_title(stack.get_visible_child().title)
+        self.emit('page-changed', stack.get_visible_child_name())
+
+    def __on_button_clicked(self, button):
+        for button_name, button_ in self._buttons.items():
+            if button_ == button:
+                self.emit('button-clicked', button_name)
+                return
+
+    def __on_destroy(self, *args):
+        self._pages.clear()
+        self._buttons.clear()
+
+
+class Page(Gtk.Box):
+    def __init__(self, icon_name, icon_css_class):
+        super().__init__(orientation=Gtk.Orientation.VERTICAL)
+        self.set_spacing(18)
+        self.set_valign(Gtk.Align.CENTER)
+
+        self.title = ''
+
+        self._heading = Gtk.Label()
+        self._heading.get_style_context().add_class('large-header')
+        self._heading.set_max_width_chars(30)
+        self._heading.set_line_wrap(True)
+        self._heading.set_halign(Gtk.Align.CENTER)
+        self._heading.set_justify(Gtk.Justification.CENTER)
+
+        icon = Gtk.Image.new_from_icon_name(icon_name, Gtk.IconSize.DIALOG)
+        icon.get_style_context().add_class(icon_css_class)
+
+        self._label = Gtk.Label()
+        self._label.set_max_width_chars(50)
+        self._label.set_line_wrap(True)
+        self._label.set_halign(Gtk.Align.CENTER)
+        self._label.set_justify(Gtk.Justification.CENTER)
+
+        self.pack_start(self._heading, False, True, 0)
+        self.pack_start(icon, False, True, 0)
+        self.pack_start(self._label, False, True, 0)
+        self.show_all()
+
+    def set_heading(self, heading):
+        self._heading.set_text(heading)
+
+    def set_text(self, text):
+        self._label.set_text(text)
+
+    def set_title(self, title):
+        self.title = title
+
+
+class ErrorPage(Page):
+    def __init__(self):
+        Page.__init__(self,
+                      icon_name='dialog-error-symbolic',
+                      icon_css_class='error-color')
+
+
+class SuccessPage(Page):
+    def __init__(self):
+        Page.__init__(self,
+                      icon_name='object-select-symbolic',
+                      icon_css_class='success-color')
diff --git a/test/gtk/assistant.py b/test/gtk/assistant.py
new file mode 100644
index 0000000000000000000000000000000000000000..8a049a72c859ff5f39042b131fb6cafcf9269385
--- /dev/null
+++ b/test/gtk/assistant.py
@@ -0,0 +1,151 @@
+import gi
+gi.require_version('Gtk', '3.0')
+from gi.repository import Gtk
+
+from gajim.common.const import CSSPriority
+
+from gajim.gtk.assistant import Assistant
+
+from test.gtk import util
+util.load_style('gajim.css', CSSPriority.APPLICATION)
+
+
+class TestAssistant(Assistant):
+    def __init__(self):
+        Assistant.__init__(self)
+
+        self.add_pages({'start': Start(),
+                        'progress': Progress()})
+
+        self.add_default_page('error')
+        self.add_default_page('success')
+
+        self.add_button('forward', 'Forward', 'suggested-action')
+        self.add_button('close', 'Close', 'destructive-action')
+        self.add_button('back', 'Back')
+
+        self.set_button_visible_func(self._visible_func)
+
+        self.connect('button-clicked', self._on_button_clicked)
+        self.connect('page-changed', self._on_page_changed)
+
+        self.show_all()
+
+    @staticmethod
+    def _visible_func(_assistant, page_name):
+        if page_name == 'start':
+            return ['forward']
+
+        if page_name == 'progress':
+            return ['forward', 'back']
+
+        if page_name == 'success':
+            return ['forward', 'back']
+
+        if page_name == 'error':
+            return ['back', 'close']
+        raise ValueError('page %s unknown' % page_name)
+
+    def _on_button_clicked(self, _assistant, button_name):
+        page = self.get_current_page()
+        if button_name == 'forward':
+            if page == 'start':
+                self.show_page('progress', Gtk.StackTransitionType.SLIDE_LEFT)
+            elif page == 'progress':
+                self.show_page('success', Gtk.StackTransitionType.SLIDE_LEFT)
+            elif page == 'success':
+                self.show_page('error', Gtk.StackTransitionType.SLIDE_LEFT)
+            return
+
+        if button_name == 'back':
+            if page == 'progress':
+                self.show_page('start')
+            if page == 'success':
+                self.show_page('progress')
+            if page == 'error':
+                self.show_page('success')
+            return
+
+        if button_name == 'close':
+            self.destroy()
+
+    def _on_page_changed(self, _assistant, page_name):
+        if page_name == 'start':
+            self.set_default_button('forward')
+
+        elif page_name == 'progress':
+            self.set_default_button('forward')
+
+        elif page_name == 'success':
+            success_page = self.get_success_page()
+            success_page.set_title('Success')
+            success_page.set_heading('Success Heading')
+            success_page.set_text('This is the success text')
+            self.set_default_button('forward')
+
+        elif page_name == 'error':
+            error_page = self.get_error_page()
+            error_page.set_title('Error')
+            error_page.set_heading('Error Heading')
+            error_page.set_text('This is the error text')
+            self.set_default_button('back')
+
+
+class Progress(Gtk.Box):
+
+    title = 'Progress'
+
+    def __init__(self):
+        super().__init__(orientation=Gtk.Orientation.VERTICAL)
+        self.set_spacing(18)
+        self.set_valign(Gtk.Align.CENTER)
+        label = Gtk.Label(label='Test label...')
+        label.set_max_width_chars(50)
+        label.set_line_wrap(True)
+        label.set_halign(Gtk.Align.CENTER)
+        label.set_justify(Gtk.Justification.CENTER)
+        spinner = Gtk.Spinner()
+        self.pack_start(label, False, True, 0)
+        self.pack_start(spinner, False, True, 0)
+        spinner.start()
+        self.show_all()
+
+
+class Start(Gtk.Box):
+
+    title = 'Start'
+
+    def __init__(self):
+        super().__init__(orientation=Gtk.Orientation.VERTICAL)
+        self.set_spacing(18)
+        self.set_valign(Gtk.Align.CENTER)
+        heading = Gtk.Label(label='Test Assistant')
+        heading.get_style_context().add_class('large-header')
+
+        label1 = Gtk.Label(label='This is label 1 with some text')
+        label1.set_max_width_chars(50)
+        label1.set_line_wrap(True)
+        label1.set_halign(Gtk.Align.CENTER)
+        label1.set_justify(Gtk.Justification.CENTER)
+        label1.set_margin_bottom(24)
+
+        label2 = Gtk.Label(label='This is label 2 with some more text')
+        label2.set_max_width_chars(50)
+        label2.set_line_wrap(True)
+        label2.set_halign(Gtk.Align.CENTER)
+        label2.set_justify(Gtk.Justification.CENTER)
+
+        self._server = Gtk.CheckButton.new_with_mnemonic('A fancy checkbox')
+        self._server.set_halign(Gtk.Align.CENTER)
+
+        self.pack_start(heading, False, True, 0)
+        self.pack_start(label1, False, True, 0)
+        self.pack_start(label2, False, True, 0)
+        self.pack_start(self._server, False, True, 0)
+        self.show_all()
+
+
+win = TestAssistant()
+win.connect('destroy', Gtk.main_quit)
+win.show_all()
+Gtk.main()