Summary
HighlightableTextInput ships three hardcoded style values that ignore the host
app's theme and are effectively impossible to override cleanly:
- a blue focus outline (
outline: 2px solid #00f) on the editable layer,
- a black caret (
caret-color: #000), invisible on dark backgrounds,
- light-only scrollbar colors (
#f1f1f1 / #888) on the shadow layer.
Because the component's stylesheet is injected unlayered, these rules win
over any consumer CSS placed in a cascade layer (e.g. Tailwind v4's @layer),
so the "obvious" override silently fails.
Environment
- Package:
react-highlightable-input@2.0.1
- Framework: React 19 + Vite + Tailwind CSS v4 (also reproducible without Tailwind)
- Browsers: all (rules are static CSS)
Current behavior
When the field is focused, a 2px solid blue box is drawn around the editable
area. When the component is wrapped (the documented styling approach), this
inner outline sits inside the wrapper's own focus ring and looks broken:
On dark backgrounds the caret is invisible (black on dark), and the shadow
layer's scrollbar renders with light-gray colors regardless of theme.
Expected behavior
- Focused fields should show an accessible focus indicator (WCAG 2.4.7),
but one that respects the host theme and can be disabled/overridden without
specificity or !important hacks.
- The caret should be visible in both light and dark themes.
- The scrollbar should follow the theme (or at least
color-scheme).
Root cause
From the published build (dist/assets/HighlightableTextInput.css):
._container_4olre_1 { width: calc(100% - 2px); min-height: 44px }
._inputDiv_4olre_6 { caret-color: #000; color: #0000 !important }
._inputDiv_4olre_6 * { color: #0000 !important }
._inputDiv_4olre_6, ._shadowDiv_4olre_16 {
width: 100%; height: 100%; position: absolute; top: 0; left: 0; overflow-y: scroll
}
._inputDiv_4olre_6:focus-visible { outline-offset: 2px; outline: 2px solid #00f }
._shadowDiv_4olre_16::-webkit-scrollbar-track { background: #f1f1f1 }
._shadowDiv_4olre_16::-webkit-scrollbar-thumb { background-color: #888; border: 3px solid #f1f1f1; border-radius: 20px }
._shadowDiv_4olre_16 { scrollbar-width: thin; scrollbar-color: #888 #f1f1f1 }
Source: lib/components/HighlightableTextInput/HighlightableTextInput.module.scss.
Two problems compound:
- Hardcoded values.
#00f, #000, #f1f1f1, #888 are literal colors
with no theming hook.
- Unlayered injection beats layered overrides. The CSS is injected via
vite-plugin-lib-inject-css as a plain (unlayered) stylesheet. In the CSS
cascade, any unlayered rule beats every rule inside a named @layer,
regardless of specificity. So a consumer override written inside
@layer components { ... } (the natural place in a Tailwind v4 app) never
applies. The only workarounds today are unlayered overrides that out-specify
the module selector, or !important.
Impact
- Looks broken in any app with its own focus styling (very common), especially
when following the README's recommended "wrap the component" pattern.
- Invisible caret in dark mode = real usability bug.
- Hard to fix from the outside because of the unlayered cascade issue.
Reproduction
- Add
HighlightableTextInput to a Vite + Tailwind v4 app.
- Wrap it in a
div with a border + focus-within:ring (as the README shows).
- Focus the field -> a blue 2px outline appears inside the wrapper's ring.
- Try to remove it with a rule inside
@layer components -> it has no effect.
- Switch the app to dark mode -> the caret is invisible.
Proposed fix
Keep accessible defaults, but route every hardcoded value through a CSS custom
property so consumers can theme or disable it without specificity fights.
// HighlightableTextInput.module.scss
.inputDiv {
caret-color: var(--rhi-caret-color, currentColor);
}
.inputDiv:focus-visible {
// System accent adapts to OS/theme; fully overridable, incl. `none`.
outline: var(--rhi-focus-outline, 2px solid Highlight);
outline-offset: 2px;
}
.shadowDiv {
scrollbar-width: thin;
scrollbar-color: var(--rhi-scrollbar-thumb, currentColor)
var(--rhi-scrollbar-track, transparent);
}
.shadowDiv::-webkit-scrollbar-thumb {
background-color: var(--rhi-scrollbar-thumb, currentColor);
}
.shadowDiv::-webkit-scrollbar-track {
background: var(--rhi-scrollbar-track, transparent);
}
Notes:
caret-color: currentColor won't work directly because .inputDiv text is
transparent; set the caret from a variable (default to a readable value or
inherit from a non-transparent ancestor) so it stays visible.
- Consider emitting the component CSS into a documented
@layer (or
publishing an unstyled entry) so consumer overrides compose predictably.
Alternatively, keep it unlayered but document that overrides must also be
unlayered.
- Optionally expose an escape hatch prop (e.g.
showFocusOutline?: boolean),
though CSS variables cover the need and also handle caret/scrollbar.
Consumer usage after the fix
/* wrap the component and let the wrapper own the focus ring */
--rhi-focus-outline: none;
--rhi-caret-color: var(--foreground);
Acceptance criteria
Workaround (until fixed)
Place overrides outside any @layer, scoped to the container id, so they
win on specificity:
#highlightableTextInput-container [contenteditable] {
caret-color: var(--foreground);
}
#highlightableTextInput-container [contenteditable]:focus-visible {
outline: none;
}
#highlightableTextInput-container [aria-hidden="true"] {
scrollbar-width: none;
}
#highlightableTextInput-container [aria-hidden="true"]::-webkit-scrollbar {
display: none;
}
Summary
HighlightableTextInputships three hardcoded style values that ignore the hostapp's theme and are effectively impossible to override cleanly:
outline: 2px solid #00f) on the editable layer,caret-color: #000), invisible on dark backgrounds,#f1f1f1/#888) on the shadow layer.Because the component's stylesheet is injected unlayered, these rules win
over any consumer CSS placed in a cascade layer (e.g. Tailwind v4's
@layer),so the "obvious" override silently fails.
Environment
react-highlightable-input@2.0.1Current behavior
When the field is focused, a 2px solid blue box is drawn around the editable
area. When the component is wrapped (the documented styling approach), this
inner outline sits inside the wrapper's own focus ring and looks broken:
On dark backgrounds the caret is invisible (black on dark), and the shadow
layer's scrollbar renders with light-gray colors regardless of theme.
Expected behavior
but one that respects the host theme and can be disabled/overridden without
specificity or
!importanthacks.color-scheme).Root cause
From the published build (
dist/assets/HighlightableTextInput.css):Source:
lib/components/HighlightableTextInput/HighlightableTextInput.module.scss.Two problems compound:
#00f,#000,#f1f1f1,#888are literal colorswith no theming hook.
vite-plugin-lib-inject-cssas a plain (unlayered) stylesheet. In the CSScascade, any unlayered rule beats every rule inside a named
@layer,regardless of specificity. So a consumer override written inside
@layer components { ... }(the natural place in a Tailwind v4 app) neverapplies. The only workarounds today are unlayered overrides that out-specify
the module selector, or
!important.Impact
when following the README's recommended "wrap the component" pattern.
Reproduction
HighlightableTextInputto a Vite + Tailwind v4 app.divwith a border +focus-within:ring(as the README shows).@layer components-> it has no effect.Proposed fix
Keep accessible defaults, but route every hardcoded value through a CSS custom
property so consumers can theme or disable it without specificity fights.
Notes:
caret-color: currentColorwon't work directly because.inputDivtext istransparent; set the caret from a variable (default to a readable value orinherit from a non-transparent ancestor) so it stays visible.
@layer(orpublishing an unstyled entry) so consumer overrides compose predictably.
Alternatively, keep it unlayered but document that overrides must also be
unlayered.
showFocusOutline?: boolean),though CSS variables cover the need and also handle caret/scrollbar.
Consumer usage after the fix
Acceptance criteria
nonewithout!important.color-scheme.@layercaveat) added to the README.
Workaround (until fixed)
Place overrides outside any
@layer, scoped to the container id, so theywin on specificity: