fix(ui): neutralize password manager autofill highlight on auth inputs

This commit is contained in:
alejandrobailo
2026-06-17 20:53:06 +02:00
parent 54770c640e
commit da2d94e6ee
4 changed files with 70 additions and 0 deletions
+2
View File
@@ -16,6 +16,7 @@ import { Button } from "@/components/shadcn";
import { useToast } from "@/components/shadcn";
import { CustomInput } from "@/components/shadcn/custom";
import { Form } from "@/components/shadcn/form";
import { stripPasswordManagerHighlight } from "@/lib/password-manager";
import { SignInFormData, signInSchema } from "@/types";
export const SignInForm = ({
@@ -146,6 +147,7 @@ export const SignInForm = ({
<AuthLayout title={title}>
<Form {...form}>
<form
ref={stripPasswordManagerHighlight}
noValidate
method="post"
className="flex flex-col gap-4"
+2
View File
@@ -25,6 +25,7 @@ import {
FormField,
FormMessage,
} from "@/components/shadcn/form";
import { stripPasswordManagerHighlight } from "@/lib/password-manager";
import { ApiError, SignUpFormData, signUpSchema } from "@/types";
const AUTH_ERROR_PATHS = {
@@ -152,6 +153,7 @@ export const SignUpForm = ({
<AuthLayout title="Sign up">
<Form {...form}>
<form
ref={stripPasswordManagerHighlight}
noValidate
method="post"
className="flex flex-col gap-4"
+45
View File
@@ -0,0 +1,45 @@
// 1Password marks autofilled fields with data-com-onepassword-filled and paints
// a "filled" background highlight via styles injected outside the author cascade
// (web component / adopted sheet), so CSS overrides can't win. Removing the
// attribute makes its selector stop matching, restoring the design styles while
// leaving autofill itself untouched.
const ONEPASSWORD_FILLED_ATTR = "data-com-onepassword-filled";
/**
* Ref callback for a form (or any container) that strips password-manager
* fill highlights from descendant inputs.
*
* Returns a cleanup function so it works as a React 19 ref-callback cleanup —
* no useEffect needed. Cheap by design: the observer is scoped to the container
* and filtered to a single attribute, so the browser only invokes it when the
* password manager toggles that attribute (once per fill), never per frame.
*
* @example
* <form ref={stripPasswordManagerHighlight}>…</form>
*/
export const stripPasswordManagerHighlight = (
container: HTMLElement | null,
): (() => void) | void => {
if (!container) return;
// removeAttribute on an element without the attribute is a no-op and emits no
// mutation record, so re-firing the observer on our own removal can't loop.
const strip = (element: Element) =>
element.removeAttribute(ONEPASSWORD_FILLED_ATTR);
container.querySelectorAll(`[${ONEPASSWORD_FILLED_ATTR}]`).forEach(strip);
const observer = new MutationObserver((mutations) => {
for (const { target } of mutations) {
if (target instanceof Element) strip(target);
}
});
observer.observe(container, {
subtree: true,
attributes: true,
attributeFilter: [ONEPASSWORD_FILLED_ATTR],
});
return () => observer.disconnect();
};
+21
View File
@@ -476,6 +476,27 @@
}
}
/* Neutralize the browser's native autofill highlight (Chromium/WebKit paint a
forced blue/yellow background on :autofill). box-shadow inset masks it; the
long transition stops the native color from flashing before the mask lands.
Kept UNLAYERED + !important: the mask is a box-shadow and collides with
shadcn's focus:ring (also a box-shadow, in @layer utilities) — a layered
rule can't win that, so this sits outside the cascade layers. */
input:autofill,
input:autofill:hover,
input:autofill:focus,
input:autofill:active,
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus,
input:-webkit-autofill:active {
-webkit-text-fill-color: var(--text-neutral-secondary) !important;
-webkit-box-shadow: 0 0 0 1000px var(--bg-input-primary) inset !important;
box-shadow: 0 0 0 1000px var(--bg-input-primary) inset !important;
caret-color: var(--text-neutral-secondary);
transition: background-color 5000s ease-in-out 0s;
}
/* Override vaul's injected user-select: none to allow text selection in drawers */
@media (hover: hover) and (pointer: fine) {
[data-vaul-drawer][data-vaul-drawer] {