CSS Forms: 16 Examples and the Styles That Stick

CSS handles how a form looks and has no say in what it submits. Inputs, textareas, buttons, labels and fieldsets all accept full styling, and accent-color now recolors native checkboxes and radios in Chrome, Firefox and Safari.

Below are 16 CSS form examples you can try and copy, followed by a guide to layout, states, validation and the controls that still resist styling.

By Bogdan Sandu · Updated October 27, 2026

Form layouts with Grid and Flexbox

Four everyday forms built on Grid, Flexbox and container queries: a contact form, a shipping address, an inline newsletter signup and a search with filters. Every form here cancels its submit event, so you can type freely and nothing leaves the page.

Contact form with :user-invalid errors

  • container query
  • :user-invalid
  • :has()

Tell us about your project

Northwind Atelier replies within two working days.

Enter your first name.

Enter your last name.

Enter an email like name@company.com.

Pick a topic.

Write at least 20 characters.

The fields sit in a one-column grid that becomes two columns once the form itself is wider than 520px, so it fits a sidebar as well as a full page. Errors key off :user-invalid, so nothing turns red until you leave a field with a bad value or press Send. A short script mirrors the state into aria-invalid and aria-describedby, because a red border is not announced.

View code
.fo-contact {
  width: min(100%, 640px);
  container-type: inline-size;
}
.fo-contact__form {
  padding: clamp(20px, 5cqi, 36px);
  border-radius: 18px;
  background: #fff;
  color: #1d2433;
  box-shadow: 0 18px 40px rgba(29, 36, 51, .08);
}
.fo-contact__title {
  margin: 0;
  font-size: 22px;
  font-weight: 700;
  line-height: 1.25;
}
.fo-contact__sub {
  margin: 6px 0 22px;
  color: #535d70;
  font-size: 15px;
}
.fo-contact__grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 16px 18px;
}
@container (min-width: 520px) {
  .fo-contact__grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
  .fo-contact__full {
    grid-column: 1 / -1;
  }
}
.fo-contact__field {
  display: grid;
  gap: 6px;
}
.fo-contact__label {
  font-size: 14px;
  font-weight: 600;
}
.fo-contact__input {
  box-sizing: border-box;
  width: 100%;
  min-height: 46px;
  padding: 10px 12px;
  border: 1px solid #7b8394;
  border-radius: 8px;
  background: #fff;
  color: #1d2433;
  font: inherit;
  font-size: max(1rem, 16px);
  transition: border-color .15s, box-shadow .15s;
}
.fo-contact__area {
  min-height: 112px;
  resize: vertical;
}
.fo-contact__input:hover {
  border-color: #3d4556;
}
.fo-contact__input:focus-visible {
  outline: none;
  border-color: #3b5bdb;
  box-shadow: 0 0 0 3px rgba(59, 91, 219, .35);
}
.fo-contact__input:user-invalid {
  border-color: #b42318;
  background: #fff8f7;
}
.fo-contact__error {
  display: none;
  margin: 0;
  color: #b42318;
  font-size: 13.5px;
  font-weight: 600;
}
.fo-contact__field:has(:user-invalid) .fo-contact__error {
  display: block;
}
.fo-contact__check {
  display: flex;
  gap: 10px;
  align-items: flex-start;
  color: #3d4556;
  font-size: 14.5px;
  line-height: 1.5;
  cursor: pointer;
}
.fo-contact__check input {
  flex: none;
  width: 20px;
  height: 20px;
  margin: 1px 0 0;
  accent-color: #3b5bdb;
}
.fo-contact__check input:focus-visible {
  outline: 3px solid #3b5bdb;
  outline-offset: 2px;
}
.fo-contact__btn {
  min-height: 48px;
  margin-top: 22px;
  padding: 0 26px;
  border: 0;
  border-radius: 10px;
  background: #3b5bdb;
  color: #fff;
  font: inherit;
  font-weight: 700;
  cursor: pointer;
  transition: background-color .15s;
}
.fo-contact__btn:hover {
  background: #2f4ac0;
}
.fo-contact__btn:focus-visible {
  outline: 3px solid #1d2433;
  outline-offset: 3px;
}
.fo-contact__status {
  margin: 12px 0 0;
  color: #1f7a4d;
  font-size: 14px;
  font-weight: 600;
}
.fo-contact__status:empty {
  display: none;
}

Shipping address with autocomplete tokens

  • autocomplete
  • container query
  • grid-column span

Where should we send it?

Each field carries the autocomplete token that browsers use to fill addresses, shown in the small tag beside the label. That attribute is also how a form meets WCAG 1.3.5 Identify Input Purpose. The layout is a six-column grid inside a container query, so city, region and postal code share a row when there is room and stack when there is not.

View code
.fo-ship {
  width: min(100%, 660px);
  container-type: inline-size;
}
.fo-ship__form {
  padding: clamp(18px, 5cqi, 34px);
  border: 1px solid #e3dccf;
  border-radius: 14px;
  background: #fbf8f3;
  color: #1f2a24;
}
.fo-ship__title {
  margin: 0 0 20px;
  font-family: "Fraunces", Georgia, serif;
  font-size: 24px;
  font-weight: 600;
  line-height: 1.2;
}
.fo-ship__grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 14px;
}
@container (min-width: 500px) {
  .fo-ship__grid {
    grid-template-columns: repeat(6, minmax(0, 1fr));
  }
  .fo-ship__w6 { grid-column: span 6; }
  .fo-ship__w4 { grid-column: span 4; }
  .fo-ship__w3 { grid-column: span 3; }
  .fo-ship__w2 { grid-column: span 2; }
}
.fo-ship__field {
  display: grid;
  gap: 6px;
  min-width: 0;
}
.fo-ship__row {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 2px 8px;
  font-size: 14px;
  font-weight: 600;
}
.fo-ship__row code {
  padding: 1px 6px;
  border-radius: 4px;
  background: #e8efe9;
  color: #1f4d3a;
  font-family: ui-monospace, Consolas, monospace;
  font-size: 11.5px;
  font-weight: 500;
}
.fo-ship__input {
  box-sizing: border-box;
  width: 100%;
  min-height: 44px;
  padding: 9px 11px;
  border: 1px solid #8b8478;
  border-radius: 6px;
  background: #fff;
  color: #1f2a24;
  font: inherit;
  font-size: max(1rem, 16px);
}
.fo-ship__input:focus-visible {
  outline: 2px solid #1f4d3a;
  outline-offset: 2px;
  border-color: #1f4d3a;
}
.fo-ship__input:user-invalid {
  border-color: #b42318;
}
.fo-ship__btn {
  width: 100%;
  min-height: 50px;
  margin-top: 22px;
  border: 0;
  border-radius: 8px;
  background: #1f4d3a;
  color: #fff;
  font: inherit;
  font-weight: 700;
  cursor: pointer;
  transition: background-color .15s;
}
.fo-ship__btn:hover {
  background: #173b2c;
}
.fo-ship__btn:focus-visible {
  outline: 3px solid #1f4d3a;
  outline-offset: 3px;
}
.fo-ship__status {
  margin: 12px 0 0;
  color: #1f4d3a;
  font-size: 14px;
  font-weight: 600;
}
.fo-ship__status:empty {
  display: none;
}

Inline newsletter signup

  • flex-wrap
  • visually hidden label
  • :user-invalid

The Saturday Sketchbook

One short letter a week on drawing, type and the small habits that keep a studio going.

Sent every Saturday. Unsubscribe from any issue in one click.

The email field takes flex: 1 with a sensible flex-basis and the button keeps its natural width, so the pair sits on one row and wraps cleanly on a phone. The label is hidden with a clip-based class instead of display: none, so screen readers still name the field. After a valid submit the status line repeats the address back.

