diff --git a/packages/web-components/src/components/fluid-password-input/__tests__/fluid-password-input-test.js b/packages/web-components/src/components/fluid-password-input/__tests__/fluid-password-input-test.js
new file mode 100644
index 000000000000..b15e3ca41cca
--- /dev/null
+++ b/packages/web-components/src/components/fluid-password-input/__tests__/fluid-password-input-test.js
@@ -0,0 +1,222 @@
+/**
+ * Copyright IBM Corp. 2025
+ *
+ * This source code is licensed under the Apache-2.0 license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+import '@carbon/web-components/es/components/fluid-password-input/index.js';
+import { fixture, html, expect } from '@open-wc/testing';
+
+describe('cds-fluid-password-input', () => {
+ const defaultInput = html`
+
+
+ `;
+
+ it('should render', async () => {
+ const el = await fixture(defaultInput);
+ const input = el.shadowRoot.querySelector('input');
+ expect(input).to.exist;
+ });
+
+ it('should support a custom class', async () => {
+ const el = await fixture(
+ html``
+ );
+ expect(el.classList.contains('test-class')).to.be.true;
+ });
+
+ it('should render label and placeholder', async () => {
+ const el = await fixture(defaultInput);
+ const label = el.shadowRoot.querySelector('label');
+ const input = el.shadowRoot.querySelector('input');
+ expect(label.textContent).to.include('Password input label');
+ expect(input.placeholder).to.equal('Placeholder text');
+ });
+
+ it('should reflect value attribute to input', async () => {
+ const el = await fixture(html`
+
+ `);
+ const input = el.shadowRoot.querySelector('input');
+ expect(input.value).to.equal('test123');
+ });
+
+ it('should apply disabled attribute', async () => {
+ const el = await fixture(html`
+
+ `);
+ const input = el.shadowRoot.querySelector('input');
+ expect(input.disabled).to.be.true;
+ });
+
+ it('should apply hide-label attribute', async () => {
+ const el = await fixture(html`
+
+ `);
+ const label = el.shadowRoot.querySelector('label');
+ const classList = label?.classList || [];
+ expect(
+ Array.from(classList).some((cls) => cls.includes('--visually-hidden'))
+ ).to.be.true;
+ });
+
+ it('should apply hide-password-label attribute', async () => {
+ const el = await fixture(html`
+
+ `);
+
+ const btn = el.shadowRoot.querySelector('button');
+ btn.click();
+
+ await el.updateComplete;
+ const tooltipContent = el.shadowRoot.querySelector(
+ 'cds-tooltip-content#content'
+ );
+ expect(tooltipContent.textContent.trim()).to.equal('Hide Password');
+ });
+
+ it('should apply show-password-label attribute', async () => {
+ const el = await fixture(html`
+
+ `);
+
+ const tooltipContent = el.shadowRoot.querySelector(
+ 'cds-tooltip-content#content'
+ );
+ expect(tooltipContent.textContent.trim()).to.equal('Show Password');
+ });
+
+ it('should apply inline attribute', async () => {
+ const el = await fixture(html`
+
+ `);
+ const wrapper = el.shadowRoot.querySelector('.cds--text-input-wrapper');
+ const classList = wrapper?.classList || [];
+ expect(
+ Array.from(classList).some((cls) =>
+ cls.includes('--text-input-wrapper--inline')
+ )
+ ).to.be.true;
+ });
+
+ it('should apply invalid attribute', async () => {
+ const el = await fixture(html`
+
+ `);
+
+ const error = el.shadowRoot.querySelector('.cds--form-requirement');
+ expect(error.textContent).to.include('This is invalid text');
+ });
+
+ it('should apply type attribute', async () => {
+ const el = await fixture(html`
+
+ `);
+
+ expect(el.getAttribute('type')).to.equal('text');
+ });
+
+ it('should apply warn attribute', async () => {
+ const el = await fixture(html`
+
+ `);
+
+ const warning = el.shadowRoot.querySelector('.cds--form-requirement');
+ expect(warning.textContent).to.include('This is warning text');
+ });
+
+ it('should call onTogglePasswordVisibility when visibility button is clicked', async () => {
+ const el = await fixture(html`
+
+ `);
+
+ const tooltip = el.shadowRoot.querySelector('cds-tooltip-content#content');
+ expect(tooltip.textContent.trim()).to.equal('Show password');
+
+ const btn = el.shadowRoot.querySelector('button');
+ btn.click();
+ await el.updateComplete;
+
+ expect(tooltip.textContent.trim()).to.equal('Hide password');
+ });
+
+ it('should apply readonly attribute', async () => {
+ const el = await fixture(html`
+
+ `);
+ const input = el.shadowRoot.querySelector('input');
+ expect(input.readOnly).to.be.true;
+ });
+
+ it('should disable hide/show password toggle button when readonly is true', async () => {
+ const el = await fixture(html`
+
+ `);
+ await el.updateComplete;
+
+ const toggleButton = el.shadowRoot.querySelector('button[type="button"]');
+ expect(toggleButton.disabled).to.be.true;
+ });
+
+ it('should render divider when isFluid is true', async () => {
+ const el = await fixture(html`
+
+ `);
+
+ const divider = el.shadowRoot.querySelector('.cds--text-input__divider');
+ expect(divider).to.exist;
+ expect(divider.tagName.toLowerCase()).to.equal('hr');
+ });
+
+ it('should not allow input change when readOnly is true', async () => {
+ const el = await fixture(html`
+
+ `);
+ await el.updateComplete;
+
+ const inputElement = el.shadowRoot.querySelector('input');
+ inputElement.focus();
+
+ // simulate a user typing “a”
+ inputElement.dispatchEvent(
+ new KeyboardEvent('keydown', {
+ key: 'a',
+ bubbles: true,
+ cancelable: true,
+ })
+ );
+ inputElement.dispatchEvent(
+ new KeyboardEvent('keypress', {
+ key: 'a',
+ bubbles: true,
+ cancelable: true,
+ })
+ );
+ inputElement.dispatchEvent(
+ new InputEvent('input', {
+ data: 'a',
+ bubbles: true,
+ cancelable: true,
+ })
+ );
+
+ await el.updateComplete;
+
+ expect(el.value).to.equal('', 'host.value remains empty');
+ expect(inputElement.value).to.equal('', 'input.value remains empty');
+ });
+});
diff --git a/packages/web-components/src/components/fluid-password-input/fluid-password-input.mdx b/packages/web-components/src/components/fluid-password-input/fluid-password-input.mdx
new file mode 100644
index 000000000000..cd59f03cb90a
--- /dev/null
+++ b/packages/web-components/src/components/fluid-password-input/fluid-password-input.mdx
@@ -0,0 +1,24 @@
+import { ArgTypes, Canvas, Markdown, Meta } from '@storybook/addon-docs/blocks';
+import { cdnJs } from '../../globals/internal/storybook-cdn';
+import * as FluidPasswordInputStories from './fluid-password-input.stories';
+
+
+
+# Fluid Password Input
+
+
+
+## Component API
+
+## `cds-fluid-password-input`
+
+
+
+
+{`${cdnJs({ components: ['fluid-password-input'] })}`}
+
+## Feedback
+
+Help us improve this component by providing feedback, asking questions on Slack,
+or updating this file on
+[GitHub](https://github.com/carbon-design-system/carbon/edit/main/packages/web-components/src/components/fluid-password-input/fluid-password-input.mdx).
diff --git a/packages/web-components/src/components/fluid-password-input/fluid-password-input.scss b/packages/web-components/src/components/fluid-password-input/fluid-password-input.scss
new file mode 100644
index 000000000000..bb8824c709c3
--- /dev/null
+++ b/packages/web-components/src/components/fluid-password-input/fluid-password-input.scss
@@ -0,0 +1,25 @@
+/**
+ * Copyright IBM Corp.2025
+ *
+ * This source code is licensed under the Apache-2.0 license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+$css--plex: true !default;
+
+@use '@carbon/styles/scss/layout' as *;
+@use '@carbon/styles/scss/type' as *;
+@use '@carbon/styles/scss/config' as *;
+@use '@carbon/styles/scss/spacing' as *;
+@use '@carbon/styles/scss/theme' as *;
+@use '@carbon/styles/scss/components/text-input';
+@use '@carbon/styles/scss/colors';
+@use '@carbon/styles/scss/utilities/convert' as *;
+@use '@carbon/styles/scss/components/fluid-text-input/index';
+
+:host(#{$prefix}-fluid-password-input) {
+ @include emit-layout-tokens();
+ .#{$prefix}--toggle-password-tooltip {
+ block-size: 100%;
+ }
+}
diff --git a/packages/web-components/src/components/fluid-password-input/fluid-password-input.stories.ts b/packages/web-components/src/components/fluid-password-input/fluid-password-input.stories.ts
new file mode 100644
index 000000000000..0c6939f126be
--- /dev/null
+++ b/packages/web-components/src/components/fluid-password-input/fluid-password-input.stories.ts
@@ -0,0 +1,137 @@
+/**
+ * Copyright IBM Corp. 2025
+ *
+ * This source code is licensed under the Apache-2.0 license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+import { html } from 'lit';
+import { ifDefined } from 'lit/directives/if-defined.js';
+import './index';
+
+const args = {
+ defaultWidth: 300,
+ placeholder: 'Placeholder text',
+ showPasswordLabel: 'Show password label',
+ hidePasswordLabel: 'Hide password label',
+ readonly: false,
+ invalid: false,
+ invalidText:
+ 'Error message that is really long can wrap to more lines but should not be excessively long.',
+ disabled: false,
+ labelText: 'Label',
+ warn: false,
+ warnText:
+ 'Warning message that is really long can wrap to more lines but should not be excessively long.',
+};
+
+const argTypes = {
+ defaultWidth: {
+ control: { type: 'range', min: 300, max: 800, step: 50 },
+ },
+ showPasswordLabel: {
+ description: 'Show password" tooltip text on password visibility toggle',
+ },
+ hidePasswordLabel: {
+ description: 'Hide password" tooltip text on password visibility toggle',
+ },
+ placeholder: {
+ control: {
+ type: 'text',
+ },
+ },
+ invalid: {
+ control: {
+ type: 'boolean',
+ },
+ },
+ invalidText: {
+ control: {
+ type: 'text',
+ },
+ },
+ disabled: {
+ control: {
+ type: 'boolean',
+ },
+ },
+ labelText: {
+ control: {
+ type: 'text',
+ },
+ },
+ warn: {
+ control: {
+ type: 'boolean',
+ },
+ },
+ warnText: {
+ control: {
+ type: 'text',
+ },
+ },
+ value: {
+ control: {
+ type: 'text',
+ },
+ },
+ readonly: {
+ control: 'boolean',
+ description: 'Read only (readonly)',
+ },
+};
+
+export const Default = {
+ args,
+ argTypes,
+ render: ({
+ defaultWidth,
+ disabled,
+ hideLabel,
+ hidePasswordLabel,
+ inline,
+ invalid,
+ invalidText,
+ labelText,
+ placeholder,
+ readonly,
+ showPasswordLabel,
+ tooltipAlignment,
+ tooltipPosition,
+ type,
+ value,
+ warn,
+ warnText,
+ }) => html`
+
+
+
+
+ `,
+};
+const meta = {
+ decorators: [
+ (story) => {
+ return html`${story()}
`;
+ },
+ ],
+ title: 'Components/Fluid Components/FluidPasswordInput',
+};
+
+export default meta;
diff --git a/packages/web-components/src/components/fluid-password-input/fluid-password-input.ts b/packages/web-components/src/components/fluid-password-input/fluid-password-input.ts
new file mode 100644
index 000000000000..542dbe1925ee
--- /dev/null
+++ b/packages/web-components/src/components/fluid-password-input/fluid-password-input.ts
@@ -0,0 +1,39 @@
+/**
+ * Copyright IBM Corp.2025
+ *
+ * This source code is licensed under the Apache-2.0 license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+import { prefix } from '../../globals/settings';
+import { html } from 'lit';
+import { carbonElement as customElement } from '../../globals/decorators/carbon-element';
+import styles from './fluid-password-input.scss?lit';
+import CDSPasswordInput from '../password-input/password-input';
+
+/**
+ * Fluid text select.
+ *
+ * @element cds-fluid-select
+ */
+@customElement(`${prefix}-fluid-password-input`)
+class CDSFluidPasswordInput extends CDSPasswordInput {
+ connectedCallback() {
+ this.setAttribute('isFluid', 'true');
+ super.connectedCallback();
+ }
+ updated() {
+ const formItem = this.shadowRoot?.querySelector(`.${prefix}--form-item`);
+ if (formItem) {
+ formItem.classList.add(`${prefix}--text-input--fluid`);
+ }
+ }
+
+ render() {
+ return html`${super.render()}`;
+ }
+
+ static styles = [CDSPasswordInput.styles, styles];
+}
+
+export default CDSFluidPasswordInput;
diff --git a/packages/web-components/src/components/fluid-password-input/index.ts b/packages/web-components/src/components/fluid-password-input/index.ts
new file mode 100644
index 000000000000..92b53362f913
--- /dev/null
+++ b/packages/web-components/src/components/fluid-password-input/index.ts
@@ -0,0 +1,7 @@
+/**
+ * Copyright IBM Corp. 2025
+ *
+ * This source code is licensed under the Apache-2.0 license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+import './fluid-password-input';
diff --git a/packages/web-components/src/components/password-input/__tests__/password-input-test.js b/packages/web-components/src/components/password-input/__tests__/password-input-test.js
index c1f1b91a1964..9ce7cd37e4b9 100644
--- a/packages/web-components/src/components/password-input/__tests__/password-input-test.js
+++ b/packages/web-components/src/components/password-input/__tests__/password-input-test.js
@@ -75,6 +75,16 @@ describe('cds-password-input', () => {
).to.be.true;
});
+ it('should render divider when isFluid is true', async () => {
+ const el = await fixture(html`
+
+ `);
+
+ const divider = el.shadowRoot.querySelector('.cds--text-input__divider');
+ expect(divider).to.exist;
+ expect(divider.tagName.toLowerCase()).to.equal('hr');
+ });
+
it('should apply hide-password-label attribute', async () => {
const el = await fixture(html`
`
: null;
+ const validationMessage =
+ normalizedProps.invalid || normalizedProps.warn
+ ? html`
+
+ ${normalizedProps['slot-text']}
+
+
`
+ : null;
+
let align = '';
if (
@@ -313,15 +331,13 @@ class CDSPasswordInput extends CDSTextInput {
: this.showPasswordLabel}
+ ${isFluid
+ ? html`
`
+ : null}
+ ${isFluid && !inline ? validationMessage : null}
- ${!inline ? helper : null}
-
-
- ${normalizedProps['slot-text']}
-
-
+ ${/* Non-fluid: validation and helper outside field wrapper */ ''}
+ ${!isFluid && !inline ? validationMessage || helper : null}
`;