Pure CSSMIT licensed

Dashed Marching Ants Button Border

Marching ants in the one place they were invented for: a crop selection over a real photograph, with corner handles, a rule-of-thirds grid and a dimension readout. The selection marches permanently because it is a live region; the Apply button next to it marches only while you are pointing at it — the same 18-unit loop, two completely different jobs.

Live Demo
Plain CSSTailwind v4
Try it

Nucleo: 44,000+ SVG icons in five families · All Icons Bundle $149

Nucleo · Affiliate link

Get the Nucleo bundle

The code

<section class="svgb-06">
  <input class="svgb-06__ti" type="checkbox" id="svgb-06-theme">
  <label class="svgb-06__toggle" for="svgb-06-theme">
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true"><circle cx="12" cy="12" r="8.5"/><path d="M12 3.5a8.5 8.5 0 0 0 0 17Z" fill="currentColor" stroke="none"/></svg>
    <span class="svgb-06__sr">Switch colour scheme</span>
  </label>
  <div class="svgb-06__app">
    <div class="svgb-06__tools">
      <button class="svgb-06__tool svgb-06__tool--on" type="button" aria-label="Crop">
        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path d="M6 2v14a2 2 0 0 0 2 2h14M2 6h14a2 2 0 0 1 2 2v14"/></svg>
      </button>
      <button class="svgb-06__tool" type="button" aria-label="Straighten">
        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path d="M3 17 17 3m-6 0h6v6"/></svg>
      </button>
      <button class="svgb-06__tool" type="button" aria-label="Levels">
        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path d="M4 19V9m5 10V5m5 14v-7m5 7V8"/></svg>
      </button>
      <button class="svgb-06__tool" type="button" aria-label="Mask">
        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><circle cx="10" cy="12" r="7"/><circle cx="15" cy="12" r="7"/></svg>
      </button>
    </div>

    <div class="svgb-06__canvas">
      <img class="svgb-06__img" src="https://picsum.photos/seed/svgb06ridge/1400/950" alt="Mountain ridge at dusk" width="1400" height="950" loading="lazy" decoding="async">
      <div class="svgb-06__sel">
        <svg class="svgb-06__marq" viewBox="0 0 400 260" preserveAspectRatio="none" aria-hidden="true">
          <rect class="svgb-06__ants-b" x="1" y="1" width="398" height="258" vector-effect="non-scaling-stroke"/>
          <rect class="svgb-06__ants" x="1" y="1" width="398" height="258" vector-effect="non-scaling-stroke"/>
        </svg>
        <span class="svgb-06__thirds" aria-hidden="true"></span>
        <span class="svgb-06__h svgb-06__h--tl" aria-hidden="true"></span>
        <span class="svgb-06__h svgb-06__h--tr" aria-hidden="true"></span>
        <span class="svgb-06__h svgb-06__h--bl" aria-hidden="true"></span>
        <span class="svgb-06__h svgb-06__h--br" aria-hidden="true"></span>
        <span class="svgb-06__dims">1620 &times; 1053</span>
      </div>
    </div>

    <div class="svgb-06__bar">
      <span class="svgb-06__ratio">3 : 2</span>
      <div class="svgb-06__actions">
        <button class="svgb-06__btn svgb-06__btn--ghost" type="button">
          <svg class="svgb-06__frame" viewBox="0 0 120 46" aria-hidden="true">
            <rect class="svgb-06__bants" x="1" y="1" width="118" height="44" rx="10" vector-effect="non-scaling-stroke"/>
          </svg>
          <span class="svgb-06__label">Reset</span>
        </button>
        <button class="svgb-06__btn" type="button">
          <svg class="svgb-06__frame" viewBox="0 0 168 46" aria-hidden="true">
            <rect class="svgb-06__bants" x="1" y="1" width="166" height="44" rx="10" vector-effect="non-scaling-stroke"/>
          </svg>
          <span class="svgb-06__label">Apply crop</span>
        </button>
      </div>
    </div>
  </div>
</section>
@import url('https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;700&family=JetBrains+Mono:wght@400;500&display=swap');

