diff --git a/docs/manual/extending/coderef/index.rst b/docs/manual/extending/coderef/index.rst
index 3f26cb44e0d368d6ec48ce7b21ed51f5c9dbeb6a..ff26e47cfdebd6ca9b7396edf6ce833872f0467b 100644
--- a/docs/manual/extending/coderef/index.rst
+++ b/docs/manual/extending/coderef/index.rst
@@ -52,6 +52,7 @@ User Accounts
    reviewboard.accounts.testing
    reviewboard.accounts.testing.queries
    reviewboard.accounts.trophies
+   reviewboard.accounts.user_details
 
 
 Actions
diff --git a/docs/manual/extending/extensions/hooks/index.rst b/docs/manual/extending/extensions/hooks/index.rst
index a79fa1c873cdc3c1d132d5c1664d7cbf9a56a962..a1cf88bab02e6c5e4b97529d877530750d9bdc35 100644
--- a/docs/manual/extending/extensions/hooks/index.rst
+++ b/docs/manual/extending/extensions/hooks/index.rst
@@ -33,6 +33,7 @@ Extension Hooks
    signal-hook
    template-hook
    url-hook
+   user-details-provider-hook
    user-infobox-hook
    user-page-sidebar-items-hook
    webapi-capabilities-hook
diff --git a/docs/manual/extending/extensions/hooks/user-details-provider-hook.rst b/docs/manual/extending/extensions/hooks/user-details-provider-hook.rst
new file mode 100644
index 0000000000000000000000000000000000000000..d430cafd28d02b96c8c441f32e3341aed7905e12
--- /dev/null
+++ b/docs/manual/extending/extensions/hooks/user-details-provider-hook.rst
@@ -0,0 +1,63 @@
+.. _user-details-provider-hook:
+
+=======================
+UserDetailsProviderHook
+=======================
+
+.. versionadded:: 7.1
+
+User Detail Providers can provide information about users for specialized
+purposes. As of now, these can provide:
+
+* Badges: Groups of badged text displayed alongside users' names in parts of
+  the UI (such as discussions and infoboxes).
+
+
+Creating User Details Providers
+===============================
+
+To create a User Details Provider, you'll need to subclass
+:py:class:`reviewboard.accounts.user_details.BaseUserDetailsProvider`. You
+must set the following:
+
+* :py:attr:`~reviewboard.accounts.user_details.BaseUserDetailsProvider.
+  user_details_provider_id`:
+
+  A unique ID of the provider. This should be prefixed with a vendor or
+  extension ID to avoid collisions.
+
+It may also define:
+
+* :py:meth:`~reviewboard.accounts.user_details.BaseUserDetailsProvider.
+  get_user_badges`:
+
+  Calculates and returns
+  :py:class:`~reviewboard.accounts.user_details.UserBadge` instances for
+  any badges to display for a user.
+
+
+Example
+=======
+
+.. code-block:: python
+
+   from reviewboard.accounts.user_details import (BaseUserDetailsProvider,
+                                                  UserBadge)
+   from reviewboard.extensions.base import Extension
+   from reviewboard.extensions.hooks import UserDetailsProviderHook
+
+   class MyUserDetailsProvider(BaseUserDetailsProvider):
+       user_details_provider_id = 'my-user-details-provider'
+
+       def get_user_badges(self, user):
+           yield UserBadge(user=user,
+                           label='Developer')
+
+           if user.is_superuser:
+               yield UserBadge(user=user,
+                               label='Administrator')
+
+
+   class SampleExtension(Extension):
+       def initialize(self):
+           UserDetailsProviderHook(self, MyUserDetailsProvider())
diff --git a/docs/manual/extending/index.rst b/docs/manual/extending/index.rst
index fbe7dd7a1672bedf37b5d2af0beb2484ef329bc7..7d0d2c8209fe8ce01bb64445c4b34e49f7db7627 100644
--- a/docs/manual/extending/index.rst
+++ b/docs/manual/extending/index.rst
@@ -80,10 +80,19 @@ extension hooks available to you.
         Defines new URLs in Review Board, which can point to your own custom
         views.
 
+**User Information Hooks**
     :ref:`avatar-service-hook`:
         Adds a new avatar service, which can be used to provide pictures for
         user accounts.
 
