Repository: vaadin/web-components
Pull request #8917: refactor!: Base text field styles
Author: login: jouni
Created: 2025-04-08T09:44:59Z | Merged: 2025-04-10T11:04:22Z

## New and changed base style properties

| Property | Description
| --- | ---
| `--_vaadin-gap-container-inline` | Renamed from `--_vaadin-gap-container`. Inline gap between textual elements inside a component that contains mainly text and icons.
| `--_vaadin-gap-container-block` | Vertical gap between text elements inside a component that contains textual elements and other containers.
| `--_vaadin-icon-cross` | Data URL for an SVG icon of a cross. Used for the clear button inside the text field.
| `--_vaadin-icon-warn` | Data URL for an SVG icon of a warning sign. Used for the field error message.

## New Text Field base style

<img width="350" alt="Screenshot 2025-04-08 at 12 48 46" src="https://github.com/user-attachments/assets/6f31e46a-2e5c-44f0-bd3f-ed31b2da900f" />


### Supported custom properties

Most of these should be self explanatory (and most are pre-existing from V24).

| Property | Description
| --- | ---
| `--vaadin-input-field-container-gap` | The gap between the field label, input field, helper, and error message elements.
| `--vaadin-input-field-border-radius` | Properties for `top-start`, `top-end`, etc. radius are also available.
| `--vaadin-input-field-border-width` | 
| `--vaadin-input-field-border-color` | 
| `--vaadin-input-field-padding` | 
| `--vaadin-input-field-gap` | 
| `--vaadin-input-field-background` | 
| `--vaadin-input-field-value-font-size` | 
| `--vaadin-input-field-value-font-weight` | 
| `--vaadin-input-field-value-line-height` | 
| `--vaadin-input-field-value-color` | 
| `--vaadin-input-field-placeholder-color` | 
| `--vaadin-input-field-label-font-size` | 
| `--vaadin-input-field-label-font-weight` | 
| `--vaadin-input-field-label-line-height` | 
| `--vaadin-input-field-label-color` | 
| `--vaadin-input-field-helper-font-size` | 
| `--vaadin-input-field-helper-font-weight` | 
| `--vaadin-input-field-helper-line-height` | 
| `--vaadin-input-field-helper-color` | 
| `--vaadin-input-field-error-font-size` | 
| `--vaadin-input-field-error-font-weight` | 
| `--vaadin-input-field-error-line-height` | 
| `--vaadin-input-field-error-color` | 
| `--vaadin-input-field-required-indicator` | The character used for the required indicator.
| `--vaadin-input-field-required-indicator-color` | The color of the required indicator character.
| `--vaadin-input-field-helper-order` | Set to `-1` to show the helper above the field. Use by the `helper-above-field` variant.

### Built-in variants

All themes are expected to support these:

- `theme="helper-above-field"`


Diff:
diff --git a/dev/common.js b/dev/common.js
index ae0a50af0ae..66a32609432 100644
--- a/dev/common.js
+++ b/dev/common.js
@@ -10,7 +10,7 @@ addGlobalThemeStyles(
       font-size: calc(14 / 16 * 1rem);
       line-height: calc(18 / 16 * 1rem);
       color: var(--_vaadin-color);
-      background: var(--_vaadin-bg);
+      background: var(--_vaadin-background);
       margin: 2rem;
     }
 
diff --git a/dev/text-field.html b/dev/text-field.html
index 8830c273816..0d930e046c5 100644
--- a/dev/text-field.html
+++ b/dev/text-field.html
@@ -4,18 +4,89 @@
     <meta charset="UTF-8" />
     <meta http-equiv="X-UA-Compatible" content="IE=edge" />
     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
-    <title>Text field</title>
+    <title>Text Field</title>
     <script type="module" src="./common.js"></script>
 
     <script type="module">
-      import '@vaadin/text-field';
-      import '@vaadin/tooltip';
+      import '@vaadin/text-field/src/vaadin-lit-text-field.js';
+      import '@vaadin/icon/src/vaadin-lit-icon.js';
+      import '@vaadin/vaadin-lumo-styles/vaadin-iconset.js';
+      import '@vaadin/icons';
     </script>
   </head>
 
   <body>