.svgb-06 {
  --svgb-06-bg: #121316;
  --svgb-06-bg: oklch(0.19 0.005 260);
  --svgb-06-chrome: #1b1d22;
  --svgb-06-chrome: oklch(0.245 0.008 260);
  --svgb-06-fg: #eceef1;
  --svgb-06-fg: oklch(0.95 0.004 260);
  --svgb-06-muted: #8b9099;
  --svgb-06-muted: oklch(0.67 0.011 260);
  --svgb-06-rest: rgba(236, 238, 241, 0.16);
  --svgb-06-rest: oklch(0.95 0.004 260 / 0.16);
  --svgb-06-stroke: #4ce0a0;
  --svgb-06-stroke: oklch(0.85 0.16 160);
  --svgb-06-speed: 700ms;
  position: relative;
  width: 100%;
  min-height: 100vh;
  min-height: 100svh;
  display: block;
  background: var(--svgb-06-bg);
  color: var(--svgb-06-fg);
  font-family: 'DM Sans', system-ui, sans-serif;
}

.svgb-06 *,
.svgb-06 *::before,
.svgb-06 *::after {
  box-sizing: border-box;
}

.svgb-06__app {
  min-height: 100svh;
  display: grid;
  grid-template-columns: 64px 1fr;
  grid-template-rows: 1fr auto;
  gap: 1px;
  background: color-mix(in oklab, var(--svgb-06-fg) 8%, transparent);
}

.svgb-06__tools {
  grid-row: 1 / span 2;
  display: flex;
  flex-direction: column;
  gap: 8px;
  align-items: center;
  padding: 14px 0;
  background: var(--svgb-06-chrome);
}

.svgb-06__tool {
  inline-size: 44px;
  block-size: 44px;
  display: grid;
  place-items: center;
  border: 0;
  border-radius: 10px;
  background: transparent;
  color: var(--svgb-06-muted);
  cursor: pointer;
  transition: background 160ms ease, color 160ms ease;
}

.svgb-06__tool:hover {
  background: color-mix(in oklab, var(--svgb-06-fg) 8%, transparent);
  color: var(--svgb-06-fg);
}

.svgb-06__tool--on {
  background: color-mix(in oklab, var(--svgb-06-stroke) 16%, transparent);
  color: var(--svgb-06-stroke);
}

.svgb-06__tool:focus-visible {
  outline: 2px solid var(--svgb-06-stroke);
  outline-offset: -2px;
}

.svgb-06__tool svg {
  inline-size: 20px;
  block-size: 20px;
}

.svgb-06__canvas {
  position: relative;
  display: grid;
  place-items: center;
  padding: clamp(20px, 4vw, 54px);
  background: repeating-conic-gradient(oklch(0.24 0 0) 0% 25%, oklch(0.21 0 0) 0% 50%) 0 0 / 22px 22px;
  overflow: hidden;
}

.svgb-06__img {
  position: absolute;
  inset: 0;
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
  opacity: 0.5;
  filter: saturate(0.8);
}

.svgb-06__sel {
  position: relative;
  inline-size: min(400px, 76vw);
  aspect-ratio: 400 / 260;
  box-shadow: 0 0 0 9999px oklch(0 0 0 / 0.55);
}

.svgb-06__marq {
  position: absolute;
  inset: 0;
  inline-size: 100%;
  block-size: 100%;
  overflow: visible;
}

.svgb-06__marq rect {
  fill: none;
}

@keyframes svgb-06-march {
  to {
    stroke-dashoffset: -18;
  }
}

.svgb-06__ants-b {
  stroke: oklch(0 0 0 / 0.75);
  stroke-width: 1.6px;
  stroke-dasharray: 10 8;
}

.svgb-06__ants {
  stroke: #fff;
  stroke-width: 1.6px;
  stroke-dasharray: 10 8;
  stroke-dashoffset: -9;
  animation: svgb-06-march var(--svgb-06-speed) linear infinite;
}

.svgb-06__thirds {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(90deg, transparent 33.2%, oklch(1 0 0 / 0.3) 33.2% 33.4%, transparent 33.4%), linear-gradient(90deg, transparent 66.5%, oklch(1 0 0 / 0.3) 66.5% 66.7%, transparent 66.7%), linear-gradient(transparent 33.2%, oklch(1 0 0 / 0.3) 33.2% 33.4%, transparent 33.4%), linear-gradient(transparent 66.5%, oklch(1 0 0 / 0.3) 66.5% 66.7%, transparent 66.7%);
}

