diff --git a/packages/vite/src/node/plugins/css.ts b/packages/vite/src/node/plugins/css.ts
index 905f0b4025cb91..91201393b9112f 100644
--- a/packages/vite/src/node/plugins/css.ts
+++ b/packages/vite/src/node/plugins/css.ts
@@ -264,14 +264,15 @@ export function cssPlugin(config: ResolvedConfig): Plugin {
       const ssr = options?.ssr === true
 
       const urlReplacer: CssUrlReplacer = async (url, importer) => {
-        if (checkPublicFile(url, config)) {
+        const decodedUrl = decodeURI(url)
+        if (checkPublicFile(decodedUrl, config)) {
           if (encodePublicUrlsInCSS(config)) {
-            return publicFileToBuiltUrl(url, config)
+            return publicFileToBuiltUrl(decodedUrl, config)
           } else {
-            return joinUrlSegments(config.base, url)
+            return joinUrlSegments(config.base, decodedUrl)
           }
         }
-        const resolved = await resolveUrl(url, importer)
+        const resolved = await resolveUrl(decodedUrl, importer)
         if (resolved) {
           return fileToUrl(resolved, config, this)
         }
@@ -279,7 +280,7 @@ export function cssPlugin(config: ResolvedConfig): Plugin {
           const isExternal = config.build.rollupOptions.external
             ? resolveUserExternal(
                 config.build.rollupOptions.external,
-                url, // use URL as id since id could not be resolved
+                decodedUrl, // use URL as id since id could not be resolved
                 id,
                 false,
               )
@@ -288,7 +289,7 @@ export function cssPlugin(config: ResolvedConfig): Plugin {
           if (!isExternal) {
             // #9800 If we cannot resolve the css url, leave a warning.
             config.logger.warnOnce(
-              `\n${url} referenced in ${id} didn't resolve at build time, it will remain unchanged to be resolved at runtime`,
+              `\n${decodedUrl} referenced in ${id} didn't resolve at build time, it will remain unchanged to be resolved at runtime`,
             )
           }
         }
diff --git a/playground/assets/__tests__/assets.spec.ts b/playground/assets/__tests__/assets.spec.ts
index 74aa0f3ff42179..607517e5e4392b 100644
--- a/playground/assets/__tests__/assets.spec.ts
+++ b/playground/assets/__tests__/assets.spec.ts
@@ -23,6 +23,10 @@ const assetMatch = isBuild
   ? /\/foo\/bar\/assets\/asset-[-\w]{8}\.png/
   : '/foo/bar/nested/asset.png'
 
+const encodedAssetMatch = isBuild
+  ? /\/foo\/bar\/assets\/asset_small_-[-\w]{8}\.png/
+  : '/foo/bar/nested/asset[small].png'
+
 const iconMatch = `/foo/bar/icon.png`
 
 const fetchPath = (p: string) => {
@@ -153,6 +157,10 @@ describe('css url() references', () => {
     expect(await getBg('.css-url-relative')).toMatch(assetMatch)
   })
 
+  test('encoded', async () => {
+    expect(await getBg('.css-url-encoded')).toMatch(encodedAssetMatch)
+  })
+
   test('image-set relative', async () => {
     const imageSet = await getBg('.css-image-set-relative')
     imageSet.split(', ').forEach((s) => {
diff --git a/playground/assets/css/css-url.css b/playground/assets/css/css-url.css
index 06f998f20bfea5..61282fb20fa3b7 100644
--- a/playground/assets/css/css-url.css
+++ b/playground/assets/css/css-url.css
@@ -10,6 +10,11 @@
   background-size: 10px;
 }
 
+.css-url-encoded {
+  background: url('/nested/asset%5Bsmall%5D.png');
+  background-size: 10px;
+}
+
 .css-image-set-relative {
   background-image: -webkit-image-set(
     url('../nested/asset.png') 1x,
diff --git a/playground/assets/index.html b/playground/assets/index.html
index e0684db9061819..5911b53b6ca341 100644
--- a/playground/assets/index.html
+++ b/playground/assets/index.html
@@ -40,6 +40,9 @@ <h2>CSS url references</h2>
 <div class="css-url-relative">
   <span style="background: #fff">CSS background (relative)</span>
 </div>
+<div class="css-url-encoded">
+  <span style="background: #fff">CSS background (encoded)</span>
+</div>
 <div class="css-image-set-relative">
   <span style="background: #fff"
     >CSS background with image-set() (relative)</span
diff --git a/playground/assets/nested/asset[small].png b/playground/assets/nested/asset[small].png
new file mode 100644
index 00000000000000..cf5a52d15fc95e
Binary files /dev/null and b/playground/assets/nested/asset[small].png differ