AI CSS Animations

Worktree Merge Lanes

Three agents on three git worktrees: each lane branches off main, a glowing head lays down commits as it goes, then curves back into main with a merge dot while the count reaches 3 of 3 merged.

Also known as: git worktree agents, parallel branches animation, git graph merge animation.

Worktrees
main wt/docs wt/tests wt/auth

HTML

<div class="aia-worktree-lanes" role="status" aria-label="Three agent worktrees merging back into main">
  <div class="aia-worktree-lanes-head"><b>Worktrees</b><span class="aia-worktree-lanes-n" aria-hidden="true"></span></div>
  <div class="aia-worktree-lanes-stage">
    <svg class="aia-worktree-lanes-graph" viewBox="0 0 226 84" fill="none" aria-hidden="true">
      <path class="aia-worktree-lanes-main" d="M30 10H222"/>
      <g class="aia-worktree-lanes-l1">
        <path d="M56 10C66 10 62 32 72 32H140C150 32 146 10 156 10" pathLength="1"/><path d="M56 10C66 10 62 32 72 32H140C150 32 146 10 156 10" pathLength="1"/>
        <circle cx="86.96" cy="32" r="2.3"/>
        <circle cx="106" cy="32" r="2.3"/>
        <circle cx="125.04" cy="32" r="2.3"/>
        <circle cx="156" cy="10" r="3.4"/>
        <path d="M56 10C66 10 62 32 72 32H140C150 32 146 10 156 10" pathLength="1"/>
      </g>
      <g class="aia-worktree-lanes-l2">
        <path d="M48 10C58 10 54 52 64 52H170C180 52 176 10 186 10" pathLength="1"/><path d="M48 10C58 10 54 52 64 52H170C180 52 176 10 186 10" pathLength="1"/>
        <circle cx="87.32" cy="52" r="2.3"/>
        <circle cx="117" cy="52" r="2.3"/>
        <circle cx="146.68" cy="52" r="2.3"/>
        <circle cx="186" cy="10" r="3.4"/>
        <path d="M48 10C58 10 54 52 64 52H170C180 52 176 10 186 10" pathLength="1"/>
      </g>
      <g class="aia-worktree-lanes-l3">
        <path d="M40 10C50 10 46 72 56 72H196C206 72 202 10 212 10" pathLength="1"/><path d="M40 10C50 10 46 72 56 72H196C206 72 202 10 212 10" pathLength="1"/>
        <circle cx="86.8" cy="72" r="2.3"/>
        <circle cx="126" cy="72" r="2.3"/>
        <circle cx="165.2" cy="72" r="2.3"/>
        <circle cx="212" cy="10" r="3.4"/>
        <path d="M40 10C50 10 46 72 56 72H196C206 72 202 10 212 10" pathLength="1"/>
      </g>
    </svg>
    <span class="aia-worktree-lanes-label aia-worktree-lanes-trunk">main</span>
    <span class="aia-worktree-lanes-label aia-worktree-lanes-l1"><i aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5.5 12.5 4 4 9-9.5"/></svg></i>wt/docs</span>
    <span class="aia-worktree-lanes-label aia-worktree-lanes-l2"><i aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5.5 12.5 4 4 9-9.5"/></svg></i>wt/tests</span>
    <span class="aia-worktree-lanes-label aia-worktree-lanes-l3"><i aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5.5 12.5 4 4 9-9.5"/></svg></i>wt/auth</span>
  </div>
</div>

CSS