+    :ref:`user-details-provider-hook`:
+        Adds a new User Details Provider, for providing extra information
+        about users.
+
+    :ref:`user-infobox-hook`:
+        Adds a new User Details Provider, for providing extra information
+        about users.
+
 :ref:`Action Hooks <action-hooks>`:
     A series of hooks used to add new actions for review requests, the diff
     viewer, and the account/support header at the top of the page.
diff --git a/docs/stories/components/users/user-badges.stories.ts b/docs/stories/components/users/user-badges.stories.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b626fa62c70636a26ebe4ebc9035eaf9a0ba6bd0
--- /dev/null
+++ b/docs/stories/components/users/user-badges.stories.ts
@@ -0,0 +1,46 @@
+import {
+    paint,
+} from '@beanbag/ink';
+
+
+export default {
+    tags: ['autodocs'],
+    title: 'Review Board/Components/Users/User Badges',
+
+    render: attrs => {
+        return paint`
+            <div class="rb-c-user-badges">
+             <span class="rb-c-user-badge">Badge 1</span>
+             <span class="rb-c-user-badge">Badge 2</span>
+             <span class="rb-c-user-badge">Badge 3</span>
+             <span class="rb-c-user-badge">
+              Very long badge with a long name that should help us test
+              some wrapping probably and we'll just keep going for a while
+              until we're seeing the wrap and this is probably enough.
+             </span>
+             <span class="rb-c-user-badge">Badge 5</span>
+            </div>
+        `;
+    },
+
+    argTypes: {
+        badges: {
+            control: 'object',
+        },
+    },
+
+    args: {
+        badges: [
+            'Badge 1',
+            'Badge 2',
+            'Badge 3',
+            ("Very long badge with a long name that should help us test " +
+             "some wrapping probably and we'll just keep going for a while " +
+             "until we're seeing the wrap and this is probably enough."),
+            'Badge 5',
+        ],
+    },
+};
+
+
+export const Badges = {};
diff --git a/reviewboard/accounts/templatetags/accounts.py b/reviewboard/accounts/templatetags/accounts.py
index e042069f0d15252a793bab18c00ce63933d88703..32d284825eb436e187c9cce968ebc2dae04236e3 100644
--- a/reviewboard/accounts/templatetags/accounts.py
+++ b/reviewboard/accounts/templatetags/accounts.py
@@ -1,19 +1,27 @@
 """Account-related template tags."""
 
+from __future__ import annotations
+
 import logging
 from datetime import datetime
+from typing import Iterator, TYPE_CHECKING
 
 import pytz
 from django import template
 from django.utils import dateformat, timezone
-from django.utils.html import escape
+from django.utils.html import escape, format_html, format_html_join
+from django.utils.safestring import SafeString, mark_safe
 from djblets.siteconfig.models import SiteConfiguration
 from djblets.util.templatetags.djblets_js import json_dumps
 
+from reviewboard.accounts.user_details import user_details_provider_registry
 from reviewboard.admin.read_only import is_site_read_only_for
 from reviewboard.avatars import avatar_services
 from reviewboard.site.urlresolvers import local_site_reverse
 
+if TYPE_CHECKING:
+    from django.contrib.auth.models import User
+
 
 logger = logging.getLogger(__name__)
 register = template.Library()
@@ -154,3 +162,46 @@ def js_user_session_info(context):
                                               request=request)
 
     return json_dumps(info)
+
+
+@register.simple_tag
+def user_badges(
+    user: User,
+) -> SafeString:
+    """Return badges shown for a user.
+
+    This will query for any badges provided for a user and yield each one.
+    No order is guaranteed.
+
+    Version Added:
+        7.1
+
+    Args:
+        user (django.contrib.auth.models.User):
+            The user to query for badges.
+
+    Yields:
+        reviewboard.accounts.user_details.UserBadge:
+        Each badge for the user.
+    """
+    def _iter_badges() -> Iterator[SafeString]:
+        for provider in user_details_provider_registry:
+            try:
+                for user_badge in provider.get_user_badges(user):
+                    yield user_badge.render_to_string()
+            except Exception as e:
+                logger.exception(
+                    'Unexpected error when fetching user badges from provider '
+                    '%r: %s',
+                    provider, e)
+
+    badges_html = format_html_join('', '{}', (
+        (html,)
+        for html in _iter_badges()
+    ))
+
+    if badges_html:
+        return format_html('<div class="rb-c-user-badges">{}</div>',
+                           badges_html)
+    else:
+        return mark_safe('')
diff --git a/reviewboard/accounts/tests/test_template_tags.py b/reviewboard/accounts/tests/test_template_tags.py
index b090496b4b633c9ce291b8eb28fa277f0f049632..9e295475062790441f6bb8f2dff627ec51a341be 100644
--- a/reviewboard/accounts/tests/test_template_tags.py
+++ b/reviewboard/accounts/tests/test_template_tags.py
@@ -9,6 +9,9 @@ from django.test.client import RequestFactory
 from django.utils.safestring import SafeText
 from djblets.testing.decorators import add_fixtures
 