View code
.fo-news {
  width: min(100%, 560px);
  box-sizing: border-box;
  padding: clamp(22px, 6vw, 40px);
  border: 2px solid #2a1d14;
  border-radius: 4px;
  background: #fff6e9;
  color: #2a1d14;
  box-shadow: 8px 8px 0 #2a1d14;
}
.fo-news__title {
  margin: 0;
  font-family: "Fraunces", Georgia, serif;
  font-size: clamp(24px, 6vw, 32px);
  font-weight: 700;
  line-height: 1.1;
}
.fo-news__copy {
  margin: 10px 0 20px;
  color: #5a4636;
  font-size: 15.5px;
  line-height: 1.55;
}
.fo-news__form {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
}
.fo-news__sr {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
.fo-news__input {
  flex: 1 1 200px;
  box-sizing: border-box;
  min-width: 0;
  min-height: 50px;
  padding: 0 14px;
  border: 2px solid #2a1d14;
  border-radius: 4px;
  background: #fff;
  color: #2a1d14;
  font: inherit;
  font-size: max(1rem, 16px);
}
.fo-news__input::placeholder {
  color: #76624f;
  opacity: 1;
}
.fo-news__input:focus-visible {
  outline: 3px solid #c2410c;
  outline-offset: 2px;
}
.fo-news__input:user-invalid {
  border-color: #b42318;
}
.fo-news__btn {
  flex: 0 0 auto;
  min-height: 50px;
  padding: 0 22px;
  border: 2px solid #2a1d14;
  border-radius: 4px;
  background: #c2410c;
  color: #fff;
  font: inherit;
  font-weight: 700;
  cursor: pointer;
  transition: background-color .15s;
}
.fo-news__btn:hover {
  background: #9a3412;
}
.fo-news__btn:focus-visible {
  outline: 3px solid #2a1d14;
  outline-offset: 3px;
}
@media (max-width: 420px) {
  .fo-news__btn {
    flex: 1 1 100%;
  }
}
.fo-news__fine {
  margin: 12px 0 0;
  color: #5a4636;
  font-size: 13.5px;
}
.fo-news__status {
  margin: 12px 0 0;
  padding: 10px 12px;
  border-left: 4px solid #2a1d14;
  background: #ffe9cc;
  font-size: 14.5px;
  font-weight: 600;
  overflow-wrap: anywhere;
}
.fo-news__status:empty {
  display: none;
}

Search with filter chips

  • role="search"
  • checkbox chips
  • output count

One form holds the search box and every filter, so a real version would serialize cleanly into a query string. The type chips are ordinary checkboxes styled through :has(:checked) on their labels, the bedroom picker is a radio group, and the match count is a role="status" line that updates as you change filters.

View code
.fo-find {
  box-sizing: border-box;
  width: min(100%, 640px);
  padding: clamp(16px, 4vw, 26px);
  border-radius: 16px;
  background: #fff;
  color: #10282a;
  box-shadow: 0 12px 34px rgba(16, 40, 42, .1);
}
.fo-find__sr {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
.fo-find__bar {
  position: relative;
  display: flex;
  gap: 8px;
}
.fo-find__icon {
  position: absolute;
  top: 50%;
  left: 14px;
  width: 20px;
  height: 20px;
  color: #4b5f61;
  transform: translateY(-50%);
  pointer-events: none;
}
.fo-find__q {
  flex: 1;
  box-sizing: border-box;
  min-width: 0;
  min-height: 50px;
  padding: 0 12px 0 44px;
  border: 1px solid #788b8c;
  border-radius: 12px;
  background: #f6f9f9;
  color: #10282a;
  font: inherit;
  font-size: max(1rem, 16px);
}
.fo-find__q::placeholder {
  color: #5a6c6e;
  opacity: 1;
}
.fo-find__q:focus-visible,
.fo-find__select:focus-visible {
  outline: 3px solid #0f766e;
  outline-offset: 1px;
}
.fo-find__go {
  flex: none;
  min-height: 50px;
  padding: 0 18px;
  border: 0;
  border-radius: 12px;
  background: #0f766e;
  color: #fff;
  font: inherit;
  font-weight: 700;
  cursor: pointer;
}
.fo-find__go:hover {
  background: #0b5e58;
}
.fo-find__go:focus-visible,
.fo-find__reset:focus-visible {
  outline: 3px solid #10282a;
  outline-offset: 2px;
}
.fo-find__set {
  min-width: 0;
  margin: 18px 0 0;
  padding: 0;
  border: 0;
}
.fo-find__legend {
  display: block;
  margin-bottom: 8px;
  padding: 0;
  font-size: 13px;
  font-weight: 700;
  letter-spacing: .02em;
}
.fo-find__chips {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}
.fo-find__chip {
  position: relative;
  display: inline-flex;
  align-items: center;
  min-height: 38px;
  padding: 0 16px;
  border: 1px solid #788b8c;
  border-radius: 999px;
  font-size: 15px;
  cursor: pointer;
  transition: background-color .15s, color .15s, border-color .15s;
}
.fo-find__chip input,
.fo-find__seg input {
  position: absolute;
  opacity: 0;
  inset: 0;
  margin: 0;
  cursor: pointer;
}
.fo-find__chip:hover {
  border-color: #10282a;
}
.fo-find__chip:has(:checked) {
  border-color: #0f766e;
  background: #0f766e;
  color: #fff;
}
.fo-find__chip:has(:focus-visible) {
  outline: 3px solid #10282a;
  outline-offset: 2px;
}
.fo-find__row {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 0 24px;
}
.fo-find__seg {
  display: inline-flex;
  overflow: hidden;
  border: 1px solid #788b8c;
  border-radius: 10px;
}
.fo-find__seg label {
  position: relative;
}
.fo-find__seg span {
  display: grid;
  place-items: center;
  min-width: 52px;
  min-height: 40px;
  padding: 0 8px;
  font-size: 15px;
}
.fo-find__seg label + label span {
  border-left: 1px solid #c7d1d1;
}
.fo-find__seg input:checked + span {
  background: #d9efed;
  color: #0b4f4a;
  font-weight: 700;
}
.fo-find__seg input:focus-visible + span {
  outline: 3px solid #10282a;
  outline-offset: -3px;
}
.fo-find__sort {
  margin-top: 18px;
}
.fo-find__select {
  min-height: 42px;
  padding: 0 10px;
  border: 1px solid #788b8c;
  border-radius: 10px;
  background: #fff;
  color: #10282a;
  font: inherit;
  font-size: max(1rem, 16px);
}
.fo-find__foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  margin-top: 20px;
  padding-top: 16px;
  border-top: 1px solid #e1e8e8;
}
.fo-find__count {
  margin: 0;
  font-weight: 700;
}
.fo-find__reset {
  min-height: 36px;
  padding: 0 4px;
  border: 0;
  background: none;
  color: #0b5e58;
  font: inherit;
  font-weight: 600;
  text-decoration: underline;
  text-underline-offset: 3px;
  cursor: pointer;
}

Styled choice and file controls

Radios, checkboxes, switches and file inputs redrawn without losing the native element underneath. Keyboard, focus and checked behavior stay with the browser, and CSS only changes how they look.

Survey with radio and checkbox cards

  • fieldset + legend
  • appearance: none
  • :has(:checked)
How often do you plan meals ahead?
Which features would you use? Choose any

Each question is a fieldset with a legend, so a screen reader hears the question along with each answer. The inputs keep their native role and keyboard behavior; appearance: none only removes the default drawing so a border, an inset shadow and an SVG checkmark can take over. The whole card highlights through :has(:checked).

View code
.fo-survey {
  box-sizing: border-box;
  width: min(100%, 620px);
  padding: clamp(18px, 5vw, 32px);
  border-radius: 20px;
  background: #f5f3ff;
  color: #241a3d;
}
.fo-survey__q {
  min-width: 0;
  margin: 0 0 22px;
  padding: 0;
  border: 0;
}
.fo-survey__legend {
  margin-bottom: 12px;
  padding: 0;
  font-size: 18px;
  font-weight: 700;
  line-height: 1.3;
}
.fo-survey__legend span {
  margin-left: 6px;
  color: #5b4f78;
  font-size: 14px;
  font-weight: 500;
}
.fo-survey__opts {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 160px), 1fr));
  gap: 10px;
}
.fo-survey__card {
  display: flex;
  gap: 12px;
  align-items: flex-start;
  padding: 14px;
  border: 2px solid #d9d2f2;
  border-radius: 14px;
  background: #fff;
  cursor: pointer;
  transition: border-color .15s, background-color .15s;
}
.fo-survey__card:hover {
  border-color: #a99be0;
}
.fo-survey__card:has(:checked) {
  border-color: #6d28d9;
  background: #faf8ff;
}
.fo-survey__card span {
  display: grid;
  gap: 2px;
  font-size: 15px;
  line-height: 1.35;
}
.fo-survey__card small {
  color: #5b4f78;
  font-size: 13.5px;
}
.fo-survey__radio,
.fo-survey__box {
  flex: none;
  appearance: none;
  width: 22px;
  height: 22px;
  margin: 0;
  border: 2px solid #6f6390;
  background: #fff;
  cursor: pointer;
}
.fo-survey__radio {
  border-radius: 50%;
}
.fo-survey__radio:checked {
  border-color: #6d28d9;
  background: #6d28d9;
  box-shadow: inset 0 0 0 4px #fff;
}
.fo-survey__box {
  border-radius: 6px;
}
.fo-survey__box:checked {
  border-color: #6d28d9;
  background: #6d28d9 url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M5 12.5l4.5 4.5L19 7.5' fill='none' stroke='%23fff' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / 16px no-repeat;
}
.fo-survey__radio:focus-visible,
.fo-survey__box:focus-visible {
  outline: 3px solid #6d28d9;
  outline-offset: 3px;
}
.fo-survey__foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px 16px;
}
.fo-survey__btn {
  min-height: 48px;
  padding: 0 24px;
  border: 0;
  border-radius: 999px;
  background: #6d28d9;
  color: #fff;
  font: inherit;
  font-weight: 700;
  cursor: pointer;
}
.fo-survey__btn:hover {
  background: #5b21b6;
}
.fo-survey__btn:focus-visible {
  outline: 3px solid #241a3d;
  outline-offset: 3px;
}
.fo-survey__status {
  margin: 0;
  color: #4c1d95;
  font-size: 14px;
  font-weight: 600;
}

Accessible star rating

  • radio group
  • :has() sibling logic
  • aria-live text

How was your stay at Harbor Lane Inn?

Overall rating

Pick a rating

The stars are five radio buttons in a fieldset, kept in source order so the arrow keys move from one star to five. Filling every star up to the chosen one uses :has() with a general sibling combinator instead of the old reversed-order trick, which used to flip the keyboard direction. Each label carries hidden text such as 4 stars, and a status line spells the choice out.