-    <vaadin-text-field label="User name" required>
-      <vaadin-tooltip slot="tooltip" text="Other users will see this"></vaadin-tooltip>
-    </vaadin-text-field>
+    <section>
+      <h2>Plain</h2>
+      <vaadin-text-field value="Value"></vaadin-text-field>
+      <vaadin-text-field placeholder="Placeholder"></vaadin-text-field>
+    </section>
+
+    <section>
+      <h2>Bells & Whistles</h2>
+      <vaadin-text-field
+          label="Label"
+          helper-text="Description for this field."
+          value="Value"
+          clear-button-visible
+          error-message="You need to write something in this field."
+          required>
+        <vaadin-icon icon="vaadin:search" slot="prefix"></vaadin-icon>
+      </vaadin-text-field>
+    </section>
+
+    <section>
+      <h2>States</h2>
+      <vaadin-text-field
+          label="Read-only"
+          helper-text="Description for this field."
+          value="Value"
+          clear-button-visible
+          error-message="You need to write something in this field."
+          required
+          readonly>
+        <vaadin-icon icon="vaadin:search" slot="prefix"></vaadin-icon>
+      </vaadin-text-field>
+
+      <vaadin-text-field
+          label="Disabled"
+          helper-text="Description for this field."
+          value="Value"
+          clear-button-visible
+          error-message="You need to write something in this field."
+          required
+          disabled>
+        <vaadin-icon icon="vaadin:search" slot="prefix"></vaadin-icon>
+      </vaadin-text-field>
+    </section>
+
+    <section>
+      <h2>Helper Above Field</h2>
+      <vaadin-text-field
+          theme="helper-above-field"
+          label="Label"
+          helper-text="Description for this field."
+          value="Value"
+          clear-button-visible
+          error-message="You need to write something in this field."
+          required>
+        <vaadin-icon icon="vaadin:search" slot="prefix"></vaadin-icon>
+      </vaadin-text-field>
+    </section>
+
+    <section>
+      <h2>Content &amp; Layout Variations</h2>
+      <p>Resize the field to test how content flows.</p>
+      <vaadin-text-field
+          label="A very long label for this field"
+          helper-text="If the label wasn't enough to tell what you should input here, then this long description should be."
+          value="I’m not sure what I should write here"
+          placeholder="Perhaps this gives you an idea what to write?"
+          clear-button-visible
+          error-message="In case you didn't write anything in this required field, this error message is here to let you know that you really should write something in this field."
+          required style="resize: horizontal; overflow: hidden; padding: 1px;">
+        <vaadin-icon icon="vaadin:envelope-o" slot="prefix"></vaadin-icon>
+      </vaadin-text-field>
+    </section>
   </body>
 </html>
diff --git a/packages/button/src/vaadin-button-base.js b/packages/button/src/vaadin-button-base.js
index ff60e6e8539..7671a655fdd 100644
--- a/packages/button/src/vaadin-button-base.js
+++ b/packages/button/src/vaadin-button-base.js
@@ -13,7 +13,7 @@ export const buttonStyles = css`
       align-items: center;
       justify-content: center;
       text-align: center;
-      gap: var(--vaadin-button-gap, 0 var(--_vaadin-gap-container));
+      gap: var(--vaadin-button-gap, 0 var(--_vaadin-gap-container-inline));
 
       white-space: nowrap;
       -webkit-tap-highlight-color: transparent;
diff --git a/packages/component-base/src/style-props.js b/packages/component-base/src/style-props.js
index eb5afe8b4a8..a64b54ff0f3 100644
--- a/packages/component-base/src/style-props.js
+++ b/packages/component-base/src/style-props.js
@@ -29,7 +29,8 @@ addGlobalThemeStyles(
         --_vaadin-padding: 8px;
         --_vaadin-padding-container: 6px 8px;
 
-        --_vaadin-gap-container: 0.5em;
+        --_vaadin-gap-container-inline: 0.5em;
+        --_vaadin-gap-container-block: 0.5em;
 
         --_vaadin-radius-s: 3px;
         --_vaadin-radius-m: 6px;
@@ -38,6 +39,9 @@ addGlobalThemeStyles(
 
         --vaadin-focus-ring-width: 2px;
         --vaadin-focus-ring-color: var(--_vaadin-color);
+
+        --_vaadin-icon-cross: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" /></svg>');
+        --_vaadin-icon-warn: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z" /></svg>');
       }
     }
   `,
diff --git a/packages/field-base/src/styles/clear-button-styles.js b/packages/field-base/src/styles/clear-button-styles.js
index efbc0310bae..a88024b6a06 100644
--- a/packages/field-base/src/styles/clear-button-styles.js
+++ b/packages/field-base/src/styles/clear-button-styles.js
@@ -3,16 +3,17 @@
  * Copyright (c) 2021 - 2025 Vaadin Ltd.
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
  */