+from reviewboard.accounts.user_details import (BaseUserDetailsProvider,
+                                               UserBadge,
+                                               user_details_provider_registry)
 from reviewboard.testing import TestCase
 
 
@@ -290,6 +293,115 @@ class JSUserSessionInfoTests(TestCase):
         return rendered
 
 
+class UserBadgesTests(TestCase):
+    """Unit tests for {% user_badges %}.
+
+    Version Added:
+        7.1
+    """
+
+    def test_with_no_badges(self) -> None:
+        """Testing {% user_badges %} with no badges"""
+        user = self.create_user()
+        t = Template(
+            '{% load accounts %}'
+            '{% user_badges user %}'
+        )
+
+        rendered = t.render(Context({
+            'user': user,
+        }))
+
+        self.assertIsInstance(rendered, SafeText)
+
+        self.assertEqual(rendered, '')
+
+    def test_with_badges(self) -> None:
+        """Testing {% user_badges %} with badges"""
+        class MyUserDetailsProvider(BaseUserDetailsProvider):
+            user_details_provider_id = 'my-user-details-provider'
+
+            def get_user_badges(self, user):
+                yield UserBadge(user=user,
+                                label='Badge 1')
+                yield UserBadge(user=user,
+                                label='Badge 2',
+                                css_class='custom-badge-css')
+
+        user = self.create_user()
+        t = Template(
+            '{% load accounts %}'
+            '{% user_badges user %}'
+        )
+
+        provider = MyUserDetailsProvider()
+
+        try:
+            user_details_provider_registry.register(provider)
+
+            rendered = t.render(Context({
+                'user': user,
+            }))
+        finally:
+            user_details_provider_registry.unregister(provider)
+
+        self.assertIsInstance(rendered, SafeText)
+
+        self.assertHTMLEqual(
+            rendered,
+            '<div class="rb-c-user-badges">'
+            '<span class="rb-c-user-badge">Badge 1</span>'
+            '<span class="rb-c-user-badge custom-badge-css">Badge 2</span>'
+            '</div>')
+
+    def test_with_badges_with_crash(self) -> None:
+        """Testing {% user_badges %} with crash in get_user_badges()"""
+        class MyUserDetailsProvider(BaseUserDetailsProvider):
+            user_details_provider_id = 'my-user-details-provider'
+
+            def get_user_badges(self, user):
+                yield UserBadge(user=user,
+                                label='Badge 1')
+
+                raise Exception('oh no')
+
+        user = self.create_user()
+        t = Template(
+            '{% load accounts %}'
+            '{% user_badges user %}'
+        )
+
+        provider = MyUserDetailsProvider()
+
+        try:
+            user_details_provider_registry.register(provider)
+
+            with self.assertLogs() as cm:
+                rendered = t.render(Context({
+                    'user': user,
+                }))
+        finally:
+            user_details_provider_registry.unregister(provider)
+
+        self.assertEqual(len(cm.output), 1)
+        self.assertRegex(
+            cm.output[0],
+            r'^ERROR:reviewboard\.accounts\.templatetags\.accounts:'
+            r'Unexpected error when fetching user badges from provider '
+            r'<reviewboard\.accounts\.tests\.test_template_tags\.'
+            r'UserBadgesTests\.test_with_badges_with_crash\.<locals>\.'
+            r'MyUserDetailsProvider object at 0x[a-f0-9]+>: oh no\n.*'
+            r'Traceback')
+
+        self.assertIsInstance(rendered, SafeText)
+
+        self.assertHTMLEqual(
+            rendered,
+            '<div class="rb-c-user-badges">'
+            '<span class="rb-c-user-badge">Badge 1</span>'
+            '</div>')
+
+
 class UserProfileDisplayNameTests(TestCase):
     """Unit tests for {% user_profile_display_name %}."""
 
