diff --git a/CHANGELOG.md b/CHANGELOG.md
index e558c91f..f31725ca 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,7 @@ All notable changes to this project will be documented in this file.
 - Add support for $NAME_JSON and $BODY_JSON placeholders in webhook payloads
 - Update the WhatsApp integration to use Twilio Content Templates
 - Add auto-refresh functionality to the Log page (#957, @mickBoat00)
+- Redesign the "Status Badges" page
 
 ### Bug Fixes
 - Fix Gotify integration to handle Gotify server URLs with paths (#964)
diff --git a/hc/front/forms.py b/hc/front/forms.py
index f5ff47c4..4edf1a2a 100644
--- a/hc/front/forms.py
+++ b/hc/front/forms.py
@@ -27,6 +27,10 @@ def _is_latin1(s: str) -> bool:
         return False
 
 
+def _choices(csv: str) -> list[tuple[str, str]]:
+    return [(v, v) for v in csv.split(",")]
+
+
 class LaxURLField(forms.URLField):
     default_validators = [WebhookValidator()]
 
@@ -85,9 +89,7 @@ class NameTagsForm(forms.Form):
 
 
 class AddCheckForm(NameTagsForm):
-    kind = forms.ChoiceField(
-        choices=(("simple", "simple"), ("cron", "cron"), ("oncalendar", "oncalendar"))
-    )
+    kind = forms.ChoiceField(choices=_choices("simple,cron,oncalendar"))
     timeout = forms.IntegerField(min_value=60, max_value=31536000)
     schedule = forms.CharField(required=False, max_length=100)
     tz = forms.CharField(max_length=36, validators=[TimezoneValidator()])
@@ -198,19 +200,16 @@ class AddUrlForm(forms.Form):
     value = LaxURLField(max_length=1000)
 
 
-METHODS = ("GET", "POST", "PUT")
-
-
 class WebhookForm(forms.Form):
     error_css_class = "has-error"
     name = forms.CharField(max_length=100, required=False)
 
-    method_down = forms.ChoiceField(initial="GET", choices=zip(METHODS, METHODS))
+    method_down = forms.ChoiceField(initial="GET", choices=_choices("GET,POST,PUT"))
     body_down = forms.CharField(max_length=1000, required=False)
     headers_down = HeadersField(required=False)
     url_down = LaxURLField(max_length=1000, required=False)
 
-    method_up = forms.ChoiceField(initial="GET", choices=zip(METHODS, METHODS))
+    method_up = forms.ChoiceField(initial="GET", choices=_choices("GET,POST,PUT"))
     body_up = forms.CharField(max_length=1000, required=False)
     headers_up = HeadersField(required=False)
     url_up = LaxURLField(max_length=1000, required=False)
@@ -404,3 +403,10 @@ class TransferForm(forms.Form):
 
 class AddTelegramForm(forms.Form):
     project = forms.UUIDField()
+
+
+class BadgeSettingsForm(forms.Form):
+    target = forms.ChoiceField(choices=_choices("all,tag"))
+    tag = forms.CharField(max_length=100, required=False)
+    fmt = forms.ChoiceField(choices=_choices("svg,json,shields"))
+    states = forms.ChoiceField(choices=_choices("2,3"))
diff --git a/hc/front/tests/test_badges.py b/hc/front/tests/test_badges.py
index d73124b1..cd15e02a 100644
--- a/hc/front/tests/test_badges.py
+++ b/hc/front/tests/test_badges.py
@@ -1,5 +1,7 @@
 from __future__ import annotations
 
+from django.test.utils import override_settings
+
 from hc.api.models import Check
 from hc.test import BaseTestCase
 
@@ -8,38 +10,71 @@ class BadgesTestCase(BaseTestCase):
     def setUp(self) -> None:
         super().setUp()
 
-        self.url = f"/projects/{self.project.code}/badges/"
-
-    def test_it_shows_badges(self) -> None:
-        Check.objects.create(project=self.project, tags="foo a-B_1  baz@")
-        Check.objects.create(project=self.bobs_project, tags="bobs-tag")
-
-        self.client.login(username="alice@example.org", password="password")
-        r = self.client.get(self.url)
-        self.assertContains(r, "foo.svg")
-        self.assertContains(r, "a-B_1.svg")
-
-        # Expect badge URLs only for tags that match \w+
-        self.assertNotContains(r, "baz@.svg")
-
-        # Expect only Alice's tags
-        self.assertNotContains(r, "bobs-tag.svg")
-
-    def test_it_uses_badge_key(self) -> None:
-        Check.objects.create(project=self.project, tags="foo bar")
-        Check.objects.create(project=self.bobs_project, tags="bobs-tag")
-
         self.project.badge_key = "alices-badge-key"
         self.project.save()
 
+        Check.objects.create(project=self.project, tags="foo a-B_1  baz@")
+
+        self.url = f"/projects/{self.project.code}/badges/"
+
+    def test_it_shows_form(self) -> None:
         self.client.login(username="alice@example.org", password="password")
         r = self.client.get(self.url)
+        self.assertContains(r, "foo")
+        self.assertContains(r, "a-B_1")
+        self.assertContains(r, self.project.badge_key)
+
+    def test_it_checks_ownership(self) -> None:
+        self.client.login(username="charlie@example.org", password="password")
+        r = self.client.get(self.url)
+        self.assertEqual(r.status_code, 404)
+
+    def test_team_access_works(self) -> None:
+        # Logging in as bob, not alice. Bob has team access so this
+        # should work.
+        self.client.login(username="bob@example.org", password="password")
+        r = self.client.get(self.url)
+        self.assertEqual(r.status_code, 200)
+
+    @override_settings(MASTER_BADGE_LABEL="Overall Status")
+    def test_it_previews_master_svg(self) -> None:
+        self.client.login(username="alice@example.org", password="password")
+        payload = {"target": "all", "fmt": "svg", "states": "2"}
+        r = self.client.post(self.url, payload)
+
+        self.assertContains(r, "![Overall Status]")
+
+    def test_it_previews_svg(self) -> None:
+        self.client.login(username="alice@example.org", password="password")
+        payload = {"target": "tag", "tag": "foo", "fmt": "svg", "states": "2"}
+        r = self.client.post(self.url, payload)
+
         self.assertContains(r, "badge/alices-badge-key/")
-        self.assertContains(r, "badge/alices-badge-key/")
+        self.assertContains(r, "foo.svg")
+        self.assertContains(r, "![foo]")
 
     def test_it_handles_special_characters_in_tags(self) -> None:
-        Check.objects.create(project=self.project, tags="db@dc1")
-
         self.client.login(username="alice@example.org", password="password")
-        r = self.client.get(self.url)
+        payload = {"target": "tag", "tag": "db@dc1", "fmt": "svg", "states": "2"}
+        r = self.client.post(self.url, payload)
         self.assertContains(r, "db%2540dc1.svg")
+        self.assertContains(r, "![db@dc1]")
+
+    def test_it_previews_json(self) -> None:
+        self.client.login(username="alice@example.org", password="password")
+        payload = {"target": "tag", "tag": "foo", "fmt": "json", "states": "2"}
+        r = self.client.post(self.url, payload)
+
+        self.assertContains(r, "fetch-json")
+        self.assertContains(r, "foo.json")
+        self.assertNotContains(r, "![foo]")
+
+    def test_it_previews_shields(self) -> None:
+        self.client.login(username="alice@example.org", password="password")
+        payload = {"target": "tag", "tag": "foo", "fmt": "shields", "states": "2"}
+        r = self.client.post(self.url, payload)
+
+        self.assertContains(r, "https://img.shields.io/endpoint")
+        self.assertContains(r, "%3A%2F%2F")  # url-encoded "://"
+        self.assertContains(r, "foo.shields")
+        self.assertContains(r, "![foo]")
diff --git a/hc/front/views.py b/hc/front/views.py
index 11389ccb..cbf013f7 100644
--- a/hc/front/views.py
+++ b/hc/front/views.py
@@ -11,7 +11,6 @@ from collections import Counter, defaultdict
 from collections.abc import Iterable
 from datetime import datetime
 from datetime import timedelta as td
-from datetime import timezone
 from email.message import EmailMessage
 from secrets import token_urlsafe
 from typing import Literal, TypedDict, cast
@@ -1097,35 +1096,38 @@ def status_single(request: AuthenticatedHttpRequest, code: UUID) -> HttpResponse
 def badges(request: AuthenticatedHttpRequest, code: UUID) -> HttpResponse:
     project, rw = _get_project_for_user(request, code)
 
+    if request.method == "POST":
+        form = forms.BadgeSettingsForm(request.POST)
+        if not form.is_valid():
+            return HttpResponseBadRequest()
+
+        if form.cleaned_data["target"] == "all":
+            label = settings.MASTER_BADGE_LABEL
+            tag = "*"
+        else:
+            label = form.cleaned_data["tag"]
+            tag = form.cleaned_data["tag"]
+        fmt = form.cleaned_data["fmt"]
+        with_late = True if form.cleaned_data["states"] == "3" else False
+        url = get_badge_url(project.badge_key, tag, fmt, with_late)
+        if fmt == "shields":
+            url = "https://img.shields.io/endpoint?" + urlencode({"url": url})
+
+        ctx = {"fmt": fmt, "label": label, "url": url}
+        return render(request, "front/badges_preview.html", ctx)
+
     tags = set()
     for check in Check.objects.filter(project=project):
         tags.update(check.tags_list())
 
     sorted_tags = sorted(tags, key=lambda s: s.lower())
-    sorted_tags.append("*")  # For the "overall status" badge
-
-    key = project.badge_key
-    urls = []
-    for tag in sorted_tags:
-        urls.append(
-            {
-                "tag": tag,
-                "svg": get_badge_url(key, tag),
-                "svg3": get_badge_url(key, tag, with_late=True),
-                "json": get_badge_url(key, tag, fmt="json"),
-                "json3": get_badge_url(key, tag, fmt="json", with_late=True),
-                "shields": get_badge_url(key, tag, fmt="shields"),
-                "shields3": get_badge_url(key, tag, fmt="shields", with_late=True),
-            }
-        )
 
     ctx = {
-        "have_tags": len(urls) > 1,
-        "page": "badges",
-        "project": project,
-        "badges": urls,
+        "tags": sorted_tags,
+        "fmt": "svg",
+        "label": settings.MASTER_BADGE_LABEL,
+        "url": get_badge_url(project.badge_key, "*"),
     }
-
     return render(request, "front/badges.html", ctx)
 
 
diff --git a/hc/lib/badges.py b/hc/lib/badges.py
index bbc31366..42a3e125 100644
--- a/hc/lib/badges.py
+++ b/hc/lib/badges.py
@@ -99,21 +99,21 @@ def get_badge_svg(tag: str, status: str) -> str:
     return render_to_string("badge.svg", ctx)
 
 
-def check_signature(username: str, tag: str, sig: str) -> bool:
-    ours = base64_hmac(str(username), tag, settings.SECRET_KEY)
+def check_signature(badge_key: str, tag: str, sig: str) -> bool:
+    ours = base64_hmac(str(badge_key), tag, settings.SECRET_KEY)
     return ours[:8] == sig[:8]
 
 
 def get_badge_url(
-    username: str, tag: str, fmt: str = "svg", with_late: bool = False
+    badge_key: str, tag: str, fmt: str = "svg", with_late: bool = False
 ) -> str:
-    sig = base64_hmac(str(username), tag, settings.SECRET_KEY)[:8]
+    sig = base64_hmac(str(badge_key), tag, settings.SECRET_KEY)[:8]
     if not with_late:
         sig += "-2"
 
     if tag == "*":
-        url = reverse("hc-badge-all", args=[username, sig, fmt])
+        url = reverse("hc-badge-all", args=[badge_key, sig, fmt])
     else:
-        url = reverse("hc-badge", args=[username, sig, tag, fmt])
+        url = reverse("hc-badge", args=[badge_key, sig, tag, fmt])
 
     return settings.SITE_ROOT + url
diff --git a/static/css/badges.css b/static/css/badges.css
index 8c06f93b..0f20d43b 100644
--- a/static/css/badges.css
+++ b/static/css/badges.css
@@ -1,16 +1,10 @@
-.table.badge-preview th {
-    border-top: 0;
-    color: var(--text-muted);
-    font-weight: normal;
-    font-size: 12px;
-    padding-top: 32px;
+#status-badges #tag {
+    margin-left: 49px;
+    width: 200px
 }
 
-#badges-json .fetch-json {
-    background: var(--pre-bg);
-    padding: 3px;
-}
-
-#badges-json, #badges-shields, .badge-preview .with-late {
-    display: none;
+#status-badges #preview img,
+#status-badges #preview pre,
+#status-badges #preview input {
+    margin-bottom: 20px;
 }