+import '@vaadin/component-base/src/style-props.js';
 import { css } from 'lit';
 
 export const clearButton = css`
   [part='clear-button'] {
     display: none;
     cursor: default;
-  }
-
-  [part='clear-button']::before {
-    content: '\\2715';
+    mask-image: var(--_vaadin-icon-cross);
+    background: var(--_vaadin-color-subtle);
+    width: var(--vaadin-icon-size, 1lh);
+    height: var(--vaadin-icon-size, 1lh);
   }
 
   :host([clear-button-visible][has-value]:not([disabled]):not([readonly])) [part='clear-button'] {
diff --git a/packages/field-base/src/styles/field-shared-styles.js b/packages/field-base/src/styles/field-shared-styles.js
index 07ab3883c1d..f0710c9579e 100644
--- a/packages/field-base/src/styles/field-shared-styles.js
+++ b/packages/field-base/src/styles/field-shared-styles.js
@@ -3,6 +3,7 @@
  * Copyright (c) 2021 - 2025 Vaadin Ltd.
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
  */
+import '@vaadin/component-base/src/style-props.js';
 import { css } from 'lit';
 
 export const fieldShared = css`
@@ -23,20 +24,60 @@ export const fieldShared = css`
     display: none !important;
   }
 
-  :host(:not([has-label])) [part='label'] {
+  :host(:not([has-label])) [part='label'],
+  :host(:not([has-helper])) [part='helper-text'],
+  :host(:not([has-error-message])) [part='error-message'] {
     display: none;
   }
 
-  @media (forced-colors: active) {
-    :host(:not([readonly])) [part='input-field'] {
-      outline: 1px solid;
-      outline-offset: -1px;
-    }
-    :host([focused]) [part='input-field'] {
-      outline-width: 2px;
-    }
-    :host([disabled]) [part='input-field'] {
-      outline-color: GrayText;
-    }
+  [part='label'] {
+    font-size: var(--vaadin-input-field-label-font-size, inherit);
+    line-height: var(--vaadin-input-field-label-line-height, inherit);
+    font-weight: var(--vaadin-input-field-label-font-weight, 500);
+    color: var(--vaadin-input-field-label-color, var(--_vaadin-color-strong));
+    order: var(--vaadin-input-field-helper-order);
+  }
+
+  [part='required-indicator'] {
+    color: var(--vaadin-input-field-required-indicator-color, inherit);
+  }
+
+  [part='required-indicator']::after {
+    content: var(--vaadin-input-field-required-indicator, '*');
+  }
+
+  :host(:not([required])) [part='required-indicator'] {
+    display: none;
+  }
+
+  [part='helper-text'] {
+    font-size: var(--vaadin-input-field-helper-font-size, inherit);
+    line-height: var(--vaadin-input-field-helper-line-height, inherit);
+    font-weight: var(--vaadin-input-field-helper-font-weight, 400);
+    color: var(--vaadin-input-field-helper-color, var(--_vaadin-color));
+    order: var(--vaadin-input-field-helper-order);
+  }
+
+  [part='error-message'] {
+    font-size: var(--vaadin-input-field-error-font-size, inherit);
+    line-height: var(--vaadin-input-field-error-line-height, inherit);
+    font-weight: var(--vaadin-input-field-error-font-weight, 400);
+    color: var(--vaadin-input-field-error-color, var(--_vaadin-color-strong));
+    display: flex;
+    gap: var(--_vaadin-gap-container-inline);
+  }
+
+  [part='error-message']::before {
+    content: '';
+    display: inline-block;
+    flex: none;
+    width: var(--vaadin-icon-size, 1lh);
+    height: var(--vaadin-icon-size, 1lh);
+    mask-image: var(--_vaadin-icon-warn);
+    background: currentColor;
+  }
+
+  :host([theme~='helper-above-field']) {
+    --vaadin-input-field-helper-order: -1;
   }
 `;
diff --git a/packages/field-base/src/styles/input-field-container-styles.js b/packages/field-base/src/styles/input-field-container-styles.js
index 8c5dd7bb367..fed6c42d565 100644
--- a/packages/field-base/src/styles/input-field-container-styles.js
+++ b/packages/field-base/src/styles/input-field-container-styles.js
@@ -3,12 +3,14 @@
  * Copyright (c) 2021 - 2025 Vaadin Ltd.
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
  */