diff --git a/reviewboard/accounts/tests/test_user_badge.py b/reviewboard/accounts/tests/test_user_badge.py
new file mode 100644
index 0000000000000000000000000000000000000000..2011739f0a8f8b81c63b17a5b418ffdc491e02dd
--- /dev/null
+++ b/reviewboard/accounts/tests/test_user_badge.py
@@ -0,0 +1,39 @@
+"""Unit tests for reviewboard.accounts.user_details.UserBadge.
+
+Version Added:
+    7.1
+"""
+
+from __future__ import annotations
+
+from reviewboard.accounts.user_details import UserBadge
+from reviewboard.testing import TestCase
+
+
+class UserBadgeTests(TestCase):
+    """Unit tests for UserBadge.
+
+    Version Added:
+        7.1
+    """
+
+    def test_render_to_string(self) -> None:
+        """Testing UserBadge.render_to_string"""
+        user = self.create_user()
+        badge = UserBadge(user=user,
+                          label='My Badge')
+
+        self.assertHTMLEqual(
+            badge.render_to_string(),
+            '<span class="rb-c-user-badge">My Badge</span>')
+
+    def test_render_to_string_with_css_class(self) -> None:
+        """Testing UserBadge.render_to_string with css_class"""
+        user = self.create_user()
+        badge = UserBadge(user=user,
+                          label='My Badge',
+                          css_class='class1 class2')
+
+        self.assertHTMLEqual(
+            badge.render_to_string(),
+            '<span class="rb-c-user-badge class1 class2">My Badge</span>')
diff --git a/reviewboard/accounts/user_details.py b/reviewboard/accounts/user_details.py
new file mode 100644
index 0000000000000000000000000000000000000000..93182a04929e56c5791807b04a52297eec99cf4d
--- /dev/null
+++ b/reviewboard/accounts/user_details.py
@@ -0,0 +1,156 @@
+"""Extra user detail introspection and representation.
+
+Version Added:
+    7.1
+"""
+
+from __future__ import annotations
+
+import logging
+from dataclasses import dataclass
+from typing import ClassVar, Iterator, Optional, TYPE_CHECKING
+
+from django.utils.html import format_html, format_html_join
+from django.utils.safestring import mark_safe
+
+from reviewboard.registries.registry import OrderedRegistry
+
+if TYPE_CHECKING:
+    from django.contrib.auth.models import User
+    from django.utils.safestring import SafeString
+
+
+logger = logging.getLogger(__name__)
+
+
+@dataclass
+class UserBadge:
+    """A badge shown alongside a user's name.
+
+    Badges can represent some detail about a user. All badges are provided
+    by extensions.
+
+    Version Added:
+        7.1
+    """
+
+    ######################
+    # Instance variables #
+    ######################
+
+    #: The visual label shown on the badge.
+    label: str
+
+    #: The user this badge belongs to.
+    user: User
+
+    #: An optional CSS class (or classes) used for the badge.
+    #:
+    #: This allows an extension to provide custom styling for a badge, such
+    #: as custom colors.
+    #:
+    #: Multiple CSS classes can be provided by separating them with spaces.
+    css_class: Optional[str] = None
+
+    def render_to_string(self) -> SafeString:
+        """Render the badge to an HTML string.
+
+        All badges use a ``rb-c-user-badge`` CSS class, along with any
+        CSS classes provided in :py:attr:`css_class`.
+
+        The label for the badge will reflect :py:attr:`label`.
+
+        Returns:
+            django.utils.safestring.SafeString:
+            The HTML for the badge.
+        """
+        try:
+            css_class = self.css_class
+
+            css_classes: list[str] = [
+                'rb-c-user-badge',
+            ]
+
+            if css_class:
+                css_classes.append(css_class)
+
+            attrs: list[tuple[str, str]] = [
+                ('class', ' '.join(css_classes)),
+            ]
+
+            return format_html(
+                '<span {attrs}>{label}</span>',
+                attrs=format_html_join(' ', '{}="{}"', attrs),
+                label=self.label)
+        except Exception as e:
+            logger.exception(
+                'Unexpected error when rendering user badge %r: %s',
+                self, e)
+
+            return mark_safe('')
+
+
+class BaseUserDetailsProvider:
+    """Base class for a provider for additional user details.
+
+    This enables extensions to provide additional information about users,
+    for specialized rendering and logic.
+
+    Version Added:
+        7.1
+    """
+
+    #: The unique ID of the user details provider.
+    user_details_provider_id: ClassVar[str]
+
+    def get_user_badges(
+        self,
+        user: User,
+    ) -> Iterator[UserBadge]:
+        """Return a list of badges to display for a user.
+
+        Badges provide a visual indicator about some aspect of a user, for
+        display purposes.
+
+        Yields:
+            UserBadge:
+            Each badge to display.
+        """
+        yield from []
+
+
+class UserDetailsProviderRegistry(OrderedRegistry[BaseUserDetailsProvider]):
+    """A registry of user detail providers.
+
+    Each provider in the registry will be queried when requesting additional
+    information on users.
+
+    Version Added:
+        7.1
+    """
+
+    lookup_attrs = ('user_details_provider_id',)
+
+    def get_user_details_provider(
+        self,
+        provider_id: str,
+    ) -> Optional[BaseUserDetailsProvider]:
+        """Return the user details provider for a given ID.
+
+        Args:
+            provider_id (str):
+                The ID of the user details provider to return.
+
+        Returns:
+            UserDetailsProvider:
+            The user details provider matching the ID, or ``None`` if not
+            found.
+        """
+        return self.get('user_details_provider_id', provider_id)
+
+
+#: The registry managing user details providers.
+#:
+#: Version Added:
+#:     7.1
+user_details_provider_registry = UserDetailsProviderRegistry()
diff --git a/reviewboard/extensions/hooks/__init__.py b/reviewboard/extensions/hooks/__init__.py
index 72cf7bcad1ee0dcda1bb8f7f7ecf3841978a4505..136ab4a89aeb01e05defb2285b13b986d61c01e9 100644
--- a/reviewboard/extensions/hooks/__init__.py
+++ b/reviewboard/extensions/hooks/__init__.py
@@ -49,6 +49,7 @@ from reviewboard.extensions.hooks.review_request_approval import \
 from reviewboard.extensions.hooks.review_ui import ReviewUIHook
 from reviewboard.extensions.hooks.scmtool import SCMToolHook
 from reviewboard.extensions.hooks.user_infobox import UserInfoboxHook
