Input Color Feedback
A tiny CSS utility I authored and published on npm. Drop it into any project and every form input starts giving live colour, glow and icon feedback as the user types — blue while focused, green when valid, amber when not. No JavaScript, no dependencies, no markup changes — accessible by default and ≈0.97 KB.
Overview
Input Color Feedback is a small open-source package I wrote and published on npm. It does one thing well: it makes form inputs feel alive. The moment a field is focused it glows; as the user types, the border shifts colour to tell them — without a word of copy — whether what they've entered is valid yet.
It started as a snippet I kept rewriting on every form I built. Rather than copy-paste it forever, I distilled it into a single stylesheet, documented it, gave it an MIT licence and shipped it to npm so anyone (including future me) can add it with one import.
The idea: a traffic light for forms
The whole thing leans on a visual language everyone already speaks: traffic lights. Green means good, amber means not yet, red means there's a problem. That's all the package really is: native form validation, mapped onto colours nobody has to learn.
For developers it's a small, shareable standard: drop it in and every form across a project (or across projects) speaks the same dialect, instead of everyone reinventing their own validation styling. For users it's a consistent, wordless way to read the status of the field they're filling in. That was the entire goal here: take a pattern I kept rebuilding and share the part I think genuinely helps both sides.
How it works
Every <input> transitions its border colour,
glow (box-shadow) and — as of v2.2.0 — a small state icon
according to its focus and validation state. There are six states, and each one tells the user
something different:
- Unfocused and empty (the resting default) — a neutral grey border, no glow: the calm starting point before the field has been touched.
- Focused and empty (placeholder still showing) — the border glows blue: "you're here, go ahead".
- Focused but invalid — the border glows amber and a warning-triangle icon appears: "keep going, not quite right yet".
- Focused and valid — the border glows green with a tick: "that works".
- Unfocused and invalid — the border turns solid red with an error cross, no glow: a calm error marker once you've moved on.
- Unfocused and valid — the border stays green with a tick, no glow: quietly confirmed.
The glow only ever appears on the focused field, so a long form never lights up like a Christmas tree — your eye is drawn to exactly the input you're working on.
The icons are more than polish: they mean the state is signalled by something other than colour.
They're painted straight into the input's own background-image as inlined data-URI
SVGs, so there's no extra markup and no network request — they travel inside the
stylesheet itself. They appear on single-line text-like inputs (text,
email, password, tel, url and a bare
<input>), which reserve a little right-hand padding so the icon never overlaps
the value; types that bring their own right-edge UI — checkbox, radio, range, colour, date, number,
search — keep just the border feedback.
Pure CSS, zero JavaScript
The whole library is one short stylesheet and not a single line of JavaScript. All the logic lives in
modern CSS selectors that the browser already evaluates for free:
:focus, :placeholder-shown, :valid/:invalid and
the user-driven :user-valid/:user-invalid — the last two are what keep the
page from yelling "invalid" at an empty field the user hasn't even reached yet.
Because there's no script, there's nothing to initialise, nothing to bundle, no runtime cost and
nothing that can break a form's existing behaviour. The transitions on border-color and
box-shadow are eased over 0.15s — snappy, and in line with common UI
timing (it was 0.3s in earlier versions) — and that duration is itself a
--transition-duration custom property you can retune or set to 0 to switch
the animation off. Under prefers-reduced-motion it's disabled entirely.
Accessible by default
Colour feedback only helps if everyone can read it, so the package builds accessibility in rather
than leaving it to the consumer — and, as of v2.2.0, it does so automatically on a bare
<input>, with no JavaScript, no classes and no extra markup:
- Colour isn't the only cue (WCAG 1.4.1). Each judged state also draws an icon — warning triangle, error cross or valid tick — so a colour-blind user isn't relying on hue alone to tell amber from green from red.
-
Borders that actually contrast
(WCAG 1.4.11).
The state colours are tuned to clear the 3:1 non-text-contrast threshold against the backgrounds
it's tested on — roughly 3:1 for the light valid-green and warning-orange on white, and the dark
palette clears it on
#121212. -
Respects
prefers-reduced-motion. The border and glow transitions are switched off for users who ask for less motion. - Keeps a focus ring in forced-colors / high-contrast mode, where the glow (a box-shadow) can't be painted at all.
One honest limit: the icons are decorative background-images, so they're not
announced to assistive tech, and the package deliberately ships no screen-reader error text. On a
real form, keep pairing the states with a visible message wired up via aria-describedby
so screen-reader users get the words too.
Themeable by design
Every colour is a CSS custom property declared on :root, so theming is a matter of
overriding a handful of variables — no Sass, no build step, no forking:
--default-color— the resting border.--focus-color— the blue focus glow.--valid-color— the green "valid" state.--warning-color— the amber "focused but invalid" state.--invalid-color— the red "left invalid" state.--shadow-strength— how intense the glow is, mixed into the shadow withcolor-mix().--transition-duration— the border + glow transition speed (e.g.0.15s;0disables it).
Point those at your brand palette and the feedback inherits your design language automatically.
Installation & usage
Install it with npm install input-color-feedback (or yarn add input-color-feedback),
then pull in the stylesheet — either from CSS with
@import "input-color-feedback/styles.css"; or, with a bundler like Vite or Webpack, from
JavaScript with import 'input-color-feedback/styles.css';.
That's the entire setup. No components to wire up and no classes to add — the styling keys off native
HTML validation, so it activates as soon as an input has a placeholder and a validation
rule such as required, pattern or minlength. Inputs without
those won't react, which is by design.
Light and dark, out of the box
The default palette is tuned for dark interfaces — like this site, and like
Biglo, the app it was born in. As of v2.1.0 it no longer stops
there: on a light background the two colours that wouldn't otherwise meet the WCAG contrast
threshold — the valid green and the focused-but-invalid amber — are swapped automatically to
follow your page's declared color-scheme via the CSS light-dark()
function: the green darkens and the amber shifts to orange so both clear 3:1 on white. Declare
nothing and browsers render light, so the light palette applies; browsers without
light-dark() fall back to the dark set. No configuration required.
And because every colour is still a CSS variable, you can re-theme it for any design — which is exactly what I did on the Abogada de la Tierra site: I scoped the library's variables to the contact form, kept the page's own resting border colour, and adjusted the palette so it sits comfortably on a cream background.
Footprint & benchmarks
The whole library is ≈0.97 KB min+gzip, with zero dependencies and zero runtime JavaScript. Measured against ten comparable packages (Bundlephobia for theirs, a local minifier for this one), that's roughly 13× smaller than the median — and smaller than the smallest dedicated JavaScript validator in the field.
I wanted the performance claims to be checkable rather than asserted, so the repo ships a
BENCHMARKS.md and a one-command runtime harness — Playwright driving real Chromium —
that anyone can rerun on their own hardware. Its headline finding: for live, per-field feedback this
package adds 0 ms of interaction scripting and an init cost equal to a bare page,
where Bootstrap's realistic live path (JavaScript toggling validation classes) adds ≈8 ms on top of
≈16 KB of framework, and standalone JS validators ≈15 ms. Set animation aside and it even recalcs a
touch lighter than Bootstrap — there's simply less stylesheet for the browser to match.
The honest framing: this does one thing — accessible visual state feedback — for almost no bytes and no script. A full framework like Bootstrap can match the accessibility, but only with its bytes, its classes, its markup and (for genuinely live feedback) its JavaScript. What it buys over this package is the screen-reader error text this one deliberately leaves to you. Both, note, hand the actual constraint checking to the browser.
Limitations & what's next
I'd rather be upfront about where it stops short:
-
It needs constrained fields. Because it keys off native validation, an input only
reacts if it actually has a rule — a
placeholderplus something likerequired,patternorminlength. An unconstrained input is always:valid, so it would sit green from the very first paint. -
But you can work around it. On
Abogada de la Tierra I paired the package with a
novalidateform and a small JS validator that toggles an.is-invalidclass for submit-time errors, plus an--optionalmodifier so empty optional fields stay neutral instead of going green. The CSS handles the live feel; a few lines of JS handle the cases native validation can't express. -
No screen-reader error text — yet. v2.2.0 closed the biggest gap here: each
judged state now carries an icon, so meaning no longer rests on hue alone (it started as my
personal implementation for Biglo and has grown well past that).
What it still doesn't ship is a message announced to assistive tech, so on real forms pair it with
your own copy via
aria-describedby.
Where it's headed — directional, not promises, and without fixed dates:
-
Feedback without required markup. Today a field only reacts when it carries a
placeholderand a constraint; a future release aims to lift that, so a plain<input>can show the same six states with no extra attributes. - Matching text colours (exploring) — optional tokens so a helper or error message can echo its input's state colour. Text asks for more contrast than borders (4.5:1 vs 3:1), so it needs its own tuned palette first.
Status
Published and live on npm as input-color-feedback, with the source on GitHub under an MIT licence. It's currently on version 2.2.0 — the release that added the accessibility icon layer — small, stable and free for anyone to use.