\ No newline at end of file
diff --git a/static/js/badges.js b/static/js/badges.js
index 4681f92a..cb1777b5 100644
--- a/static/js/badges.js
+++ b/static/js/badges.js
@@ -1,37 +1,23 @@
 $(function() {
-
-    $(".fetch-json").each(function(idx, el) {
-        $.getJSON(el.dataset.url, function(data) {
-            el.innerText = JSON.stringify(data);
+    function updatePreview() {
+        var params = $("#badge-settings-form").serialize();
+        var token = $('input[name=csrfmiddlewaretoken]').val();
+        $.ajax({
+            url: window.location.href,
+            type: "post",
+            headers: {"X-CSRFToken": token},
+            data: params,
+            success: function(data) {
+                document.getElementById("preview").innerHTML = data;
+                $(".fetch-json").each(function(idx, el) {
+                    $.getJSON(el.dataset.url, function(data) {
+                        el.innerText = JSON.stringify(data);
+                    });
+                });
+            }
         });
-    });
-
-    $("#show-svg").click(function() {
-        $("#badges-svg").show();
-        $("#badges-json").hide();
-        $("#badges-shields").hide();
-    })
-
-    $("#show-json").click(function() {
-        $("#badges-svg").hide();
-        $("#badges-json").show();
-        $("#badges-shields").hide();
-    })
-
-    $("#show-shields").click(function() {
-        $("#badges-svg").hide();
-        $("#badges-json").hide();
-        $("#badges-shields").show();
-    })
-
-    $("#show-with-late").click(function() {
-        $(".no-late").hide();
-        $(".with-late").show();
-    })
-
-    $("#show-no-late").click(function() {
-        $(".with-late").hide();
-        $(".no-late").show();
-    })
+    }
 
+    $("input[type=radio]").change(updatePreview);
+    $("select").change(updatePreview);
 });
diff --git a/templates/front/badges.html b/templates/front/badges.html
index 2b47d208..ba0f1fb1 100644
--- a/templates/front/badges.html
+++ b/templates/front/badges.html
@@ -4,132 +4,86 @@
 {% block title %}Status Badges - {{ site_name }}{% endblock %}
 
 {% block content %}
-<div class="row">
-    <div class="col-sm-10">
+<div id="status-badges" class="row">
+    <div class="col-sm-12">
         <h1>Status Badges</h1>
-
-        <p id="badges-description">
+        <p>
             {{ site_name }} provides status badges for each of the tags
             you have used. The badges have public, but hard-to-guess
             URLs. You can use them in your READMEs,
             dashboards or status pages.
         </p>
-
         <p>Each badge can be in one of the following states:</p>
         <ul>
             <li><strong>up</strong> – all matching checks are up.</li>
             <li><strong>down</strong> – at least one check is currently down.</li>
         </ul>
-
         <p>
             As an option, the badges can report a third state:
            <strong>late</strong> (when at least one check is running late but has not
            exceeded its grace time yet).
         </p>
-
-        <br />
-
-        <div class="btn-group" data-toggle="buttons">
-            <label id="show-svg" class="btn btn-default active">
-                <input type="radio" checked> SVG
-            </label>
-            <label id="show-json" class="btn btn-default">
-                <input type="radio"> JSON
-            </label>
-            <label id="show-shields" class="btn btn-default">
-                <input type="radio"> Shields.io
-            </label>
-        </div>
-        &nbsp;
-
-        <div class="btn-group" data-toggle="buttons">
-            <label id="show-no-late" class="btn btn-default active">
-                <input type="radio" checked> Badge states: <b>up</b> or <b>down</b>
-            </label>
-            <label id="show-with-late" class="btn btn-default">
-                <input type="radio"> Badge states: <b>up</b>, <b>late</b> or <b>down</b>
-            </label>
-        </div>
-
-        <table id="badges-svg" class="table badge-preview">
-            {% if have_tags %}
-            <tr><th colspan="2">Tags</th></tr>
-            {% endif %}
-
-            {% for urldict in badges %}
-                {% if urldict.tag == "*" %}
-                <tr>
-                    <th colspan="2">Overall Status</th>
-                </tr>
-                {% endif %}
-
-                <tr>
-                    <td>
-                        <img class="no-late" src="{{ urldict.svg }}" alt="" />
-                        <img class="with-late" src="{{ urldict.svg3 }}" alt="" />
-                    </td>
-                    <td>
-                        <code class="no-late">{{ urldict.svg }}</code>
-                        <code class="with-late">{{ urldict.svg3 }}</code>
-                    </td>
-                </tr>
-            {% endfor %}
-        </table>
-        <table id="badges-json" class="table badge-preview">
-            {% if have_tags %}
-            <tr>
-                <th colspan="2">Tags</th>
-            </tr>
-            {% endif %}
-
-            {% for urldict in badges %}
-                {% if urldict.tag == "*" %}
-                <tr>
-                    <th colspan="2">Overall Status</th>
-                </tr>
-                {% endif %}
-
-                <tr>
-                    <td>
-                        <code class="fetch-json no-late" data-url="{{ urldict.json }}"></code>
-                        <code class="fetch-json with-late" data-url="{{ urldict.json3 }}"></code>
-                    </td>
-                    <td>
-                        <code class="no-late">{{ urldict.json }}</code>
-                        <code class="with-late">{{ urldict.json3 }}</code>
-                    </td>
-                </tr>
-            {% endfor %}
-        </table>
-
-        <table id="badges-shields" class="table badge-preview">
-            {% if have_tags %}
-            <tr>
-                <th>Shields.io badge</th>
-                <th>JSON endpoint for Shields.io <a href="https://shields.io/endpoint">(how to use)</a></th>
-            </tr>
-            {% endif %}
-
-            {% for urldict in badges %}
-                {% if urldict.tag == "*" %}
-                <tr>
-                    <th colspan="2">Overall Status</th>
-                </tr>
-                {% endif %}
-
-                <tr>
-                    <td>
-                        <img class="no-late" src="https://img.shields.io/endpoint?url={{ urldict.shields|urlencode:"" }}" alt="" />
-                        <img class="with-late" src="https://img.shields.io/endpoint?url={{ urldict.shields3|urlencode:"" }}" alt="" />
-                    </td>
-                    <td>
-                        <code class="no-late">{{ urldict.shields }}</code>
-                        <code class="with-late">{{ urldict.shields3 }}</code>
-                    </td>
-                </tr>
-            {% endfor %}
-        </table>
     </div>
+
+    <div class="col-md-4">
+        <h2>Badge Generator</h2>
+        {% csrf_token %}
+        <form id="badge-settings-form">
+        <div class="form-group">
+            <p>Generate a badge for</p>
+
+            <label class="radio-container">
+                <input type="radio" name="target" value="all" autocomplete="off" checked>
+                <span class="radiomark"></span>
+                All checks in the project
+            </label>
+            {% if tags %}
+            <label class="radio-container">
+                <input type="radio" name="target" value="tag" autocomplete="off">
+                <span class="radiomark"></span>
+                The checks tagged with this tag:
+            </label>
+
+            <select
+                id="tag"
+                name="tag"
+                class="form-control"
+                autocomplete="off">
+                {% for tag in tags %}
+                <option>{{ tag }}</option>
+                {% endfor %}
+            </select>
+            {% endif %}
+        </div>
+        <div class="form-group">
+        <p>Badge format</p>
+            <label class="radio-container">
+                <input type="radio" name="fmt" value="svg" autocomplete="off" checked>
+                <span class="radiomark"></span> SVG
+            </label>
+            <label class="radio-container">
+                <input type="radio" name="fmt" value="json" autocomplete="off">
+                <span class="radiomark"></span> JSON
+            </label>
+            <label class="radio-container">
+                <input type="radio" name="fmt" value="shields" autocomplete="off">
+                <span class="radiomark"></span> Shields.io
+            </label>
+        </div>
+        <div class="form-group">
+            <p>Badge states</p>
+            <label class="radio-container">
+                <input type="radio" name="states" value="2" autocomplete="off" checked>
+                <span class="radiomark"></span> Two states (<strong>up</strong>, <strong>down</strong>)
+            </label>
+            <label class="radio-container">
+                <input type="radio" name="states" value="3" autocomplete="off">
+                <span class="radiomark"></span> Three states (<strong>up</strong>, <strong>late</strong> or <strong>down</strong>)
+            </label>
+        </div>
+        </form>
+    </div>
+    <div id="preview" class="col-md-8">{% include "front/badges_preview.html" %}</div>
 </div>
 {% endblock %}
 
diff --git a/templates/front/badges_preview.html b/templates/front/badges_preview.html
new file mode 100644
index 00000000..b6d01da0
--- /dev/null
+++ b/templates/front/badges_preview.html
@@ -0,0 +1,31 @@
+<h2>Preview</h2>
+
+{% if fmt == "json" %}
+<pre class="fetch-json" data-url="{{ url }}"></pre>
+
+<p>Badge URL</p>
+<input
+    class="form-control"
+    type="text" name=""
+    value="{{ url }}" readonly>
+
+{% else %}
+<img src="{{ url }}">
+<p>Badge URL</p>
+<input
+    class="form-control"
+    type="text" name=""
+    value="{{ url }}" readonly>
+<p>HTML code</p>
+<input
+    class="form-control"
+    type="text"
+    name=""
+    value="&lt;a href=&quot;{{ url }}&quot;&gt;{{ label }}&lt;/a&gt;" readonly>
+<p>Markdown code</p>
+<input
+    class="form-control"
+    type="text"
+    name=""
+    value="![{{ label }}]({{ url }})" readonly>
+{% endif %}
\ No newline at end of file