+from reviewboard.extensions.hooks.users import UserDetailsProviderHook
 from reviewboard.extensions.hooks.webapi_capabilities import \
     WebAPICapabilitiesHook
 
@@ -93,6 +94,7 @@ __all__ = [
     'SignalHook',
     'TemplateHook',
     'URLHook',
+    'UserDetailsProviderHook',
     'UserInfoboxHook',
     'UserPageSidebarItemsHook',
     'WebAPICapabilitiesHook',
diff --git a/reviewboard/extensions/hooks/users.py b/reviewboard/extensions/hooks/users.py
new file mode 100644
index 0000000000000000000000000000000000000000..22fe6fb0436b7060c03524e9f8f25c09bfb5043f
--- /dev/null
+++ b/reviewboard/extensions/hooks/users.py
@@ -0,0 +1,25 @@
+"""Hooks for working with user-related information.
+
+Version Added:
+    7.1
+"""
+
+from __future__ import annotations
+
+from djblets.extensions.hooks import BaseRegistryHook, ExtensionHookPoint
+
+from reviewboard.accounts.user_details import (BaseUserDetailsProvider,
+                                               user_details_provider_registry)
+
+
+class UserDetailsProviderHook(BaseRegistryHook[BaseUserDetailsProvider],
+                              metaclass=ExtensionHookPoint):
+    """Hook to register custom user detail providers.
+
+    See :ref:`user-details-provider-hook` for instructions.
+
+    Version Added:
+        7.1
+    """
+
+    registry = user_details_provider_registry
diff --git a/reviewboard/extensions/tests/test_user_details_provider_hook.py b/reviewboard/extensions/tests/test_user_details_provider_hook.py
new file mode 100644
index 0000000000000000000000000000000000000000..1c773f22f5a7f2c68b0073a7d15a2dbe0fedbe77
--- /dev/null
+++ b/reviewboard/extensions/tests/test_user_details_provider_hook.py
@@ -0,0 +1,39 @@
+"""Tests for reviewboard.extensions.hooks.UserDetailsProviderHook.
+
+Version Added:
+    7.1
+"""
+
+from __future__ import annotations
+
+from reviewboard.accounts.user_details import (BaseUserDetailsProvider,
+                                               user_details_provider_registry)
+from reviewboard.extensions.hooks import UserDetailsProviderHook
+from reviewboard.extensions.tests.testcases import BaseExtensionHookTestCase
+
+
+class MyUserDetailsProvider(BaseUserDetailsProvider):
+    user_details_provider_id = 'my-user-details-provider'
+
+
+class UserDetailsProviderHookTests(BaseExtensionHookTestCase):
+    """Tests for UserDetailsProviderHook."""
+
+    def test_register(self) -> None:
+        """Testing UserDetailsProviderHook initializing"""
+        provider = MyUserDetailsProvider()
+        UserDetailsProviderHook(self.extension, provider)
+
+        self.assertIs(
+            user_details_provider_registry.get_user_details_provider(
+                'my-user-details-provider'),
+            provider)
+
+    def test_unregister(self) -> None:
+        """Testing UserDetailsProviderHook uninitializing"""
+        hook = UserDetailsProviderHook(self.extension, MyUserDetailsProvider())
+        hook.disable_hook()
+
+        self.assertIsNone(
+            user_details_provider_registry.get_user_details_provider(
+                'my-user-details-provider'))
diff --git a/reviewboard/static/rb/css/bundles/common.less b/reviewboard/static/rb/css/bundles/common.less
index 1c443dae26f0fc0ea88269a1eaefea1f7f96b44a..1db07f680f96f199d4404ca5bb4fdf90481645fc 100644
--- a/reviewboard/static/rb/css/bundles/common.less
+++ b/reviewboard/static/rb/css/bundles/common.less
@@ -29,5 +29,6 @@
 @import 'rb/css/ui/tabs.less';
 @import 'rb/css/ui/text-editor.less';
 @import 'rb/css/ui/trophies.less';
+@import 'rb/css/ui/user-badge.less';
 @import 'rb/css/common.less';
 @import 'rb/css/syntax.less';
diff --git a/reviewboard/static/rb/css/ui/infoboxes.less b/reviewboard/static/rb/css/ui/infoboxes.less
index cc43f90737d6be0be14176d77259738e87537823..022ae4d6d7343c8bf75ae1739d684d2b9f7d93b9 100644
--- a/reviewboard/static/rb/css/ui/infoboxes.less
+++ b/reviewboard/static/rb/css/ui/infoboxes.less
@@ -43,7 +43,7 @@
     color: inherit;
     font-size: @infobox-header-font-size;
     font-weight: bold;
-    margin: 0 0 0.5em 0;
+    margin: 0;
     padding: 0;
     vertical-align: top;
 
@@ -58,6 +58,14 @@
   }
 }
 
