diff --git a/aleksis/core/forms.py b/aleksis/core/forms.py
index 0fb1187ebde9f49de19a1dc51ab3390b0644d271..cd7f2184544e8aa464076a6d65a76be94cb1f42d 100644
--- a/aleksis/core/forms.py
+++ b/aleksis/core/forms.py
@@ -9,10 +9,12 @@ from django.utils import timezone
 from django.utils.translation import gettext_lazy as _
 
 from django_select2.forms import ModelSelect2MultipleWidget, Select2Widget
+from dynamic_preferences.forms import PreferenceForm
 from material import Layout, Fieldset, Row
 
 from .mixins import ExtensibleForm
 from .models import Group, Person, Announcement, AnnouncementRecipient
+from .registries import site_preferences_registry, person_preferences_registry, group_preferences_registry
 
 
 class PersonAccountForm(forms.ModelForm):
@@ -261,3 +263,16 @@ class AnnouncementForm(ExtensibleForm):
 
 class ChildGroupsForm(forms.Form):
     child_groups = forms.ModelMultipleChoiceField(queryset=Group.objects.all())
+
+
+class SitePreferenceForm(PreferenceForm):
+    registry = site_preferences_registry
+
+
+class PersonPreferenceForm(PreferenceForm):
+    registry = person_preferences_registry
+
+
+class GroupPreferenceForm(PreferenceForm):
+    registry = group_preferences_registry
+
diff --git a/aleksis/core/static/js/main.js b/aleksis/core/static/js/main.js
index b625c105f4e0e8a51b581104049bd1d76bcbc200..dd57cbe262eb60e24646b680fa435c940008c4aa 100644
--- a/aleksis/core/static/js/main.js
+++ b/aleksis/core/static/js/main.js
@@ -59,6 +59,9 @@ $(document).ready( function () {
     // Initialize Modals [MAT]
     $('.modal').modal();
 
+    // Intialize Tabs [Materialize]
+    $('.tabs').tabs();
+
     $('table.datatable').each(function (index) {
         $(this).DataTable({
             "paging": false
diff --git a/aleksis/core/templates/dynamic_preferences/form.html b/aleksis/core/templates/dynamic_preferences/form.html
new file mode 100644
index 0000000000000000000000000000000000000000..d8de37802c09a14327b0d949b5935fb69660c666
--- /dev/null
+++ b/aleksis/core/templates/dynamic_preferences/form.html
@@ -0,0 +1,28 @@
+{% extends "core/base.html" %}
+{% load i18n material_form %}
+
+{% block browser_title %}
+  {% trans "Preferences" %}
+{% endblock %}
+{% block page_title %}
+  {% if registry_name == "site" %}
+    {% blocktrans %}Site preferences{% endblocktrans %}
+  {% elif registry_name == "person" and instance == request.user.person %}
+    {% blocktrans %}My preferences{% endblocktrans %}
+  {% else %}
+    {% blocktrans with instace=instance %}Preferences for {{ instance }}{% endblocktrans %}
+  {% endif %}
+{% endblock %}
+
+{% block content %}
+  <div class="row">
+    {% include "dynamic_preferences/sections.html" with registry=registry sections=registry.sections active_section=section %}
+  </div>
+  <div class="row">
+    <form action="" enctype="multipart/form-data" method="post">
+      {% csrf_token %}
+      {% form form=form %}{% endform %}
+      {% include "core/save_button.html" with caption=_("Save preferences") %}
+    </form>
+  </div>
+{% endblock %}
diff --git a/aleksis/core/templates/dynamic_preferences/sections.html b/aleksis/core/templates/dynamic_preferences/sections.html
new file mode 100644
index 0000000000000000000000000000000000000000..8630933ba01305f14fc99f4d5bd0a0e02d4dcd1e
--- /dev/null
+++ b/aleksis/core/templates/dynamic_preferences/sections.html
@@ -0,0 +1,18 @@
+{% load i18n %}
+<ul class="tabs">
+  <li class="tab ">
+    <a href="{% url registry_url %}"
+       class="{% if not active_section %}active{% endif %}"
+       target="_self">
+      {% trans "All" %}
+    </a>
+    {% for section in registry.section_objects.values %}
+      <li class="tab">
+        <a class="{% if active_section == section.name %}active{% endif %}"
+           href="{% url registry_url section %}"
+           target="_self">
+          {{ section.verbose_name }}
+        </a>
+      </li>
+    {% endfor %}
+</ul>
diff --git a/aleksis/core/urls.py b/aleksis/core/urls.py
index be3143e994af82371519416e066e9fb99fe42e50..c8995b4c3cce5f80db99c99f8783cdedbcaf636a 100644
--- a/aleksis/core/urls.py
+++ b/aleksis/core/urls.py
@@ -48,6 +48,18 @@ urlpatterns = [
     path("jsreverse.js", urls_js, name='js_reverse'),
     path("calendarweek_i18n.js", calendarweek.django.i18n_js, name="calendarweek_i18n_js"),
     path('gettext.js', JavaScriptCatalog.as_view(), name='javascript-catalog'),
+    path("preferences/site/", views.preferences, {"registry_name": "site"}, name="preferences_site"),
+    path("preferences/person/", views.preferences, {"registry_name": "person"}, name="preferences_person"),
+    path("preferences/group/", views.preferences, {"registry_name": "group"}, name="preferences_group"),
+    path("preferences/site/<int:pk>/", views.preferences, {"registry_name": "site"}, name="preferences_site"),
+    path("preferences/person/<int:pk>/", views.preferences, {"registry_name": "person"}, name="preferences_person"),
+    path("preferences/group/<int:pk>/", views.preferences, {"registry_name": "group"}, name="preferences_group"),
+    path("preferences/site/<int:pk>/<str:section>/", views.preferences, {"registry_name": "site"}, name="preferences_site" ),
+    path("preferences/person/<int:pk>/<str:section>/", views.preferences, {"registry_name": "person"}, name="preferences_person"),
+    path("preferences/group/<int:pk>/<str:section>/", views.preferences, {"registry_name": "group"}, name="preferences_group"),
+    path("preferences/site/<str:section>/", views.preferences, {"registry_name": "site"}, name="preferences_site"),
+    path("preferences/person/<str:section>/", views.preferences, {"registry_name": "person"}, name="preferences_person"),
+    path("preferences/group/<str:section>/", views.preferences, {"registry_name": "group"}, name="preferences_group"),
 ]
 
 # Serve static files from STATIC_ROOT to make it work with runserver
diff --git a/aleksis/core/views.py b/aleksis/core/views.py
index 4f504300612f3c6e3f326a7c0a4ea2d9ffe6efff..f5face969248b2d8762312d5b20e405967f813ae 100644
--- a/aleksis/core/views.py
+++ b/aleksis/core/views.py
@@ -5,11 +5,12 @@ from django.apps import apps
 from django.contrib.auth.mixins import PermissionRequiredMixin
 from django.core.exceptions import PermissionDenied
 from django.core.paginator import Paginator
-from django.http import Http404, HttpRequest, HttpResponse
+from django.http import HttpRequest, HttpResponse, HttpResponseNotFound
 from django.shortcuts import get_object_or_404, redirect, render
 from django.utils.translation import gettext_lazy as _
 
 from django_tables2 import RequestConfig
+from dynamic_preferences.forms import preference_form_builder
 from guardian.shortcuts import get_objects_for_user
 from haystack.inputs import AutoQuery
 from haystack.query import SearchQuerySet
@@ -23,8 +24,12 @@ from .forms import (
     PersonsAccountsFormSet,
     AnnouncementForm,
     ChildGroupsForm,
+    SitePreferenceForm,
+    PersonPreferenceForm,
+    GroupPreferenceForm,
 )
 from .models import Activity, Group, Notification, Person, DashboardWidget, Announcement
+from .registries import site_preferences_registry, group_preferences_registry, person_preferences_registry
 from .tables import GroupsTable, PersonsTable
 from .util import messages
 from .util.apps import AppConfig
@@ -369,3 +374,49 @@ class PermissionSearchView(PermissionRequiredMixin, SearchView):
         if not self.has_permission():
             return self.handle_no_permission()
         return render(self.request, self.template, context)
+
+
+def preferences(request: HttpRequest, registry_name: str = "person", pk: Optional[int] = None, section: Optional[str] = None) -> HttpResponse:
+    """ View for changing preferences """
+
+    context = {}
+
+    if registry_name == "site":
+        registry = site_preferences_registry
+        instance = request.site
+        form_class = SitePreferenceForm
+
+    elif registry_name == "person":
+        registry = person_preferences_registry
+        if pk:
+            instance = get_object_or_404(Person, pk=pk)
+        else:
+            instance = request.user.person
+        form_class = PersonPreferenceForm
+
+    elif registry_name == "group":
+        registry = group_preferences_registry
+        instance = get_object_or_404(Group, pk=pk)
+        form_class = GroupPreferenceForm
+
+    else:
+        return HttpResponseNotFound()
+
+    form_class = preference_form_builder(form_class, instance=instance, section=section)
+
+    if request.method == "POST":
+        form = form_class(request.POST)
+        if form.is_valid():
+            form.update_preferences()
+            messages.success(request, _("The preferences has been saved successfully."))
+    else:
+        form = form_class()
+
+    context["registry"] = registry
+    context["registry_name"] = registry_name
+    context["section"] = section
+    context["registry_url"] = "preferences_" + registry_name
+    context["form"] = form
+    context["instance"] = instance
+
+    return render(request, "dynamic_preferences/form.html", context)