diff --git a/packages/components-dev/form-field/module.ts b/packages/components-dev/form-field/module.ts
index 008b8fd72e..a5f54e6757 100644
--- a/packages/components-dev/form-field/module.ts
+++ b/packages/components-dev/form-field/module.ts
@@ -30,6 +30,7 @@ import {
FormFieldWithoutBordersExample,
FormFieldWithPrefixAndSuffixExample
} from 'packages/docs-examples/components/form-field';
+import { FormFieldsetWithButtonExample } from 'packages/docs-examples/components/forms/form-fieldset-with-button/form-fieldset-with-button-example';
import { DevThemeToggle } from '../theme-toggle';
import { DevFileFlatNode, DevFileNode } from '../tree/module';
@@ -45,9 +46,12 @@ import { DevFileFlatNode, DevFileNode } from '../tree/module';
FormFieldOverviewExample,
FormFieldWithoutBordersExample,
FormFieldWithPrefixAndSuffixExample,
- FormFieldHorizontalExample
+ FormFieldHorizontalExample,
+ FormFieldsetWithButtonExample
],
template: `
+
+
diff --git a/packages/components/form-field/e2e.playwright-spec.ts b/packages/components/form-field/e2e.playwright-spec.ts
index 694af95ce7..11966851fd 100644
--- a/packages/components/form-field/e2e.playwright-spec.ts
+++ b/packages/components/form-field/e2e.playwright-spec.ts
@@ -33,5 +33,29 @@ test.describe('KbqFormFieldModule', () => {
await expect(getScreenshotTarget(locator)).toHaveScreenshot('03-light.png');
});
+
+ // All height tokens involved are whole CSS pixels, so at a 1x device pixel ratio the
+ // form-field's height (sum of independently-rounded border/padding/line-height values) and
+ // the button's height (a single token) land on the same integer and a plain boundingBox
+ // comparison would pass either way. Fractional device pixel ratios force the browser to snap
+ // each side's boxes to physical pixels independently, which is what actually exposes a
+ // regression of the merged-border seam.
+ for (const deviceScaleFactor of [1.5, 2.5]) {
+ test.describe(`at ${deviceScaleFactor}x device scale`, () => {
+ test.use({ deviceScaleFactor });
+
+ test('button and form-field stay flush at the shared border seam', async ({ page }) => {
+ await page.goto('/E2eFormFieldset');
+ const locator = getComponent(page);
+
+ const fieldBox = (await locator.locator('.kbq-form-field__container').boundingBox())!;
+ const buttonBox = (await locator.locator('button.kbq-button').boundingBox())!;
+
+ expect(fieldBox.height).toBeCloseTo(buttonBox.height, 1);
+ expect(fieldBox.y).toBeCloseTo(buttonBox.y, 1);
+ expect(fieldBox.y + fieldBox.height).toBeCloseTo(buttonBox.y + buttonBox.height, 1);
+ });
+ });
+ }
});
});
diff --git a/packages/components/form-field/fieldset.scss b/packages/components/form-field/fieldset.scss
index aac7cd683f..78d9b8691b 100644
--- a/packages/components/form-field/fieldset.scss
+++ b/packages/components/form-field/fieldset.scss
@@ -20,6 +20,16 @@
}
.kbq-form-field {
+ // Force a single-token height, matching how the button computes its own height, so both
+ // round to the same pixel value. Without this, form-field's height is the sum of independently
+ // rounded border/padding/line-height values, which can drift 1px from the button's height after
+ // a resize-triggered subpixel layout recalculation, breaking the merged border line.
+ height: var(--kbq-form-field-size-height);
+
+ .kbq-form-field__content {
+ height: 100%;
+ }
+
// set selector specifity to 0 , so focused state will override z-index
:where(&.kbq-form-field_invalid, &.kbq-form-field_has-validate-directive.ng-invalid) {
z-index: 2;