+.infobox-content {
+  width: 100%;
+}
+
+.infobox-details {
+  display: flex;
+}
+
 .infobox-extra {
   box-sizing: border-box;
   padding: 0.5em;
@@ -66,8 +74,6 @@
 
 .infobox-hover-item,
 .infobox-item {
-  margin: 0.5em 0;
-
   .fa {
     min-width: @infobox-item-small-icon-size;
     text-align: right;
@@ -134,9 +140,6 @@
 }
 
 .infobox-pic {
-  display: inline-block;
-  vertical-align: top;
-
   img {
     border-radius: @infobox-inner-border-radius 0
                    0 @infobox-inner-border-radius;
@@ -182,7 +185,9 @@
 
 .infobox-text {
   box-sizing: border-box;
-  display: inline-block;
+  display: flex;
+  flex-direction: column;
+  gap: var(--ink-u-spacing-s) 0;
   padding: @infobox-padding;
   position: relative;
   vertical-align: top;
@@ -225,14 +230,14 @@
   }
 
   .infobox-text {
-    min-height: @user-infobox-avatar-size;
     min-width: 20em;
     max-width: 40em;
     width: auto;
 
     .infobox-links {
-      position: absolute;
-      bottom: @infobox-padding;
+      align-content: end;
+      flex-grow: 1;
+      padding-top: var(--ink-u-spacing-l);
     }
   }
 
@@ -267,6 +272,7 @@
 
 .review-request-infobox {
   .infobox-pic {
+    display: inline-block;
     border-radius: 0;
     vertical-align: middle;
   }
diff --git a/reviewboard/static/rb/css/ui/user-badge.less b/reviewboard/static/rb/css/ui/user-badge.less
new file mode 100644
index 0000000000000000000000000000000000000000..3ce6d206936ec4414c9ae0cdd59ded565f964445
--- /dev/null
+++ b/reviewboard/static/rb/css/ui/user-badge.less
@@ -0,0 +1,42 @@
+/**
+ * A badge shown alongside a user.
+ *
+ * User badges provide additional detail about a user. These are provided by
+ * extensions.
+ *
+ * Badges should have a standard style, but can be specialized to include
+ * custom colors.
+ *
+ * Version Added:
+ *     7.1
+ *
+ * Structure:
+ *     <span class="rb-c-user-badge [...]">
+ *      text...
+ *     </span>
+ */
+.rb-c-user-badge {
+  background: var(--ink-p-cool-grey-300);
+  border-radius: var(--ink-u-border-radius-l);
+  font-size: var(--ink-u-font-s);
+  font-weight: normal;
+  padding: var(--ink-u-spacing-xs) var(--ink-u-spacing-sm);
+}
+
+
+/**
+ * A group of badges to show alongside a user.
+ *
+ * Version Added:
+ *     7.1
+ *
+ * Structure:
+ *     <div class="rb-c-user-badges">
+ *      <div class="rb-c-user-badge ...">...</div> [1+]
+ *     </div>
+ */
+.rb-c-user-badges {
+  display: inline-flex;
+  flex-wrap: wrap;
+  gap: var(--ink-u-spacing-s);
+}
diff --git a/reviewboard/templates/accounts/user_infobox.html b/reviewboard/templates/accounts/user_infobox.html
index 6fcb1de42b510056227aee2d2163f78a90ce77ab..ad5195c8c27de49b246f040a9ea50b814020e2ff 100644
--- a/reviewboard/templates/accounts/user_infobox.html
+++ b/reviewboard/templates/accounts/user_infobox.html
@@ -1,4 +1,4 @@
-{% load avatars i18n %}
+{% load accounts avatars i18n %}
 
 <div class="infobox-content {{extra_content|yesno:'has-extra-content,'}}">
  <div class="infobox-details">
@@ -14,6 +14,10 @@
    <h2><a href="{% url 'user' infobox_user %}">{{infobox_user.username}}</a></h2>
 {% endif %}
 
+   <div class="infobox-item">
+    {% user_badges entry.avatar_user %}
+   </div>
+
 {% if show_profile %}
 {%  if request.user.is_authenticated %}
    <div class="infobox-item email">
@@ -51,4 +55,4 @@
 {% if extra_content %}
  <div class="infobox-extra">{{extra_content}}</div>
 {% endif %}
-</div>
\ No newline at end of file
+</div>
diff --git a/reviewboard/templates/reviews/entries/review.html b/reviewboard/templates/reviews/entries/review.html
index d615a5949f7c4c59c85c1feab779408a348e8ec3..617156b7025e27fede9ec3f619f6497c3890a47a 100644
--- a/reviewboard/templates/reviews/entries/review.html
+++ b/reviewboard/templates/reviews/entries/review.html
@@ -30,6 +30,7 @@
 
 {% block entry_title %}
 <a href="{% url 'user' entry.review.user %}" class="user">{% user_profile_display_name entry.review.user %}</a>
+{%  user_badges entry.avatar_user %}
 {% endblock entry_title %}
 
 
diff --git a/reviewboard/templates/reviews/review_reply.html b/reviewboard/templates/reviews/review_reply.html
index eb2170a7dcfa12a7f8755e9aef51346dfb175442..15fec48fcbdd722b47c9974b47890d3f6490b788 100644
--- a/reviewboard/templates/reviews/review_reply.html
+++ b/reviewboard/templates/reviews/review_reply.html
@@ -15,6 +15,7 @@
     <div class="rb-icon rb-icon-new-updates" title="{% trans 'New reply' %}"></div>
 {% endif %}
     <a href="{% url 'user' reply_user %}" class="user">{% user_profile_display_name reply_user %}</a>
+    {% user_badges entry.avatar_user %}
     <a href="#{{anchor_name}}" class="timestamp">
      {% localtime on %}{% blocktrans with timestamp as timestamp and timestamp|date:"c" as timestamp_raw %}<time class="timesince" datetime="{{timestamp_raw}}">{{timestamp}}</time>{% endblocktrans %}{% endlocaltime %}</a>
    </div>
