diff --git a/demo/e2e/screenshot.spec.ts-snapshots/inputs-dark.png b/demo/e2e/screenshot.spec.ts-snapshots/inputs-dark.png
index 9fbfbcf..03b529c 100644
Binary files a/demo/e2e/screenshot.spec.ts-snapshots/inputs-dark.png and b/demo/e2e/screenshot.spec.ts-snapshots/inputs-dark.png differ
diff --git a/demo/e2e/screenshot.spec.ts-snapshots/inputs.png b/demo/e2e/screenshot.spec.ts-snapshots/inputs.png
index 2cc0727..5267ba4 100644
Binary files a/demo/e2e/screenshot.spec.ts-snapshots/inputs.png and b/demo/e2e/screenshot.spec.ts-snapshots/inputs.png differ
diff --git a/demo/src/app/index/pages/inputs/inputs.page.html b/demo/src/app/index/pages/inputs/inputs.page.html
index 6b889bb..a6da813 100644
--- a/demo/src/app/index/pages/inputs/inputs.page.html
+++ b/demo/src/app/index/pages/inputs/inputs.page.html
@@ -40,6 +40,14 @@
inputs
+
+
+
+
+
+
+
+
@@ -62,6 +70,67 @@ inputs
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/demo/src/app/index/pages/inputs/inputs.page.spec.ts b/demo/src/app/index/pages/inputs/inputs.page.spec.ts
index 0f6891c..df2f299 100644
--- a/demo/src/app/index/pages/inputs/inputs.page.spec.ts
+++ b/demo/src/app/index/pages/inputs/inputs.page.spec.ts
@@ -18,4 +18,38 @@ describe('InputsPage', () => {
it('should create', () => {
expect(component).toBeTruthy();
});
+
+ it('should render helper and error input patterns', () => {
+ const element = fixture.nativeElement as HTMLElement;
+ const helperInput = element.querySelector('ion-input[helpertext]');
+ const errorInput = element.querySelector('ion-input[errortext]');
+ const helperTextarea = element.querySelector('ion-textarea[helpertext]');
+ const errorTextarea = element.querySelector('ion-textarea[errortext]');
+
+ expect(helperInput?.getAttribute('helpertext')).toBe('This is helper text');
+ expect(errorInput?.getAttribute('errortext')).toBe('This is error text');
+ expect(errorInput?.classList.contains('ion-invalid')).toBe(true);
+ expect(errorInput?.classList.contains('ion-touched')).toBe(true);
+ expect(helperTextarea?.getAttribute('helpertext')).toBe('This is helper text');
+ expect(errorTextarea?.getAttribute('errortext')).toBe('This is error text');
+ expect(errorTextarea?.classList.contains('ion-invalid')).toBe(true);
+ expect(errorTextarea?.classList.contains('ion-touched')).toBe(true);
+ });
+
+ it('should render outlined helper and error patterns', () => {
+ const element = fixture.nativeElement as HTMLElement;
+ const outlinedInputs = element.querySelectorAll('ion-input[fill="outline"]');
+ const outlinedTextareas = element.querySelectorAll('ion-textarea[fill="outline"]');
+
+ expect(outlinedInputs).toHaveLength(4);
+ expect(outlinedInputs[2].getAttribute('helpertext')).toBe('This is helper text');
+ expect(outlinedInputs[3].getAttribute('errortext')).toBe('This is error text');
+ expect(outlinedInputs[3].classList.contains('ion-invalid')).toBe(true);
+ expect(outlinedInputs[3].classList.contains('ion-touched')).toBe(true);
+ expect(outlinedTextareas).toHaveLength(4);
+ expect(outlinedTextareas[2].getAttribute('helpertext')).toBe('This is helper text');
+ expect(outlinedTextareas[3].getAttribute('errortext')).toBe('This is error text');
+ expect(outlinedTextareas[3].classList.contains('ion-invalid')).toBe(true);
+ expect(outlinedTextareas[3].classList.contains('ion-touched')).toBe(true);
+ });
});
diff --git a/demo/src/global.scss b/demo/src/global.scss
index 5a1bba3..d7bf220 100644
--- a/demo/src/global.scss
+++ b/demo/src/global.scss
@@ -71,6 +71,12 @@ html.ionic-v8 {
&:is(.textarea-label-placement-stacked, .textarea-label-placement-floating) {
block-size: 68px;
}
+
+ &.textarea-fill-outline .textarea-wrapper {
+ block-size: 124px;
+ box-sizing: border-box;
+ padding-top: 12px;
+ }
}
}
diff --git a/src/styles/components/ion-inputs.scss b/src/styles/components/ion-inputs.scss
new file mode 100644
index 0000000..06f8e6c
--- /dev/null
+++ b/src/styles/components/ion-inputs.scss
@@ -0,0 +1,82 @@
+@mixin outline-field($control) {
+ --placeholder-opacity: 1;
+ --padding-top: 12px;
+ --padding-bottom: 0;
+ margin-bottom: 12px;
+
+ .#{$control}-control,
+ .#{$control}-wrapper {
+ flex-direction: column;
+ }
+
+ .label-text-wrapper {
+ max-width: 100%;
+ margin: 0 2px 4px;
+ padding: 0;
+ font-size: 0.875rem;
+ line-height: 1.25rem;
+ transform: none;
+ }
+
+ .native-wrapper {
+ min-height: 48px;
+ border: 1px solid
+ var(--ion-item-border-color, var(--ion-border-color, var(--ion-color-step-250, var(--ion-background-color-step-250, #c8c7cc))));
+ border-radius: 10px;
+ background: var(--ion-background-color, #fff);
+ transition: border-color 150ms ease;
+ }
+
+ .#{$control}-bottom {
+ inset-inline: 0;
+ padding: 4px 2px 0;
+ border: 0;
+ font-size: 0.75rem;
+ line-height: 1rem;
+ }
+
+ &.has-focus {
+ .label-text-wrapper {
+ color: var(--highlight-color-focused);
+ }
+
+ .native-wrapper {
+ border-color: var(--highlight-color-focused);
+ }
+ }
+
+ &.ion-touched.ion-invalid .native-wrapper {
+ border-color: var(--highlight-color-invalid);
+ }
+
+ &.has-focus.ion-touched.ion-invalid .label-text-wrapper {
+ color: var(--highlight-color-invalid);
+ }
+}
+
+ion-input.ios:not(.ios26-disabled) {
+ &.input-fill-outline {
+ @include outline-field('input');
+
+ .native-input {
+ min-height: 46px;
+ padding-inline: 12px;
+ }
+ }
+}
+
+ion-textarea.ios:not(.ios26-disabled) {
+ &.textarea-fill-outline {
+ @include outline-field('textarea');
+
+ .native-wrapper {
+ min-height: 88px;
+ }
+
+ .native-textarea {
+ min-height: 86px;
+ padding-block: 12px;
+ padding-inline: 12px;
+ }
+ }
+}
diff --git a/src/styles/ionic-theme-ios26.scss b/src/styles/ionic-theme-ios26.scss
index 97e5dc1..48dc0af 100644
--- a/src/styles/ionic-theme-ios26.scss
+++ b/src/styles/ionic-theme-ios26.scss
@@ -7,6 +7,7 @@
@use 'components/ion-content';
@use 'components/ion-datetime';
@use 'components/ion-fab';
+@use 'components/ion-inputs';
@use 'components/ion-list';
@use 'components/ion-loading';
@use 'components/ion-menu';
diff --git a/src/styles/utils/structured-list.scss b/src/styles/utils/structured-list.scss
index 07b1dee..8541fbe 100644
--- a/src/styles/utils/structured-list.scss
+++ b/src/styles/utils/structured-list.scss
@@ -63,6 +63,18 @@
}
ion-item {
+ &:has(ion-input.input-fill-outline),
+ &:has(ion-textarea.textarea-fill-outline),
+ &:has(ion-input .input-bottom > :is(.helper-text:not(:empty), .error-text:not(:empty))),
+ &:has(ion-textarea .textarea-bottom > :is(.helper-text:not(:empty), .error-text:not(:empty))) {
+ --inner-border-width: 0;
+ --inner-padding-bottom: 4px;
+ }
+ ion-input .input-bottom:not(:has(> :is(.helper-text:not(:empty), .error-text:not(:empty)))),
+ ion-textarea .textarea-bottom:not(:has(> :is(.helper-text:not(:empty), .error-text:not(:empty)))) {
+ display: none;
+ }
+
&:has(> ion-label:not([slot])):has(> ion-note:not([slot])) {
&::part(container) {
flex-direction: column;