About this set
File upload states that usually need JavaScript, done with HTML validation and modern selectors. Dropzone with CSS-only states has a header with an icon, a title and a Required badge above a large dashed file input that is itself the drop target. The input is required, so it matches :invalid while empty and :valid once a file is chosen; the box turns solid green, and :has() passes the state up so the icon becomes a tick and the badge reads Ready to send. Press Send with nothing chosen and the browser blocks the submit, the input matches :user-invalid, the box turns red, the badge reads Missing and an error line appears. Document checklist with a CSS counter lists three required documents for a rental application; each row gets a tick and a green background when its file is chosen, and a counter incremented by :has(input:valid) prints 2 of 3 documents attached under the list. The native input stays visible and prints the chosen file name itself, and the form’s method=dialog only keeps the demo on the page. CSS cannot react to a file being dragged over the box or check a file’s size, so add a script if you need those.
What’s included
- No JavaScript in the component
- Empty, chosen and missing states from :valid and :user-invalid
- :has() lifts the input state to the icon, badge and row
- CSS counter that counts attached documents
- Focus ring drawn by a sibling so it works without :has()
Accessibility
Each input has a visible label, the dropzone is described by its hint and its error line, and the browser's own validation message appears on a submit with a missing file. The native input announces the chosen file name itself, so the CSS badges and counter are marked aria-hidden rather than repeated. Focus rings are drawn on the box or on the whole row, and colour transitions switch off under prefers-reduced-motion.
Customise it
Change --mz-accent, --mz-ok and --mz-bad on .mz for the neutral, chosen and missing colours, and the badge wording in the ::before content rules. Bootstrap styles .form-control:valid without .was-validated; Tailwind uses valid:, user-invalid: and group-has-[input:valid]: variants.