View code
.fo-rate {
  box-sizing: border-box;
  width: min(100%, 460px);
  padding: clamp(20px, 5vw, 32px);
  border-radius: 18px;
  background: #fff;
  color: #1c1917;
  box-shadow: 0 16px 36px rgba(28, 25, 23, .1);
}
.fo-rate__title {
  margin: 0 0 12px;
  font-family: "Fraunces", Georgia, serif;
  font-size: 22px;
  font-weight: 600;
  line-height: 1.25;
}
.fo-rate__sr {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
.fo-rate__set {
  margin: 0;
  padding: 0;
  border: 0;
}
.fo-rate__stars {
  display: flex;
  gap: 4px;
}
.fo-rate__star {
  position: relative;
}
.fo-rate__star label {
  display: grid;
  place-items: center;
  width: 48px;
  height: 48px;
  border-radius: 10px;
  cursor: pointer;
}
.fo-rate__star svg {
  width: 38px;
  height: 38px;
  fill: transparent;
  stroke: #6b6560;
  stroke-width: 1.6;
  stroke-linejoin: round;
  transition: fill .12s, stroke .12s;
}
.fo-rate__star:has(input:checked) svg,
.fo-rate__star:has(~ .fo-rate__star input:checked) svg {
  fill: #f59e0b;
  stroke: #92400e;
}
.fo-rate__stars:hover .fo-rate__star svg {
  fill: transparent;
  stroke: #6b6560;
}
.fo-rate__stars:hover .fo-rate__star:hover svg,
.fo-rate__stars:hover .fo-rate__star:has(~ .fo-rate__star:hover) svg {
  fill: #fcd34d;
  stroke: #92400e;
}
.fo-rate__star:has(input:focus-visible) label {
  outline: 3px solid #1c1917;
  outline-offset: -2px;
}
@media (prefers-reduced-motion: no-preference) {
  .fo-rate__star label {
    transition: transform .15s ease;
  }
  .fo-rate__star label:active {
    transform: scale(.88);
  }
}
.fo-rate__word {
  min-height: 22px;
  margin: 8px 0 18px;
  color: #57534e;
  font-size: 15px;
  font-weight: 600;
}
.fo-rate__label {
  display: block;
  margin-bottom: 6px;
  font-size: 14px;
  font-weight: 600;
}
.fo-rate__label span {
  color: #57534e;
  font-weight: 400;
}
.fo-rate__note {
  box-sizing: border-box;
  width: 100%;
  min-height: 90px;
  padding: 10px 12px;
  border: 1px solid #857e78;
  border-radius: 10px;
  color: #1c1917;
  font: inherit;
  font-size: max(1rem, 16px);
  resize: vertical;
}
.fo-rate__note:focus-visible {
  outline: 3px solid #b45309;
  outline-offset: 1px;
}
.fo-rate__btn {
  width: 100%;
  min-height: 48px;
  margin-top: 16px;
  border: 0;
  border-radius: 10px;
  background: #1c1917;
  color: #fff;
  font: inherit;
  font-weight: 700;
  cursor: pointer;
}
.fo-rate__btn:hover {
  background: #44403c;
}
.fo-rate__btn:focus-visible {
  outline: 3px solid #b45309;
  outline-offset: 3px;
}
.fo-rate__status {
  margin: 10px 0 0;
  font-size: 14px;
  font-weight: 600;
}
.fo-rate__status:empty {
  display: none;
}

Settings panel with switches

  • role="switch"
  • visually hidden checkbox
  • aria-describedby

Notifications

When someone tags you in a task or comment.
A Monday summary of what moved in your projects.
Add a phone number in Profile to turn this on.

Each switch is a real checkbox with role="switch", hidden with a clip class and drawn by the span after it, so Space toggles it and screen readers announce on or off. The thumb slides only when motion is allowed. Descriptions are tied to their switch with aria-describedby, and the disabled row stays readable even though it is exempt from contrast rules.

View code
.fo-set {
  box-sizing: border-box;
  width: min(100%, 520px);
  border: 1px solid #dcdfe4;
  border-radius: 14px;
  background: #fff;
  color: #17191c;
}
.fo-set__title {
  margin: 0;
  padding: 18px 20px;
  border-bottom: 1px solid #e6e8eb;
  font-size: 17px;
  font-weight: 700;
}
.fo-set__row {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 16px 20px;
  border-bottom: 1px solid #eef0f2;
}
.fo-set__text {
  display: grid;
  gap: 3px;
  font-size: 15px;
  font-weight: 600;
}
.fo-set__text span {
  color: #5c6370;
  font-size: 13.5px;
  font-weight: 400;
  line-height: 1.45;
}
.fo-set__text label {
  cursor: pointer;
}
.fo-set__input {
  position: absolute;
  top: 50%;
  right: 20px;
  width: 48px;
  height: 28px;
  margin: -14px 0 0;
  opacity: 0;
  cursor: pointer;
  z-index: 1;
}
.fo-set__track {
  position: relative;
  flex: none;
  width: 48px;
  height: 28px;
  box-sizing: border-box;
  border-radius: 999px;
  background: #6b7280;
}
.fo-set__track::after {
  content: "";
  position: absolute;
  top: 3px;
  left: 3px;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background: #fff;
  box-shadow: 0 1px 3px rgba(0, 0, 0, .3);
}
.fo-set__input:checked + .fo-set__track {
  background: #15803d;
}
.fo-set__input:checked + .fo-set__track::after {
  transform: translateX(20px);
}
.fo-set__input:focus-visible + .fo-set__track {
  outline: 3px solid #17191c;
  outline-offset: 3px;
}
.fo-set__input:disabled {
  cursor: not-allowed;
}
.fo-set__input:disabled + .fo-set__track {
  background: #d1d5db;
}
.fo-set__row:has(:disabled) .fo-set__text {
  color: #5c6370;
}
@media (prefers-reduced-motion: no-preference) {
  .fo-set__track,
  .fo-set__track::after {
    transition: transform .2s ease, background-color .2s;
  }
}
.fo-set__row--select {
  flex-wrap: wrap;
}
.fo-set__select {
  min-height: 40px;
  padding: 0 10px;
  border: 1px solid #858b96;
  border-radius: 8px;
  background: #fff;
  color: #17191c;
  font: inherit;
  font-size: max(1rem, 16px);
}
.fo-set__select:focus-visible {
  outline: 3px solid #15803d;
  outline-offset: 1px;
}
.fo-set__foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: flex-end;
  gap: 10px 16px;
  padding: 16px 20px;
  background: #f7f8f9;
  border-radius: 0 0 14px 14px;
}
.fo-set__status {
  margin: 0;
  color: #5c6370;
  font-size: 14px;
}
.fo-set__btn {
  min-height: 44px;
  padding: 0 20px;
  border: 0;
  border-radius: 8px;
  background: #17191c;
  color: #fff;
  font: inherit;
  font-weight: 700;
  cursor: pointer;
}
.fo-set__btn:hover {
  background: #353a41;
}
.fo-set__btn:focus-visible {
  outline: 3px solid #15803d;
  outline-offset: 3px;
}

Job application with a styled file input

  • ::file-selector-button
  • accept
  • File API

Apply: Senior Product Designer

Kestrel Labs, remote within Europe

