From e5c89048c24cee24adca4501fe4fdf38b2e31b1d Mon Sep 17 00:00:00 2001 From: Makisuo Date: Fri, 13 Nov 2026 19:26:08 +0100 Subject: [PATCH 1/3] fix(ui): selection is an opaque pair, primary-foreground on primary A wash with a forced text colour still failed on a filled surface: the label of a primary badge selected to 2.1:1 in dark. Both halves are opaque now, so selected text reads the same over a muted run, a tone or a fill. The native select drops its own selection utilities, which set this same pair. --- docs/design-rules.md | 4 ++-- packages/ui/src/styles/tokens.css | 8 ++++---- packages/views/src/ui/native-select.styles.ts | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/design-rules.md b/docs/design-rules.md index fbabdc87d0..fe43e756e7 100644 --- a/docs/design-rules.md +++ b/docs/design-rules.md @@ -97,8 +97,8 @@ bordered box takes its parent's radius minus 1px: `before:rounded-lg-inset` insi step of the hue in light, where the fill itself is near 3:1 as type, and the fill in dark. Fills, borders and strokes keep the tone. Write the utility; never a fill token as `color` in a style object. -- Selected text takes `primary-a4` as its highlight and reads in `foreground`, set once in - tokens.css. No `selection:` utility at a call site. +- Selected text is `primary-foreground` on `primary`, set once in tokens.css: an opaque pair, so it + reads the same over muted text, a tone or a fill. No `selection:` utility at a call site. - Washes are translucent and read the same on any surface: `surface-faint`, `surface-sunken`, `surface-strong`. - A see-through surface is a token, never `bg-background/NN` or `bg-card/NN` diff --git a/packages/ui/src/styles/tokens.css b/packages/ui/src/styles/tokens.css index e470314dc7..2124f77373 100644 --- a/packages/ui/src/styles/tokens.css +++ b/packages/ui/src/styles/tokens.css @@ -812,11 +812,11 @@ * { @apply border-border outline-ring/50; } - /* Selected text takes the primary's strong wash instead of the system blue, and reads in - `foreground`: a muted or toned run would fall under 4.5:1 on the wash. */ + /* Selected text takes the primary instead of the system blue. Both halves are opaque, so the + pair reads the same over a muted run, a tone, or a filled badge. */ ::selection { - background-color: var(--color-primary-a4); - color: var(--color-foreground); + background-color: var(--color-primary); + color: var(--color-primary-foreground); } body { @apply font-sans bg-background text-foreground; diff --git a/packages/views/src/ui/native-select.styles.ts b/packages/views/src/ui/native-select.styles.ts index ca737f5460..c8922d6a95 100644 --- a/packages/views/src/ui/native-select.styles.ts +++ b/packages/views/src/ui/native-select.styles.ts @@ -1,7 +1,7 @@ export const nativeSelectWrapperStyles = "group/native-select relative w-fit has-[select:disabled]:opacity-50" export const nativeSelectStyles = - "border-input placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 dark:hover:bg-input/50 dark:active:bg-input/50 focus-visible:border-ring focus-visible:ring-ring aria-invalid:ring-destructive-a3 dark:aria-invalid:ring-destructive-a4 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 h-8 w-full min-w-0 appearance-none rounded-lg border bg-transparent py-1 pr-8 pl-2.5 text-xs select-none focus-visible:ring-1 aria-invalid:ring-1 data-[size=sm]:h-7 data-[size=sm]:rounded-lg data-[size=sm]:py-0.5 data-leading:pl-7 outline-none disabled:pointer-events-none disabled:cursor-not-allowed" + "border-input placeholder:text-muted-foreground dark:bg-input/30 dark:hover:bg-input/50 dark:active:bg-input/50 focus-visible:border-ring focus-visible:ring-ring aria-invalid:ring-destructive-a3 dark:aria-invalid:ring-destructive-a4 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 h-8 w-full min-w-0 appearance-none rounded-lg border bg-transparent py-1 pr-8 pl-2.5 text-xs select-none focus-visible:ring-1 aria-invalid:ring-1 data-[size=sm]:h-7 data-[size=sm]:rounded-lg data-[size=sm]:py-0.5 data-leading:pl-7 outline-none disabled:pointer-events-none disabled:cursor-not-allowed" export const nativeSelectIconStyles = "text-muted-foreground top-1/2 right-2.5 size-4 -translate-y-1/2 pointer-events-none absolute select-none" From d9f7ba3412b7efb60a3ccb4725bb7c17c22130ec Mon Sep 17 00:00:00 2001 From: Makisuo Date: Fri, 13 Nov 2026 20:44:55 +0100 Subject: [PATCH 2/3] fix(ui): a chip keeps its own look when selected The opaque selection pair put an orange block in the middle of a filled chip: a red FATAL badge with an orange slab and red ends. A kit badge, and a label on a solid fill (text-on-fill, text-primary-foreground), now keep their fill and ink when selected. The text still selects and copies. --- docs/design-rules.md | 4 +++- packages/ui/src/styles/tokens.css | 11 +++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/docs/design-rules.md b/docs/design-rules.md index fe43e756e7..2a5a338153 100644 --- a/docs/design-rules.md +++ b/docs/design-rules.md @@ -98,7 +98,9 @@ bordered box takes its parent's radius minus 1px: `before:rounded-lg-inset` insi borders and strokes keep the tone. Write the utility; never a fill token as `color` in a style object. - Selected text is `primary-foreground` on `primary`, set once in tokens.css: an opaque pair, so it - reads the same over muted text, a tone or a fill. No `selection:` utility at a call site. + reads the same over muted text or a tone. A chip keeps its own look when selected (a kit `badge`, + or a label on a solid fill: `text-on-fill`, `text-primary-foreground`): a band inside a fill + only fights it. No `selection:` utility at a call site. - Washes are translucent and read the same on any surface: `surface-faint`, `surface-sunken`, `surface-strong`. - A see-through surface is a token, never `bg-background/NN` or `bg-card/NN` diff --git a/packages/ui/src/styles/tokens.css b/packages/ui/src/styles/tokens.css index 2124f77373..652ce55706 100644 --- a/packages/ui/src/styles/tokens.css +++ b/packages/ui/src/styles/tokens.css @@ -818,6 +818,17 @@ background-color: var(--color-primary); color: var(--color-primary-foreground); } + /* A chip keeps its own look when selected: a band inside a fill or a tint only fights it. The + text is still selected and still copies. */ + [data-slot="badge"]::selection, + [data-slot="badge"] ::selection, + .text-on-fill::selection, + .text-on-fill ::selection, + .text-primary-foreground::selection, + .text-primary-foreground ::selection { + background-color: transparent; + color: currentColor; + } body { @apply font-sans bg-background text-foreground; } From 9d51585094707091276c3d9fe5c6c61116908d34 Mon Sep 17 00:00:00 2001 From: Makisuo Date: Fri, 13 Nov 2026 20:51:26 +0100 Subject: [PATCH 3/3] fix(ui): an underline marks the selected letters of a chip With no mark at all a partial selection inside a chip could not be seen. A thin underline in the chip's own ink shows which letters are selected and leaves the fill alone. --- docs/design-rules.md | 2 +- packages/ui/src/styles/tokens.css | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/design-rules.md b/docs/design-rules.md index 2a5a338153..aa03269fc3 100644 --- a/docs/design-rules.md +++ b/docs/design-rules.md @@ -100,7 +100,7 @@ bordered box takes its parent's radius minus 1px: `before:rounded-lg-inset` insi - Selected text is `primary-foreground` on `primary`, set once in tokens.css: an opaque pair, so it reads the same over muted text or a tone. A chip keeps its own look when selected (a kit `badge`, or a label on a solid fill: `text-on-fill`, `text-primary-foreground`): a band inside a fill - only fights it. No `selection:` utility at a call site. + only fights it, so a thin underline in the chip's own ink marks the selected letters. No `selection:` utility at a call site. - Washes are translucent and read the same on any surface: `surface-faint`, `surface-sunken`, `surface-strong`. - A see-through surface is a token, never `bg-background/NN` or `bg-card/NN` diff --git a/packages/ui/src/styles/tokens.css b/packages/ui/src/styles/tokens.css index 652ce55706..60023a559d 100644 --- a/packages/ui/src/styles/tokens.css +++ b/packages/ui/src/styles/tokens.css @@ -818,8 +818,8 @@ background-color: var(--color-primary); color: var(--color-primary-foreground); } - /* A chip keeps its own look when selected: a band inside a fill or a tint only fights it. The - text is still selected and still copies. */ + /* A chip keeps its own look when selected: a band inside a fill or a tint only fights it. A + thin underline in its own ink marks the selected letters instead. */ [data-slot="badge"]::selection, [data-slot="badge"] ::selection, .text-on-fill::selection, @@ -828,6 +828,9 @@ .text-primary-foreground ::selection { background-color: transparent; color: currentColor; + text-decoration: underline; + text-decoration-thickness: 1px; + text-underline-offset: 2px; } body { @apply font-sans bg-background text-foreground;