diff --git a/app/assets/javascripts/discourse/app/components/software-update-prompt.gjs b/app/assets/javascripts/discourse/app/components/software-update-prompt.gjs
new file mode 100644
index 00000000000..8c29bdc965d
--- /dev/null
+++ b/app/assets/javascripts/discourse/app/components/software-update-prompt.gjs
@@ -0,0 +1,117 @@
+import Component from "@glimmer/component";
+import { tracked } from "@glimmer/tracking";
+import { action } from "@ember/object";
+import { cancel } from "@ember/runloop";
+import { service } from "@ember/service";
+import DButton from "discourse/components/d-button";
+import concatClass from "discourse/helpers/concat-class";
+import { isTesting } from "discourse-common/config/environment";
+import i18n from "discourse-common/helpers/i18n";
+import discourseLater from "discourse-common/lib/later";
+import { bind } from "discourse-common/utils/decorators";
+
+export default class SoftwareUpdatePrompt extends Component {
+  @service messageBus;
+  @service session;
+
+  @tracked showPrompt = false;
+  @tracked animatePrompt = false;
+  timeoutHandler;
+
+  constructor() {
+    super(...arguments);
+
+    this.messageBus.subscribe("/refresh_client", this.onRefresh);
+    this.messageBus.subscribe("/global/asset-version", this.onAsset);
+  }
+
+  willDestroy() {
+    super.willDestroy(...arguments);
+
+    this.messageBus.unsubscribe("/refresh_client", this.onRefresh);
+    this.messageBus.unsubscribe("/global/asset-version", this.onAsset);
+
+    cancel(this.timeoutHandler);
+  }
+
+  @bind
+  onRefresh() {
+    this.session.requiresRefresh = true;
+  }
+
+  @bind
+  onAsset(version) {
+    if (this.session.assetVersion !== version) {
+      this.session.requiresRefresh = true;
+    }
+
+    if (!this.timeoutHandler && this.session.requiresRefresh) {
+      if (isTesting()) {
+        this.updatePromptState(true);
+      } else {
+        // Since we can do this transparently for people browsing the forum
+        // hold back the message 24 hours.
+        this.timeoutHandler = discourseLater(
+          () => this.updatePromptState(true),
+          1000 * 60 * 24 * 60
+        );
+      }
+    }
+  }
+
+  updatePromptState(value) {
+    // when adding the message, we inject the HTML then add the animation
+    // when dismissing, things need to happen in the opposite order
+    const firstProp = value ? "showPrompt" : "animatePrompt";
+    const secondProp = value ? "animatePrompt" : "showPrompt";
+
+    this[firstProp] = value;
+
+    if (isTesting()) {
+      this[secondProp] = value;
+    } else {
+      discourseLater(() => (this[secondProp] = value), 500);
+    }
+  }
+
+  @action
+  refreshPage() {
+    document.location.reload();
+  }
+
+  @action
+  dismiss() {
+    this.updatePromptState(false);
+  }
+
+  <template>
+    {{#if this.showPrompt}}
+      <div
+        class={{concatClass
+          "software-update-prompt"
+          (if this.animatePrompt "require-software-refresh")
+        }}
+      >
+        <div class="wrap">
+          <div aria-live="polite" class="update-prompt-main-content">
+            <DButton
+              @action={{this.refreshPage}}
+              @icon="redo"
+              @label="software_update_prompt.message"
+              class="btn-transparent update-prompt-message"
+            />
+
+            <span class="update-prompt-dismiss">
+              <DButton
+                @action={{this.dismiss}}
+                @icon="times"
+                aria-label={{i18n "software_update_prompt.dismiss"}}
+                class="btn-transparent"
+              />
+            </span>
+          </div>
+        </div>
+      </div>
+    {{/if}}
+  </template>
+}
diff --git a/app/assets/javascripts/discourse/app/components/software-update-prompt.hbs b/app/assets/javascripts/discourse/app/components/software-update-prompt.hbs
deleted file mode 100644
index 7e5d4af96e3..00000000000
--- a/app/assets/javascripts/discourse/app/components/software-update-prompt.hbs
+++ /dev/null
@@ -1,24 +0,0 @@
-{{#if this.showPrompt}}
-  <div
-    class="software-update-prompt{{if
-        this.animatePrompt
-        ' require-software-refresh'
-      }}"
-  >
-    <div class="wrap">
-      <div class="update-prompt-main-content" aria-live="polite">
-        <span
-          role="button"
-          onclick={{action "refreshPage"}}
-          class="update-prompt-message"
-        >{{d-icon "redo"}}
-          {{html-safe (i18n "software_update_prompt.message")}}</span>
-        <span class="update-prompt-dismiss"><span
-            aria-label={{i18n "software_update_prompt.dismiss"}}
-            role="button"
-            onclick={{action "dismiss"}}
-          >{{d-icon "times"}}</span></span>
-      </div>
-    </div>
-  </div>
-{{/if}}
\ No newline at end of file
diff --git a/app/assets/javascripts/discourse/app/components/software-update-prompt.js b/app/assets/javascripts/discourse/app/components/software-update-prompt.js
deleted file mode 100644
index 8d3ec944da6..00000000000
--- a/app/assets/javascripts/discourse/app/components/software-update-prompt.js
+++ /dev/null
@@ -1,90 +0,0 @@
-import Component from "@ember/component";
-import { action } from "@ember/object";
-import { cancel } from "@ember/runloop";
-import { isTesting } from "discourse-common/config/environment";
-import getURL from "discourse-common/lib/get-url";
-import discourseLater from "discourse-common/lib/later";
-import discourseComputed, { bind, on } from "discourse-common/utils/decorators";
-
-export default Component.extend({
-  tagName: "",
-
-  showPrompt: false,
-  animatePrompt: false,
-  _timeoutHandler: null,
-
-  init() {
-    this._super(...arguments);
-
-    this.messageBus.subscribe("/refresh_client", this.onRefresh);
-    this.messageBus.subscribe("/global/asset-version", this.onAsset);
-  },
-
-  willDestroy() {
-    this._super(...arguments);
-
-    this.messageBus.unsubscribe("/refresh_client", this.onRefresh);
-    this.messageBus.unsubscribe("/global/asset-version", this.onAsset);
-  },
-
-  @bind
-  onRefresh() {
-    this.session.requiresRefresh = true;
-  },
-
-  @bind
-  onAsset(version) {
-    if (this.session.assetVersion !== version) {
-      this.session.requiresRefresh = true;
-    }
-
-    if (!this._timeoutHandler && this.session.requiresRefresh) {
-      if (isTesting()) {
-        this.updatePromptState(true);
-      } else {
-        // Since we can do this transparently for people browsing the forum
-        // hold back the message 24 hours.
-        this._timeoutHandler = discourseLater(() => {
-          this.updatePromptState(true);
-        }, 1000 * 60 * 24 * 60);
-      }
-    }
-  },
-
-  @discourseComputed
-  rootUrl() {
-    return getURL("/");
-  },
-
-  updatePromptState(value) {
-    // when adding the message, we inject the HTML then add the animation
-    // when dismissing, things need to happen in the opposite order
-    const firstProp = value ? "showPrompt" : "animatePrompt",
-      secondProp = value ? "animatePrompt" : "showPrompt";
-
-    this.set(firstProp, value);
-    if (isTesting()) {
-      this.set(secondProp, value);
-    } else {
-      discourseLater(() => {
-        this.set(secondProp, value);
-      }, 500);
-    }
-  },
-
-  @action
-  refreshPage() {
-    document.location.reload();
-  },
-
-  @action
-  dismiss() {
-    this.updatePromptState(false);
-  },
-
-  @on("willDestroyElement")
-  _resetTimeoutHandler() {
-    this._timeoutHandler && cancel(this._timeoutHandler);
-    this._timeoutHandler = null;
-  },
-});
diff --git a/app/assets/stylesheets/common/software-update-prompt.scss b/app/assets/stylesheets/common/software-update-prompt.scss
index 3fb47e6ee85..fe3e28049b0 100644
--- a/app/assets/stylesheets/common/software-update-prompt.scss
+++ b/app/assets/stylesheets/common/software-update-prompt.scss
@@ -5,7 +5,6 @@
   left: 0;
   top: var(--header-offset, 60px);
   background-color: var(--tertiary-low);
-  color: var(--tertiary);
   max-height: 0;
   overflow: hidden;
   transition: max-height 0.3s;
@@ -20,15 +19,15 @@
   }
 
   .update-prompt-message {
-    cursor: pointer;
+    color: var(--tertiary);
     padding: 0.75em 0;
 
     .d-icon {
-      margin-right: 0.33em;
+      color: var(--tertiary);
       font-size: 0.9em;
     }
 
-    span {
+    .d-button-label > span {
       text-decoration: underline;
     }
   }
@@ -39,16 +38,8 @@
     height: 44px;
     flex: 1;
 
-    span {
-      cursor: pointer;
-      height: 100%;
-      display: flex;
-      align-items: center;
-      padding-left: 20px;
-
-      &:hover {
-        color: var(--tertiary-hover);
-      }
+    .d-icon {
+      color: var(--tertiary);
     }
   }