PDF or DOCX, up to 5 MB each.

    The native file input stays visible and focusable; ::file-selector-button restyles its button, with font and color set explicitly because they do not reliably inherit. The dashed drop area highlights through :has(:focus-visible). After you pick files, a short script lists their names and sizes with textContent and flags anything over 5 MB.

    View code
    .fo-job {
      box-sizing: border-box;
      width: min(100%, 600px);
      padding: clamp(18px, 5vw, 32px);
      border-radius: 12px;
      background: #fff;
      color: #111827;
      box-shadow: 0 1px 2px rgba(17, 24, 39, .08), 0 14px 32px rgba(17, 24, 39, .08);
    }
    .fo-job__title {
      margin: 0;
      font-size: 21px;
      font-weight: 700;
    }
    .fo-job__meta {
      margin: 4px 0 20px;
      color: #4b5563;
      font-size: 14.5px;
    }
    .fo-job__two {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(min(100%, 200px), 1fr));
      gap: 0 14px;
    }
    .fo-job__field {
      display: grid;
      gap: 6px;
      margin-bottom: 14px;
      font-size: 14px;
      font-weight: 600;
    }
    .fo-job__input {
      box-sizing: border-box;
      width: 100%;
      min-height: 44px;
      padding: 0 12px;
      border: 1px solid #7c8492;
      border-radius: 8px;
      color: #111827;
      font: inherit;
      font-size: max(1rem, 16px);
      font-weight: 400;
    }
    .fo-job__input::placeholder {
      color: #5b6472;
      opacity: 1;
    }
    .fo-job__input:focus-visible {
      outline: 3px solid #1d4ed8;
      outline-offset: 1px;
    }
    .fo-job__drop {
      padding: 18px;
      border: 2px dashed #7c8492;
      border-radius: 12px;
      background: #f8fafc;
      transition: border-color .15s, background-color .15s;
    }
    .fo-job__drop:has(:focus-visible),
    .fo-job__drop:hover {
      border-color: #1d4ed8;
      background: #eff4ff;
    }
    .fo-job__drop-label {
      display: block;
      font-size: 15px;
      font-weight: 700;
    }
    .fo-job__hint {
      margin: 2px 0 12px;
      color: #4b5563;
      font-size: 13.5px;
    }
    .fo-job__file {
      max-width: 100%;
      color: #374151;
      font: inherit;
      font-size: 14px;
    }
    .fo-job__file::file-selector-button {
      min-height: 40px;
      margin-right: 12px;
      padding: 0 16px;
      border: 1px solid #1d4ed8;
      border-radius: 8px;
      background: #fff;
      color: #1d4ed8;
      font-family: inherit;
      font-size: 14px;
      font-weight: 700;
      cursor: pointer;
      transition: background-color .15s, color .15s;
    }
    .fo-job__file::file-selector-button:hover {
      background: #1d4ed8;
      color: #fff;
    }
    .fo-job__file:focus-visible {
      outline: 3px solid #1d4ed8;
      outline-offset: 3px;
      border-radius: 8px;
    }
    .fo-job__files {
      display: grid;
      gap: 6px;
      margin: 12px 0 0;
      padding: 0;
      list-style: none;
    }
    .fo-job__files:empty {
      display: none;
    }
    .fo-job__files li {
      display: flex;
      justify-content: space-between;
      gap: 12px;
      padding: 8px 12px;
      border-radius: 8px;
      background: #fff;
      border: 1px solid #d6dae1;
      font-size: 14px;
    }
    .fo-job__files li span:first-child {
      min-width: 0;
      overflow-wrap: anywhere;
    }
    .fo-job__files li span:last-child {
      flex: none;
      color: #4b5563;
    }
    .fo-job__files .fo-job__bad {
      border-color: #b42318;
      color: #b42318;
    }
    .fo-job__files .fo-job__bad span:last-child {
      color: #b42318;
      font-weight: 700;
    }
    .fo-job__btn {
      width: 100%;
      min-height: 48px;
      margin-top: 18px;
      border: 0;
      border-radius: 8px;
      background: #1d4ed8;
      color: #fff;
      font: inherit;
      font-weight: 700;
      cursor: pointer;
    }
    .fo-job__btn:hover {
      background: #1e40af;
    }
    .fo-job__btn:focus-visible {
      outline: 3px solid #111827;
      outline-offset: 3px;
    }
    .fo-job__status {
      margin: 10px 0 0;
      font-size: 14px;
      font-weight: 600;
    }
    .fo-job__status:empty {
      display: none;
    }

    Multi-step, booking and payment forms

    Longer flows where structure matters as much as styling: a checkout split into steps, a table booking with date and time, a payment layout with the right keyboard hints, and an error summary that moves focus.

    Multi-step checkout with progress

    • fieldset per step
    • aria-current="step"
    • reportValidity()
    1. Contact
    2. Delivery
    3. Review
    Contact details

    Each step is a fieldset, and the ones you are not on carry the hidden attribute, so they leave the tab order and the accessibility tree. Next checks only the current step with reportValidity(), then moves focus to the new step so keyboard and screen reader users land in the right place. The progress list marks the active step with aria-current="step", and CSS styles that attribute directly.

    View code
    .fo-steps {
      box-sizing: border-box;
      width: min(100%, 520px);
      padding: clamp(18px, 5vw, 30px);
      border-radius: 16px;
      background: #fff;
      color: #0f172a;
      box-shadow: 0 14px 34px rgba(15, 23, 42, .1);
    }
    .fo-steps__track {
      display: grid;
      grid-template-columns: repeat(3, 1fr);
      margin: 0 0 24px;
      padding: 0;
      list-style: none;
      counter-reset: fo-step;
    }
    .fo-steps__track li {
      position: relative;
      display: grid;
      justify-items: center;
      gap: 6px;
      color: #475569;
      font-size: 13.5px;
      font-weight: 600;
      text-align: center;
      counter-increment: fo-step;
    }
    .fo-steps__track li::before {
      content: counter(fo-step);
      position: relative;
      z-index: 1;
      display: grid;
      place-items: center;
      width: 30px;
      height: 30px;
      border: 2px solid #94a3b8;
      border-radius: 50%;
      background: #fff;
      font-size: 13px;
    }
    .fo-steps__track li + li::after {
      content: "";
      position: absolute;
      top: 14px;
      right: 50%;
      width: 100%;
      height: 2px;
      background: #cbd5e1;
    }
    .fo-steps__track li[aria-current="step"] {
      color: #0b6bcb;
    }
    .fo-steps__track li[aria-current="step"]::before {
      border-color: #0b6bcb;
      background: #0b6bcb;
      color: #fff;
    }
    .fo-steps__track li.fo-steps__done::before {
      content: "\2713";
      border-color: #0b6bcb;
      color: #0b6bcb;
    }
    .fo-steps__track li.fo-steps__done + li::after,
    .fo-steps__track li.fo-steps__done + li[aria-current="step"]::after {
      background: #0b6bcb;
    }
    .fo-steps__step {
      min-width: 0;
      margin: 0;
      padding: 0;
      border: 0;
    }
    .fo-steps__step:focus {
      outline: none;
    }
    .fo-steps__step:not([hidden]) {
      display: grid;
      gap: 8px;
    }
    @media (prefers-reduced-motion: no-preference) {
      .fo-steps__step:not([hidden]) {
        animation: fo-step-in .25s ease;
      }
    }
    @keyframes fo-step-in {
      from {
        opacity: 0;
        transform: translateX(12px);
      }
    }
    .fo-steps__legend {
      margin-bottom: 6px;
      padding: 0;
      font-size: 19px;
      font-weight: 700;
    }
    .fo-steps__label {
      margin-top: 6px;
      font-size: 14px;
      font-weight: 600;
    }
    .fo-steps__input {
      box-sizing: border-box;
      width: 100%;
      min-height: 46px;
      padding: 0 12px;
      border: 1px solid #7a8699;
      border-radius: 10px;
      color: #0f172a;
      font: inherit;
      font-size: max(1rem, 16px);
    }
    .fo-steps__input:focus-visible {
      outline: 3px solid #0b6bcb;
      outline-offset: 1px;
    }
    .fo-steps__input:user-invalid {
      border-color: #b42318;
    }
    .fo-steps__option {
      display: flex;
      align-items: center;
      gap: 12px;
      min-height: 56px;
      padding: 0 16px;
      border: 1px solid #cbd5e1;
      border-radius: 12px;
      font-size: 15px;
      cursor: pointer;
    }
    .fo-steps__option:has(:checked) {
      border-color: #0b6bcb;
      box-shadow: inset 0 0 0 1px #0b6bcb;
      background: #f0f7ff;
    }
    .fo-steps__option input {
      width: 20px;
      height: 20px;
      margin: 0;
      accent-color: #0b6bcb;
    }
    .fo-steps__option input:focus-visible {
      outline: 3px solid #0b6bcb;
      outline-offset: 2px;
    }
    .fo-steps__option span {
      flex: 1;
    }
    .fo-steps__review {
      display: grid;
      grid-template-columns: max-content minmax(0, 1fr);
      gap: 10px 18px;
      margin: 0;
      padding: 16px;
      border-radius: 12px;
      background: #f1f5f9;
      font-size: 15px;
    }
    .fo-steps__review dt {
      color: #475569;
    }
    .fo-steps__review dd {
      margin: 0;
      font-weight: 600;
      overflow-wrap: anywhere;
    }
    .fo-steps__nav {
      display: flex;
      gap: 10px;
      margin-top: 22px;
    }
    .fo-steps__next,
    .fo-steps__place,
    .fo-steps__back {
      min-height: 48px;
      padding: 0 22px;
      border-radius: 10px;
      font: inherit;
      font-weight: 700;
      cursor: pointer;
    }
    .fo-steps__next,
    .fo-steps__place {
      flex: 1;
      border: 0;
      background: #0b6bcb;
      color: #fff;
    }
    .fo-steps__next:hover,
    .fo-steps__place:hover {
      background: #0956a3;
    }
    .fo-steps__back {
      border: 1px solid #7a8699;
      background: #fff;
      color: #0f172a;
    }
    .fo-steps__next:focus-visible,
    .fo-steps__place:focus-visible,
    .fo-steps__back:focus-visible {
      outline: 3px solid #0f172a;
      outline-offset: 3px;
    }
    .fo-steps__next[hidden],
    .fo-steps__place[hidden],
    .fo-steps__back[hidden] {
      display: none;
    }
    .fo-steps__status {
      margin: 10px 0 0;
      font-size: 14px;
      font-weight: 600;
    }
    .fo-steps__status:empty {
      display: none;
    }

    Table booking with date and time slots

    • input type="date"
    • radio time slots
    • stepper buttons

    Book a table at Maison Ardoise

    Time

    Table for 2 at 19:30. Pick a date.

    The date uses the native picker, which CSS cannot style beyond the field box, and a script sets its min to today so past dates are off the table. Time slots are radios styled as pills, and the full slot is a disabled radio with hidden text saying so. The guest stepper wraps a number input with two labelled buttons, and the summary updates as you choose.

    View code
    .fo-book {
      box-sizing: border-box;
      width: min(100%, 520px);
      padding: clamp(18px, 5vw, 32px);
      border-radius: 6px;
      background: #fbf6ef;
      color: #2b1714;
      border-top: 6px solid #7f1d1d;
      box-shadow: 0 14px 30px rgba(43, 23, 20, .12);
    }
    .fo-book__title {
      margin: 0 0 18px;
      font-family: "Fraunces", Georgia, serif;
      font-size: 23px;
      font-weight: 600;
      line-height: 1.2;
    }
    .fo-book__sr {
      position: absolute;
      width: 1px;
      height: 1px;
      overflow: hidden;
      clip-path: inset(50%);
      white-space: nowrap;
    }
    .fo-book__top {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(min(100%, 170px), 1fr));
      gap: 14px;
    }
    .fo-book__field {
      display: grid;
      gap: 6px;
      font-size: 14px;
      font-weight: 600;
    }
    .fo-book__input,
    .fo-book__num {
      box-sizing: border-box;
      min-height: 46px;
      border: 1px solid #8c7a72;
      border-radius: 6px;
      background: #fff;
      color: #2b1714;
      font: inherit;
      font-size: max(1rem, 16px);
      font-weight: 400;
      color-scheme: light;
    }
    .fo-book__input {
      width: 100%;
      padding: 0 10px;
    }
    .fo-book__input:focus-visible,
    .fo-book__num:focus-visible {
      outline: 3px solid #7f1d1d;
      outline-offset: 1px;
    }
    .fo-book__stepper {
      display: flex;
    }
    .fo-book__num {
      width: 100%;
      min-width: 0;
      border-radius: 0;
      border-left: 0;
      border-right: 0;
      text-align: center;
      appearance: textfield;
    }
    .fo-book__num::-webkit-inner-spin-button,
    .fo-book__num::-webkit-outer-spin-button {
      appearance: none;
      margin: 0;
    }
    .fo-book__step {
      flex: none;
      width: 46px;
      min-height: 46px;
      border: 1px solid #8c7a72;
      background: #fff;
      color: #7f1d1d;
      font: inherit;
      font-size: 22px;
      line-height: 1;
      cursor: pointer;
    }
    .fo-book__step:first-child {
      border-radius: 6px 0 0 6px;
    }
    .fo-book__step:last-child {
      border-radius: 0 6px 6px 0;
    }
    .fo-book__step:hover {
      background: #f5e6e1;
    }
    .fo-book__step:focus-visible {
      outline: 3px solid #7f1d1d;
      outline-offset: 1px;
      position: relative;
    }
    .fo-book__slots {
      min-width: 0;
      margin: 20px 0 0;
      padding: 0;
      border: 0;
    }
    .fo-book__slots legend {
      margin-bottom: 8px;
      padding: 0;
      font-size: 14px;
      font-weight: 600;
    }
    .fo-book__grid {
      display: grid;
      grid-template-columns: repeat(auto-fill, minmax(78px, 1fr));
      gap: 8px;
    }
    .fo-book__slot {
      position: relative;
    }
    .fo-book__slot input {
      position: absolute;
      opacity: 0;
      margin: 0;
    }
    .fo-book__slot label {
      display: grid;
      place-items: center;
      min-height: 44px;
      border: 1px solid #8c7a72;
      border-radius: 999px;
      background: #fff;
      font-size: 15px;
      font-variant-numeric: tabular-nums;
      cursor: pointer;
      transition: background-color .15s, color .15s;
    }
    .fo-book__slot label:hover {
      border-color: #7f1d1d;
    }
    .fo-book__slot input:checked + label {
      border-color: #7f1d1d;
      background: #7f1d1d;
      color: #fff;
      font-weight: 700;
    }
    .fo-book__slot input:focus-visible + label {
      outline: 3px solid #2b1714;
      outline-offset: 2px;
    }
    .fo-book__slot input:disabled + label {
      border-style: dashed;
      background: transparent;
      color: #6f5e57;
      text-decoration: line-through;
      cursor: not-allowed;
    }
    .fo-book__summary {
      margin: 18px 0 0;
      padding: 12px 14px;
      border-radius: 6px;
      background: #f1e4da;
      font-size: 15px;
      font-weight: 600;
    }
    .fo-book__btn {
      width: 100%;
      min-height: 50px;
      margin-top: 14px;
      border: 0;
      border-radius: 6px;
      background: #7f1d1d;
      color: #fff;
      font: inherit;
      font-weight: 700;
      cursor: pointer;
    }
    .fo-book__btn:hover {
      background: #651515;
    }
    .fo-book__btn:focus-visible {
      outline: 3px solid #2b1714;
      outline-offset: 3px;
    }
    .fo-book__status {
      margin: 10px 0 0;
      font-size: 14px;
      font-weight: 600;
    }
    .fo-book__status:empty {
      display: none;
    }

    Payment form with cc-* autocomplete

    • inputmode="numeric"
    • autocomplete cc-*
    • live card preview

    Pay Lumen Print Studio $48.00

    The card fields use type="text" with inputmode="numeric", which brings up a number keypad without the spinners and rounding of type="number". The cc-name, cc-number, cc-exp and cc-csc tokens let the browser offer a saved card. A small script groups the digits in fours and mirrors them, masked, on the card preview. Nothing is charged or sent.

    View code
    .fo-pay {
      box-sizing: border-box;
      width: min(100%, 420px);
      padding: clamp(18px, 5vw, 28px);
      border-radius: 20px;
      background: #fff;
      color: #14213d;
      box-shadow: 0 18px 44px rgba(20, 33, 61, .14);
    }
    .fo-pay__card {
      display: grid;
      gap: 18px;
      aspect-ratio: 1.586;
      max-height: 210px;
      box-sizing: border-box;
      padding: 20px;
      border-radius: 14px;
      background: #14213d;
      color: #f5f1e6;
    }
    .fo-pay__chip {
      width: 40px;
      height: 30px;
      border-radius: 6px;
      background: #d9b24c;
    }
    .fo-pay__digits {
      align-self: end;
      font-family: ui-monospace, Consolas, monospace;
      font-size: clamp(16px, 5vw, 20px);
      letter-spacing: .06em;
      white-space: nowrap;
    }
    .fo-pay__row {
      display: flex;
      justify-content: space-between;
      gap: 12px;
      font-size: 13px;
      text-transform: uppercase;
      letter-spacing: .06em;
    }
    .fo-pay__holder {
      min-width: 0;
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
    }
    .fo-pay__title {
      margin: 20px 0 14px;
      font-size: 16px;
    }
    .fo-pay__field {
      display: grid;
      gap: 6px;
      margin-bottom: 14px;
      font-size: 14px;
      font-weight: 600;
    }
    .fo-pay__split {
      display: grid;
      grid-template-columns: repeat(2, minmax(0, 1fr));
      gap: 12px;
    }
    .fo-pay__input {
      box-sizing: border-box;
      width: 100%;
      min-height: 46px;
      padding: 0 12px;
      border: 1px solid #7d8597;
      border-radius: 10px;
      color: #14213d;
      font: inherit;
      font-size: max(1rem, 16px);
      font-weight: 400;
    }
    .fo-pay__mono {
      font-family: ui-monospace, Consolas, monospace;
      letter-spacing: .04em;
    }
    .fo-pay__input::placeholder {
      color: #5f6779;
      opacity: 1;
    }
    .fo-pay__input:focus-visible {
      outline: none;
      border-color: #14213d;
      box-shadow: 0 0 0 3px #f2b705;
    }
    .fo-pay__input:user-invalid {
      border-color: #b42318;
    }
    .fo-pay__btn {
      width: 100%;
      min-height: 50px;
      margin-top: 4px;
      border: 0;
      border-radius: 10px;
      background: #14213d;
      color: #fff;
      font: inherit;
      font-weight: 700;
      cursor: pointer;
    }
    .fo-pay__btn:hover {
      background: #23345c;
    }
    .fo-pay__btn:focus-visible {
      outline: 3px solid #f2b705;
      outline-offset: 3px;
    }
    .fo-pay__status {
      margin: 10px 0 0;
      font-size: 14px;
      font-weight: 600;
    }
    .fo-pay__status:empty {
      display: none;
    }

    Error summary with focus management

    • Constraint Validation API
    • focus()
    • aria-describedby

    Register for Field Notes Live

    The form uses novalidate, so the browser bubbles stay quiet and the script reads each field validity state instead. That lets every message name the actual problem, such as a missing value, a wrong email format or too many guests. On submit, a summary listing every error appears above the form and takes focus, and each link in it moves focus to its field.

    View code
    .fo-sum {
      box-sizing: border-box;
      width: min(100%, 520px);
      padding: clamp(18px, 5vw, 32px);
      background: #fff;
      color: #0b0c0c;
      border: 1px solid #b1b4b6;
    }
    .fo-sum__title {
      margin: 0 0 20px;
      font-size: 24px;
      font-weight: 700;
      line-height: 1.2;
    }
    .fo-sum__box {
      margin-bottom: 22px;
      padding: 16px 18px;
      border: 4px solid #b42318;
    }
    .fo-sum__box:focus {
      outline: 3px solid #f5b800;
      outline-offset: 0;
    }
    .fo-sum__box[hidden] {
      display: none;
    }
    .fo-sum__box-title {
      margin: 0 0 8px;
      font-size: 18px;
      font-weight: 700;
    }
    .fo-sum__list {
      margin: 0;
      padding-left: 20px;
    }
    .fo-sum__list li + li {
      margin-top: 6px;
    }
    .fo-sum__list a {
      color: #b42318;
      font-weight: 700;
      text-underline-offset: 3px;
    }
    .fo-sum__list a:focus-visible {
      outline: 3px solid #f5b800;
      outline-offset: 0;
      background: #f5b800;
      color: #0b0c0c;
      text-decoration: none;
    }
    .fo-sum__field {
      margin-bottom: 18px;
    }
    .fo-sum__field:has([aria-invalid="true"]) {
      padding-left: 14px;
      border-left: 4px solid #b42318;
    }
    .fo-sum__label {
      display: block;
      margin-bottom: 6px;
      font-size: 16px;
      font-weight: 700;
    }
    .fo-sum__error {
      margin: 0 0 6px;
      color: #b42318;
      font-size: 15px;
      font-weight: 700;
    }
    .fo-sum__error[hidden] {
      display: none;
    }
    .fo-sum__input {
      box-sizing: border-box;
      width: 100%;
      min-height: 44px;
      padding: 0 10px;
      border: 2px solid #0b0c0c;
      border-radius: 0;
      background: #fff;
      color: #0b0c0c;
      font: inherit;
      font-size: max(1rem, 16px);
    }
    .fo-sum__short {
      max-width: 8rem;
    }
    .fo-sum__input[aria-invalid="true"] {
      border-color: #b42318;
    }
    .fo-sum__input:focus-visible {
      outline: 3px solid #f5b800;
      outline-offset: 0;
      box-shadow: inset 0 0 0 2px #0b0c0c;
    }
    .fo-sum__btn {
      min-height: 46px;
      padding: 0 22px;
      border: 0;
      border-radius: 0;
      background: #1d6b3a;
      color: #fff;
      font: inherit;
      font-weight: 700;
      box-shadow: 0 2px 0 #0b3a1f;
      cursor: pointer;
    }
    .fo-sum__btn:hover {
      background: #165a30;
    }
    .fo-sum__btn:focus-visible {
      outline: 3px solid #f5b800;
      outline-offset: 0;
      background: #f5b800;
      color: #0b0c0c;
      box-shadow: 0 2px 0 #0b0c0c;
    }
    .fo-sum__status {
      margin: 12px 0 0;
      font-size: 15px;
      font-weight: 600;
    }
    .fo-sum__status:empty {
      display: none;
    }

    Field details and themes

    Smaller techniques that make a form feel finished: a textarea that grows with its content, floating labels, a dark theme driven by accent-color, and help text that screen readers announce with the field.

    Support ticket with an auto-growing textarea

    • field-sizing: content
    • @supports
    • segmented radios

    Open a support ticket

    Priority

    0 of 800 characters

    Inside @supports (field-sizing: content), the description box grows line by line as you type, with a min-height so it never collapses and a max-height so a long paste cannot push the button away. Browsers without the property keep a fixed, vertically resizable box. The character count is tied to the field with aria-describedby instead of a live region, so it does not interrupt every keystroke.

    View code
    .fo-ticket {
      box-sizing: border-box;
      width: min(100%, 540px);
      padding: clamp(18px, 5vw, 30px);
      border-radius: 14px;
      background: #f8fafc;
      color: #1e1b4b;
      border: 1px solid #dfe3f0;
    }
    .fo-ticket__title {
      margin: 0 0 18px;
      font-size: 20px;
      font-weight: 700;
    }
    .fo-ticket__label {
      display: block;
      margin: 0 0 6px;
      padding: 0;
      font-size: 14px;
      font-weight: 600;
    }
    .fo-ticket__input,
    .fo-ticket__body {
      box-sizing: border-box;
      width: 100%;
      padding: 10px 12px;
      border: 1px solid #7c809a;
      border-radius: 10px;
      background: #fff;
      color: #1e1b4b;
      font: inherit;
      font-size: max(1rem, 16px);
      line-height: 1.5;
    }
    .fo-ticket__input {
      min-height: 46px;
    }
    .fo-ticket__input::placeholder,
    .fo-ticket__body::placeholder {
      color: #5d6078;
      opacity: 1;
    }
    .fo-ticket__input:focus-visible,
    .fo-ticket__body:focus-visible {
      outline: none;
      border-color: #4338ca;
      box-shadow: 0 0 0 3px rgba(67, 56, 202, .35);
    }
    .fo-ticket__body {
      min-height: 110px;
      max-height: 60vh;
      resize: vertical;
    }
    @supports (field-sizing: content) {
      .fo-ticket__body {
        field-sizing: content;
        min-height: calc(3lh + 22px);
        max-height: min(60vh, 320px);
        resize: none;
      }
    }
    .fo-ticket__prio {
      min-width: 0;
      margin: 18px 0;
      padding: 0;
      border: 0;
    }
    .fo-ticket__seg {
      display: grid;
      grid-template-columns: repeat(3, 1fr);
      padding: 4px;
      border-radius: 12px;
      background: #e5e7f3;
    }
    .fo-ticket__seg label {
      position: relative;
    }
    .fo-ticket__seg input {
      position: absolute;
      inset: 0;
      opacity: 0;
      margin: 0;
      cursor: pointer;
    }
    .fo-ticket__seg span {
      display: grid;
      place-items: center;
      min-height: 38px;
      border-radius: 9px;
      font-size: 15px;
      font-weight: 600;
      color: #3f3d64;
      transition: background-color .15s, color .15s;
    }
    .fo-ticket__seg input:checked + span {
      background: #fff;
      color: #1e1b4b;
      box-shadow: 0 1px 3px rgba(30, 27, 75, .2);
    }
    .fo-ticket__seg input[value="urgent"]:checked + span {
      background: #b42318;
      color: #fff;
    }
    .fo-ticket__seg input:focus-visible + span {
      outline: 3px solid #4338ca;
      outline-offset: 1px;
    }
    .fo-ticket__count {
      margin: 6px 0 0;
      color: #4a4d66;
      font-size: 13px;
      text-align: right;
      font-variant-numeric: tabular-nums;
    }
    .fo-ticket__btn {
      min-height: 48px;
      margin-top: 14px;
      padding: 0 22px;
      border: 0;
      border-radius: 10px;
      background: #4338ca;
      color: #fff;
      font: inherit;
      font-weight: 700;
      cursor: pointer;
    }
    .fo-ticket__btn:hover {
      background: #3730a3;
    }
    .fo-ticket__btn:focus-visible {
      outline: 3px solid #1e1b4b;
      outline-offset: 3px;
    }
    .fo-ticket__status {
      margin: 10px 0 0;
      font-size: 14px;
      font-weight: 600;
    }
    .fo-ticket__status:empty {
      display: none;
    }

    Outlined fields with floating labels

    • :placeholder-shown
    • label after input
    • transform

    Request a garden design quote

    Each input has a single-space placeholder, so :placeholder-shown tells CSS whether it is empty. The real label sits after the input, starts inside the field and moves up onto the border when the field has focus or a value, with a background patch that cuts the outline. The select label stays up because a select always shows a value.

    View code
    .fo-float {
      box-sizing: border-box;
      width: min(100%, 460px);
      padding: clamp(20px, 5vw, 34px);
      border-radius: 24px;
      background: #fcfdf9;
      color: #14321f;
      box-shadow: 0 0 0 1px #d8e4d7, 0 18px 40px rgba(20, 50, 31, .1);
    }
    .fo-float__title {
      margin: 0 0 22px;
      font-family: "Fraunces", Georgia, serif;
      font-size: 22px;
      font-weight: 600;
      line-height: 1.25;
    }
    .fo-float__field {
      position: relative;
      margin-bottom: 18px;
    }
    .fo-float__input {
      box-sizing: border-box;
      display: block;
      width: 100%;
      min-height: 54px;
      padding: 14px 14px 10px;
      border: 1px solid #6f8a76;
      border-radius: 12px;
      background: #fcfdf9;
      color: #14321f;
      font: inherit;
      font-size: max(1rem, 16px);
    }
    .fo-float__area {
      min-height: 108px;
      resize: vertical;
    }
    .fo-float__input:focus-visible {
      outline: none;
      border-color: #166534;
      box-shadow: inset 0 0 0 1px #166534, 0 0 0 3px rgba(22, 101, 52, .25);
    }
    .fo-float__input:user-invalid {
      border-color: #b42318;
    }
    .fo-float__label {
      position: absolute;
      top: 16px;
      left: 10px;
      max-width: calc(100% - 24px);
      padding: 0 5px;
      overflow: hidden;
      border-radius: 4px;
      background: #fcfdf9;
      color: #4b5f50;
      font-size: 16px;
      line-height: 1.3;
      white-space: nowrap;
      text-overflow: ellipsis;
      pointer-events: none;
      transform-origin: left top;
    }
    .fo-float__input:focus + .fo-float__label,
    .fo-float__input:not(:placeholder-shown) + .fo-float__label,
    .fo-float__label--up {
      color: #166534;
      font-weight: 600;
      transform: translateY(-26px) scale(.82);
    }
    .fo-float__input:user-invalid + .fo-float__label {
      color: #b42318;
    }
    @media (prefers-reduced-motion: no-preference) {
      .fo-float__label {
        transition: transform .18s ease, color .18s;
      }
    }
    .fo-float__btn {
      width: 100%;
      min-height: 52px;
      border: 0;
      border-radius: 999px;
      background: #166534;
      color: #fff;
      font: inherit;
      font-weight: 700;
      cursor: pointer;
    }
    .fo-float__btn:hover {
      background: #11512a;
    }
    .fo-float__btn:focus-visible {
      outline: 3px solid #14321f;
      outline-offset: 3px;
    }
    .fo-float__status {
      margin: 10px 0 0;
      font-size: 14px;
      font-weight: 600;
    }
    .fo-float__status:empty {
      display: none;
    }

    Dark project brief with accent-color

    • color-scheme: dark
    • accent-color
    • <output>

    Start a project with Night Shift Studio

    What do you need?
    Timeline
    $24,000

    One color-scheme: dark declaration switches native parts such as scrollbars, the select popup and the date picker to their dark versions, and accent-color: #a3e635 recolors every checkbox, radio and range slider in one line. The browser still draws the checkmarks and adjusts them for contrast. The budget value is an output element linked to the slider with its for attribute.

    View code
    .fo-dark {
      box-sizing: border-box;
      width: min(100%, 500px);
      padding: clamp(20px, 5vw, 32px);
      border: 1px solid #2a303c;
      border-radius: 18px;
      background: #151922;
      color: #e7eaf0;
      color-scheme: dark;
      accent-color: #a3e635;
    }
    .fo-dark__title {
      margin: 0 0 20px;
      font-size: 21px;
      font-weight: 700;
      line-height: 1.25;
    }
    .fo-dark__field {
      display: grid;
      gap: 8px;
      margin-bottom: 18px;
      font-size: 14px;
      font-weight: 600;
    }
    .fo-dark__input {
      box-sizing: border-box;
      width: 100%;
      min-height: 46px;
      padding: 0 12px;
      border: 1px solid #6b7385;
      border-radius: 10px;
      background: #0d1017;
      color: #e7eaf0;
      font: inherit;
      font-size: max(1rem, 16px);
      font-weight: 400;
    }
    .fo-dark__input:focus-visible {
      outline: 2px solid #a3e635;
      outline-offset: 2px;
      border-color: #a3e635;
    }
    .fo-dark__set {
      display: flex;
      flex-wrap: wrap;
      gap: 8px;
      min-width: 0;
      margin: 0 0 18px;
      padding: 0;
      border: 0;
    }
    .fo-dark__set legend {
      margin-bottom: 8px;
      padding: 0;
      font-size: 14px;
      font-weight: 600;
    }
    .fo-dark__opt {
      display: inline-flex;
      align-items: center;
      gap: 8px;
      min-height: 40px;
      padding: 0 14px 0 10px;
      border: 1px solid #3a4150;
      border-radius: 999px;
      font-size: 15px;
      cursor: pointer;
      transition: border-color .15s, background-color .15s;
    }
    .fo-dark__opt input {
      width: 18px;
      height: 18px;
      margin: 0;
    }
    .fo-dark__opt:has(:checked) {
      border-color: #a3e635;
      background: #1d2414;
    }
    .fo-dark__opt:has(:focus-visible) {
      outline: 2px solid #a3e635;
      outline-offset: 2px;
    }
    .fo-dark__range-head {
      display: flex;
      justify-content: space-between;
      align-items: baseline;
    }
    .fo-dark__out {
      color: #a3e635;
      font-family: ui-monospace, Consolas, monospace;
      font-size: 16px;
    }
    .fo-dark__range {
      width: 100%;
      margin: 0;
    }
    .fo-dark__range:focus-visible {
      outline: 2px solid #a3e635;
      outline-offset: 4px;
    }
    .fo-dark__btn {
      width: 100%;
      min-height: 50px;
      border: 0;
      border-radius: 10px;
      background: #a3e635;
      color: #10140a;
      font: inherit;
      font-weight: 700;
      cursor: pointer;
    }
    .fo-dark__btn:hover {
      background: #bef264;
    }
    .fo-dark__btn:focus-visible {
      outline: 2px solid #e7eaf0;
      outline-offset: 3px;
    }
    .fo-dark__status {
      margin: 10px 0 0;
      color: #c8cdd8;
      font-size: 14px;
      font-weight: 600;
    }
    .fo-dark__status:empty {
      display: none;
    }

    Inline help text with aria-describedby

    • aria-describedby
    • live rule checklist
    • input suffix

    Create your Hivedesk workspace

    This is the link your team will use to sign in.

    • 3 to 30 characters, not met
    • Lowercase letters, numbers and hyphens only, not met
    • Starts and ends with a letter or number, not met

    Invoices go here. It can be a shared finance inbox.

    Help text sits between the label and the field, and aria-describedby points the field at it, so screen readers read the hint right after the label. The workspace address field lists its rules, and a script ticks each rule as you type by toggling a class and swapping hidden met or not met text. The hint never replaces the label, and nothing relies on color alone.

    View code
    .fo-help {
      box-sizing: border-box;
      width: min(100%, 520px);
      padding: clamp(18px, 5vw, 32px);
      border-radius: 16px;
      background: #fff;
      color: #27180f;
      box-shadow: 0 14px 34px rgba(39, 24, 15, .1);
    }
    .fo-help__title {
      margin: 0 0 20px;
      font-size: 21px;
      font-weight: 700;
    }
    .fo-help__sr {
      position: absolute;
      width: 1px;
      height: 1px;
      overflow: hidden;
      clip-path: inset(50%);
      white-space: nowrap;
    }
    .fo-help__field {
      margin-bottom: 20px;
    }
    .fo-help__label {
      display: block;
      font-size: 15px;
      font-weight: 700;
    }
    .fo-help__hint {
      margin: 2px 0 8px;
      color: #5e5148;
      font-size: 14px;
      line-height: 1.45;
    }
    .fo-help__group {
      display: flex;
      border: 1px solid #857467;
      border-radius: 10px;
      overflow: hidden;
      background: #fff;
    }
    .fo-help__group:has(:focus-visible) {
      outline: 3px solid #9a3412;
      outline-offset: 1px;
    }
    .fo-help__input {
      box-sizing: border-box;
      flex: 1;
      width: 100%;
      min-width: 0;
      min-height: 46px;
      padding: 0 12px;
      border: 0;
      background: transparent;
      color: #27180f;
      font: inherit;
      font-size: max(1rem, 16px);
    }
    .fo-help__input:focus {
      outline: none;
    }
    .fo-help__input--solo {
      border: 1px solid #857467;
      border-radius: 10px;
    }
    .fo-help__input--solo:focus-visible {
      outline: 3px solid #9a3412;
      outline-offset: 1px;
    }
    .fo-help__suffix {
      display: grid;
      place-items: center;
      padding: 0 12px;
      border-left: 1px solid #d8cfc8;
      background: #f7f1ec;
      color: #5e5148;
      font-size: 14px;
      white-space: nowrap;
    }
    @media (max-width: 420px) {
      .fo-help__suffix {
        font-size: 12.5px;
        padding: 0 8px;
      }
    }
    .fo-help__rules {
      display: grid;
      gap: 4px;
      margin: 10px 0 0;
      padding: 0;
      list-style: none;
      font-size: 14px;
      color: #5e5148;
    }
    .fo-help__rules li {
      position: relative;
      padding-left: 24px;
    }
    .fo-help__rules li::before {
      content: "";
      position: absolute;
      top: 4px;
      left: 2px;
      width: 10px;
      height: 10px;
      border: 2px solid #857467;
      border-radius: 50%;
    }
    .fo-help__rules li.fo-help__met {
      color: #1e6b37;
      font-weight: 600;
    }
    .fo-help__rules li.fo-help__met::before {
      content: "\2713";
      top: 0;
      left: 0;
      width: auto;
      height: auto;
      border: 0;
      font-size: 15px;
      line-height: 1.3;
      font-weight: 700;
    }
    .fo-help__btn {
      min-height: 48px;
      padding: 0 22px;
      border: 0;
      border-radius: 10px;
      background: #9a3412;
      color: #fff;
      font: inherit;
      font-weight: 700;
      cursor: pointer;
    }
    .fo-help__btn:hover {
      background: #7c2d12;
    }
    .fo-help__btn:focus-visible {
      outline: 3px solid #27180f;
      outline-offset: 3px;
    }
    .fo-help__status {
      margin: 10px 0 0;
      font-size: 14px;
      font-weight: 600;
    }
    .fo-help__status:empty {
      display: none;
    }

    What are CSS forms and which controls can they style?

    Everything CSS does to a form is visual. It changes how inputs, selects, textareas, buttons and labels look, where they sit, and how they react once someone focuses or fills them. Submission is untouched.

    Behavior belongs to HTML attributes and the Constraint Validation API. CSS only reads the result, so a required field can turn red, but CSS never decides whether it is valid.

    Browsers and operating systems draw their own default controls, which is why an unstyled form looks different from one machine to the next. MDN Web Docs now calls form widgets mostly stylable, with a few exceptions.

    ControlStyling freedomMain CSS hooks
    Text inputs and textareaFullborder, padding, background, ::placeholder, caret-color
    ButtonsFullfont, border-radius, :hover, :disabled
    Label, fieldset, legendFullStandard box and text properties
    Checkbox and radioPartialaccent-color, appearance
    Select, file and datePartial to limitedClosed box styleable, popup mostly native

    Select popups and date pickers are the main exceptions, along with file inputs. Workarounds for all of them come further down.

    How to reset browser defaults before styling a form

    A decent reset comes down to border-box sizing, inherited fonts, native appearance removed only where you redraw a control, and a focus style written before any outline goes.

    MDN Web Docs notes that some widgets do not inherit font-family or font-size from their parent. In Chrome, submit inputs fall back to system-ui instead.

    1. Set box-sizing: border-box on input, select, textarea and button so padding and borders stay inside the declared width.
    2. Add font: inherit to the same controls so they pick up the page typeface and size.
    3. Apply appearance: none only to controls you will redraw, such as checkboxes and radios. Text inputs rarely need it.
    4. Write a visible focus style before removing any outline.

    A short custom reset scoped to form controls is predictable and easy to review. Normalize.css is the other route. It keeps useful browser defaults and smooths differences between engines, so it’s fine when the project already ships it.

    What goes wrong is a blanket outline: none or all: unset on controls. It strips the focus ring along with the native look, and keyboard users lose their place in the form.

    How to lay out a form with Grid and Flexbox

    Grid is the better pick when labels and fields need to line up across rows. Flexbox fits a single row where a control sits next to a companion, like an input beside a button.

    Both layouts take gap for spacing, so individual fields need no margins. Grid adds grid-template-columns for the column structure.

    Label above the field

    Start here by default.

    It’s one column, so it works at any width, and long or translated labels wrap without breaking alignment. Spacing comes from a single gap value on the form.

    The cost is height. A long form gets tall, which is where two columns earn their place.

    Label beside the field

    A fixed label column, say grid-template-columns: 10rem 1fr, gives clean alignment. The catch is that a long label wraps awkwardly or overflows.

    Sizing the column to its content with max-content 1fr avoids that, since the label column matches the longest label and nothing wraps. It works when labels and inputs are direct children of the grid. Wrapped fields need the same template on each wrapper.

    If you’d rather sketch it first, a CSS grid generator speeds up working out that template before it goes into the stylesheet.

    Multi-column and inline forms

    Paired fields like first name and last name sit in a nested grid using repeat(2, 1fr), collapsing to one column on narrow screens.

    Search and newsletter forms are Flexbox territory. Give the input flex: 1 and leave the button at its natural width. flex-wrap stacks them when space runs out.

    It falls apart when a Flexbox row holds 3 or more fields. Each row sizes itself, so the wrapping comes out uneven. Move that block to Grid.

    How to style text inputs, textareas and buttons

    Text inputs and textareas respond to the usual box properties (border, border-radius, padding, background and box-shadow), and buttons do too, with hover and disabled states on top.

    Text inputs and textareas

    Borders do the identifying work, so W3C’s WCAG 2.2 (success criterion 1.4.11) sets 3:1 contrast for them against adjacent colors, unless the browser draws the control and you leave it unmodified.

    A 1px border in a mid-gray that clears 3:1 against the background does most of the work. Padding around 0.6rem to 0.8rem keeps text off the edge, and border-radius between 4px and 8px is the usual range. A light box-shadow is fine, or none at all, since a heavy one competes with the focus ring.

    There’s a separate walkthrough of the markup and CSS for one field in this guide to styling a text input field.

    Placeholder text

    MDN Web Docs notes that WCAG requires a 4.5:1 contrast ratio for placeholder text, the same as body copy.

    Browser defaults differ, so an explicit color beats trusting them. Only properties that apply to ::first-line work inside a ::placeholder rule. Firefox defaults to the input text color at 54% opacity, while Chrome uses #757575.

    Check the color contrast of that gray against your field background, or set currentColor and control opacity yourself.

    A placeholder is not a label. It disappears once the user types, so the label element stays.

    Auto-growing textareas

    field-sizing: content replaces the JavaScript resize handler with one line.

    The default value is fixed, so existing forms stay untouched until you opt in. The property applies to textarea, input and select.

    Firefox 152 added support, which made field-sizing Baseline Newly available across all major engines (web.dev, June 2026). Pages that still list it as limited availability predate that release.

    Chrome for Developers flags a catch, which is that content-based sizing can shrink or stretch inputs too far. Set min-height so an empty textarea does not collapse, and add a relative max-height so a long paste does not push the submit button off screen.

    Buttons

    Buttons need the font reset first. In Chrome, a submit input keeps system-ui even after font-family: inherit, per MDN Web Docs, which is one more reason to use a button element.

    After that comes padding, border-radius and a background that keeps the label readable, then hover and disabled treatments.

    Which pseudo-classes style hover, focus and disabled states?

    Most form states come down to :hover, :focus, :focus-visible, :disabled, :checked, :placeholder-shown and :required.

    • :hover darkens the border or background, for example border-color: #4a5a6a
    • :focus matches any focused control, and outline: 2px solid #1a56db works as a starting point
    • :focus-visible only matches when the browser decides a ring is needed, mostly keyboard focus
    • Muted inactive controls come from :disabled, for example background: #f1f3f5
    • :checked styles the on state of a checkbox or radio you have redrawn
    • :placeholder-shown targets an empty field with an active placeholder
    • :required adds a marker, like a colored border-left

    Use :focus-visible for the ring. MDN Web Docs explains that it changes how the indicator looks without changing when the browser shows it.

    It has worked in every major browser since March 2022 and reached Baseline Widely available on 14 September 2024 (web-platform-dx web-features). For older browsers, repeat the style inside @supports not selector(:focus-visible).

    The real mistake is outline: none on :focus with nothing in its place. Keyboard users cannot see where they are, and whatever ring replaces it falls under the same 3:1 contrast rule.

    The :has() selector lets a wrapper react to its control, for example .field:has(:focus-visible) to highlight the whole field group. It became Baseline Newly available in December 2023 (web.dev) and Baseline Widely available on 19 June 2026 (web-platform-dx web-features).

    Transitions and richer hover treatments are collected in this set of CSS hover effects.

    Disabled controls are exempt from the 1.4.11 contrast requirement, so muted styling is acceptable there. Keep the text readable anyway.

    How to style validation states without JavaScript

    Style validity with :user-invalid, not :invalid. Both match failing constraints, but :user-invalid waits until the user has interacted with the field or tried to submit.

    That timing matters on every required field. Plain :invalid matches an empty required input on page load, so the form opens covered in error styling before anyone types a character.

    SelectorMatches whenWatch out for
    :invalidA value fails any constraint, blank required fields includedMatches on page load
    :user-invalidA value fails a constraint after interaction or a submit attemptCheck support before dropping :invalid
    :in-range and :out-of-rangeA number or date sits inside or outside min and maxAn empty input is never out of range, so :out-of-range skips it

    The :user-invalid selector became Baseline Newly available on 2 November 2023 and Baseline Widely available on 2 May 2026 (web-platform-dx web-features, MDN Web Docs).

    The date follows the release schedule, with Chrome 119 on 31 October and Edge 119 on 2 November. Firefox 88 and Safari 16.5 supported it earlier (Can I use).

    Set the border color in an input:user-invalid rule. To show the message, use a sibling or :has() rule, for example .field:has(:user-invalid) .error { display: block }, and keep the message element in the markup, hidden until the rule fires.

    CSS stops at one message per field, though. A message that names the exact failed rule, such as too short or wrong pattern, needs JavaScript and the validity states of the Constraint Validation API.

    And a red border alone tells some users nothing. Put text next to it.

    How to customize checkboxes, radio buttons and toggle switches

    Use accent-color to recolor native checkboxes and radios, and range sliders too, in one line. Switch to a redrawn control only when the design needs a different shape.

    The browser keeps its own checkmark and adjusts it for legibility against your accent color. MDN Web Docs notes that not every control or state uses the accent at all.

    The upside of accent-color is that native keyboard, focus and checked behavior stay intact, and it’s a single declaration set once on the form. The downside is that it only handles color, so size and shape stay with the browser. The appearance: none route gives full control over size, shape and the checkmark, but you redraw the checked, indeterminate, disabled and focus states yourself.

    A toggle switch is still a checkbox underneath. Keep the real input in the DOM and hide it visually only. The track and thumb get drawn on the label or a sibling element.

    The full markup for a toggle switch build follows that same pattern.

    W3C WAI notes that browsers treat the label as a larger clickable area for its control. So the label belongs to the hit area, not just the box.

    More custom designs are collected in these CSS checkbox examples.

    display: none on the real input removes it from the keyboard order, which means the switch then works for mouse users only.

    When does CSS styling stop working on form controls?

    CSS has no reach into the parts the browser or operating system draws itself. That covers the select dropdown list, the date picker and the file chooser dialog. The closed controls stay styleable and the popups don’t, with one new exception on select.

    The file input is partly fixable. ::file-selector-button styles the button while the chooser dialog stays native. It is a whole element, so set its font and color explicitly, because they do not necessarily inherit from the input (MDN Web Docs).

    For select, appearance: base-select opts the select and its ::picker(select) into full styling. Chrome for Developers lists the cost: the select stops sizing to its longest option, and the popup stays inside the browser pane.

    Date inputs only give you the field box. The calendar popup remains browser-drawn.

    Support differs enough that the figures are worth listing with dates.

    • accent-color has limited availability and is not Baseline. Chrome 93, Firefox 92 and Safari 26.2 support it, Chrome for Android only partially (web-platform-dx web-features, MDN Web Docs)
    • ::file-selector-button has been Baseline Widely available since 26 October 2023, with Chrome 89, Firefox 82 and Safari 14.1 (web-platform-dx web-features)
    • appearance: base-select shipped in Chrome version 135, announced 24 March 2025 (Chrome for Developers)
    • Elsewhere, appearance: base-select works in Safari 27 (released September 2026), while Safari 26.6 and earlier do not. Firefox keeps it disabled by default, behind preferences since Firefox 149 (current stable: 159), and the picker itself is not yet styleable there. It is not Baseline (MDN browser compatibility data and Mozilla’s experimental features documentation, checked 27 October 2026)

    Wrap base-select rules in @supports (appearance: base-select) so Firefox and older versions of Safari and other browsers fall back to a normal select. If identical looks across browsers are a hard requirement, style the closed select and leave the popup native.

    Closed-box styling for a standard dropdown is covered in this guide to styling a select dropdown.

    Swapping in a JavaScript dropdown restores full control but leaves you rebuilding keyboard and screen reader behavior by hand.

    How to make CSS forms responsive and accessible

    On the responsive side it comes down to fluid widths and 16px text in inputs. Accessibility asks for more: real labels, grouped radios and checkboxes, and focus styles that survive restyling.

    Responsive sizing and mobile zoom

    Safari on iPhone zooms into any input whose font-size is below 16px, according to Telerik’s knowledge base.

    Set font-size: max(1rem, 16px) on inputs, selects and textareas and the zoom never fires. Telerik also documents a viewport meta workaround, but raising the font size costs less and leaves the viewport tag untouched.

    Give fields width: 100% plus a max-width so they fill phones without stretching on desktop. Setting container-type: inline-size on the form wrapper lets the column layout follow the space it gets.

    Page-level breakpoints still run through media queries. Container queries just keep the same form working in a sidebar and on a full-width page.

    Focus, contrast and labels

    Labels come first. WebAIM’s Million report for 2026 found 33.1% of home page form inputs without a proper label, counting label, aria-label, aria-labelledby and title as valid.

    W3C WAI says to identify each control with a label element, tied by matching for and id. Radio buttons belong in a fieldset with a legend, and CSS can override the fieldset border and legend position instead of swapping in divs (W3C WCAG technique H71). Hidden labels should use a clip-based utility class, never display: none.

    Keep legends short. W3C WAI notes that screen readers read the legend with every field, once, or rarely not at all, depending on configuration.

    Re-test border and ring colors on every theme. A focus ring that works on white can vanish on a dark surface.

    For markup-level practices beyond styling, see this guide to accessible forms.

    CSS Forms FAQ

    Should I use Bootstrap or Tailwind for forms, or write custom CSS?

    Write custom CSS when the design is distinct or the stylesheet must stay small. Use a framework when the project already runs on one.

    Bootstrap’s form classes restyle inputs, selects and validation states consistently, though you take on its class conventions and override rules.

    How can I build a floating label with CSS only?

    Place the label after the input, give the input a single-space placeholder, then move and shrink the label when the input has focus or no longer shows its placeholder.

    The trigger is input:not(:placeholder-shown) + label, paired with input:focus + label. Keep the real label element, since it carries the accessible name.

    How do I mark required fields with CSS?

    Target the label with label:has(+ :required)::after and add an asterisk or the word “required” as generated content.

    The required attribute still does the real work, because screen readers announce it. An asterisk alone carries no meaning.

    How do I style a form for dark mode?

    Store colors as custom properties and redefine them inside a prefers-color-scheme: dark media query.

    Then add color-scheme: light dark to the root, so native controls such as checkboxes and scrollbars switch themes with the page.

    Can CSS transitions animate form fields?

    Yes. Transitions on border-color, box-shadow and background-color animate hover and focus changes smoothly.

    Skip width and height, which force layout recalculation on every frame. Wrap the motion in a prefers-reduced-motion check.

    Do CSS forms work without JavaScript?

    Yes. Layout, state styling and validity feedback run on CSS and HTML attributes alone, and the form still submits normally.

    JavaScript only adds behavior CSS cannot express, such as asynchronous submission.

    What to Change First on an Existing Form

    Before any visual redesign, start with the cheap fixes. None of them touches the markup.

    Add a visible focus ring on :focus-visible and make sure every input uses 16px text. Then swap :invalid for :user-invalid.

    That order follows failure cost. A missing ring strands keyboard users, and iPhone zoom hits every field below 16px, while the :user-invalid swap only changes when error styling appears.

    The trade-off sits in that last swap. A pre-filled invalid value stays unmarked until the user touches the field or attempts to submit.

    The advice for styling select dropdowns changes once Firefox ships appearance: base-select in a stable release. Safari already shipped it in version 27. That position was checked on 27 October 2026.

    A compact CSS login form makes a good first test of all three changes.