.svgb-06__h {
  position: absolute;
  inline-size: 13px;
  block-size: 13px;
  border: 2px solid #fff;
  background: oklch(0 0 0 / 0.4);
}

.svgb-06__h--tl {
  inset-block-start: -7px;
  inset-inline-start: -7px;
}

.svgb-06__h--tr {
  inset-block-start: -7px;
  inset-inline-end: -7px;
}

.svgb-06__h--bl {
  inset-block-end: -7px;
  inset-inline-start: -7px;
}

.svgb-06__h--br {
  inset-block-end: -7px;
  inset-inline-end: -7px;
}

.svgb-06__dims {
  position: absolute;
  inset-block-end: -34px;
  inset-inline-start: 0;
  font-family: 'JetBrains Mono', ui-monospace, monospace;
  font-size: 11px;
  padding: 4px 8px;
  border-radius: 6px;
  background: oklch(0 0 0 / 0.6);
  color: #fff;
  font-variant-numeric: tabular-nums;
}

.svgb-06__bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 14px clamp(14px, 3vw, 26px);
  background: var(--svgb-06-chrome);
}

.svgb-06__ratio {
  font-family: 'JetBrains Mono', ui-monospace, monospace;
  font-size: 12px;
  color: var(--svgb-06-muted);
}

.svgb-06__actions {
  display: flex;
  gap: 12px;
}

