Repository: swisspost/design-system
Pull request #5339: fix(styles): restore validation icons on browser-autocompleted form fields
Author: login: alionazherdetska
Created: 2025-04-23T16:23:00Z | Merged: 2025-05-08T12:43:43Z

## 📄 Description

- Added dedicated `.validation-icon` span element
- Background images were substituted with mask images on these elements

## 🚀 Demo

If applicable, please add a screenshot or video to illustrate the changes.

---

## 📝 Checklist

- ✅ My code follows the style guidelines of this project
- 🛠️ I have performed a self-review of my own code
- 📄 I have made corresponding changes to the documentation
- ⚠️ My changes generate no new warnings or errors
- 🧪 I have added tests that prove my fix is effective or that my feature works
- ✔️ New and existing unit tests pass locally with my changes



Diff:
diff --git a/.changeset/cuddly-bulldogs-cheer.md b/.changeset/cuddly-bulldogs-cheer.md
new file mode 100644
index 0000000000..aaa055780b
--- /dev/null
+++ b/.changeset/cuddly-bulldogs-cheer.md
@@ -0,0 +1,6 @@
+---
+'@swisspost/design-system-documentation': major
+'@swisspost/design-system-styles': major
+---
+
+Fixed the disappearing validation icons on text input fields when browser autocomplete is applied.
\ No newline at end of file
diff --git a/packages/documentation/src/stories/components/forms/validation/validation.stories.ts b/packages/documentation/src/stories/components/forms/validation/validation.stories.ts
index f9c637d4a0..a00b4d03c7 100644
--- a/packages/documentation/src/stories/components/forms/validation/validation.stories.ts
+++ b/packages/documentation/src/stories/components/forms/validation/validation.stories.ts
@@ -84,7 +84,6 @@ export const Checkbox: Story = {
   render: (args: Args, context: StoryContext) => {
     const component = context.name.replace(/\s+/g, '');
     const props = getValidationProps(component, args);
-
     const feedbackTemplate = renderFeedback(props.validFeedbackId, props.invalidFeedbackId);
     return html`<div class="form-check">
       <input
diff --git a/packages/styles/.gitignore b/packages/styles/.gitignore
index f9fa25047e..ac6035b0ab 100644
--- a/packages/styles/.gitignore
+++ b/packages/styles/.gitignore
@@ -10,7 +10,7 @@ npm-debug.log
 stylelint-report.txt
 storybook-static
 src/utilities/_not-defined.scss
-src/utilities/_post-icon-version.scss
+src/utilities/_env-variables.scss
 
 # Mac OSX Finder files.
 **/.DS_Store
diff --git a/packages/styles/gulpfile.js b/packages/styles/gulpfile.js
index caca49f611..d8fc5ea930 100644
--- a/packages/styles/gulpfile.js
+++ b/packages/styles/gulpfile.js
@@ -112,13 +112,13 @@ gulp.task('transform-package-json', done => {
  * This allows the `post-icon` mixin to dynamically resolve the correct icon URL
  * based on the version defined in package.json.
  */
-gulp.task('generate-icon-version-scss', done => {
+gulp.task('prebuild-env-vars', done => {
   const version = require('./package.json').version;
 
   const content = `$post-icon-version: '${version}';\n`;
 
   fs.writeFileSync(
-    path.join(__dirname, 'src/utilities/_post-icon-version.scss'),
+    path.join(__dirname, 'src/utilities/_env-variables.scss'),
     content,
     'utf8'
   );
@@ -235,15 +235,17 @@ gulp.task(
  */
 exports.default = gulp.task(
   'build',
-  gulp.parallel(
-    gulp.series(
-      'generate-not-defined-components-scss',
-      'map-icons',
-      'copy',
-      'generate-icon-version-scss',
-      'autoprefixer',
-      'transform-package-json',
-    ),
-    gulp.series('temporarily-copy-token-files', 'sass'),
+  gulp.series(
+    'prebuild-env-vars',
+    gulp.parallel(
+      gulp.series(
+        'generate-not-defined-components-scss',
+        'map-icons',
+        'copy',
+        'autoprefixer',
+        'transform-package-json',
+      ),
+      gulp.series('temporarily-copy-token-files', 'sass'),
+    )
   ),
-);
+);
\ No newline at end of file
diff --git a/packages/styles/src/components/form-input.scss b/packages/styles/src/components/form-input.scss
index a813403d74..999b0abde5 100644
--- a/packages/styles/src/components/form-input.scss
+++ b/packages/styles/src/components/form-input.scss
@@ -1,9 +1,9 @@
 @use './../functions/tokens';
 @use './../tokens/components';
 @use './../tokens/elements';
-
 @use './../mixins/forms' as forms-mx;
 @use './../mixins/utilities';
+@use './../mixins/icons' as icon-mx;
 @use './../variables/components/forms';
 
 tokens.$default-map: components.$post-text-input;
@@ -179,9 +179,6 @@ input.form-control {
   &.is-valid,
   &.is-invalid {
     &:not(:disabled) {
-      background-size: tokens.get('input-sizing-icon');
-      background-position: center right tokens.get('input-validation-icon-position-inline-end');
-      background-repeat: no-repeat;
       padding-inline-end: calc(
         #{tokens.get('input-padding-inline-end')} + #{tokens.get(
             'input-validation-icon-position-inline-end'
@@ -189,8 +186,6 @@ input.form-control {
       );
     }
   }
-
-  @include forms-mx.validation-icons;
 }
 
 .form-label:has(+ input.form-control[disabled]) {
@@ -200,6 +195,40 @@ input.form-control {
 .form-floating {
   position: relative;
 
+  &:has(> input.form-control.is-valid:not(:disabled))::after,
+  &:has(> input.form-control:-webkit-autofill.is-valid)::after {
+    content: '';
+    position: absolute;
+    top: calc(tokens.get('input-sizing-height') / 2);
+    transform: translateY(-50%);
+    right: tokens.get('input-validation-icon-position-inline-end');
+    z-index: 2;
+    pointer-events: none;
+    @include icon-mx.post-icon(
+      $name: 'success-solid',
+      $color: tokens.get('post-validation-success', components.$post-validation),
+      $width: tokens.get('input-sizing-icon'),
+      $height: tokens.get('input-sizing-icon')
+    );
+  }
+
+  &:has(> input.form-control.is-invalid:not(:disabled))::after,
+  &:has(> input.form-control:-webkit-autofill.is-invalid)::after {
+    content: '';
+    position: absolute;
+    top: calc(tokens.get('input-sizing-height') / 2);
+    transform: translateY(-50%);
+    right: tokens.get('input-validation-icon-position-inline-end');
+    z-index: 2;
+    pointer-events: none;
+    @include icon-mx.post-icon(
+      $name: 'warning-solid',
+      $color: tokens.get('post-validation-error', components.$post-validation),
+      $width: tokens.get('input-sizing-icon'),
+      $height: tokens.get('input-sizing-icon')
+    );
+  }
+
   > input.form-control ~ label {
     position: absolute;
     z-index: 1;
diff --git a/packages/styles/src/mixins/_icons.scss b/packages/styles/src/mixins/_icons.scss
index a2b8ca2de9..6656fc77ed 100644
--- a/packages/styles/src/mixins/_icons.scss
+++ b/packages/styles/src/mixins/_icons.scss
@@ -6,6 +6,7 @@
 @use './../variables/components/button';
 @use './../functions/icons' as icon-fn;
 @use './../mixins/utilities';
+@use './../utilities/env-variables' as icon-version;
 
 @mixin icon($name) {
   $icon: icon-fn.get-svg-url($name);
@@ -19,11 +20,13 @@
   $base:
     'https://unpkg.com/@swisspost/design-system-icons@#{icon-version.$post-icon-version}/public/post-icons',
   $name,
-  $color: inherit
+  $color: inherit,
+  $width: 1em,
+  $height: 1em
 ) {
   display: inline-block;
-  width: 1em;
-  height: 1em;
+  width: $width;
+  height: $height;
   vertical-align: -0.15em;
   background-color: currentColor;
   color: $color;


Reviews:
user: login: gfellerph
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-04-30T05:03:31Z
user: login: oliverschuerch
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T13:25:53Z
user: login: alionazherdetska
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T06:28:14Z
user: login: oliverschuerch
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T09:21:33Z
user: login: gfellerph
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-08T09:56:18Z

Review comments:
diff_hunk: @@ -123,6 +122,7 @@ export const Input: Story = {
         placeholder="Placeholder"
       />
       <label class="form-label" for="Input_1${props.validationState}${props.scheme}">Label</label>
+      <span class="validation-icon"></span>
path: packages/documentation/src/stories/components/forms/validation/validation.stories.ts
user: login: gfellerph
body: Changing the structure of the HTML for HTML/CSS components is a major breaking change because every project has to update every input field in their whole project for the changes to take effect. It would be great to have a solution that does not alter the markup required.

Since input fields cannot have pseudo elements, I was thinking that we maybe can put a pseudo element on the wrapper element around each input field and use `:has` to determine the validity state:

```css
.form-input:has(input[type="text"].is-invalid)::before { content ''; background-image: /*invalid icon*/}
```
created_at: 2025-04-30T05:03:14Z
author_association: MEMBER
diff_hunk: @@ -200,6 +195,40 @@ input.form-control {
 .form-floating {
   position: relative;
 
+  &:has(> input.form-control.is-valid:not(:disabled))::after,
+  &:has(> input.form-control:-webkit-autofill.is-valid)::after {
+    content: '';
+    position: absolute;
+    top: calc(tokens.get('input-sizing-height') / 2);
+    transform: translateY(-50%);
+    right: tokens.get('input-validation-icon-position-inline-end');
+    z-index: 2;
+    pointer-events: none;
+    @include icon-mx.post-icon(
+      $name: 'success-solid',
+      $color: tokens.get('post-validation-success', components.$post-validation),
+      $width: 24px,
+      $height: 24px
path: packages/styles/src/components/form-input.scss
user: login: oliverschuerch
body: These sizes should come from a token, imo.

```scss
$width: tokens.get('post-validation-icon-size', components.$post-validation),
$height: tokens.get('post-validation-icon-size', components.$post-validation)
```

Same for the invalid icon below.
created_at: 2025-05-06T12:18:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -235,15 +235,17 @@ gulp.task(
  */
 exports.default = gulp.task(
   'build',
-  gulp.parallel(
-    gulp.series(
-      'generate-not-defined-components-scss',
-      'map-icons',
-      'copy',
-      'generate-icon-version-scss',
-      'autoprefixer',
-      'transform-package-json',
-    ),
-    gulp.series('temporarily-copy-token-files', 'sass'),
+  gulp.series(
+    'generate-icon-version-scss',
path: packages/styles/gulpfile.js
user: login: oliverschuerch
body: I know this has been done in a former PR, but I'm asking myself, if we should rename the task `generate-icon-version-scss` and also the output file name and path.

The package version is used in the post-icon mixin only (at the moment).
Actually, it's just a global information that could be used from any file. But for sure it is not similar to our other scss utilities modules and has nothing in common with them.

IMO it would match much better, if we'd rename the gulp task to something like `prebuild-env-vars` and the output file to something like `environment.scss` or `env-variables.scss`.
Or at least find a naming which is a bit more generic.

@alionazherdetska what do you think?
created_at: 2025-05-06T13:25:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -235,15 +235,17 @@ gulp.task(
  */
 exports.default = gulp.task(
   'build',
-  gulp.parallel(
-    gulp.series(
-      'generate-not-defined-components-scss',
-      'map-icons',
-      'copy',
-      'generate-icon-version-scss',
-      'autoprefixer',
-      'transform-package-json',
-    ),
-    gulp.series('temporarily-copy-token-files', 'sass'),
+  gulp.series(
+    'generate-icon-version-scss',
path: packages/styles/gulpfile.js
user: login: alionazherdetska
body: Renaming to `prebuild-env-vars` and `environment.scss` is a good idea since leaves room for adding other environment variables in the future.
created_at: 2025-05-08T06:28:14Z
author_association: CONTRIBUTOR

PR comments:
user: login: changeset-bot[bot]
created_at: 2025-04-23T16:23:04Z
body: ###  🦋  Changeset detected

Latest commit: 9866cc57bf258a01cf94ff8bcb85d2a45dd82d17

**The changes in this PR will be included in the next version bump.**

<details><summary>This PR includes changesets to release 12 packages</summary>

  | Name                                                  | Type  |
| ----------------------------------------------------- | ----- |
| @swisspost/design-system-documentation                | Major |
| @swisspost/design-system-styles                       | Major |
| @swisspost/design-system-components-angular-workspace | Patch |
| @swisspost/design-system-components                   | Major |
| @swisspost/internet-header                            | Patch |
| @swisspost/design-system-nextjs-integration           | Patch |
| @swisspost/design-system-styles-primeng-workspace     | Patch |
| @swisspost/design-system-styles-primeng               | Major |
| @swisspost/design-system-components-react             | Major |
| @swisspost/design-system-components-angular           | Major |
| @swisspost/design-system-tokens                       | Major |
| @swisspost/design-system-icons                        | Major |

</details>

Not sure what this means? [Click here  to learn what changesets are](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add another changeset to this PR](https://github.com/swisspost/design-system/new/5287-bug-autocompleted-fields-dont-show-validation?filename=.changeset/sour-buses-buy.md&value=---%0A%22%40swisspost%2Fdesign-system-documentation%22%3A%20patch%0A%22%40swisspost%2Fdesign-system-styles%22%3A%20patch%0A---%0A%0Afix(styles)%3A%20restore%20validation%20icons%20on%20browser-autocompleted%20form%20fields%0A)


author_association: NONE
user: login: swisspost-bot
created_at: 2025-04-23T16:54:22Z
body: **Related Previews**
- https://preview-5339--swisspost-design-system-next.netlify.app

author_association: CONTRIBUTOR
user: login: vercel[bot]
created_at: 2025-04-30T08:24:05Z
body: [vc]: #psYshuarvP2pMy9Kksb5w2zxRqa2k256lP5jseZxDVM=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJkZXNpZ24tc3lzdGVtLWRvY3VtZW50YXRpb24iLCJyb290RGlyZWN0b3J5IjpudWxsLCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vc3dpc3Nwb3N0L2Rlc2lnbi1zeXN0ZW0tZG9jdW1lbnRhdGlvbi9CQ2RWekNjc1N0QkRMMjl1UGtWVzI1dERacWhtIiwicHJldmlld1VybCI6ImRlc2lnbi1zeXN0ZW0tZG9jdW1lbnRhdGlvbi1naXQtNTI4Ny1idWctYXV0b2MtZTEyODQ1LXN3aXNzcG9zdC52ZXJjZWwuYXBwIiwibmV4dENvbW1pdFN0YXR1cyI6IkRFUExPWUVEIiwibGl2ZUZlZWRiYWNrIjp7InJlc29sdmVkIjowLCJ1bnJlc29sdmVkIjowLCJ0b3RhbCI6MCwibGluayI6ImRlc2lnbi1zeXN0ZW0tZG9jdW1lbnRhdGlvbi1naXQtNTI4Ny1idWctYXV0b2MtZTEyODQ1LXN3aXNzcG9zdC52ZXJjZWwuYXBwIn19XX0=
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **design-system-documentation** | ✅ Ready ([Inspect](https://vercel.com/swisspost/design-system-documentation/BCdVzCcsStBDL29uPkVW25tDZqhm)) | [Visit Preview](https://design-system-documentation-git-5287-bug-autoc-e12845-swisspost.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/design-system-documentation-git-5287-bug-autoc-e12845-swisspost.vercel.app?via=pr-comment-feedback-link) | May 7, 2025 1:57pm |


author_association: NONE
user: login: sonarqubecloud[bot]
created_at: 2025-05-08T06:26:40Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=swisspost_common-web-frontend&pullRequest=5339) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=swisspost_common-web-frontend&pullRequest=5339&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=swisspost_common-web-frontend&pullRequest=5339&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=swisspost_common-web-frontend&pullRequest=5339&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=swisspost_common-web-frontend&pullRequest=5339&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=swisspost_common-web-frontend&pullRequest=5339&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=swisspost_common-web-frontend&pullRequest=5339)


author_association: NONE