CSS Search Box: 16 Examples + the Expanding One

Visitors scan a page for a spot to type, and when they find one, they use it. On useit.com, Jakob Nielsen (2001) recorded a 91% rise in search engine use after a type-in box replaced the homepage search link.

Below are 16 CSS search box examples to try and copy, followed by a guide to markup, icons, focus states and the iOS zoom fix.

By Bogdan Sandu · Updated October 28, 2026

Search boxes in site headers

Where most search boxes live: a header bar, a docs toolbar, a phone app bar and a hero. Every one is a search landmark with a labeled input type="search", and none of the forms submit anywhere.

Classic field with icon button

  • search element
  • absolute button
  • appearance: textfield

The default pattern: a relatively positioned form, an absolutely positioned 44px submit button with an inline SVG, and right padding on the field so typed text stops before the icon. The search element wraps the form and gives screen readers a search landmark.

View code
.sx-classic {
  width: min(100%, 680px);
}
.sx-classic__bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 12px 24px;
  padding: 14px 16px 14px 20px;
  background: #fff;
  border: 1px solid #e3e1db;
  border-radius: 14px;
  box-shadow: 0 8px 24px rgba(30, 28, 24, .06);
}
.sx-classic__logo {
  font-family: "Fraunces", serif;
  font-size: 1.25rem;
  font-weight: 600;
  color: #1f2a24;
  white-space: nowrap;
}
.sx-classic__search {
  display: block;
  flex: 1 1 240px;
  max-width: 400px;
  margin-left: auto;
}
.sx-classic__form {
  position: relative;
}
.sx-classic__vh {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
.sx-classic__form input {
  box-sizing: border-box;
  width: 100%;
  height: 52px;
  margin: 0;
  padding: 0 56px 0 16px;
  font: inherit;
  font-size: max(16px, 1rem);
  color: #1f2a24;
  background: #f7f6f2;
  border: 1px solid #767570;
  border-radius: 12px;
  -webkit-appearance: textfield;
  appearance: textfield;
}
.sx-classic__form input::-webkit-search-decoration {
  -webkit-appearance: none;
}
.sx-classic__form input::placeholder {
  color: #5c665f;
  opacity: 1;
}
.sx-classic__form input:focus {
  outline: 2px solid #1f5b45;
  outline-offset: 2px;
  border-color: #1f5b45;
  background: #fff;
}
.sx-classic__btn {
  position: absolute;
  inset: 4px 4px 4px auto;
  display: grid;
  place-items: center;
  width: 44px;
  padding: 0;
  color: #fff;
  background: #1f5b45;
  border: 0;
  border-radius: 9px;
  cursor: pointer;
  transition: background-color .15s;
}
.sx-classic__btn:hover {
  background: #153f30;
}
.sx-classic__btn:focus-visible {
  outline: 2px solid #1f5b45;
  outline-offset: 2px;
}

Expanding search in a navbar

  • aria-expanded
  • width transition
  • container queries

The icon is a real button with aria-expanded. Opening widens the field and moves focus into it, Escape closes it and returns focus to the button. The collapsed field is visibility: hidden, so it never sits in the tab order, and the open state drops visibility from its transition so focus() works on the first frame.

View code
.sx-exp {
  width: min(100%, 680px);
  container-type: inline-size;
}
.sx-exp__bar {
  display: flex;
  align-items: center;
  gap: 20px;
  min-height: 64px;
  padding: 8px 10px 8px 20px;
  background: #14213d;
  border-radius: 16px;
}
.sx-exp__logo {
  flex: none;
  font-family: "Fraunces", serif;
  font-size: 1.15rem;
  font-weight: 600;
  color: #fff;
  white-space: nowrap;
}
.sx-exp__links {
  display: flex;
  gap: 18px;
}
.sx-exp__links a {
  font-size: .95rem;
  color: #dfe6f5;
  text-decoration: none;
}
.sx-exp__links a:hover {
  text-decoration: underline;
}
.sx-exp__links a:focus-visible {
  outline: 2px solid #f4b942;
  outline-offset: 4px;
  border-radius: 2px;
}
.sx-exp__search {
  display: block;
  min-width: 0;
  margin-left: auto;
}
.sx-exp__form {
  display: flex;
  min-width: 0;
  align-items: center;
  gap: 6px;
}
.sx-exp__vh {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
.sx-exp__field {
  flex: 0 1 auto;
  width: 0;
  min-width: 0;
  overflow: hidden;
  visibility: hidden;
}
.sx-exp__form.is-open .sx-exp__field {
  width: 9rem;
  width: clamp(8rem, 40cqi, 16rem);
  visibility: visible;
}
.sx-exp__field input {
  box-sizing: border-box;
  width: 100%;
  height: 44px;
  margin: 0;
  padding: 0 14px;
  font: inherit;
  font-size: max(16px, 1rem);
  color: #fff;
  background: #22335a;
  border: 1px solid #8ea0c8;
  border-radius: 10px;
  -webkit-appearance: textfield;
  appearance: textfield;
}
.sx-exp__field input::placeholder {
  color: #b9c4dc;
  opacity: 1;
}
.sx-exp__field input:focus {
  outline: 2px solid #f4b942;
  outline-offset: -2px;
}
.sx-exp__toggle {
  display: grid;
  place-items: center;
  flex: none;
  width: 44px;
  height: 44px;
  padding: 0;
  color: #14213d;
  background: #f4b942;
  border: 0;
  border-radius: 10px;
  cursor: pointer;
  transition: background-color .15s;
}
.sx-exp__toggle:hover {
  background: #ffcf66;
}
.sx-exp__toggle:focus-visible {
  outline: 2px solid #f4b942;
  outline-offset: 3px;
}
.sx-exp__icon-close,
.sx-exp__toggle[aria-expanded="true"] .sx-exp__icon-open {
  display: none;
}
.sx-exp__toggle[aria-expanded="true"] .sx-exp__icon-close {
  display: block;
}
@container (max-width: 520px) {
  .sx-exp__links {
    display: none;
  }
}
@container (max-width: 380px) {
  .sx-exp__bar:has(.is-open) .sx-exp__logo {
    display: none;
  }
  .sx-exp__form.is-open .sx-exp__field {
    width: calc(100cqi - 90px);
  }
}
@media (prefers-reduced-motion: no-preference) {
  .sx-exp__field {
    transition: width .3s ease, visibility 0s linear .3s;
  }
  .sx-exp__form.is-open .sx-exp__field {
    transition: width .3s ease;
  }
}

Command palette trigger with Ctrl K

  • kbd
  • aria-keyshortcuts
  • @starting-style

The header shows a button dressed as a field, with the shortcut in kbd elements and in aria-keyshortcuts. The label switches to the Command symbol on Apple devices. Here the shortcut listener sits on the demo so it does not hijack this page; on a real site it goes on the document.

View code
.sx-cmd {
  position: relative;
  width: min(100%, 640px);
  min-height: 400px;
}
.sx-cmd__bar {
  position: relative;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 12px 14px 12px 20px;
  background: #fff;
  border: 1px solid #dfe3ea;
  border-radius: 14px;
}
.sx-cmd__logo {
  font-weight: 700;
  font-size: 1.05rem;
  color: #111827;
}
.sx-cmd__logo span {
  font-weight: 400;
  color: #4b5563;
}
.sx-cmd__trigger {
  display: flex;
  align-items: center;
  gap: 10px;
  flex: 0 1 260px;
  min-width: 0;
  min-height: 44px;
  padding: 0 8px 0 14px;
  font: inherit;
  font-size: .95rem;
  color: #4b5563;
  background: #f3f4f6;
  border: 1px solid #c3c8d1;
  border-radius: 10px;
  cursor: pointer;
  transition: border-color .15s, background-color .15s;
}
.sx-cmd__trigger:hover {
  background: #fff;
  border-color: #6b7280;
}
.sx-cmd__trigger:focus-visible {
  outline: 2px solid #4f46e5;
  outline-offset: 2px;
}
.sx-cmd__label {
  flex: 1;
  overflow: hidden;
  text-align: left;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.sx-cmd__keys {
  display: flex;
  gap: 4px;
}
.sx-cmd kbd {
  min-width: 1.6em;
  padding: 2px 6px;
  font: 600 .75rem/1.4 ui-monospace, Consolas, monospace;
  color: #374151;
  text-align: center;
  background: #fff;
  border: 1px solid #c3c8d1;
  border-bottom-width: 2px;
  border-radius: 6px;
}
.sx-cmd__panel {
  position: absolute;
  top: calc(100% + 12px);
  left: 50%;
  z-index: 2;
  width: min(100%, 500px);
  translate: -50% 0;
  background: #fff;
  border: 1px solid #d1d5db;
  border-radius: 16px;
  box-shadow: 0 24px 48px rgba(17, 24, 39, .16);
  opacity: 1;
  transition: opacity .15s;
}
.sx-cmd__panel[hidden] {
  display: none;
}
.sx-cmd__form {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 6px 14px;
  color: #4b5563;
  border-bottom: 1px solid #e5e7eb;
}
.sx-cmd__vh {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
.sx-cmd__form input {
  flex: 1;
  min-width: 0;
  height: 48px;
  margin: 0;
  padding: 0;
  font: inherit;
  font-size: max(16px, 1.05rem);
  color: #111827;
  background: transparent;
  border: 0;
  -webkit-appearance: textfield;
  appearance: textfield;
}
.sx-cmd__form input:focus {
  outline: none;
}
.sx-cmd__form:focus-within {
  box-shadow: inset 0 -2px 0 #4f46e5;
}
.sx-cmd__form input::placeholder {
  color: #6b7280;
  opacity: 1;
}
.sx-cmd__heading {
  margin: 12px 18px 6px;
  font-size: .8rem;
  font-weight: 600;
  color: #4b5563;
}
.sx-cmd__list {
  margin: 0;
  padding: 0 8px 10px;
  list-style: none;
}
.sx-cmd__list button {
  display: flex;
  justify-content: space-between;
  align-items: center;
  width: 100%;
  min-height: 44px;
  padding: 0 10px;
  font: inherit;
  font-size: .95rem;
  color: #111827;
  text-align: left;
  background: transparent;
  border: 0;
  border-radius: 8px;
  cursor: pointer;
}
.sx-cmd__list button span {
  font-size: .8rem;
  color: #4b5563;
}
.sx-cmd__list button:hover,
.sx-cmd__list button:focus-visible {
  background: #eef2ff;
  outline: none;
}
.sx-cmd__list button:focus-visible {
  box-shadow: inset 0 0 0 2px #4f46e5;
}
@media (prefers-reduced-motion: no-preference) {
  .sx-cmd__panel {
    transition: opacity .15s, translate .15s;
  }
  @starting-style {
    .sx-cmd__panel {
      opacity: 0;
      translate: -50% -8px;
    }
  }
}

Search bar in a phone header

  • sticky
  • :focus-within
  • enterkeyhint

A full-width bar under the app title, with a 16px field so iOS Safari does not zoom in on focus. A Cancel button slides in while the form has focus; it cancels its own mousedown so the field keeps focus when you tap it, which matters in Safari where buttons do not take focus on click. enterkeyhint="search" labels the Enter key on virtual keyboards.

View code
.sx-ph {
  position: sticky;
  top: 0;
  z-index: 1;
  padding: 14px 16px 12px;
  background: #f1f5ee;
  border-bottom: 1px solid #d6dfd0;
}
.sx-ph__top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 12px;
}
.sx-ph__logo {
  font-family: "Fraunces", serif;
  font-size: 1.4rem;
  font-weight: 600;
  color: #24452f;
}
.sx-ph__avatar {
  width: 40px;
  height: 40px;
  padding: 0;
  font: inherit;
  font-size: .8rem;
  font-weight: 700;
  color: #fff;
  background: #2f6b45;
  border: 0;
  border-radius: 50%;
  cursor: pointer;
}
.sx-ph__avatar:focus-visible {
  outline: 2px solid #2f6b45;
  outline-offset: 2px;
}
.sx-ph__form {
  display: flex;
  align-items: center;
  gap: 10px;
}
.sx-ph__field {
  position: relative;
  flex: 1;
  min-width: 0;
  color: #4f5d53;
}
.sx-ph__field svg {
  position: absolute;
  top: 50%;
  left: 12px;
  translate: 0 -50%;
  pointer-events: none;
}
.sx-ph__vh {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
.sx-ph__field input {
  box-sizing: border-box;
  width: 100%;
  height: 44px;
  margin: 0;
  padding: 0 12px 0 38px;
  font: inherit;
  font-size: 16px;
  color: #1b2a20;
  background: #fff;
  border: 1px solid #7d8b80;
  border-radius: 12px;
  -webkit-appearance: textfield;
  appearance: textfield;
}
.sx-ph__field input::placeholder {
  color: #59665d;
  opacity: 1;
}
.sx-ph__field input:focus {
  outline: 2px solid #2f6b45;
  outline-offset: 1px;
  border-color: #2f6b45;
}
.sx-ph__cancel {
  display: none;
  min-height: 44px;
  padding: 0 4px;
  font: inherit;
  font-size: 1rem;
  font-weight: 600;
  color: #24683d;
  background: none;
  border: 0;
  cursor: pointer;
}
.sx-ph__cancel:focus-visible {
  outline: 2px solid #2f6b45;
  outline-offset: 2px;
  border-radius: 4px;
}
.sx-ph__form:focus-within .sx-ph__cancel {
  display: block;
}
@media (prefers-reduced-motion: no-preference) {
  .sx-ph__form:focus-within .sx-ph__cancel {
    animation: sx-ph-in .2s ease-out;
  }
}
@keyframes sx-ph-in {
  from {
    opacity: 0;
    translate: 12px 0;
  }
}

Pill search over a hero photo

  • object-fit
  • scrim
  • pill radius
Pine-covered cliffs and a winding road above a calm sea at sunset

Stays where the road meets the sea

A solid white pill on a darkened photo keeps the field readable whatever the image does. The scrim is a flat translucent layer, not a gradient, and the popular destinations are buttons that fill the field rather than links.

View code
.sx-hero {
  position: relative;
  display: grid;
  place-items: center;
  min-height: 400px;
  padding: 40px 20px;
  overflow: hidden;
  background: #2b2a26;
}
.sx-hero__img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.sx-hero::after {
  content: "";
  position: absolute;
  inset: 0;
  background: rgba(16, 20, 24, .5);
}
.sx-hero__inner {
  position: relative;
  z-index: 1;
  width: min(100%, 560px);
  text-align: center;
}
.sx-hero__title {
  margin: 0 0 22px;
  font-family: "Fraunces", serif;
  font-size: clamp(1.6rem, 5vw, 2.4rem);
  font-weight: 600;
  line-height: 1.15;
  letter-spacing: -.01em;
  color: #fff;
  text-shadow: 0 1px 12px rgba(0, 0, 0, .35);
}
.sx-hero__search {
  display: block;
}
.sx-hero__form {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 6px 6px 18px;
  color: #3d4a52;
  background: #fff;
  border-radius: 999px;
  box-shadow: 0 16px 40px rgba(0, 0, 0, .25);
}
.sx-hero__form:focus-within {
  outline: 3px solid #ffd166;
  outline-offset: 3px;
}
.sx-hero__form svg {
  flex: none;
}
.sx-hero__vh {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
.sx-hero__form input {
  flex: 1;
  min-width: 0;
  height: 48px;
  margin: 0;
  padding: 0;
  font: inherit;
  font-size: max(16px, 1rem);
  color: #172026;
  background: transparent;
  border: 0;
  -webkit-appearance: textfield;
  appearance: textfield;
}
.sx-hero__form input:focus {
  outline: none;
}
.sx-hero__form input::placeholder {
  color: #5a666e;
  opacity: 1;
}
.sx-hero__btn {
  flex: none;
  min-height: 48px;
  padding: 0 22px;
  font: inherit;
  font-weight: 600;
  color: #fff;
  background: #0f5c6e;
  border: 0;
  border-radius: 999px;
  cursor: pointer;
  transition: background-color .15s;
}
.sx-hero__btn:hover {
  background: #0a4452;
}
.sx-hero__btn:focus-visible {
  outline: 2px solid #0f5c6e;
  outline-offset: 2px;
}
.sx-hero__popular {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: center;
  gap: 8px;
  margin: 18px 0 0;
  font-size: .9rem;
  color: #fff;
}
.sx-hero__popular button {
  min-height: 32px;
  padding: 0 12px;
  font: inherit;
  color: #fff;
  background: rgba(0, 0, 0, .45);
  border: 1px solid rgba(255, 255, 255, .7);
  border-radius: 999px;
  cursor: pointer;
  transition: background-color .15s;
}
.sx-hero__popular button:hover {
  background: rgba(0, 0, 0, .7);
}
.sx-hero__popular button:focus-visible {
  outline: 2px solid #ffd166;
  outline-offset: 2px;
}
@media (max-width: 420px) {
  .sx-hero__btn {
    padding: 0 16px;
  }
}

Search with suggestions, history and filters

The panels that open under a field. The suggestions list follows the ARIA combobox pattern, the others use plain buttons, so you can compare how much each one asks of the keyboard.

Suggestions list as a combobox

  • role="combobox"
  • aria-activedescendant
  • listbox

The input keeps DOM focus the whole time; Down and Up Arrow move aria-activedescendant through the options, Enter picks one, and Escape closes the list or, when it is already closed, clears the field. Matches are built with textContent and a strong element, never HTML strings, and the recipe data is a local array.

View code
.sx-sug {
  width: min(100%, 440px);
  min-height: 360px;
}
.sx-sug__search {
  display: block;
}
.sx-sug__label {
  display: block;
  margin-bottom: 8px;
  font-weight: 600;
  color: #3b1f0e;
}
.sx-sug__box {
  position: relative;
  color: #7a4a26;
}
.sx-sug__box svg {
  position: absolute;
  top: 14px;
  left: 14px;
  pointer-events: none;
}
.sx-sug__box input {
  box-sizing: border-box;
  width: 100%;
  height: 48px;
  margin: 0;
  padding: 0 14px 0 44px;
  font: inherit;
  font-size: max(16px, 1rem);
  color: #2a160a;
  background: #fffaf3;
  border: 2px solid #9c6b45;
  border-radius: 12px;
  -webkit-appearance: textfield;
  appearance: textfield;
}
.sx-sug__box input::placeholder {
  color: #7a5a44;
  opacity: 1;
}
.sx-sug__box input:focus {
  outline: 3px solid #e8a33d;
  outline-offset: 1px;
  border-color: #7a4a26;
}
.sx-sug__box input[aria-expanded="true"] {
  border-radius: 12px 12px 0 0;
}
.sx-sug__list {
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  z-index: 2;
  margin: 0;
  padding: 6px;
  list-style: none;
  background: #fff;
  border: 2px solid #7a4a26;
  border-top: 0;
  border-radius: 0 0 12px 12px;
  box-shadow: 0 16px 32px rgba(59, 31, 14, .14);
}
.sx-sug__list li {
  padding: 10px 12px;
  font-size: .95rem;
  color: #3b2a1e;
  border-radius: 8px;
  cursor: pointer;
}
.sx-sug__list strong {
  font-weight: 700;
  color: #2a160a;
}
.sx-sug__list li:hover {
  background: #fbf0e2;
}
.sx-sug__list li[aria-selected="true"] {
  color: #fff;
  background: #7a4a26;
}
.sx-sug__list li[aria-selected="true"] strong {
  color: #fff;
}

Recent searches dropdown

  • focusout
  • replaceChildren
  • role="group"

The panel opens when the empty field gets focus and closes when focus leaves the form or on Escape. Each entry is a button you can reach with Tab, with its own remove button, and submitting a query moves it to the top. The history lives in memory only, so nothing is stored in the browser.

View code
.sx-rec {
  width: min(100%, 420px);
  min-height: 330px;
}
.sx-rec__search {
  display: block;
}
.sx-rec__form {
  position: relative;
}
.sx-rec__vh {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
.sx-rec__box {
  position: relative;
  color: #3c4a5c;
}
.sx-rec__box svg {
  position: absolute;
  top: 14px;
  left: 16px;
  pointer-events: none;
}
.sx-rec__box input {
  box-sizing: border-box;
  width: 100%;
  height: 48px;
  margin: 0;
  padding: 0 16px 0 46px;
  font: inherit;
  font-size: max(16px, 1rem);
  color: #172433;
  background: #fff;
  border: 1px solid #c4cbd5;
  border-radius: 24px;
  box-shadow: 0 2px 6px rgba(23, 36, 51, .12);
  -webkit-appearance: textfield;
  appearance: textfield;
}
.sx-rec__box input::placeholder {
  color: #5b6878;
  opacity: 1;
}
.sx-rec__box input:focus {
  outline: 2px solid #1a73c9;
  outline-offset: 2px;
}
.sx-rec__panel {
  position: absolute;
  top: calc(100% + 8px);
  left: 0;
  right: 0;
  z-index: 2;
  padding: 8px 0;
  background: #fff;
  border: 1px solid #d5dbe3;
  border-radius: 16px;
  box-shadow: 0 16px 36px rgba(23, 36, 51, .16);
}
.sx-rec__panel[hidden] {
  display: none;
}
.sx-rec__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 8px 0 18px;
}
.sx-rec__title {
  margin: 0;
  font-size: .85rem;
  font-weight: 600;
  color: #3c4a5c;
}
.sx-rec__clear {
  min-height: 32px;
  padding: 0 10px;
  font: inherit;
  font-size: .85rem;
  color: #155ea8;
  background: none;
  border: 0;
  border-radius: 6px;
  cursor: pointer;
}
.sx-rec__clear:hover {
  text-decoration: underline;
}
.sx-rec__list {
  margin: 4px 0 0;
  padding: 0;
  list-style: none;
}
.sx-rec__list li {
  display: flex;
  align-items: center;
}
.sx-rec__item {
  display: flex;
  align-items: center;
  gap: 12px;
  flex: 1;
  min-width: 0;
  min-height: 44px;
  padding: 0 8px 0 18px;
  font: inherit;
  font-size: .95rem;
  color: #172433;
  text-align: left;
  background: none;
  border: 0;
  cursor: pointer;
}
.sx-rec__item::before {
  content: "";
  flex: none;
  width: 16px;
  height: 16px;
  background: #5b6878;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Ccircle cx='12' cy='12' r='9' fill='none' stroke='%23000' stroke-width='2'/%3E%3Cpath d='M12 7v5l3 2' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round'/%3E%3C/svg%3E") center / contain no-repeat;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Ccircle cx='12' cy='12' r='9' fill='none' stroke='%23000' stroke-width='2'/%3E%3Cpath d='M12 7v5l3 2' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round'/%3E%3C/svg%3E") center / contain no-repeat;
}
.sx-rec__remove {
  flex: none;
  width: 36px;
  height: 36px;
  margin-right: 8px;
  padding: 0;
  font-size: 1.25rem;
  line-height: 1;
  color: #4a5768;
  background: none;
  border: 0;
  border-radius: 50%;
  cursor: pointer;
}
.sx-rec__item:hover,
.sx-rec__remove:hover {
  background: #eef2f7;
}
.sx-rec__item:focus-visible,
.sx-rec__remove:focus-visible,
.sx-rec__clear:focus-visible {
  outline: 2px solid #1a73c9;
  outline-offset: -2px;
}
.sx-rec__empty {
  margin: 8px 18px 6px;
  font-size: .9rem;
  color: #4a5768;
}

Search with a category select prefix

  • select
  • field-sizing
  • :focus-within

A native select, the field and the button share one bordered shell, and :focus-within outlines the whole shell. Both controls have their own hidden label. Where field-sizing: content is supported the select shrinks to its current option; elsewhere it keeps a fixed width.

View code
.sx-cat {
  width: min(100%, 520px);
}
.sx-cat__search {
  display: block;
}
.sx-cat__form {
  display: flex;
  align-items: stretch;
  height: 50px;
  overflow: hidden;
  background: #fff;
  border: 2px solid #1d1d1f;
  border-radius: 12px;
}
.sx-cat__form:focus-within {
  outline: 3px solid #ff7a1a;
  outline-offset: 2px;
}
.sx-cat__vh {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
.sx-cat__select {
  flex: none;
  width: 104px;
  margin: 0;
  padding: 0 30px 0 14px;
  font: inherit;
  font-size: max(16px, .95rem);
  font-weight: 600;
  color: #1d1d1f;
  background: #f1eee8 url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='m7 10 5 5 5-5' fill='none' stroke='%231d1d1f' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") right 8px center / 16px no-repeat;
  border: 0;
  border-right: 2px solid #1d1d1f;
  border-radius: 0;
  -webkit-appearance: none;
  appearance: none;
  cursor: pointer;
}
@supports (field-sizing: content) {
  .sx-cat__select {
    width: auto;
    field-sizing: content;
  }
}
.sx-cat__select:hover {
  background-color: #e6e1d7;
}
.sx-cat__select:focus-visible {
  outline: 2px solid #1d1d1f;
  outline-offset: -5px;
}
.sx-cat__form input {
  flex: 1;
  min-width: 0;
  margin: 0;
  padding: 0 14px;
  font: inherit;
  font-size: max(16px, 1rem);
  color: #1d1d1f;
  background: transparent;
  border: 0;
  -webkit-appearance: textfield;
  appearance: textfield;
}
.sx-cat__form input:focus {
  outline: none;
}
.sx-cat__form input::placeholder {
  color: #5e5a52;
  opacity: 1;
}
.sx-cat__btn {
  flex: none;
  display: grid;
  place-items: center;
  width: 54px;
  padding: 0;
  color: #1d1d1f;
  background: #ff7a1a;
  border: 0;
  border-left: 2px solid #1d1d1f;
  cursor: pointer;
  transition: background-color .15s;
}
.sx-cat__btn:hover {
  background: #ff9447;
}
.sx-cat__btn:focus-visible {
  outline: 2px solid #1d1d1f;
  outline-offset: -5px;
}

Search with filter chips

  • aria-pressed
  • :has()
  • flex-wrap

Each chip is a toggle button with aria-pressed, and the checkmark and fill are styled from that attribute. A Clear filters button appears through :has() only when at least one chip is pressed, and the chips wrap onto new lines on narrow screens instead of scrolling sideways.

View code
.sx-chip {
  width: min(100%, 540px);
}
.sx-chip__search {
  display: block;
}
.sx-chip__vh {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
.sx-chip__box {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 5px 5px 5px 14px;
  color: #3f4a43;
  background: #fff;
  border: 1px solid #7c867f;
  border-radius: 10px;
}
.sx-chip__box:focus-within {
  outline: 2px solid #1b4332;
  outline-offset: 2px;
}
.sx-chip__box svg {
  flex: none;
}
.sx-chip__box input {
  flex: 1;
  min-width: 0;
  height: 44px;
  margin: 0;
  padding: 0;
  font: inherit;
  font-size: max(16px, 1rem);
  color: #16201a;
  background: transparent;
  border: 0;
  -webkit-appearance: textfield;
  appearance: textfield;
}
.sx-chip__box input:focus {
  outline: none;
}
.sx-chip__box input::placeholder {
  color: #59645d;
  opacity: 1;
}
.sx-chip__go {
  flex: none;
  min-height: 44px;
  padding: 0 18px;
  font: inherit;
  font-weight: 600;
  color: #fff;
  background: #1b4332;
  border: 0;
  border-radius: 7px;
  cursor: pointer;
  transition: background-color .15s;
}
.sx-chip__go:hover {
  background: #2d6a4f;
}
.sx-chip__go:focus-visible {
  outline: 2px solid #1b4332;
  outline-offset: 2px;
}
.sx-chip__chips {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 14px;
}
.sx-chip__chip {
  display: inline-flex;
  align-items: center;
  gap: 0;
  min-height: 36px;
  padding: 0 14px;
  font: inherit;
  font-size: .9rem;
  color: #1b4332;
  background: #fff;
  border: 1px solid #6f8f7e;
  border-radius: 999px;
  cursor: pointer;
  transition: background-color .15s, color .15s;
}
.sx-chip__chip svg {
  width: 0;
  overflow: hidden;
}
.sx-chip__chip[aria-pressed="true"] {
  gap: 6px;
  color: #fff;
  background: #1b4332;
  border-color: #1b4332;
}
.sx-chip__chip[aria-pressed="true"] svg {
  width: 16px;
}
.sx-chip__chip:hover {
  border-color: #1b4332;
}
.sx-chip__chip:focus-visible,
.sx-chip__reset:focus-visible {
  outline: 2px solid #1b4332;
  outline-offset: 2px;
}
.sx-chip__reset {
  display: none;
  min-height: 36px;
  padding: 0 8px;
  font: inherit;
  font-size: .9rem;
  color: #1b4332;
  text-decoration: underline;
  background: none;
  border: 0;
  border-radius: 4px;
  cursor: pointer;
}
.sx-chip__chips:has([aria-pressed="true"]) .sx-chip__reset {
  display: inline-block;
}
@media (prefers-reduced-motion: no-preference) {
  .sx-chip__chip,
  .sx-chip__chip svg {
    transition: background-color .15s, color .15s, width .15s, gap .15s;
  }
}

Overlays, results and live feedback

What happens after typing: a full-screen overlay, a filtered list that announces its count, highlighted matches and a voice button. The data is local to each demo.

Full-screen search overlay

  • role="dialog"
  • inert
  • focus return

The header icon opens an overlay that covers the frame, moves focus to a large field, makes the header inert and keeps Tab inside the overlay. Escape or the close button returns focus to the icon. On a live site a dialog element opened with showModal() gives you the top layer, the inert page and Escape for free.

View code
.sx-ov {
  position: relative;
  min-height: 440px;
  overflow: hidden;
  background: #fbf8f2;
}
.sx-ov__header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 12px 16px 12px 24px;
  border-bottom: 1px solid #d9d2c3;
}
.sx-ov__logo {
  font-family: "Fraunces", serif;
  font-size: 1.2rem;
  font-weight: 700;
  color: #1c1a17;
}
.sx-ov__open,
.sx-ov__close {
  display: grid;
  place-items: center;
  flex: none;
  width: 44px;
  height: 44px;
  padding: 0;
  color: #1c1a17;
  background: none;
  border: 1px solid transparent;
  border-radius: 50%;
  cursor: pointer;
  transition: background-color .15s;
}
.sx-ov__open:hover,
.sx-ov__close:hover {
  background: #ece5d6;
}
.sx-ov__open:focus-visible,
.sx-ov__close:focus-visible {
  outline: 2px solid #a8321f;
  outline-offset: 2px;
}
.sx-ov__page {
  max-width: 560px;
  padding: 48px 24px;
}
.sx-ov__kicker {
  margin: 0 0 10px;
  font-size: .85rem;
  color: #a8321f;
}
.sx-ov__headline {
  margin: 0 0 12px;
  font-family: "Fraunces", serif;
  font-size: clamp(1.6rem, 5vw, 2.3rem);
  line-height: 1.15;
  color: #1c1a17;
}
.sx-ov__deck {
  margin: 0;
  color: #4a443b;
}
.sx-ov__panel {
  position: absolute;
  inset: 0;
  z-index: 3;
  padding: 64px clamp(20px, 6vw, 64px) 32px;
  background: #1c1a17;
  opacity: 1;
  transition: opacity .2s;
}
.sx-ov__panel[hidden] {
  display: none;
}
.sx-ov__close {
  position: absolute;
  top: 12px;
  right: 16px;
  color: #fbf8f2;
}
.sx-ov__close:hover {
  background: #36322b;
}
.sx-ov__close:focus-visible {
  outline-color: #f2b33d;
}
.sx-ov__form {
  max-width: 640px;
}
.sx-ov__title {
  display: block;
  margin-bottom: 8px;
  font-size: .9rem;
  color: #d9d2c3;
}
.sx-ov__form input {
  box-sizing: border-box;
  width: 100%;
  margin: 0;
  padding: 6px 0 10px;
  font-family: "Fraunces", serif;
  font-size: clamp(1.6rem, 6vw, 2.6rem);
  color: #fbf8f2;
  background: transparent;
  border: 0;
  border-bottom: 2px solid #8f877a;
  border-radius: 0;
  -webkit-appearance: textfield;
  appearance: textfield;
}
.sx-ov__form input::placeholder {
  color: #a59d90;
  opacity: 1;
}
.sx-ov__form input:focus {
  outline: none;
  border-bottom-color: #f2b33d;
}
.sx-ov__sub {
  margin: 28px 0 10px;
  font-size: .9rem;
  color: #d9d2c3;
}
.sx-ov__trend {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin: 0;
  padding: 0;
  list-style: none;
}
.sx-ov__trend button {
  min-height: 40px;
  padding: 0 16px;
  font: inherit;
  font-size: .95rem;
  color: #fbf8f2;
  background: #36322b;
  border: 1px solid #5a544a;
  border-radius: 999px;
  cursor: pointer;
}
.sx-ov__trend button:hover {
  background: #4a453c;
}
.sx-ov__trend button:focus-visible {
  outline: 2px solid #f2b33d;
  outline-offset: 2px;
}
@media (prefers-reduced-motion: no-preference) {
  .sx-ov__panel {
    transition: opacity .2s, translate .2s;
  }
  @starting-style {
    .sx-ov__panel {
      opacity: 0;
      translate: 0 -12px;
    }
  }
}

Docs filter with a live results count

  • role="status"
  • hidden
  • debounce

9 articles

  • Connect a bank accountSetup
  • Send your first invoiceInvoices
  • Recurring invoice schedulesInvoices
  • Export a VAT reportReports
  • Invite your accountantTeam
  • Match card paymentsBanking
  • Refund a paid invoiceInvoices
  • Close the monthReports
  • Two-step sign-inSecurity

Typing filters a local list of help articles by toggling the hidden attribute. The count sits in a role="status" live region and updates after a short pause, so screen readers hear one summary instead of a word per keystroke.

View code
.sx-docs {
  box-sizing: border-box;
  width: min(100%, 440px);
  padding: 20px;
  background: #fff;
  border: 1px solid #dde3ea;
  border-radius: 14px;
}
.sx-docs__search {
  display: block;
}
.sx-docs__label {
  display: block;
  margin-bottom: 6px;
  font-size: .9rem;
  font-weight: 600;
  color: #0f2537;
}
.sx-docs__form input {
  box-sizing: border-box;
  width: 100%;
  height: 44px;
  margin: 0;
  padding: 0 12px;
  font: inherit;
  font-size: max(16px, 1rem);
  color: #0f2537;
  background: #f5f8fb;
  border: 1px solid #6e7f8f;
  border-radius: 8px;
  -webkit-appearance: textfield;
  appearance: textfield;
}
.sx-docs__form input::placeholder {
  color: #566676;
  opacity: 1;
}
.sx-docs__form input:focus {
  outline: 2px solid #0b6bcb;
  outline-offset: 1px;
  border-color: #0b6bcb;
  background: #fff;
}
.sx-docs__count {
  margin: 12px 0 6px;
  font-size: .85rem;
  color: #3e5163;
}
.sx-docs__list {
  margin: 0;
  padding: 0;
  list-style: none;
}
.sx-docs__list li {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 9px 2px;
  font-size: .95rem;
  color: #0f2537;
  border-top: 1px solid #edf1f5;
}
.sx-docs__list li[hidden] {
  display: none;
}
.sx-docs__tag {
  flex: none;
  padding: 2px 8px;
  font-size: .75rem;
  color: #0a4f96;
  background: #e6f0fb;
  border-radius: 4px;
}

Find in page with highlighted matches

  • mark
  • createTextNode
  • aria-label count

Proofing sourdough in a cold kitchen

Below 20 degrees the dough slows down, which is not a problem as long as you stop watching the clock. Give the dough an extra hour and judge it by feel: it should hold a gentle dent.

A warm oven with only the light switched on makes a decent proofing box. Cover the bowl so the surface of the dough does not dry out and form a skin.

A find bar wraps every match in a mark element, rebuilt from text nodes so no HTML string is ever parsed. Enter or the arrow buttons step through matches, the current one gets a stronger color, and the count reads as 2 of 5 in a status region.

View code
.sx-find {
  box-sizing: border-box;
  width: min(100%, 520px);
  overflow: hidden;
  background: #fffdf8;
  border: 1px solid #e4dccb;
  border-radius: 14px;
}
.sx-find__bar {
  display: flex;
  align-items: center;
  gap: 4px;
  padding: 8px 8px 8px 12px;
  background: #f3eee2;
  border-bottom: 1px solid #e4dccb;
}
.sx-find__vh {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
.sx-find__bar input {
  flex: 1;
  min-width: 0;
  height: 40px;
  margin: 0;
  padding: 0 10px;
  font: inherit;
  font-size: max(16px, 1rem);
  color: #26201a;
  background: #fff;
  border: 1px solid #8a7f6c;
  border-radius: 8px;
  -webkit-appearance: textfield;
  appearance: textfield;
}
.sx-find__bar input:focus {
  outline: 2px solid #8a4b0f;
  outline-offset: 1px;
}
.sx-find__count {
  flex: none;
  min-width: 4.5em;
  padding: 0 6px;
  font-size: .85rem;
  color: #4d4337;
  text-align: center;
  font-variant-numeric: tabular-nums;
}
.sx-find__nav {
  display: grid;
  place-items: center;
  flex: none;
  width: 40px;
  height: 40px;
  padding: 0;
  color: #26201a;
  background: none;
  border: 0;
  border-radius: 8px;
  cursor: pointer;
}
.sx-find__nav:hover {
  background: #e6dfcf;
}
.sx-find__nav:focus-visible {
  outline: 2px solid #8a4b0f;
  outline-offset: -2px;
}
.sx-find__text {
  padding: 18px 20px 6px;
}
.sx-find__title {
  margin: 0 0 10px;
  font-family: "Fraunces", serif;
  font-size: 1.3rem;
  font-weight: 600;
  line-height: 1.25;
  letter-spacing: 0;
  color: #26201a;
}
.sx-find__text p {
  margin: 0 0 14px;
  font-size: .95rem;
  line-height: 1.6;
  color: #3a3229;
}
.sx-find__mark {
  padding: 0 1px;
  color: #26201a;
  background: #ffe8a3;
  border-radius: 2px;
}
.sx-find__mark.is-current {
  color: #fff;
  background: #a34700;
  box-shadow: 0 0 0 2px #a34700;
}

Search with a voice button

  • aria-pressed
  • role="status"
  • keyframes

Tap the microphone and say a show name.

The microphone is a toggle button with aria-pressed, and its listening state drives the red fill, the level bars and the status text. This demo stops at the UI and records nothing; to make it real, start speech recognition on press and write the transcript into the field.

View code
.sx-voice {
  width: min(100%, 460px);
}
.sx-voice__search {
  display: block;
}
.sx-voice__form {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 6px 6px 6px 18px;
  color: #4a3f63;
  background: #fff;
  border: 1px solid #cfc7e3;
  border-radius: 999px;
  box-shadow: 0 10px 28px rgba(54, 38, 102, .1);
}
.sx-voice__form:focus-within {
  border-color: #5b3fd1;
  box-shadow: 0 0 0 3px #d9d0fb;
}
.sx-voice__form > svg {
  flex: none;
}
.sx-voice__vh {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
.sx-voice__form input {
  flex: 1;
  min-width: 0;
  height: 44px;
  margin: 0;
  padding: 0;
  font: inherit;
  font-size: max(16px, 1rem);
  color: #221a38;
  background: transparent;
  border: 0;
  -webkit-appearance: textfield;
  appearance: textfield;
}
.sx-voice__form input:focus {
  outline: none;
}
.sx-voice__form input::placeholder {
  color: #625a78;
  opacity: 1;
}
.sx-voice__mic {
  display: grid;
  place-items: center;
  flex: none;
  width: 46px;
  height: 46px;
  padding: 0;
  color: #fff;
  background: #5b3fd1;
  border: 0;
  border-radius: 50%;
  cursor: pointer;
  transition: background-color .15s;
}
.sx-voice__mic:hover {
  background: #4429b3;
}
.sx-voice__mic:focus-visible {
  outline: 2px solid #5b3fd1;
  outline-offset: 3px;
}
.sx-voice__mic[aria-pressed="true"] {
  background: #c62a3d;
}
.sx-voice__mic[aria-pressed="true"]:focus-visible {
  outline-color: #c62a3d;
}
.sx-voice__foot {
  display: flex;
  align-items: center;
  gap: 12px;
  min-height: 32px;
  margin-top: 14px;
  padding: 0 18px;
}
.sx-voice__bars {
  display: flex;
  align-items: center;
  gap: 3px;
  height: 22px;
}
.sx-voice__bars span {
  width: 4px;
  height: 6px;
  background: #b4a9d6;
  border-radius: 2px;
}
.sx-voice.is-listening .sx-voice__bars span {
  background: #c62a3d;
}
.sx-voice.is-listening .sx-voice__bars span:nth-child(2),
.sx-voice.is-listening .sx-voice__bars span:nth-child(4) {
  height: 14px;
}
.sx-voice.is-listening .sx-voice__bars span:nth-child(3) {
  height: 20px;
}
.sx-voice__status {
  margin: 0;
  font-size: .9rem;
  color: #4a3f63;
}
@media (prefers-reduced-motion: no-preference) {
  .sx-voice.is-listening .sx-voice__bars span {
    animation: sx-voice-level .9s ease-in-out infinite alternate;
  }
  .sx-voice.is-listening .sx-voice__bars span:nth-child(2) {
    animation-delay: -.3s;
  }
  .sx-voice.is-listening .sx-voice__bars span:nth-child(3) {
    animation-delay: -.6s;
  }
  .sx-voice.is-listening .sx-voice__bars span:nth-child(4) {
    animation-delay: -.15s;
  }
}
@keyframes sx-voice-level {
  from {
    height: 4px;
  }
  to {
    height: 22px;
  }
}

Styling the field itself

Three looks for the input alone: a restyled native clear button, a dark theme with scope pills and a minimal underline field.

Restyled native clear button

  • ::-webkit-search-cancel-button
  • data URI SVG
  • appearance: none

Click into the field to see the focus state.

The field ships with a value, so the clear button appears once you focus or hover it. Setting appearance: none on ::-webkit-search-cancel-button removes the stock glyph, and a sized circle with an SVG cross takes its place. The pseudo-element is non-standard and works only in WebKit and Blink browsers such as Safari, Chrome and Edge; other engines ignore the rule.

View code
.sx-clr {
  width: min(100%, 400px);
}
.sx-clr__search {
  display: block;
}
.sx-clr__label {
  display: block;
  margin-bottom: 8px;
  font-weight: 600;
  color: #24164f;
}
.sx-clr__box {
  position: relative;
  color: #4b3d85;
}
.sx-clr__box svg {
  position: absolute;
  top: 15px;
  left: 16px;
  pointer-events: none;
}
.sx-clr__box input {
  box-sizing: border-box;
  width: 100%;
  height: 50px;
  margin: 0;
  padding: 0 12px 0 46px;
  font: inherit;
  font-size: max(16px, 1rem);
  color: #1a1238;
  background: #f4f1ff;
  border: 2px solid #6d5bc6;
  border-radius: 14px;
  -webkit-appearance: textfield;
  appearance: textfield;
}
.sx-clr__box input::placeholder {
  color: #5a4f80;
  opacity: 1;
}
.sx-clr__box input:focus {
  outline: 3px solid #b8acf5;
  outline-offset: 0;
  border-color: #3d2c8d;
  background: #fff;
}
.sx-clr__box input::-webkit-search-decoration {
  -webkit-appearance: none;
}
.sx-clr__box input::-webkit-search-cancel-button {
  -webkit-appearance: none;
  appearance: none;
  width: 26px;
  height: 26px;
  margin-left: 8px;
  border-radius: 50%;
  background: #3d2c8d url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M7 7l10 10M17 7 7 17' fill='none' stroke='%23fff' stroke-width='2.6' stroke-linecap='round'/%3E%3C/svg%3E") center / 14px no-repeat;
  cursor: pointer;
}
.sx-clr__box input::-webkit-search-cancel-button:hover {
  background-color: #24164f;
}

Dark theme with scope pills

  • color-scheme: dark
  • custom properties
  • radio pills
Search in

Click into the field to see the focus state.

Colors live in custom properties, and color-scheme: dark makes the native parts, such as the caret and the clear button, match. The scope pills are real radio buttons in a fieldset, hidden visually but not from keyboards, so arrow keys move between them.

View code
.sx-dark {
  --sx-dark-bg: #16181d;
  --sx-dark-field: #23262e;
  --sx-dark-border: #6b7182;
  --sx-dark-text: #eef0f4;
  --sx-dark-muted: #a9afbd;
  --sx-dark-accent: #7ee0b5;
  box-sizing: border-box;
  width: min(100%, 480px);
  padding: 22px;
  color-scheme: dark;
  background: var(--sx-dark-bg);
  border: 1px solid #2c3039;
  border-radius: 18px;
}
.sx-dark__search {
  display: block;
}
.sx-dark__vh {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
.sx-dark__box {
  position: relative;
  color: var(--sx-dark-muted);
}
.sx-dark__box svg {
  position: absolute;
  top: 14px;
  left: 14px;
  pointer-events: none;
}
.sx-dark__box input {
  box-sizing: border-box;
  width: 100%;
  height: 48px;
  margin: 0;
  padding: 0 14px 0 44px;
  font: inherit;
  font-size: max(16px, 1rem);
  color: var(--sx-dark-text);
  background: var(--sx-dark-field);
  border: 1px solid var(--sx-dark-border);
  border-radius: 10px;
  caret-color: var(--sx-dark-accent);
  -webkit-appearance: textfield;
  appearance: textfield;
}
.sx-dark__box input::placeholder {
  color: var(--sx-dark-muted);
  opacity: 1;
}
.sx-dark__box input:focus {
  outline: 2px solid var(--sx-dark-accent);
  outline-offset: 2px;
  border-color: var(--sx-dark-accent);
}
.sx-dark__scope {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  min-inline-size: 0;
  margin: 14px 0 0;
  padding: 0;
  border: 0;
}
.sx-dark__scope label {
  position: relative;
}
.sx-dark__radio {
  position: absolute;
  inset: 0;
  margin: 0;
  opacity: 0;
  cursor: pointer;
}
.sx-dark__scope span {
  display: inline-flex;
  align-items: center;
  min-height: 34px;
  padding: 0 14px;
  font-size: .9rem;
  color: var(--sx-dark-text);
  background: #262a33;
  border: 1px solid #3a3f4b;
  border-radius: 999px;
  transition: background-color .15s, color .15s;
}
.sx-dark__radio:hover + span {
  border-color: var(--sx-dark-border);
}
.sx-dark__radio:checked + span {
  font-weight: 600;
  color: #0d1f17;
  background: var(--sx-dark-accent);
  border-color: var(--sx-dark-accent);
}
.sx-dark__radio:focus-visible + span {
  outline: 2px solid var(--sx-dark-accent);
  outline-offset: 2px;
}

Minimal underline search

  • border-bottom
  • scaleX underline
  • Fraunces

Click into the field to see the focus state.

Only a bottom rule, large serif type and an arrow button. On focus a darker line draws across from the left; with reduced motion it simply appears. The resting rule passes the 3:1 contrast needed to identify the field, so it does not depend on the focus animation.

View code
.sx-line {
  box-sizing: border-box;
  width: min(100%, 520px);
  padding: 36px clamp(18px, 5vw, 28px);
  background: #f4efe6;
  border-radius: 4px;
}
.sx-line__search {
  display: block;
}
.sx-line__label {
  display: block;
  margin-bottom: 4px;
  font-size: .9rem;
  color: #5a5146;
}
.sx-line__row {
  position: relative;
  display: flex;
  align-items: center;
  gap: 8px;
  border-bottom: 2px solid #8b8578;
}
.sx-line__row::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: -2px;
  height: 3px;
  background: #a6341c;
  transform: scaleX(0);
  transform-origin: left;
}
.sx-line__row:focus-within::after {
  transform: scaleX(1);
}
.sx-line__row input {
  flex: 1;
  min-width: 0;
  margin: 0;
  padding: 8px 0;
  font-family: "Fraunces", serif;
  font-size: clamp(1.3rem, 5vw, 1.7rem);
  color: #231e18;
  background: transparent;
  border: 0;
  border-radius: 0;
  -webkit-appearance: textfield;
  appearance: textfield;
}
.sx-line__row input:focus {
  outline: none;
}
.sx-line__row input::placeholder {
  color: #6b6255;
  opacity: 1;
}
.sx-line__go {
  display: grid;
  place-items: center;
  flex: none;
  width: 44px;
  height: 44px;
  padding: 0;
  color: #231e18;
  background: none;
  border: 0;
  border-radius: 50%;
  cursor: pointer;
  transition: color .15s, background-color .15s;
}
.sx-line__go:hover {
  color: #a6341c;
  background: #ebe3d4;
}
.sx-line__go:focus-visible {
  outline: 2px solid #a6341c;
  outline-offset: 2px;
}
@media (prefers-reduced-motion: no-preference) {
  .sx-line__row::after {
    transition: transform .35s ease;
  }
}

What is a CSS search box?

Strip away the styling and you have an input field and a button inside a form. CSS controls how it looks, how its states behave, how it lays out and how it responds on small screens.

That’s where the styling layer ends. Submitting the query, matching results and suggesting terms belong to the form action, the server and JavaScript.

Like any user interface component, a search box has a fixed anatomy. The form wraps everything and carries the action and the method. Inside it sit the text field where the query goes and the submit button, which is often just an icon. The magnifying glass on that button gets drawn with SVG, a background image or an icon font. Add the placeholder, the hint text visible while the field is empty, and that’s every part.

Each part is plain HTML until CSS gives it shape, which means the markup decisions in the next section limit every style choice after them.

You’ll usually find the box in the header or a navbar. Some sites put it at the top of the content area, and others hide it behind an icon that opens a fullscreen overlay.

A styled box with nothing behind its form action is only a picture of a search box.

Which HTML markup does a search box need?

Skip any of the pieces (a form with an action, a labeled input with a name, a submit button, a search landmark) and either the query doesn’t submit or assistive technology can’t find the box.

Here’s the full pattern using the native search element:

<search>
  <form action="/search" method="get">
    <label for="q">Search</label>
    <input type="search" id="q" name="q" placeholder="Search articles">
    <button type="submit">Search</button>
  </form>
</search>

Input type search or text

Browser support isn’t the question here. type="search" has been Baseline Widely Available since January 29, 2018 (web-features data).

Functionally it matches a text field. Browsers are free to give it their own styling, and some add a clear (x) button (MDN).

Use type="search" for a real search query, as long as you’re willing to reset the WebKit defaults. Stick with type="text" when the design has to render identically everywhere with zero reset work, or when a shared component library already styles text inputs and you can’t touch it.

Many popular tutorials use type="text" and skip the reset. The semantic search field is worth the extra CSS, in my view.

Label, button and landmark

Tie a label to the input through for and id. Placeholder text is not a replacement for it (MDN). The input also needs a name attribute, because without one the field value is not submitted (MDN).

For the button, button type="submit" beats input type="submit", since only a button can hold an SVG or a pseudo-element. Speckyboy’s search box tutorial switched from an input to a button for exactly that reason.

The label, name and button rules are the same basics behind any accessible form pattern.

The HTML specification added the search element on March 24, 2023 (Scott O’Hara). Safari 17, Firefox 118, Chrome 118 and Edge 118 all shipped it between September 18 and October 13, 2023.

The web-features data lists it as Baseline 2023, and it reached Baseline Widely Available status on April 13, 2026.

The element wraps the form instead of replacing it, and it never holds the results themselves. When search runs entirely in JavaScript, the form and submit button are optional (MDN).

For browsers without support, put role="search" on the form, which exposes the same landmark through the ARIA search role.

Core styles for the input and the button

The base look comes down to border, border-radius, padding, font-size, background and box-shadow. Everything else is polish.

  • A 1px solid border in a mid-dark grey.
  • border-radius at 999px gives a pill, and 4px to 8px gives a soft rectangle.
  • Vertical padding sets the height. The icon side needs extra.
  • Set font-size to 1rem after font: inherit, because form controls do not pass on page fonts by default.
  • Always set background and color together, never one alone.
  • Keep box-shadow soft and outer for lift. Inset only works for a pressed look.
.search-form input[type="search"] {
  padding: 0.65rem 1rem;
  font: inherit;
  font-size: 1rem;
  color: #1a1a1a;
  background: #fff;
  border: 1px solid #767676;
  border-radius: 999px;
  box-shadow: 0 1px 2px rgb(0 0 0 / 0.08);
}

Keep colors in CSS custom properties such as --search-border and --search-accent, so a dark theme swaps values instead of rewriting rules.

A box shadow generator shortens the trial and error on shadow values.

Hongkiat’s CSS3 search field tutorial stacks several box-shadows, inset ones included, to fake depth. It looks rich, but every theme change means editing each layer.

How to place a search icon inside the input

The usual approach is an inline SVG inside a submit button. Position that button absolutely over the right edge of a relatively positioned wrapper, then add right padding to the field so typed text stops before the icon.

MethodClickable iconFailure modePick when
Absolute button over the fieldYesText runs under the icon without right paddingDefault for most sites
Flexbox siblingsYesField and button need matching height and radiusButton sits beside the field
Background imageNoIcon can overlap typed text after blurPlaceholder-only fields
Icon font or SVG in the buttonYesFont file adds a request, SVG needs a set sizeIcon-only designs

Layout choices

The absolute pattern comes down to position: relative on the wrapper and position: absolute on the button, plus right padding on the field so text doesn’t run under the icon.

MIT’s Sun Lab, Georgia Tech’s GTISC lab and Michigan State’s engineering site serve an identical search-box.css built this way, with inset: 0 0 0 auto, a square aspect-ratio on the button and padding-right: 40px on the field.

Flexbox is the other option. Put the field and button side by side as siblings under display: flex, with matched heights and radii so the join looks clean. CSS Grid is worth it only when the same row also holds a select or a filter.

Icon sources

A background image is fine for a purely decorative icon. The known catch is an icon that reappears over typed text once the field loses focus.

W3Schools’ search button example places a Font Awesome icon inside the button, and the cost is a font download for one glyph.

Inline SVG gets my vote. It needs no extra request and inherits currentColor. It also lives inside the button, which matters because a pseudo-element on an input never renders.

Keeping the SVG directly in the markup also lets CSS recolor the icon on hover and focus.

An icon-only button needs an accessible name, so add visually hidden text or aria-label="Search".

Hover, focus and placeholder states

Hover goes on the button, focus on the field, and :focus-within on the wrapper. Two more selectors handle the placeholder: ::placeholder for the hint text and :placeholder-shown for the empty field.

Hover and focus-within

Put :focus-within on the form wrapper so the border and shadow wrap the field and the button together. It has been available across browsers since January 2020 (MDN) and reached Baseline Widely Available status on July 15, 2022 (web-features data).

Hover styles belong on the button and stay small: a background shift, a slight lift. Bigger hover effects suit a gallery card and look noisy on a search button.

Touch screens have no hover, so no state should depend on it.

Keeping focus visible

W3C’s Understanding document for WCAG 1.4.11 sets a minimum of 3:1 between a field boundary or focus indicator and the colors next to it.

Two of its examples: a #767676 grey border on white passes, while a #FFF000 yellow star rating on white fails at 1.2:1.

Many tutorials, Kirupa’s among them, remove the default outline early with outline: none. That is fine only if a replacement follows, such as a 2px outline or a box-shadow ring in a dark accent, which gives keyboard users the visible indicator WCAG 2.4.7 Focus Visible asks for.

Check the result with a color contrast checker instead of judging by eye.

Placeholder text

Use ::placeholder for the hint text. The field itself, while that hint is showing, gets targeted with :placeholder-shown instead.

Set the color explicitly, because most browsers default to a translucent or light gray placeholder (MDN). Keep it lighter than typed text but still readable, since placeholder text with strong contrast can be mistaken for entered input (MDN).

Pair :not(:placeholder-shown) with a sibling selector to reveal a clear control only after typing.

How to expand a search box on focus

Start the wrapper narrow and widen it on :focus-within, with a transition on width doing the animation. No JavaScript is involved.

  1. Set the wrapper to a compact width, such as 12rem, with max-width: 100%.
  2. Keep the icon button absolutely positioned so it stays put while the field grows.
  3. Add transition: width 0.3s ease to the wrapper.
  4. Set the wider width, such as 20rem, on :focus-within.
  5. Turn the transition off for users who ask for reduced motion.
.search-form {
  width: 12rem;
  max-width: 100%;
  transition: width 0.3s ease;
}

.search-form:focus-within {
  width: 20rem;
}

@media (prefers-reduced-motion: reduce) {
  .search-form {
    transition: none;
  }
}

MDN lists the reduced-motion query as available across browsers since January 2020, so there is no reason to skip step five.

An expanding field is a textbook micro-interaction: small, instant, tied to one action.

Trigger options

:focus-within is the default pick. It fires from a click, a tap or the keyboard, and it works on the wrapper. Plain :focus only makes sense when the field itself is the element changing size. Hover on the icon is mouse only, so keyboard and touch users never see the expansion.

Treat hover as a bonus for mouse users, never as the only trigger.

Where the expanding box breaks

MDN lists the any-hover media feature as available across browsers since December 2018, so hover-only styles can sit behind a capability check.

Android adds a wrinkle. Some versions emulate hover on a long press, which is why Michelle Barker’s write-up pairs (hover: hover) with (pointer: fine) to detect real hover.

  • Animating width forces layout on every frame, so neighbors in a flex navbar slide around. Reserve the expanded width, or let the field grow over its neighbors with absolute positioning.
  • Click-to-open and fullscreen overlays toggle state, so they need JavaScript.

Resetting the native search input appearance

Start with appearance: textfield on the field, which hands border and padding control back to you. The clear (x) that WebKit and Blink draw is a separate decision, so deal with it afterward by removing or restyling it.

What WebKit adds by default

WebKit styles a search input before any author CSS runs. That means an inset border and rounded corners, with strict typographic control (CSS-Tricks), and tight limits on which properties author CSS can change (CSS-Tricks). Once the field holds text, a clear (x) shows up too (MDN).

The normalize.css documentation names -webkit-appearance: textfield as the fix that keeps the benefits of search inputs. CSS-Tricks lists both none and textfield, and the choice matters, because MDN notes that some widgets disappear completely under appearance: none.

In practice that means textfield on the input and none only on the decoration pseudo-elements.

input[type="search"] {
  -webkit-appearance: textfield;
  appearance: textfield;
}

input[type="search"]::-webkit-search-decoration,
input[type="search"]::-webkit-search-cancel-button,
input[type="search"]::-webkit-search-results-button,
input[type="search"]::-webkit-search-results-decoration {
  -webkit-appearance: none;
  appearance: none;
}

Search inputs are a textbook cross-browser compatibility problem: identical markup, different defaults per engine.

The clear button

MDN marks ::-webkit-search-cancel-button as non-standard, supported only in WebKit and Blink, and visible only when the field is not empty.

ApproachClear (x) buttonBorder and padding control
Leave defaultsShown once text is typedRestricted
appearance: textfieldStill shownNormal
textfield plus hidden cancel buttonHidden, add a custom controlNormal

Hiding the button removes a built-in way to clear the field, so pair the hide rule with a custom clear control. Restyling instead of hiding goes through the same pseudo-element, and engines outside WebKit and Blink ignore the rule.

One documented exception: an early normalize.css release noted that Safari, unlike Chrome, clips the cancel button when a search input has padding under textfield appearance.

That note dates from early releases, so retest padding in current Safari before relying on it.

How to make a search box responsive

Make the wrapper fluid and let the field take whatever space is left. Keep the input text at 16px or more, otherwise iOS Safari zooms in on focus.

Fluid width and navbar placement

Give the wrapper width: 100% and a max-width around 32rem.

Let the field take the leftover row space with flex: 1 and min-width: 0, so the button is never pushed out of the box.

On a phone the box usually becomes a full-width bar under the logo, or an icon that opens the field. Tablet and desktop get a fixed-width field pushed right with margin-left: auto. In a navbar, W3Schools’ topnav example floats the input right and stacks links and search vertically on narrow screens.

That stack is a plain media query breakpoint. Set it where the layout actually breaks, not at a device width.

iOS focus zoom

iOS Safari zooms into a focused field whose rendered text is below 16px (Telerik knowledge base, West Wind 2023). A Shopware theme vendor’s documentation adds that the page does not zoom back out after blur.

The 1rem field size holds only while the root font size stays at 16px. West Wind lists a smaller root size as a common reason the zoom comes back.

When the root is out of your hands, font-size: max(16px, 1rem) sets a floor.

Raising the font size keeps pinch zoom intact, though the field needs a layout check at the larger size.

The other route is locking zoom with the viewport tag. Telerik’s knowledge base proposes maximum-scale=1 and says manual zoom still works. A 2016 draft technique posted to a W3C mailing list, on the other hand, treats restricted scale as a failure of WCAG 1.4.4 Resize Text on touch devices, with user-scalable=no and maximum-scale=1.5 as failing examples.

The sources disagree on whether locking is harmless, and the W3C reading is the stricter one. Raise the font size and leave the viewport meta tag alone.

Where a CSS search box stops working

Looks end where behavior begins. Running the query and suggesting terms sit outside CSS, and a few of its tricks also stop at the browser engine or the input device.

Behavior that needs a script or a server

  • Live filtering of a list on the page
  • Suggestion dropdowns
  • Result counts and match highlighting
  • Searching without a page reload

Each item needs JavaScript for the interaction and data from somewhere. Slider Revolution’s roundup labels its functional animated example as HTML, CSS and JavaScript for exactly that reason.

The query still lands on a backend endpoint that returns results, and the search box only styles the way there.

To react when someone clears a search field, scripts listen for the search event, which MDN marks as non-standard.

Failure points on touch and small screens

WCAG 2.2 SC 2.5.8 sets a minimum pointer target of 24 by 24 CSS pixels, with spacing and equivalent-control exceptions (W3C).

An icon-only button drawn at 16 by 16 pixels with no padding fails unless the spacing exception applies. Size the button with padding or min-width and min-height, not the SVG.

Numbers to keep in mind

iOS Safari zooms into a focused field when the text is below 16px (Telerik, West Wind 2023). The icon button needs a pointer target of at least 24 by 24 CSS pixels (W3C, WCAG 2.2 SC 2.5.8). Text has to resize to 200% and still work, which a locked viewport scale breaks (W3C, WCAG 1.4.4).

The any-hover media feature has been available across browsers since December 2018 (MDN). Support for the cancel button pseudo-element, meanwhile, is limited to WebKit and Blink only (MDN).

CSS Search Box FAQ

How do I center a search box in CSS?

Make the parent a flex container with justify-content: center, or give the form a max-width and margin-inline: auto.

Both keep the box centered as the viewport narrows.

How wide should a search box be?

Wide enough for a typical query, which design systems put at about 27 characters. Queensland Government’s design system defaults to that width, and the University of Virginia’s sets it as the minimum.

In CSS, width: 27ch with max-width: 100% is a workable starting point.

Can a search box support dark mode without extra markup?

Yes. Keep colors in custom properties and redefine them inside @media (prefers-color-scheme: dark).

Recheck the border and focus ring against the dark surface, because a border that passes on white can fail on a dark background.

Does a search box need a visible submit button?

No. A form with one text field submits on Enter, and Slider Revolution’s roundup includes a placeholder-only box built with HTML and CSS alone.

Keep a visually hidden submit button in the markup so every user still has a submit control.

What to Test Before a CSS Search Box Goes Live

Before launch, check keyboard focus and current Safari, then put the page on a real iPhone.

  • Tab to the field and confirm a visible focus ring
  • Type, blur and clear in Safari with the reset applied
  • Focus the field on an iPhone and watch for page zoom

Keyboard comes first because the test needs no device and the failure repeats on every page. The phone test comes last, as the slowest to run.

The trade-off is that the larger text that stops iOS zoom makes a compact header box bigger on phones.

MDN lists the cancel button pseudo-element as non-standard (verified October 2026), so the Safari step stays.

The keyboard and phone checks apply unchanged to the other fields in any CSS form.