CSS appearance: none, control by control

appearance: none tells the browser to stop drawing a form control its own way. The control keeps working. Everything it looked like becomes your job.

appearance: none stops the browser from drawing a form control in its native style. On a checkbox, the box and tick go. On a select, the arrow goes. On a range slider, the track goes.

The control still works, still submits and still takes focus. You now draw every state yourself.

Press the button below. The same six controls switch between appearance: auto and appearance: none, and the numbers show each one's rendered size.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>appearance: auto vs none</title>
<style>
  body { margin: 0; padding: 14px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  #flip {
    font: 600 14px system-ui, sans-serif; padding: 9px 14px; margin-bottom: 12px;
    border: 0; border-radius: 8px; background: #1d4ed8; color: #fff; cursor: pointer;
  }
  .set { background: #fff; border-radius: 12px; padding: 4px 14px; }
  .row {
    display: grid; grid-template-columns: 80px 1fr 64px; align-items: center; gap: 8px;
    min-height: 46px; border-bottom: 1px solid #eceef1; font-size: 14px;
  }
  .row:last-child { border-bottom: 0; }
  .row > :nth-child(2) { justify-self: start; max-width: 100%; }
  .size { font: 12px ui-monospace, Consolas, monospace; color: #6b7280; text-align: right; }

  /* the whole experiment: one declaration on every control */
  .set.none :is(input, select, button) {
    -webkit-appearance: none;
    appearance: none;
  }
</style>
</head>
<body>
<button id="flip" type="button">Switch to appearance: none</button>

<div class="set" id="set">
  <div class="row"><span>checkbox</span><input type="checkbox" checked><span class="size"></span></div>
  <div class="row"><span>radio</span><input type="radio" checked><span class="size"></span></div>
  <div class="row"><span>select</span><select><option>Monthly</option><option>Yearly</option></select><span class="size"></span></div>
  <div class="row"><span>range</span><input type="range"><span class="size"></span></div>
  <div class="row"><span>button</span><button type="button">Save</button><span class="size"></span></div>
  <div class="row"><span>search</span><input type="search" value="shoes"><span class="size"></span></div>
</div>

<script>
  const set = document.getElementById('set');
  const flip = document.getElementById('flip');

  // print each control's rendered width x height next to it
  function measure() {
    set.querySelectorAll('.row').forEach((row) => {
      const r = row.children[1].getBoundingClientRect();
      row.querySelector('.size').textContent = Math.round(r.width) + ' x ' + Math.round(r.height);
    });
  }

  flip.addEventListener('click', () => {
    const on = set.classList.toggle('none');
    flip.textContent = on ? 'Switch to appearance: auto' : 'Switch to appearance: none';
    measure();
  });

  measure();
</script>
</body>
</html>
One declaration on six controls. Watch the checkbox and radio drop to 0 x 0.

The checkbox and the radio vanish completely. The select loses its arrow and shrinks. The button falls back to a plain grey face with a hard border. That is the starting point you style from.

What appearance: none removes on each control

The property only changes how a control is drawn. The element, its value and its keyboard behaviour stay the same. Anything the browser painted for you, though, is gone.

The drawing goes. The behaviour and the state selectors stay.
The drawing goes. The behaviour and the state selectors stay.
Control What disappears What you add
Checkbox, radio The box, tick, circle and dot, and the size width, height, border, a :checked style
<select> The arrow Right padding and your own arrow
Range slider The track Track and thumb through their pseudo-elements
Button The native face border, background, padding, font
Search input The native field style A border and padding. The clear (x) is a separate part

One more thing stops working: accent-color. It only tints the native drawing, so it has nothing left to colour. Use background on :checked instead.

Why the checkbox vanishes

A checkbox with appearance: none has no drawing and no default size. In the demo above it renders at 0 by 0 pixels.

The input is still there: a click on its label ticks it, and Space still toggles it once it has focus. Nobody can see it, though.

Without a size, the box is gone. A width, a height and a border bring it back.
Without a size, the box is gone. A width, a height and a border bring it back.

So the first three lines after appearance: none are always the same:

input[type="checkbox"] {
  appearance: none;
  width: 22px; height: 22px;
  border: 2px solid #6b7280;
}

The radio button works the same way, with border-radius: 50%. The checkbox guide covers values, labels and Select All. If you want an on/off switch instead of a tick, the toggle switch builds one from the same input.

Rebuild the states: checked, focus, disabled

Every state the browser used to show is now blank until you style it. Work through them in order.

Four states to draw after appearance: none, in the order to write them.
Four states to draw after appearance: none, in the order to write them.
  1. Base. Width, height, border and radius, as above.
  2. Checked. :checked changes the fill. A ::before pseudo-element draws the tick or the dot. It works on inputs once appearance is none.
  3. Focus. Keep a visible :focus-visible ring. The browser's default focus outline still applies, but a reset that sets outline: none removes it, and nothing replaces it.
  4. Disabled. :disabled greys the box. Dim the label too, with label:has(input:disabled).

Try the result with the mouse, then with Tab and Space:

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Rebuild a checkbox and radio after appearance: none</title>
<style>
  body { margin: 0; padding: 16px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  form { background: #fff; border-radius: 12px; padding: 14px 16px; }
  fieldset { border: 0; margin: 0 0 10px; padding: 0; }
  legend { font-weight: 700; font-size: 14px; margin-bottom: 6px; padding: 0; }
  label { display: flex; align-items: center; gap: 10px; padding: 5px 0; font-size: 15px; cursor: pointer; }

  input[type="checkbox"], input[type="radio"] {
    -webkit-appearance: none;
    appearance: none;
    margin: 0;
    /* 1. size: without it the box collapses to 0 x 0 */
    width: 22px; height: 22px; flex: none;
    border: 2px solid #6b7280; background: #fff;
    display: inline-grid; place-content: center;
    cursor: pointer;
  }
  input[type="checkbox"] { border-radius: 5px; }
  input[type="radio"] { border-radius: 50%; }

  /* 2. the mark, hidden until checked */
  input[type="checkbox"]::before {
    content: ""; width: 12px; height: 12px; transform: scale(0);
    background: #fff;
    clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0, 43% 62%);
  }
  input[type="radio"]::before {
    content: ""; width: 10px; height: 10px; border-radius: 50%; transform: scale(0);
    background: #1d4ed8;
  }

  /* 3. checked state */
  input[type="checkbox"]:checked { background: #1d4ed8; border-color: #1d4ed8; }
  input[type="radio"]:checked { border-color: #1d4ed8; }
  input:checked::before { transform: scale(1); }

  /* 4. keyboard focus ring */
  input:focus-visible { outline: 3px solid #f59e0b; outline-offset: 2px; }

  /* 5. disabled */
  input:disabled { border-color: #c4c8cf; background: #f1f2f4; cursor: not-allowed; }
  label:has(input:disabled) { color: #9aa0a9; cursor: not-allowed; }

  button { font: 600 14px system-ui, sans-serif; padding: 9px 14px; border: 0; border-radius: 8px; background: #1d4ed8; color: #fff; cursor: pointer; }
  output { display: block; margin-top: 10px; font: 13px ui-monospace, Consolas, monospace; color: #0f5132; min-height: 1.4em; }
</style>
</head>
<body>
<form id="f">
  <fieldset>
    <legend>Email me about</legend>
    <label><input type="checkbox" name="topic" value="news" checked> Product news</label>
    <label><input type="checkbox" name="topic" value="tips"> Weekly tips</label>
    <label><input type="checkbox" name="topic" value="beta" disabled> Beta invites (closed)</label>
  </fieldset>
  <fieldset>
    <legend>How often</legend>
    <label><input type="radio" name="freq" value="daily"> Daily</label>
    <label><input type="radio" name="freq" value="weekly" checked> Weekly</label>
  </fieldset>
  <button>Submit</button>
  <output id="out"></output>
</form>

<script>
  // the inputs still submit like normal ones: read them with FormData
  document.getElementById('f').addEventListener('submit', (e) => {
    e.preventDefault();
    const data = new FormData(e.target);
    document.getElementById('out').textContent =
      'topic=' + data.getAll('topic').join(',') + '  freq=' + data.get('freq');
  });
</script>
</body>
</html>
Checkbox and radio rebuilt from appearance: none. Submit shows what the form would send.

The submit line proves the point of the whole technique. The custom boxes send the same topic and freq values as plain inputs, because they are plain inputs. If your checkbox can be half-ticked from script, add an :indeterminate style as well.

Select, range, search and button: the extra parts

Some controls are built from several parts, and appearance: none on the element does not reach all of them.

  • Select. The arrow goes, so add padding-right and draw one with a background image. The open list is still drawn by the browser. The select tag guide covers what CSS can and cannot reach there.
  • Range. The track goes, but the thumb has its own reset. Target ::-webkit-slider-thumb and ::-moz-range-thumb. The range slider guide styles both parts in full.
  • Search. The clear (x) button that some browsers draw is a separate part. It stays until you hide it.
  • Button. The browser's own background and border stay until you replace them. The button CSS guide starts from this reset.

The two lines that catch people out:

input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; }
input[type="search"]::-webkit-search-cancel-button { display: none; }

A finished form

Here all four kinds of control share one reset and get their own drawing back. Every control also gets the same amber focus ring, so Tab through it to check.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>A form styled from appearance: none</title>
<style>
  :root { --brand: #0f766e; --line: #c9cdd4; }
  body { margin: 0; padding: 16px; font-family: system-ui, sans-serif; background: #eef2f1; color: #1d2330; }
  form { background: #fff; border-radius: 14px; padding: 16px; display: grid; gap: 14px; max-width: 420px; }
  .field { display: grid; gap: 6px; font-size: 14px; font-weight: 600; }

  /* shared reset for the controls on this page */
  input, select, button {
    -webkit-appearance: none;
    appearance: none;
    font: inherit; font-weight: 400; color: inherit;
  }
  input[type="search"], select {
    border: 1.5px solid var(--line); border-radius: 10px; background-color: #fff;
    padding: 10px 12px;
  }

  /* search: the clear (x) button is a separate part */
  input[type="search"]::-webkit-search-cancel-button { display: none; }

  /* select: the arrow is gone, so draw one with two gradients */
  select {
    padding-right: 38px; cursor: pointer;
    background-image:
      linear-gradient(45deg, transparent 50%, #4b5563 50%),
      linear-gradient(135deg, #4b5563 50%, transparent 50%);
    background-position: right 18px center, right 12px center;
    background-size: 6px 6px; background-repeat: no-repeat;
  }

  /* range: the input and the thumb both need a reset */
  input[type="range"] { height: 24px; background: transparent; cursor: pointer; }
  input[type="range"]::-webkit-slider-runnable-track { height: 6px; border-radius: 3px; background: #d7dcdb; }
  input[type="range"]::-moz-range-track { height: 6px; border-radius: 3px; background: #d7dcdb; }
  input[type="range"]::-webkit-slider-thumb {
    -webkit-appearance: none; appearance: none;
    width: 22px; height: 22px; margin-top: -8px; border-radius: 50%;
    background: var(--brand); border: 3px solid #fff; box-shadow: 0 1px 4px rgba(0, 0, 0, .3);
  }
  input[type="range"]::-moz-range-thumb {
    width: 16px; height: 16px; border-radius: 50%;
    background: var(--brand); border: 3px solid #fff; box-shadow: 0 1px 4px rgba(0, 0, 0, .3);
  }

  /* button: the browser's own border and background stay until you replace them */
  button {
    border: 0; border-radius: 10px; padding: 11px 16px; background: var(--brand); color: #fff;
    font-weight: 600; cursor: pointer;
  }
  button:hover { filter: brightness(1.1); }

  /* every control gets a visible keyboard focus ring back */
  :is(input, select, button):focus-visible { outline: 3px solid #f59e0b; outline-offset: 2px; }

  output { font: 13px ui-monospace, Consolas, monospace; color: #0f5132; min-height: 1.4em; overflow-wrap: anywhere; }
</style>
</head>
<body>
<form id="f">
  <label class="field">Search products
    <input type="search" name="q" value="running shoes">
  </label>
  <label class="field">Plan
    <select name="plan">
      <option value="monthly">Monthly</option>
      <option value="yearly" selected>Yearly</option>
    </select>
  </label>
  <label class="field"><span>Budget: <span id="bv">60</span></span>
    <input type="range" name="budget" min="0" max="200" step="10" value="60" id="budget">
  </label>
  <button>Show results</button>
  <output id="out"></output>
</form>

<script>
  const budget = document.getElementById('budget');
  budget.addEventListener('input', () => { document.getElementById('bv').textContent = budget.value; });

  document.getElementById('f').addEventListener('submit', (e) => {
    e.preventDefault();  // demo only: show what would be sent
    const data = new FormData(e.target);
    document.getElementById('out').textContent = new URLSearchParams(data).toString();
  });
</script>
</body>
</html>
Search, select, range and button, all styled from appearance: none. The output shows the submitted fields.
  • One reset for input, select, button, plus font: inherit so controls use the page font.
  • The select arrow is two small gradients in the background, so no image file is needed.
  • The focus ring uses outline with outline-offset. An outline takes no space, so the ring does not move the layout. The outline guide covers the details.

appearance: auto, the -webkit- prefix and base-select

Form controls get appearance: auto from the browser's own stylesheet. Setting it yourself hands drawing back to the browser. Use it to undo a global reset on one control, for example input.native { appearance: auto; }.

Older Safari versions only read -webkit-appearance. Writing both lines, prefixed first, costs nothing and covers them.

A newer value, appearance: base-select, is meant for <select>. It opts into a customizable select whose open list CSS can style. Browsers that do not support it ignore the value and show the normal select, so it is safe to add inside @supports (appearance: base-select).

When it does not work

What you see Cause Fix
The checkbox or radio disappeared No size after appearance: none Set width, height and border
Clicking changes nothing you can see No :checked style Change the fill on :checked
Tab lands on the control but nothing shows Focus outline removed and not replaced Add a :focus-visible outline
accent-color has no effect The native drawing it tints is gone Colour :checked with background
The range thumb keeps its default shape The thumb has its own appearance Reset ::-webkit-slider-thumb, style ::-moz-range-thumb
The x still shows in a search box The clear button is a separate part ::-webkit-search-cancel-button { display: none; }
The button still has a grey face and border Those are normal default styles, not the native drawing Set border and background
The select opens with no arrow The arrow was part of the native drawing Draw one and add right padding

Custom form controls are hard to review from a screenshot. The reviewer needs to click the boxes, Tab through the fields and see the focus ring on their own device.

To send the working version, paste the page into a NOS document and choose Create share link. HTML to link walks through it.

The page renders as written and its scripts run, so the people you send it to can tick, slide and submit the form themselves. If you change the styles later, the same link shows the new version.

Questions people ask

What does appearance: none do in CSS?

It turns off the browser's native drawing of a form control, such as the checkbox box, the select arrow or the range track. The element keeps its behaviour: it still takes focus, toggles or changes value, and submits with the form.

Why did my checkbox disappear after appearance: none?

A checkbox or radio with appearance: none has no drawing and, unless you set one, no size. It renders at 0 by 0 pixels. Give it a width, a height and a border, then style the :checked state.

Does appearance: none break keyboard use or screen readers?

No. The element is still a real input, so Tab, Space, the arrow keys and label clicks work, and it is announced the same way. What can break is visibility: with no size and no focus style, a keyboard user cannot see where they are.

Do I still need -webkit-appearance?

Older Safari versions only read the prefixed -webkit-appearance, so many stylesheets write both lines. The range thumb and the search clear button are separate parts that need their own reset through ::-webkit-slider-thumb and ::-webkit-search-cancel-button.

How do I undo appearance: none on one element?

Set appearance: auto on it. That hands the drawing back to the browser, which is useful when a reset stylesheet removed the native look from every input and one of them should look native again.

Keep reading