diff --git a/changelog/20430.txt b/changelog/20430.txt
new file mode 100644
index 000000000000..5ac95f104cdb
--- /dev/null
+++ b/changelog/20430.txt
@@ -0,0 +1,3 @@
+```release-note:bug
+ui: Fix secret render when path includes %. Resolves #11616.
+```
diff --git a/ui/app/utils/path-encoding-helpers.js b/ui/app/utils/path-encoding-helpers.js
index 2fa650cd3749..ea0f9976c860 100644
--- a/ui/app/utils/path-encoding-helpers.js
+++ b/ui/app/utils/path-encoding-helpers.js
@@ -3,14 +3,25 @@
  * SPDX-License-Identifier: MPL-2.0
  */
 
-import RouteRecognizer from 'route-recognizer';
-
-const {
-  Normalizer: { normalizePath, encodePathSegment },
-} = RouteRecognizer;
+function encodePath(path) {
+  return path
+    ? path
+        .split('/')
+        .map((segment) => encodeURIComponent(segment))
+        .join('/')
+    : path;
+}
 
-export function encodePath(path) {
-  return path ? path.split('/').map(encodePathSegment).join('/') : path;
+function normalizePath(path) {
+  // Unlike normalizePath from route-recognizer, this method assumes
+  // we do not have percent-encoded data octets as defined in
+  // https://datatracker.ietf.org/doc/html/rfc3986
+  return path
+    ? path
+        .split('/')
+        .map((segment) => decodeURIComponent(segment))
+        .join('/')
+    : '';
 }
 
-export { normalizePath, encodePathSegment };
+export { normalizePath, encodePath };
diff --git a/ui/lib/core/addon/components/key-value-header.js b/ui/lib/core/addon/components/key-value-header.js
index ff5ebc672eec..b0d03fac96d4 100644
--- a/ui/lib/core/addon/components/key-value-header.js
+++ b/ui/lib/core/addon/components/key-value-header.js
@@ -83,7 +83,7 @@ export default class KeyValueHeader extends Component {
         label: parts[index],
         text: this.stripTrailingSlash(parts[index]),
         path: path,
-        model: ancestor,
+        model: encodePath(ancestor),
       });
     });
 
diff --git a/ui/package.json b/ui/package.json
index ba7a1fdc79af..7e27fb9f0764 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -190,7 +190,6 @@
     "pvutils": "^1.0.17",
     "qunit": "^2.19.1",
     "qunit-dom": "^2.0.0",
-    "route-recognizer": "^0.3.4",
     "sass-svg-uri": "^1.0.0",
     "shell-quote": "^1.6.1",
     "string.prototype.endswith": "^0.2.0",
diff --git a/ui/tests/acceptance/secrets/backend/kv/secret-test.js b/ui/tests/acceptance/secrets/backend/kv/secret-test.js
index d7bc62f9edad..5cdd543aab11 100644
--- a/ui/tests/acceptance/secrets/backend/kv/secret-test.js
+++ b/ui/tests/acceptance/secrets/backend/kv/secret-test.js
@@ -515,6 +515,37 @@ module('Acceptance | secrets/secret/create, read, delete', function (hooks) {
     assert.strictEqual(currentURL(), `/vault/secrets/${enginePath}/show/${encodedSecretPath}?version=1`);
   });
 
+  test('UI handles secret with % in path correctly', async function (assert) {
+    const enginePath = `kv-engine-${this.uid}`;
+    const secretPath = 'per%cent/%fu ll';
+    const [firstPath, secondPath] = secretPath.split('/');
+    const commands = [`write sys/mounts/${enginePath} type=kv`, `write '${enginePath}/${secretPath}' 3=4`];
+    await consoleComponent.runCommands(commands);
+    await listPage.visitRoot({ backend: enginePath });
+    assert.dom(`[data-test-secret-link="${firstPath}/"]`).exists('First section item exists');
+    await click(`[data-test-secret-link="${firstPath}/"]`);
+
+    assert.strictEqual(
+      currentURL(),
+      `/vault/secrets/${enginePath}/list/${encodeURIComponent(firstPath)}/`,
+      'First part of path is encoded in URL'
+    );
+    assert.dom(`[data-test-secret-link="${secretPath}"]`).exists('Link to secret exists');
+    await click(`[data-test-secret-link="${secretPath}"]`);
+    assert.strictEqual(
+      currentURL(),
+      `/vault/secrets/${enginePath}/show/${encodeURIComponent(firstPath)}/${encodeURIComponent(secondPath)}`,
+      'secret path is encoded in URL'
+    );
+    assert.dom('h1').hasText(secretPath, 'Path renders correctly on show page');
+    await click(`[data-test-secret-breadcrumb="${firstPath}"]`);
+    assert.strictEqual(
+      currentURL(),
+      `/vault/secrets/${enginePath}/list/${encodeURIComponent(firstPath)}/`,
+      'Breadcrumb link encodes correctly'
+    );
+  });
+
   // the web cli does not handle a quote as part of a path, so we test it here via the UI
   test('creating a secret with a single or double quote works properly', async function (assert) {
     assert.expect(4);