+import '@vaadin/component-base/src/style-props.js';
 import { css } from 'lit';
 
 export const inputFieldContainer = css`
   [class$='container'] {
     display: flex;
     flex-direction: column;
+    gap: var(--vaadin-input-field-container-gap, var(--_vaadin-gap-container-block));
     min-width: 100%;
     max-width: 100%;
     width: var(--vaadin-field-default-width, 12em);
diff --git a/packages/input-container/src/vaadin-input-container-styles.js b/packages/input-container/src/vaadin-input-container-styles.js
index 2787b46a966..34002f5a4a3 100644
--- a/packages/input-container/src/vaadin-input-container-styles.js
+++ b/packages/input-container/src/vaadin-input-container-styles.js
@@ -3,6 +3,7 @@
  * Copyright (c) 2021 - 2025 Vaadin Ltd.
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
  */
+import '@vaadin/component-base/src/style-props.js';
 import { css } from 'lit';
 
 export const inputContainerStyles = css`
@@ -10,26 +11,33 @@ export const inputContainerStyles = css`
     display: flex;
     align-items: center;
     flex: 0 1 auto;
+    --_radius: var(--vaadin-input-field-border-radius, var(--_vaadin-radius-m));
     border-radius:
-            /* See https://developer.mozilla.org/en-US/docs/Web/CSS/border-radius */
-      var(--vaadin-input-field-top-start-radius, var(--__border-radius))
-      var(--vaadin-input-field-top-end-radius, var(--__border-radius))
-      var(--vaadin-input-field-bottom-end-radius, var(--__border-radius))
-      var(--vaadin-input-field-bottom-start-radius, var(--__border-radius));
-    --_border-radius: var(--vaadin-input-field-border-radius, 0);
-    --_input-border-width: var(--vaadin-input-field-border-width, 0px);
-    --_input-border-color: var(--vaadin-input-field-border-color, transparent);
-    /* stylelint-disable-next-line length-zero-no-unit */
-    box-shadow: inset 0 0 0 var(--_input-border-width, 0) var(--_input-border-color);
+      /* See https://developer.mozilla.org/en-US/docs/Web/CSS/border-radius */
+      var(--vaadin-input-field-top-start-radius, var(--_radius))
+      var(--vaadin-input-field-top-end-radius, var(--_radius))
+      var(--vaadin-input-field-bottom-end-radius, var(--_radius))
+      var(--vaadin-input-field-bottom-start-radius, var(--_radius));
+    border: var(--vaadin-input-field-border-width, 1px) solid
+      var(--vaadin-input-field-border-color, var(--_vaadin-border-color-strong));
+    box-sizing: border-box;
+    padding: var(--vaadin-input-field-padding, var(--_vaadin-padding-container));
+    gap: var(--vaadin-input-field-gap, var(--_vaadin-gap-container-inline));
+    background: var(--vaadin-input-field-background, var(--_vaadin-background));
+    color: var(--vaadin-input-field-value-color, var(--_vaadin-color-strong));
+    font-size: var(--vaadin-input-field-value-font-size, inherit);
+    line-height: var(--vaadin-input-field-value-line-height, inherit);
+    font-weight: var(--vaadin-input-field-value-font-weight, 400);
   }
 
   :host([dir='rtl']) {
+    --_radius: var(--vaadin-input-field-border-radius, var(--_vaadin-radius-m));
     border-radius:
-            /* Don't use logical props, see https://github.com/vaadin/vaadin-time-picker/issues/145 */
-      var(--vaadin-input-field-top-end-radius, var(--_border-radius))
-      var(--vaadin-input-field-top-start-radius, var(--_border-radius))
-      var(--vaadin-input-field-bottom-start-radius, var(--_border-radius))
-      var(--vaadin-input-field-bottom-end-radius, var(--_border-radius));
+      /* Don't use logical props, see https://github.com/vaadin/vaadin-time-picker/issues/145 */
+      var(--vaadin-input-field-top-end-radius, var(--_radius))
+      var(--vaadin-input-field-top-start-radius, var(--_radius))
+      var(--vaadin-input-field-bottom-start-radius, var(--_radius))
+      var(--vaadin-input-field-bottom-end-radius, var(--_radius));
   }
 
   :host([hidden]) {
@@ -38,13 +46,12 @@ export const inputContainerStyles = css`
 
   /* Reset the native input styles */
   ::slotted(input) {
-    -webkit-appearance: none;
-    -moz-appearance: none;
+    appearance: none;
     flex: auto;
     white-space: nowrap;
     overflow: hidden;
     width: 100%;
-    height: 100%;
+    height: auto;
     outline: none;
     margin: 0;
     padding: 0;
@@ -52,11 +59,8 @@ export const inputContainerStyles = css`
     border-radius: 0;
     min-width: 0;
     font: inherit;
-    line-height: normal;
     color: inherit;
-    background-color: transparent;
-    /* Disable default invalid style in Firefox */
-    box-shadow: none;
+    background: transparent;
   }
 
   ::slotted(*) {
@@ -68,7 +72,32 @@ export const inputContainerStyles = css`
     /* because ::slotted(...)::placeholder does not work in Safari. */
     font: inherit;
     color: inherit;
-    /* Override default opacity in Firefox */
-    opacity: 1;
+  }
+
+  ::slotted(input:placeholder-shown) {
+    color: var(--vaadin-input-field-placeholder-color, var(--_vaadin-color));
+  }
+
+  :host(:focus-within) {
+    outline: var(--vaadin-focus-ring-width) solid var(--vaadin-focus-ring-color);
+    outline-offset: calc(var(--vaadin-input-field-border-width, 1px) * -1);
+  }
+
+  :host([readonly]) {
+    border-style: dashed;
+  }
+
+  :host([readonly]:focus-within) {
+    outline-style: dashed;
+    --vaadin-input-field-border-color: transparent;
+  }
+
+  :host([disabled]) {
+    --vaadin-input-field-value-color: var(--vaadin-input-field-disabled-text-color, var(--_vaadin-color-subtle));
+    --vaadin-input-field-background: var(
+      --vaadin-input-field-disabled-background,
+      var(--_vaadin-background-container-strong)
+    );
+    --vaadin-input-field-border-color: transparent;
   }
 `;


Reviews:
user: login: knoobie
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T10:21:39Z
user: login: jouni
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-08T11:28:45Z
user: login: anezthes
body: LGTM!
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T13:38:22Z
user: login: knoobie
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T13:48:20Z
user: login: web-padawan
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-08T14:01:13Z
user: login: web-padawan
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-08T14:11:44Z
user: login: web-padawan
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-08T14:15:13Z
user: login: jouni
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-09T07:39:13Z
user: login: jouni
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-09T08:39:11Z
user: login: jouni
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-09T08:40:46Z
user: login: jouni
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-09T08:45:10Z
user: login: anezthes
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T08:49:02Z
user: login: knoobie
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T08:51:31Z
user: login: jouni
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-09T09:02:18Z
user: login: jouni
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-09T09:15:36Z

Review comments:
diff_hunk: @@ -23,20 +24,52 @@ export const fieldShared = css`
     display: none !important;
   }
 