@property --aia-worktree-lanes-t {
  syntax: "<number>";
  inherits: true;
  initial-value: 0;
}
@property --aia-worktree-lanes-k {
  syntax: "<integer>";
  inherits: false;
  initial-value: 0;
}
.aia-worktree-lanes {
  --c: var(--aia-color, #7c5cff);
  font-size: var(--aia-size, 16px);
  font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
  line-height: 1.4;
  position: relative;
  box-sizing: border-box;
  padding: 0.9em 1em;
  border-radius: 0.9em;
  background: color-mix(in srgb, currentColor 3%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, currentColor 11%, transparent), 0 0.1em 0.2em color-mix(in srgb, currentColor 4%, transparent), 0 0.9em 2em -1.1em rgb(0 0 0 / 0.3);
  --d: 9s;
  --t: var(--aia-worktree-lanes-t);
  --ok: color-mix(in srgb, #16a34a 78%, currentColor);
  padding: 0.7em 0.8em 0.6em;
  animation: aia-worktree-lanes-clock var(--d) linear infinite;
}
.aia-worktree-lanes-head {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin: 0 0.15em 0.35em;
  font-size: 0.8em;
}
.aia-worktree-lanes-n {
  --aia-worktree-lanes-k: calc(clamp(0, (var(--t) - 0.46) * 200, 1) + clamp(0, (var(--t) - 0.64) * 200, 1) + clamp(0, (var(--t) - 0.8) * 200, 1));
  counter-reset: k var(--aia-worktree-lanes-k);
  color: color-mix(in srgb, var(--ok) calc(var(--aia-worktree-lanes-k) * 33%), color-mix(in srgb, currentColor 55%, transparent));
  font: 600 0.85em ui-monospace, "SF Mono", "JetBrains Mono", Menlo, Consolas, monospace;
  animation: aia-worktree-lanes-fade var(--d) linear infinite;
}
.aia-worktree-lanes-n::before { content: counter(k) " of 3 merged"; }
.aia-worktree-lanes-stage {
  position: relative;
  width: 22.6em;
  height: 8.4em;
  animation: aia-worktree-lanes-fade var(--d) linear infinite;
}
.aia-worktree-lanes-graph {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: visible;
  stroke-linecap: round;
}
.aia-worktree-lanes-main {
  stroke: color-mix(in srgb, currentColor 30%, transparent);
  stroke-width: 2;
}
.aia-worktree-lanes-l1 { --p: clamp(0, (var(--t) - 0.1) * 2.7778, 1); }
.aia-worktree-lanes-l2 { --p: clamp(0, (var(--t) - 0.07) * 1.7544, 1); }
.aia-worktree-lanes-l3 { --p: clamp(0, (var(--t) - 0.04) * 1.3158, 1); }
.aia-worktree-lanes-graph g {
  --m: clamp(0, (var(--p) - 0.995) * 200, 1);
}
/* the lane's track */
.aia-worktree-lanes-graph g path:nth-of-type(1) {
  stroke: color-mix(in srgb, currentColor 11%, transparent);
  stroke-width: 1.6;
}
/* the lane, drawn as the agent works */
.aia-worktree-lanes-graph g path:nth-of-type(2) {
  stroke: var(--c);
  stroke-width: 2;
  stroke-dasharray: 1 2;
  stroke-dashoffset: calc(1 - var(--p));
  opacity: clamp(0, var(--p) * 60, 1);
}
/* the head: a zero length dash with a round cap rides the lane */
.aia-worktree-lanes-graph g path:nth-of-type(3) {
  stroke: var(--c);
  stroke-width: 7;
  stroke-dasharray: 0 1;
  stroke-dashoffset: calc(var(--p) * -1);
  opacity: calc(clamp(0, var(--p) * 40, 1) * (1 - var(--m)));
  filter: drop-shadow(0 0 0.35em var(--c));
}
.aia-worktree-lanes-graph circle {
  fill: var(--c);
  transform-box: fill-box;
  transform-origin: center;
}
.aia-worktree-lanes-l1 circle:nth-of-type(1) { --c-on: clamp(0, (var(--t) - 0.226) * 83.3333, 1); }
.aia-worktree-lanes-l1 circle:nth-of-type(2) { --c-on: clamp(0, (var(--t) - 0.28) * 83.3333, 1); }
.aia-worktree-lanes-l1 circle:nth-of-type(3) { --c-on: clamp(0, (var(--t) - 0.334) * 83.3333, 1); }
.aia-worktree-lanes-l2 circle:nth-of-type(1) { --c-on: clamp(0, (var(--t) - 0.2708) * 83.3333, 1); }
.aia-worktree-lanes-l2 circle:nth-of-type(2) { --c-on: clamp(0, (var(--t) - 0.355) * 83.3333, 1); }
.aia-worktree-lanes-l2 circle:nth-of-type(3) { --c-on: clamp(0, (var(--t) - 0.4392) * 83.3333, 1); }
.aia-worktree-lanes-l3 circle:nth-of-type(1) { --c-on: clamp(0, (var(--t) - 0.3108) * 83.3333, 1); }
.aia-worktree-lanes-l3 circle:nth-of-type(2) { --c-on: clamp(0, (var(--t) - 0.42) * 83.3333, 1); }
.aia-worktree-lanes-l3 circle:nth-of-type(3) { --c-on: clamp(0, (var(--t) - 0.5292) * 83.3333, 1); }
.aia-worktree-lanes-graph circle:not(:last-of-type) {
  opacity: var(--c-on);
  transform: scale(calc(0.3 + var(--c-on) * 0.7));
}
/* the merge commit on main */
.aia-worktree-lanes-graph circle:last-of-type {
  fill: var(--ok);
  stroke: color-mix(in srgb, #16a34a 30%, transparent);
  stroke-width: calc(var(--m) * 3);
  opacity: var(--m);
  transform: scale(calc(0.3 + var(--m) * 0.7));
}
.aia-worktree-lanes-label {
  position: absolute;
  left: 0;
  display: flex;
  align-items: center;
  gap: 0.35em;
  height: 1.4em;
  margin-top: -0.7em;
  font: 600 0.6em ui-monospace, "SF Mono", "JetBrains Mono", Menlo, Consolas, monospace;
  white-space: nowrap;
}
.aia-worktree-lanes-label.aia-worktree-lanes-trunk {
  top: calc(1em / 0.66);
  color: color-mix(in srgb, currentColor 55%, transparent);
  font-size: 0.66em;
  margin-top: -0.7em;
}
.aia-worktree-lanes-label.aia-worktree-lanes-l1 { top: calc(3.2em / 0.6); }
.aia-worktree-lanes-label.aia-worktree-lanes-l2 { top: calc(5.2em / 0.6); }
.aia-worktree-lanes-label.aia-worktree-lanes-l3 { top: calc(7.2em / 0.6); }
.aia-worktree-lanes-label i {
  --m: clamp(0, (var(--p) - 0.995) * 200, 1);
  position: relative;
  display: grid;
  place-items: center;
  width: 1.35em;
  height: 1.35em;
}
.aia-worktree-lanes-label i::before {
  content: "";
  position: absolute;
  inset: 0.1em;
  border-radius: 50%;
  border: 0.18em solid color-mix(in srgb, var(--c) 25%, transparent);
  border-top-color: var(--c);
  opacity: calc(1 - var(--m));
  animation: aia-worktree-lanes-spin 0.8s linear infinite;
}
.aia-worktree-lanes-label svg {
  width: 1.35em;
  height: 1.35em;
  color: var(--ok);
  stroke-width: 3.2;
  opacity: var(--m);
  transform: scale(calc(0.4 + var(--m) * 0.6));
}
@keyframes aia-worktree-lanes-clock {
  from { --aia-worktree-lanes-t: 0; }
  to { --aia-worktree-lanes-t: 1; }
}
@keyframes aia-worktree-lanes-fade {
  0% { opacity: 0; }
  3%, 93% { opacity: 1; }
  97.5%, 100% { opacity: 0; }
}
@keyframes aia-worktree-lanes-spin {
  to { transform: rotate(360deg); }
}
@media (prefers-reduced-motion: reduce) {
  .aia-worktree-lanes,
  .aia-worktree-lanes-n,
  .aia-worktree-lanes-stage {
    animation: none;
    --aia-worktree-lanes-t: 0.7;
  }
  .aia-worktree-lanes-label i::before { animation-duration: 2.4s; }
}

@media (prefers-reduced-motion: reduce) {
  .aia-worktree-lanes, .aia-worktree-lanes *, .aia-worktree-lanes::before, .aia-worktree-lanes::after, .aia-worktree-lanes *::before, .aia-worktree-lanes *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

Customizing this snippet

  • Color: set --aia-color on the element or a parent. Currently previewing #3b82f6.
  • Base size: set --aia-size. Every other dimension is in em, so the whole thing scales together. Currently previewing 16px.
  • Speed: change the duration on the transition or animation shorthand. Staggered delays are relative, so they keep their rhythm.
  • Reduced motion: the rule at the end of the CSS turns the motion off for people who asked their OS for less of it. Keep it.

Similar ai animations