How to make a custom checkbox using CSS and SVG

This post shows how to build a clean, cross-browser custom checkbox with a CSS-only box and an SVG checkmark.

Problem

Default browser checkboxes are difficult to style consistently, especially on mobile where they can render much larger than intended. Standard size constraints like width: 14px often have no effect on native widgets.

Solution

Use appearance: none to remove the native rendering, then draw the box and checkmark yourself.

Live example

CSS-only custom checkbox

custom-checkbox.css
label {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  cursor: pointer;
}

input[type='checkbox'] {
  appearance: none;
  -webkit-appearance: none;
  width: 16px;
  height: 16px;
  min-width: 16px;
  margin: 0;
  border: 1.5px solid #b0c4d8;
  border-radius: 4px;
  background: #f8fafc;
  box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.05);
  cursor: pointer;
  position: relative;
  transition:
    background 0.15s ease,
    border-color 0.15s ease;
}

input[type='checkbox']:hover {
  border-color: #00b88a;
}

input[type='checkbox']:checked {
  background: #00b88a url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath fill='none' stroke='%23ffffff' stroke-linecap='round' stroke-linejoin='round' stroke-width='2.5' d='M3.5 8.5l3 3 6-7'/%3E%3C/svg%3E") center/60% no-repeat;
  border-color: #00b88a;
  box-shadow: none;
}
index.html
<!doctype html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Custom checkbox demo</title>
  <style>
    body {
      font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
      background: #e8eef5;
      padding: 2rem;
    }

    .demo {
      display: flex;
      flex-direction: column;
      gap: 1rem;
      max-width: 300px;
    }

    .demo label {
      display: flex;
      align-items: center;
      gap: 0.6rem;
      padding: 0.65rem;
      background: rgba(255, 255, 255, 0.7);
      border-radius: 8px;
      cursor: pointer;
      color: #1a2a3a;
      transition: background 0.15s ease;
    }

    .demo label:hover {
      background: rgba(120, 169, 255, 0.08);
    }

    .demo input[type='checkbox'] {
      appearance: none;
      -webkit-appearance: none;
      width: 16px;
      height: 16px;
      min-width: 16px;
      margin: 0;
      border: 1.5px solid #b0c4d8;
      border-radius: 4px;
      background: #f8fafc;
      box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.05);
      cursor: pointer;
      position: relative;
      transition:
        background 0.15s ease,
        border-color 0.15s ease;
    }

    .demo input[type='checkbox']:hover {
      border-color: #00b88a;
    }

    .demo input[type='checkbox']:checked {
      background: #00b88a url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath fill='none' stroke='%23ffffff' stroke-linecap='round' stroke-linejoin='round' stroke-width='2.5' d='M3.5 8.5l3 3 6-7'/%3E%3C/svg%3E") center/60% no-repeat;
      border-color: #00b88a;
      box-shadow: none;
    }
  </style>
</head>
<body>
  <div class="demo">
    <label><input type="checkbox" checked><span>Enable notifications</span></label>
    <label><input type="checkbox"><span>Subscribe to newsletter</span></label>
    <label><input type="checkbox" checked><span>Accept terms</span></label>
  </div>
</body>
</html>

How it looks and feels

  • Unchecked: a 16px rounded square with a soft blue-grey border, light fill, and a faint inset shadow. It looks flat but tactile.
  • Hover: the border shifts to mint green with a quick 0.15s transition, giving immediate feedback.
  • Checked: the box fills with mint green and a crisp white SVG checkmark appears centered inside. The inset shadow disappears so the checked state feels solid.

The SVG checkmark stays sharp on high-DPI screens because it is a vector, and the whole widget stays small on mobile because appearance: none removes the native rendering that ignores width and height.

Key points

  • appearance: none and -webkit-appearance: none strip the native widget.
  • min-width keeps the box from being crushed inside a flex container.
  • An inline SVG data URL keeps the checkmark sharp at any size without an extra request.
  • transition gives the hover and checked states a polished feel.

Check out similar posts by category: CSS Web Development