-  :host(:not([has-label])) [part='label'] {
+  :host(:not([has-label])) [part='label'],
+  :host(:not([has-helper])) [part='helper-text'],
+  :host(:not([has-error-message])) [part='error-message'] {
     display: none;
   }
 
-  @media (forced-colors: active) {
-    :host(:not([readonly])) [part='input-field'] {
-      outline: 1px solid;
-      outline-offset: -1px;
-    }
-    :host([focused]) [part='input-field'] {
-      outline-width: 2px;
-    }
-    :host([disabled]) [part='input-field'] {
-      outline-color: GrayText;
-    }
+  [part='label'] {
+    font-size: var(--vaadin-input-field-label-font-size, inherit);
+    line-height: var(--vaadin-input-field-label-line-height, inherit);
+    font-weight: var(--vaadin-input-field-label-font-weight, 500);
+    color: var(--vaadin-input-field-label-color, var(--_vaadin-color-strong));
+    order: var(--vaadin-input-field-helper-order);
+  }
+
+  :host([required]) [part='required-indicator']::after {
+    content: var(--vaadin-input-field-required-indicator, '*');
path: packages/field-base/src/styles/field-shared-styles.js
user: login: knoobie
body: I would suggest to also include / migrate `--lumo-required-field-indicator-color` to the base styles - e.g. as `--vaadin-input-field-required-indicator-color`
created_at: 2025-04-08T10:21:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -23,20 +24,52 @@ export const fieldShared = css`
     display: none !important;
   }
 
-  :host(:not([has-label])) [part='label'] {
+  :host(:not([has-label])) [part='label'],
+  :host(:not([has-helper])) [part='helper-text'],
+  :host(:not([has-error-message])) [part='error-message'] {
     display: none;
   }
 
-  @media (forced-colors: active) {
-    :host(:not([readonly])) [part='input-field'] {
-      outline: 1px solid;
-      outline-offset: -1px;
-    }
-    :host([focused]) [part='input-field'] {
-      outline-width: 2px;
-    }
-    :host([disabled]) [part='input-field'] {
-      outline-color: GrayText;
-    }
+  [part='label'] {
+    font-size: var(--vaadin-input-field-label-font-size, inherit);
+    line-height: var(--vaadin-input-field-label-line-height, inherit);
+    font-weight: var(--vaadin-input-field-label-font-weight, 500);
+    color: var(--vaadin-input-field-label-color, var(--_vaadin-color-strong));
+    order: var(--vaadin-input-field-helper-order);
+  }
+
+  :host([required]) [part='required-indicator']::after {
+    content: var(--vaadin-input-field-required-indicator, '*');
path: packages/field-base/src/styles/field-shared-styles.js
user: login: jouni
body: Right, good catch. Even though it has been a Lumo-specific prop, I think it makes sense as a base property as well.
created_at: 2025-04-08T11:28:45Z
author_association: MEMBER
diff_hunk: @@ -38,6 +39,9 @@ addGlobalThemeStyles(
 
         --vaadin-focus-ring-width: 2px;
         --vaadin-focus-ring-color: var(--_vaadin-color);
+
+        --_vaadin-icon-cross: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" /></svg>');
+        --_vaadin-icon-warn: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z" /></svg>');
path: packages/component-base/src/style-props.js
user: login: knoobie
body: That is something completly new, or? I'm wondering why is this within the base-theme without a "simple" way to hide it / go back to the previous version without icon?
created_at: 2025-04-08T13:48:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -4,18 +4,89 @@
     <meta charset="UTF-8" />
     <meta http-equiv="X-UA-Compatible" content="IE=edge" />
     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
-    <title>Text field</title>
+    <title>Text Field</title>
     <script type="module" src="./common.js"></script>
 
     <script type="module">
-      import '@vaadin/text-field';
-      import '@vaadin/tooltip';
+      import '@vaadin/text-field/src/vaadin-lit-text-field.js';
+      import '@vaadin/icon/src/vaadin-lit-icon.js';
+      import '@vaadin/vaadin-lumo-styles/vaadin-iconset.js';
+      import '@vaadin/icons';
     </script>
   </head>
 
   <body>
-    <vaadin-text-field label="User name" required>
-      <vaadin-tooltip slot="tooltip" text="Other users will see this"></vaadin-tooltip>
-    </vaadin-text-field>
+    <section>
+      <h2>Plain</h2>
+      <vaadin-text-field value="Value"></vaadin-text-field>
+      <vaadin-text-field placeholder="Placeholder"></vaadin-text-field>
+    </section>
+
+    <section>
+      <h2>Bells & Whistles</h2>
+      <vaadin-text-field
+          label="Label"
path: dev/text-field.html
user: login: web-padawan
body: Consider applying Prettier formatting to this file.
created_at: 2025-04-08T14:01:12Z
author_association: MEMBER
diff_hunk: @@ -68,7 +72,27 @@ export const inputContainerStyles = css`
     /* because ::slotted(...)::placeholder does not work in Safari. */
     font: inherit;
     color: inherit;
-    /* Override default opacity in Firefox */
-    opacity: 1;
+  }
+
+  ::slotted(input:placeholder-shown) {
+    color: var(--vaadin-input-field-placeholder-color, var(--_vaadin-color));
+  }
+
+  :host(:focus-within) {
+    outline: var(--vaadin-focus-ring-width) solid var(--vaadin-focus-ring-color);
+    outline-offset: calc(var(--vaadin-input-field-border-width, 1px) * -1);
+  }
+
+  :host([readonly]) {
+    border-style: dashed;
+  }
+
+  :host([disabled]) {
path: packages/input-container/src/vaadin-input-container-styles.js
user: login: web-padawan
body: I guess we could move `pointer-events: none;` here as well?

https://github.com/vaadin/web-components/blob/3399e37512975db5f5e8a045d3af5a341db67d34/packages/vaadin-lumo-styles/mixins/input-field-shared.js#L107-L108
created_at: 2025-04-08T14:11:44Z
author_association: MEMBER
diff_hunk: @@ -68,7 +72,27 @@ export const inputContainerStyles = css`
     /* because ::slotted(...)::placeholder does not work in Safari. */
     font: inherit;
     color: inherit;
-    /* Override default opacity in Firefox */
-    opacity: 1;
+  }
+
+  ::slotted(input:placeholder-shown) {
+    color: var(--vaadin-input-field-placeholder-color, var(--_vaadin-color));
+  }
+
+  :host(:focus-within) {
+    outline: var(--vaadin-focus-ring-width) solid var(--vaadin-focus-ring-color);
+    outline-offset: calc(var(--vaadin-input-field-border-width, 1px) * -1);
+  }
+
+  :host([readonly]) {
+    border-style: dashed;
path: packages/input-container/src/vaadin-input-container-styles.js
user: login: web-padawan
body: Not sure if this is a major problem but the above `outline` overrides this border when readonly field is focused, making it impossible to visually distinguish readonly focused field from editable focused field.
created_at: 2025-04-08T14:15:13Z
author_association: MEMBER
diff_hunk: @@ -4,18 +4,89 @@
     <meta charset="UTF-8" />
     <meta http-equiv="X-UA-Compatible" content="IE=edge" />
     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
-    <title>Text field</title>
+    <title>Text Field</title>
     <script type="module" src="./common.js"></script>
 
     <script type="module">
-      import '@vaadin/text-field';
-      import '@vaadin/tooltip';
+      import '@vaadin/text-field/src/vaadin-lit-text-field.js';
+      import '@vaadin/icon/src/vaadin-lit-icon.js';
+      import '@vaadin/vaadin-lumo-styles/vaadin-iconset.js';
+      import '@vaadin/icons';
     </script>
   </head>
 
   <body>
-    <vaadin-text-field label="User name" required>
-      <vaadin-tooltip slot="tooltip" text="Other users will see this"></vaadin-tooltip>
-    </vaadin-text-field>
+    <section>
+      <h2>Plain</h2>
+      <vaadin-text-field value="Value"></vaadin-text-field>
+      <vaadin-text-field placeholder="Placeholder"></vaadin-text-field>
+    </section>
+
+    <section>
+      <h2>Bells & Whistles</h2>
+      <vaadin-text-field
+          label="Label"
path: dev/text-field.html
user: login: jouni
body: But it makes it really hard to read 😅 
created_at: 2025-04-09T07:39:13Z
author_association: MEMBER
diff_hunk: @@ -38,6 +39,9 @@ addGlobalThemeStyles(
 
         --vaadin-focus-ring-width: 2px;
         --vaadin-focus-ring-color: var(--_vaadin-color);
+
+        --_vaadin-icon-cross: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" /></svg>');
+        --_vaadin-icon-warn: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z" /></svg>');
path: packages/component-base/src/style-props.js
user: login: jouni
body: This is a completely new approach for icons in our components, yes. One small benefit over font icons is that we don't need a build step, and SVGs aren't as vulnerable to font rendering differences across different operating systems.

Whether or not to have icons in the base styles, well, our aim is to have the components fully usable without any additional theme/styles. To me that means icons are necessary.

But you have a point about making it easy to opt out from the base icons. Obviously you can set `--_vaadin-icon-*: none` to disable the mask image, and then define new icons in some other way. But perhaps we could add a convenient `--vaadin-mask-icons: none` property that disables all the icon mask images and background colors?
created_at: 2025-04-09T08:39:11Z
author_association: MEMBER
diff_hunk: @@ -68,7 +72,27 @@ export const inputContainerStyles = css`
     /* because ::slotted(...)::placeholder does not work in Safari. */
     font: inherit;
     color: inherit;
-    /* Override default opacity in Firefox */
-    opacity: 1;
+  }
+
+  ::slotted(input:placeholder-shown) {
+    color: var(--vaadin-input-field-placeholder-color, var(--_vaadin-color));
+  }
+
+  :host(:focus-within) {
+    outline: var(--vaadin-focus-ring-width) solid var(--vaadin-focus-ring-color);
+    outline-offset: calc(var(--vaadin-input-field-border-width, 1px) * -1);
+  }
+
+  :host([readonly]) {
+    border-style: dashed;
+  }
+
+  :host([disabled]) {
path: packages/input-container/src/vaadin-input-container-styles.js
user: login: jouni
body: Maybe? I think the main reason for that style is to prevent `:hover` and `:active` styles from getting applied. But doesn't that also disable tooltips?
created_at: 2025-04-09T08:40:46Z
author_association: MEMBER
diff_hunk: @@ -68,7 +72,27 @@ export const inputContainerStyles = css`
     /* because ::slotted(...)::placeholder does not work in Safari. */
     font: inherit;
     color: inherit;
-    /* Override default opacity in Firefox */
-    opacity: 1;
+  }
+
+  ::slotted(input:placeholder-shown) {
+    color: var(--vaadin-input-field-placeholder-color, var(--_vaadin-color));
+  }
+
+  :host(:focus-within) {
+    outline: var(--vaadin-focus-ring-width) solid var(--vaadin-focus-ring-color);
+    outline-offset: calc(var(--vaadin-input-field-border-width, 1px) * -1);
+  }
+
+  :host([readonly]) {
+    border-style: dashed;
path: packages/input-container/src/vaadin-input-container-styles.js
user: login: jouni
body: Yeah, fair point, I didn't think about that. I think we could turn the focus outline to a dashed style as well, to make it clear. What do you think? Though, perhas the focus outline should stay the same style regardless of the component state?

<img width="174" alt="Screenshot 2025-04-09 at 11 44 18" src="https://github.com/user-attachments/assets/4a973607-2a8f-42d3-91be-d635f218a128" />

created_at: 2025-04-09T08:45:10Z
author_association: MEMBER
diff_hunk: @@ -68,7 +72,27 @@ export const inputContainerStyles = css`
     /* because ::slotted(...)::placeholder does not work in Safari. */
     font: inherit;
     color: inherit;
-    /* Override default opacity in Firefox */
-    opacity: 1;
+  }
+
+  ::slotted(input:placeholder-shown) {
+    color: var(--vaadin-input-field-placeholder-color, var(--_vaadin-color));
+  }
+
+  :host(:focus-within) {
+    outline: var(--vaadin-focus-ring-width) solid var(--vaadin-focus-ring-color);
+    outline-offset: calc(var(--vaadin-input-field-border-width, 1px) * -1);
+  }
+
+  :host([readonly]) {
+    border-style: dashed;
path: packages/input-container/src/vaadin-input-container-styles.js
user: login: anezthes
body: Dashed, but emphasized similarly to the regular focus outline in terms of color and width. If we override the border style, we lose the visual read-only indicator.
created_at: 2025-04-09T08:49:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -38,6 +39,9 @@ addGlobalThemeStyles(
 
         --vaadin-focus-ring-width: 2px;
         --vaadin-focus-ring-color: var(--_vaadin-color);
+
+        --_vaadin-icon-cross: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" /></svg>');
+        --_vaadin-icon-warn: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z" /></svg>');
path: packages/component-base/src/style-props.js
user: login: knoobie
body: I was suprised by this :) because now it's an internal property and lumo would probably override an internal property to `none` to make it backward compatible. I'm not against this change, I kinda like it. I would just prefer a "public api" / official customizing of it. In this example it could be `--vaadin-input-field-error-icon: svg / none` to make it easy for people to customize it if needed. 

For example we have a similar use-case where we show different error-icons depending on the severity of an error (Flow's Binder's ErrorLevel) allowing us to show a big red cross or blue explanation mark as information.
created_at: 2025-04-09T08:51:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -38,6 +39,9 @@ addGlobalThemeStyles(
 
         --vaadin-focus-ring-width: 2px;
         --vaadin-focus-ring-color: var(--_vaadin-color);
+
+        --_vaadin-icon-cross: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" /></svg>');
+        --_vaadin-icon-warn: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z" /></svg>');
path: packages/component-base/src/style-props.js
user: login: jouni
body: I could consider making these part of the public API. But I think we’ll see along the way, how that starts to feel like, if this is _the_ recommended way to customize the built-in component icons.
created_at: 2025-04-09T09:02:18Z
author_association: MEMBER
diff_hunk: @@ -68,7 +72,27 @@ export const inputContainerStyles = css`
     /* because ::slotted(...)::placeholder does not work in Safari. */
     font: inherit;
     color: inherit;
-    /* Override default opacity in Firefox */
-    opacity: 1;
+  }
+
+  ::slotted(input:placeholder-shown) {
+    color: var(--vaadin-input-field-placeholder-color, var(--_vaadin-color));
+  }
+
+  :host(:focus-within) {
+    outline: var(--vaadin-focus-ring-width) solid var(--vaadin-focus-ring-color);
+    outline-offset: calc(var(--vaadin-input-field-border-width, 1px) * -1);
+  }
+
+  :host([readonly]) {
+    border-style: dashed;
path: packages/input-container/src/vaadin-input-container-styles.js
user: login: jouni
body: I went with the dashed focus outline now. Let’s see how that feels.
created_at: 2025-04-09T09:15:35Z
author_association: MEMBER

PR comments:
user: login: sonarqubecloud[bot]
created_at: 2025-04-09T09:17:03Z
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=vaadin_web-components&pullRequest=8917) **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=vaadin_web-components&pullRequest=8917&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=vaadin_web-components&pullRequest=8917&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=vaadin_web-components&pullRequest=8917&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=vaadin_web-components&pullRequest=8917&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=vaadin_web-components&pullRequest=8917&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=vaadin_web-components&pullRequest=8917)


author_association: NONE