fix(login): migrate legacy Tailwind v4 opacity utilities and fix checkbox contrast color (#12360)

# Which Problems Are Solved

1. **Secondary button hover effect** was rendering a solid white/gray
background instead of a subtle transparent overlay. The legacy
`bg-opacity-*` utility (e.g. `hover:bg-gray-500 hover:bg-opacity-20`)
doesn't compose with `bg-*` in Tailwind v4 — the opacity is ignored,
leaving a solid color. The same issue affected `ring-opacity-*` and
`border-opacity-*` across other components.

2. **Checkbox checkmark color** was hardcoded to white (`fill='white'`
in the SVG), ignoring the theme's primary contrast color. On themes with
a light primary color, the white checkmark was invisible.

# How the Problems Are Solved


1. Migrated all legacy opacity utilities to the Tailwind v4 slash
syntax:
   - `hover:bg-gray-500 hover:bg-opacity-20` → `hover:bg-gray-500/20`
- `ring-primary-light-500 ring-opacity-60` → `ring-primary-light-500/60`
- `focus:ring-opacity-50` + `focus:ring-indigo-200` →
`focus:ring-indigo-200/50`
- Removed redundant `border-opacity-20` where `border-black/10` was
already applied

2. Replaced the static `background-image` checkbox SVG with a
`mask-image` + `::after` pseudo-element approach. The checkmark color
now uses `var(--theme-light-primary-contrast-500)` /
`var(--theme-dark-primary-contrast-500)`, so it dynamically follows the
theme's contrast color.

---------

Co-authored-by: Livio Spring <9405495+livio-a@users.noreply.github.com>
This commit is contained in:
Max Peintner
2026-07-03 12:39:09 +00:00
committed by GitHub
co-authored by Livio Spring
parent 92ea32a34d
commit e6aaea563e
5 changed files with 32 additions and 6 deletions
@@ -29,7 +29,7 @@ export function AuthenticationMethodRadio({
value={method}
data-testid={method + "-radio"}
className={({ focus, checked }) =>
`${focus ? "ring-primary-light-500 ring-opacity-60 ring-2 dark:ring-white/20" : ""} ${
`${focus ? "ring-primary-light-500/60 ring-2 dark:ring-white/20" : ""} ${
checked
? "bg-background-light-400 ring-primary-light-500 dark:bg-background-dark-400 dark:ring-primary-dark-500 ring-2"
: "bg-background-light-400 dark:bg-background-dark-400"
+2 -2
View File
@@ -42,9 +42,9 @@ export const getButtonClasses = (
variant === ButtonVariants.Primary && color !== ButtonColors.Warn,
"bg-warn-light-500 dark:bg-warn-dark-500 hover:bg-warn-light-400 hover:dark:bg-warn-dark-400 text-white dark:text-white":
variant === ButtonVariants.Primary && color === ButtonColors.Warn,
"border border-button-light-border dark:border-button-dark-border text-gray-950 hover:bg-gray-500 hover:bg-opacity-20 hover:dark:bg-white hover:dark:bg-opacity-10 focus:bg-gray-500 focus:bg-opacity-20 focus:dark:bg-white focus:dark:bg-opacity-10 dark:text-white disabled:text-gray-600 disabled:hover:bg-transparent disabled:dark:hover:bg-transparent disabled:cursor-not-allowed disabled:dark:text-gray-900":
"border border-button-light-border dark:border-button-dark-border text-gray-950 hover:bg-gray-500/20 hover:dark:bg-white/10 focus:bg-gray-500/20 focus:dark:bg-white/10 dark:text-white disabled:text-gray-600 disabled:hover:bg-transparent disabled:dark:hover:bg-transparent disabled:cursor-not-allowed disabled:dark:text-gray-900":
variant === ButtonVariants.Secondary,
"border border-button-light-border dark:border-button-dark-border text-warn-light-500 dark:text-warn-dark-500 hover:bg-warn-light-500 hover:bg-opacity-10 dark:hover:bg-warn-light-500 dark:hover:bg-opacity-10 focus:bg-warn-light-500 focus:bg-opacity-20 dark:focus:bg-warn-light-500 dark:focus:bg-opacity-20":
"border border-button-light-border dark:border-button-dark-border text-warn-light-500 dark:text-warn-dark-500 hover:bg-warn-light-500/10 dark:hover:bg-warn-light-500/10 focus:bg-warn-light-500/20 dark:focus:bg-warn-light-500/20":
color === ButtonColors.Warn && variant !== ButtonVariants.Primary,
"px-16 py-2": size === ButtonSizes.Large,
"px-4 h-[36px]": size === ButtonSizes.Small,
+1 -1
View File
@@ -31,7 +31,7 @@ export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(function Che
disabled={disabled}
type="checkbox"
className={classNames(
"form-checkbox text-primary-light-500 focus:ring-opacity-50 dark:text-primary-dark-500 rounded border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-offset-0",
"form-checkbox text-primary-light-500 dark:text-primary-dark-500 rounded border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200/50 focus:ring-offset-0",
className,
)}
{...props}
+1 -1
View File
@@ -128,7 +128,7 @@ export const SPACING_STYLES = {
// Appearance styling (complete design philosophies)
export const APPEARANCE_STYLES = {
flat: {
card: "bg-background-light-400 dark:bg-background-dark-500 border border-opacity-20 border border-black/10 dark:border-white/10",
card: "bg-background-light-400 dark:bg-background-dark-500 border border-black/10 dark:border-white/10",
button: "border border-button-light-border dark:border-button-dark-border", // No shadows for flat design
"idp-button": "border border-button-light-border dark:border-button-dark-border", // No shadows for flat design
typography: "font-normal",
+27 -1
View File
@@ -18,8 +18,34 @@ html {
--dark-background-color: #000000;
}
.form-checkbox {
position: relative;
}
.form-checkbox:checked {
background-image: url("/checkbox.svg");
background-image: none !important;
background-color: var(--theme-light-primary-500);
}
.dark .form-checkbox:checked {
background-color: var(--theme-dark-primary-500);
}
.form-checkbox:checked::after {
content: "";
position: absolute;
inset: 0;
-webkit-mask-image: url("/checkbox.svg");
mask-image: url("/checkbox.svg");
-webkit-mask-size: 100% 100%;
mask-size: 100% 100%;
-webkit-mask-repeat: no-repeat;
mask-repeat: no-repeat;
background-color: var(--theme-light-primary-contrast-500);
}
.dark .form-checkbox:checked::after {
background-color: var(--theme-dark-primary-contrast-500);
}
.skeleton {