.svgb-06__btn {
  position: relative;
  inline-size: 168px;
  block-size: 46px;
  display: grid;
  place-items: center;
  border: 0;
  padding: 0;
  border-radius: 10px;
  background: color-mix(in oklab, var(--svgb-06-stroke) 14%, transparent);
  color: inherit;
  font: inherit;
  font-size: 14px;
  font-weight: 500;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

.svgb-06__btn--ghost {
  inline-size: 120px;
  background: color-mix(in oklab, var(--svgb-06-fg) 6%, transparent);
}

.svgb-06__btn:focus-visible {
  outline: 2px solid var(--svgb-06-stroke);
  outline-offset: 3px;
}

.svgb-06__label {
  position: relative;
  z-index: 1;
}

.svgb-06__frame {
  position: absolute;
  inset: 0;
  inline-size: 100%;
  block-size: 100%;
  overflow: visible;
}

.svgb-06__frame rect {
  fill: none;
}

.svgb-06__bants {
  stroke: var(--svgb-06-rest);
  stroke-width: 1.6px;
  stroke-dasharray: 10 8;
  animation: svgb-06-march var(--svgb-06-speed) linear infinite;
  animation-play-state: paused;
  transition: stroke 200ms ease;
}

.svgb-06__btn:hover .svgb-06__bants,
.svgb-06__btn:focus-visible .svgb-06__bants {
  animation-play-state: running;
  stroke: var(--svgb-06-stroke);
}

.svgb-06__ti {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}

.svgb-06__toggle {
  position: absolute;
  z-index: 5;
  inset-block-start: 14px;
  inset-inline-end: 14px;
  display: grid;
  place-items: center;
  inline-size: 44px;
  block-size: 44px;
  border-radius: 999px;
  border: 1px solid color-mix(in oklab, currentColor 20%, transparent);
  background: color-mix(in oklab, currentColor 12%, transparent);
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
  color: inherit;
  cursor: pointer;
  transition: background 160ms ease, transform 160ms ease;
}

.svgb-06__toggle:hover {
  background: color-mix(in oklab, currentColor 18%, transparent);
}

.svgb-06__toggle:active {
  transform: scale(0.94);
}

.svgb-06__toggle svg {
  inline-size: 18px;
  block-size: 18px;
  transition: transform 400ms cubic-bezier(0.2, 0.7, 0.3, 1);
}

.svgb-06__ti:checked + .svgb-06__toggle svg {
  transform: rotate(180deg);
}

.svgb-06__ti:focus-visible + .svgb-06__toggle {
  outline: 2px solid currentColor;
  outline-offset: 3px;
}

.svgb-06__sr {
  position: absolute;
  inline-size: 1px;
  block-size: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.svgb-06:has(#svgb-06-theme:checked) {
  --svgb-06-bg: #eceef0;
  --svgb-06-bg: oklch(0.945 0.004 260);
  --svgb-06-chrome: #ffffff;
  --svgb-06-chrome: oklch(1 0 0);
  --svgb-06-fg: #14171b;
  --svgb-06-fg: oklch(0.20 0.006 260);
  --svgb-06-muted: #5c626b;
  --svgb-06-muted: oklch(0.505 0.011 260);
  --svgb-06-rest: rgba(20, 23, 27, 0.18);
  --svgb-06-rest: oklch(0.20 0.006 260 / 0.18);
  --svgb-06-stroke: #0a7a52;
  --svgb-06-stroke: oklch(0.50 0.13 162);
}

.svgb-06:has(#svgb-06-theme:checked) .svgb-06__img {
  opacity: 0.85;
}

.svgb-06:has(#svgb-06-theme:checked) .svgb-06__sel {
  box-shadow: 0 0 0 9999px oklch(1 0 0 / 0.55);
}

.svgb-06:has(#svgb-06-theme:checked) .svgb-06__ants {
  stroke: #0b0d10;
}

.svgb-06:has(#svgb-06-theme:checked) .svgb-06__ants-b {
  stroke: oklch(1 0 0 / 0.85);
}

.svgb-06:has(#svgb-06-theme:checked) .svgb-06__h {
  border-color: #0b0d10;
  background: oklch(1 0 0 / 0.6);
}

.svgb-06:has(#svgb-06-theme:checked) .svgb-06__thirds {
  background: linear-gradient(90deg, transparent 33.2%, oklch(0 0 0 / 0.28) 33.2% 33.4%, transparent 33.4%), linear-gradient(90deg, transparent 66.5%, oklch(0 0 0 / 0.28) 66.5% 66.7%, transparent 66.7%), linear-gradient(transparent 33.2%, oklch(0 0 0 / 0.28) 33.2% 33.4%, transparent 33.4%), linear-gradient(transparent 66.5%, oklch(0 0 0 / 0.28) 66.5% 66.7%, transparent 66.7%);
}

@media (prefers-reduced-motion: reduce) {
  .svgb-06__ants,
    .svgb-06__bants {
    animation: none;
  }

  .svgb-06__ants {
    stroke-dashoffset: 0;
  }

  .svgb-06__btn:hover .svgb-06__bants {
    stroke-dasharray: 6 4;
  }

  .svgb-06__tool,
    .svgb-06__toggle,
    .svgb-06__toggle svg {
    transition: none;
  }
}
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS SVG Stroke Button from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Dashed Marching Ants Button Border
Source: https://codefronts.com/components/css-svg-stroke-buttons/dashed-marching-ants-button-border/

Marching ants in the one place they were invented for: a crop selection over a real photograph, with corner handles, a rule-of-thirds grid and a dimension readout. The selection marches permanently because it is a live region; the Apply button next to it marches only while you are pointing at it — the same 18-unit loop, two completely different jobs.
## HTML
```html
<section class="svgb-06">
  <input class="svgb-06__ti" type="checkbox" id="svgb-06-theme">
  <label class="svgb-06__toggle" for="svgb-06-theme">
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true"><circle cx="12" cy="12" r="8.5"/><path d="M12 3.5a8.5 8.5 0 0 0 0 17Z" fill="currentColor" stroke="none"/></svg>
    <span class="svgb-06__sr">Switch colour scheme</span>
  </label>
  <div class="svgb-06__app">
    <div class="svgb-06__tools">
      <button class="svgb-06__tool svgb-06__tool--on" type="button" aria-label="Crop">
        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path d="M6 2v14a2 2 0 0 0 2 2h14M2 6h14a2 2 0 0 1 2 2v14"/></svg>
      </button>
      <button class="svgb-06__tool" type="button" aria-label="Straighten">
        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path d="M3 17 17 3m-6 0h6v6"/></svg>
      </button>
      <button class="svgb-06__tool" type="button" aria-label="Levels">
        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path d="M4 19V9m5 10V5m5 14v-7m5 7V8"/></svg>
      </button>
      <button class="svgb-06__tool" type="button" aria-label="Mask">
        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><circle cx="10" cy="12" r="7"/><circle cx="15" cy="12" r="7"/></svg>
      </button>
    </div>

    <div class="svgb-06__canvas">
      <img class="svgb-06__img" src="https://picsum.photos/seed/svgb06ridge/1400/950" alt="Mountain ridge at dusk" width="1400" height="950" loading="lazy" decoding="async">
      <div class="svgb-06__sel">
        <svg class="svgb-06__marq" viewBox="0 0 400 260" preserveAspectRatio="none" aria-hidden="true">
          <rect class="svgb-06__ants-b" x="1" y="1" width="398" height="258" vector-effect="non-scaling-stroke"/>
          <rect class="svgb-06__ants" x="1" y="1" width="398" height="258" vector-effect="non-scaling-stroke"/>
        </svg>
        <span class="svgb-06__thirds" aria-hidden="true"></span>
        <span class="svgb-06__h svgb-06__h--tl" aria-hidden="true"></span>
        <span class="svgb-06__h svgb-06__h--tr" aria-hidden="true"></span>
        <span class="svgb-06__h svgb-06__h--bl" aria-hidden="true"></span>
        <span class="svgb-06__h svgb-06__h--br" aria-hidden="true"></span>
        <span class="svgb-06__dims">1620 &times; 1053</span>
      </div>
    </div>

    <div class="svgb-06__bar">
      <span class="svgb-06__ratio">3 : 2</span>
      <div class="svgb-06__actions">
        <button class="svgb-06__btn svgb-06__btn--ghost" type="button">
          <svg class="svgb-06__frame" viewBox="0 0 120 46" aria-hidden="true">
            <rect class="svgb-06__bants" x="1" y="1" width="118" height="44" rx="10" vector-effect="non-scaling-stroke"/>
          </svg>
          <span class="svgb-06__label">Reset</span>
        </button>
        <button class="svgb-06__btn" type="button">
          <svg class="svgb-06__frame" viewBox="0 0 168 46" aria-hidden="true">
            <rect class="svgb-06__bants" x="1" y="1" width="166" height="44" rx="10" vector-effect="non-scaling-stroke"/>
          </svg>
          <span class="svgb-06__label">Apply crop</span>
        </button>
      </div>
    </div>
  </div>
</section>
```
## CSS
```css
@import url('https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;700&family=JetBrains+Mono:wght@400;500&display=swap');

.svgb-06 {
  --svgb-06-bg: #121316;
  --svgb-06-bg: oklch(0.19 0.005 260);
  --svgb-06-chrome: #1b1d22;
  --svgb-06-chrome: oklch(0.245 0.008 260);
  --svgb-06-fg: #eceef1;
  --svgb-06-fg: oklch(0.95 0.004 260);
  --svgb-06-muted: #8b9099;
  --svgb-06-muted: oklch(0.67 0.011 260);
  --svgb-06-rest: rgba(236, 238, 241, 0.16);
  --svgb-06-rest: oklch(0.95 0.004 260 / 0.16);
  --svgb-06-stroke: #4ce0a0;
  --svgb-06-stroke: oklch(0.85 0.16 160);
  --svgb-06-speed: 700ms;
  position: relative;
  width: 100%;
  min-height: 100vh;
  min-height: 100svh;
  display: block;
  background: var(--svgb-06-bg);
  color: var(--svgb-06-fg);
  font-family: 'DM Sans', system-ui, sans-serif;
}

.svgb-06 *,
.svgb-06 *::before,
.svgb-06 *::after {
  box-sizing: border-box;
}

.svgb-06__app {
  min-height: 100svh;
  display: grid;
  grid-template-columns: 64px 1fr;
  grid-template-rows: 1fr auto;
  gap: 1px;
  background: color-mix(in oklab, var(--svgb-06-fg) 8%, transparent);
}

.svgb-06__tools {
  grid-row: 1 / span 2;
  display: flex;
  flex-direction: column;
  gap: 8px;
  align-items: center;
  padding: 14px 0;
  background: var(--svgb-06-chrome);
}

.svgb-06__tool {
  inline-size: 44px;
  block-size: 44px;
  display: grid;
  place-items: center;
  border: 0;
  border-radius: 10px;
  background: transparent;
  color: var(--svgb-06-muted);
  cursor: pointer;
  transition: background 160ms ease, color 160ms ease;
}

.svgb-06__tool:hover {
  background: color-mix(in oklab, var(--svgb-06-fg) 8%, transparent);
  color: var(--svgb-06-fg);
}

.svgb-06__tool--on {
  background: color-mix(in oklab, var(--svgb-06-stroke) 16%, transparent);
  color: var(--svgb-06-stroke);
}

.svgb-06__tool:focus-visible {
  outline: 2px solid var(--svgb-06-stroke);
  outline-offset: -2px;
}

.svgb-06__tool svg {
  inline-size: 20px;
  block-size: 20px;
}

.svgb-06__canvas {
  position: relative;
  display: grid;
  place-items: center;
  padding: clamp(20px, 4vw, 54px);
  background: repeating-conic-gradient(oklch(0.24 0 0) 0% 25%, oklch(0.21 0 0) 0% 50%) 0 0 / 22px 22px;
  overflow: hidden;
}

.svgb-06__img {
  position: absolute;
  inset: 0;
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
  opacity: 0.5;
  filter: saturate(0.8);
}

.svgb-06__sel {
  position: relative;
  inline-size: min(400px, 76vw);
  aspect-ratio: 400 / 260;
  box-shadow: 0 0 0 9999px oklch(0 0 0 / 0.55);
}

.svgb-06__marq {
  position: absolute;
  inset: 0;
  inline-size: 100%;
  block-size: 100%;
  overflow: visible;
}

.svgb-06__marq rect {
  fill: none;
}

@keyframes svgb-06-march {
  to {
    stroke-dashoffset: -18;
  }
}

.svgb-06__ants-b {
  stroke: oklch(0 0 0 / 0.75);
  stroke-width: 1.6px;
  stroke-dasharray: 10 8;
}

.svgb-06__ants {
  stroke: #fff;
  stroke-width: 1.6px;
  stroke-dasharray: 10 8;
  stroke-dashoffset: -9;
  animation: svgb-06-march var(--svgb-06-speed) linear infinite;
}

.svgb-06__thirds {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(90deg, transparent 33.2%, oklch(1 0 0 / 0.3) 33.2% 33.4%, transparent 33.4%), linear-gradient(90deg, transparent 66.5%, oklch(1 0 0 / 0.3) 66.5% 66.7%, transparent 66.7%), linear-gradient(transparent 33.2%, oklch(1 0 0 / 0.3) 33.2% 33.4%, transparent 33.4%), linear-gradient(transparent 66.5%, oklch(1 0 0 / 0.3) 66.5% 66.7%, transparent 66.7%);
}

.svgb-06__h {
  position: absolute;
  inline-size: 13px;
  block-size: 13px;
  border: 2px solid #fff;
  background: oklch(0 0 0 / 0.4);
}

.svgb-06__h--tl {
  inset-block-start: -7px;
  inset-inline-start: -7px;
}

.svgb-06__h--tr {
  inset-block-start: -7px;
  inset-inline-end: -7px;
}

.svgb-06__h--bl {
  inset-block-end: -7px;
  inset-inline-start: -7px;
}

.svgb-06__h--br {
  inset-block-end: -7px;
  inset-inline-end: -7px;
}

.svgb-06__dims {
  position: absolute;
  inset-block-end: -34px;
  inset-inline-start: 0;
  font-family: 'JetBrains Mono', ui-monospace, monospace;
  font-size: 11px;
  padding: 4px 8px;
  border-radius: 6px;
  background: oklch(0 0 0 / 0.6);
  color: #fff;
  font-variant-numeric: tabular-nums;
}

.svgb-06__bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 14px clamp(14px, 3vw, 26px);
  background: var(--svgb-06-chrome);
}

.svgb-06__ratio {
  font-family: 'JetBrains Mono', ui-monospace, monospace;
  font-size: 12px;
  color: var(--svgb-06-muted);
}

.svgb-06__actions {
  display: flex;
  gap: 12px;
}

.svgb-06__btn {
  position: relative;
  inline-size: 168px;
  block-size: 46px;
  display: grid;
  place-items: center;
  border: 0;
  padding: 0;
  border-radius: 10px;
  background: color-mix(in oklab, var(--svgb-06-stroke) 14%, transparent);
  color: inherit;
  font: inherit;
  font-size: 14px;
  font-weight: 500;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

.svgb-06__btn--ghost {
  inline-size: 120px;
  background: color-mix(in oklab, var(--svgb-06-fg) 6%, transparent);
}

.svgb-06__btn:focus-visible {
  outline: 2px solid var(--svgb-06-stroke);
  outline-offset: 3px;
}

.svgb-06__label {
  position: relative;
  z-index: 1;
}

.svgb-06__frame {
  position: absolute;
  inset: 0;
  inline-size: 100%;
  block-size: 100%;
  overflow: visible;
}

.svgb-06__frame rect {
  fill: none;
}

.svgb-06__bants {
  stroke: var(--svgb-06-rest);
  stroke-width: 1.6px;
  stroke-dasharray: 10 8;
  animation: svgb-06-march var(--svgb-06-speed) linear infinite;
  animation-play-state: paused;
  transition: stroke 200ms ease;
}

.svgb-06__btn:hover .svgb-06__bants,
.svgb-06__btn:focus-visible .svgb-06__bants {
  animation-play-state: running;
  stroke: var(--svgb-06-stroke);
}

.svgb-06__ti {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}

.svgb-06__toggle {
  position: absolute;
  z-index: 5;
  inset-block-start: 14px;
  inset-inline-end: 14px;
  display: grid;
  place-items: center;
  inline-size: 44px;
  block-size: 44px;
  border-radius: 999px;
  border: 1px solid color-mix(in oklab, currentColor 20%, transparent);
  background: color-mix(in oklab, currentColor 12%, transparent);
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
  color: inherit;
  cursor: pointer;
  transition: background 160ms ease, transform 160ms ease;
}

.svgb-06__toggle:hover {
  background: color-mix(in oklab, currentColor 18%, transparent);
}

.svgb-06__toggle:active {
  transform: scale(0.94);
}

.svgb-06__toggle svg {
  inline-size: 18px;
  block-size: 18px;
  transition: transform 400ms cubic-bezier(0.2, 0.7, 0.3, 1);
}

.svgb-06__ti:checked + .svgb-06__toggle svg {
  transform: rotate(180deg);
}

.svgb-06__ti:focus-visible + .svgb-06__toggle {
  outline: 2px solid currentColor;
  outline-offset: 3px;
}

.svgb-06__sr {
  position: absolute;
  inline-size: 1px;
  block-size: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.svgb-06:has(#svgb-06-theme:checked) {
  --svgb-06-bg: #eceef0;
  --svgb-06-bg: oklch(0.945 0.004 260);
  --svgb-06-chrome: #ffffff;
  --svgb-06-chrome: oklch(1 0 0);
  --svgb-06-fg: #14171b;
  --svgb-06-fg: oklch(0.20 0.006 260);
  --svgb-06-muted: #5c626b;
  --svgb-06-muted: oklch(0.505 0.011 260);
  --svgb-06-rest: rgba(20, 23, 27, 0.18);
  --svgb-06-rest: oklch(0.20 0.006 260 / 0.18);
  --svgb-06-stroke: #0a7a52;
  --svgb-06-stroke: oklch(0.50 0.13 162);
}

.svgb-06:has(#svgb-06-theme:checked) .svgb-06__img {
  opacity: 0.85;
}

.svgb-06:has(#svgb-06-theme:checked) .svgb-06__sel {
  box-shadow: 0 0 0 9999px oklch(1 0 0 / 0.55);
}

.svgb-06:has(#svgb-06-theme:checked) .svgb-06__ants {
  stroke: #0b0d10;
}

.svgb-06:has(#svgb-06-theme:checked) .svgb-06__ants-b {
  stroke: oklch(1 0 0 / 0.85);
}

.svgb-06:has(#svgb-06-theme:checked) .svgb-06__h {
  border-color: #0b0d10;
  background: oklch(1 0 0 / 0.6);
}

.svgb-06:has(#svgb-06-theme:checked) .svgb-06__thirds {
  background: linear-gradient(90deg, transparent 33.2%, oklch(0 0 0 / 0.28) 33.2% 33.4%, transparent 33.4%), linear-gradient(90deg, transparent 66.5%, oklch(0 0 0 / 0.28) 66.5% 66.7%, transparent 66.7%), linear-gradient(transparent 33.2%, oklch(0 0 0 / 0.28) 33.2% 33.4%, transparent 33.4%), linear-gradient(transparent 66.5%, oklch(0 0 0 / 0.28) 66.5% 66.7%, transparent 66.7%);
}

@media (prefers-reduced-motion: reduce) {
  .svgb-06__ants,
    .svgb-06__bants {
    animation: none;
  }

  .svgb-06__ants {
    stroke-dashoffset: 0;
  }

  .svgb-06__btn:hover .svgb-06__bants {
    stroke-dasharray: 6 4;
  }

  .svgb-06__tool,
    .svgb-06__toggle,
    .svgb-06__toggle svg {
    transition: none;
  }
}
```

How this works

The repeat length is the sum of the array. A pattern of 10 8 repeats every 18 units. Shift the offset by 18 over the animation and the final frame is pixel-identical to the first, so the loop has no seam. Shift it by 10, or by the perimeter, and you get a jolt at every cycle — the single most common bug in marching-ants code.

@keyframes svgb-06-march { to { stroke-dashoffset: -18; } }
.svgb-06__ants {
  stroke-dasharray: 10 8;         /* repeat = 18 */
  animation: svgb-06-march 700ms linear infinite paused;
}
.svgb-06__btn:hover .svgb-06__bants { animation-play-state: running; }

Make the perimeter divisible by the repeat. Even with a seamless offset loop, the point where the path itself closes can show a short dash if the perimeter is not a whole multiple of 18. The marquee is 398×258, so its perimeter is 1312 — not a multiple of 18, which is exactly why the closure sits at the top-left corner where a short dash reads as a corner mark rather than a defect. When the size is yours to choose, pick one that divides; when it is not, normalise with pathLength.

Speed is a semantic. Around 40 units per second reads as calm and available; above 100 it reads as urgent or in-progress. Match the speed to what the state means: an idle dropzone should breathe, an active upload can hurry.

Where it belongs. Marching ants say this region is selected. The marquee marches permanently because the selection is permanently live; the Apply button marches only on hover, because a button is not a selection. Note the dark dashed path underneath the white one, offset by half a repeat — a two-tone dash is how a selection stays legible over both a bright sky and a dark ridge, which no single colour can do.

Make it yours

  • Change 10 8 to 2 6 for a fine technical stipple, or 18 10 for a bold poster dash — then update the keyframe offset to the new sum.
  • Reverse the march by animating to a positive offset instead of a negative one; anticlockwise reads as removing in some editing tools.
  • Drop the duration to 400ms and raise the stroke to 2.5px for an active-upload state, and back to 900ms at 1.5px when idle.
  • Add a second dashed path with the same pattern and a half-repeat offset in a lighter colour for a two-tone weave.
  • Swap the marquee rect for a lasso or polygon path — the mechanism is identical for any closed shape.

Gotchas — read before shipping

  • Animating the offset by anything other than one whole pattern repeat produces a visible jump once per cycle.
  • Easing on an infinite march reads as a stutter. Continuous loops want linear; only one-shot draws want a curve.
  • A dashed pattern on a path whose length is not a multiple of the repeat leaves a short dash at the closing point. Size the shape, or accept and place the seam.
  • A permanently marching border is a permanently active compositor layer. Pause it at rest and it costs nothing; leave it running on twelve cards and mobile battery drains for decoration.
  • Never let the march be the only indicator of a selection — pair it with handles, a dimension readout and real text, because motion alone is invisible to a screen reader and to anyone with motion sensitivity.

Browser support

ChromeSafariFirefoxEdge
111+15.4+121+111+

Dash animation is universal; the :has() dropzone state needs Chrome 105, Safari 15.4 and Firefox 121. Without :has() the hover and focus marches still work and only the sticky selected state is lost.

Techniques used in this demo

Search CodeFronts

Loading…