Weekend in Lisbon
Three days of tiles, tarts and trams.
A file drop zone whose dashed outline starts marching and turns accent on hover while the upload arrow bobs. The dashes live on an SVG rect with pathLength 100, so the loop has no seam.
Also known as: marching ants border css, animated dashed border, file upload drop zone hover.
<div class="hfx-card-dashed-upload"> <svg class="hfx-card-dashed-upload-frame" aria-hidden="true"><rect width="100%" height="100%" rx="12" pathLength="100"/></svg> <span class="hfx-card-dashed-upload-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 15v3a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-3"/><path class="hfx-card-dashed-upload-arrow" d="M12 15V4m-4.5 4.5L12 4l4.5 4.5"/></svg></span> <strong>Drop files here</strong> <span class="hfx-card-dashed-upload-hint">or <u>browse</u>, up to 10 MB</span> </div>
.hfx-card-dashed-upload {
--c: var(--hfx-color, #3b82f6);
font-size: var(--hfx-size, 16px);
position: relative;
display: grid;
justify-items: center;
gap: 0.3em;
box-sizing: border-box;
width: 15em;
padding: 1.3em 1em 1.4em;
border-radius: 0.8em;
background: color-mix(in srgb, var(--c) 3%, transparent);
font-family: system-ui, sans-serif;
text-align: center;
cursor: pointer;
transition: background-color 0.3s ease;
}
.hfx-card-dashed-upload:hover { background: color-mix(in srgb, var(--c) 10%, transparent); }
.hfx-card-dashed-upload-frame {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
overflow: visible;
pointer-events: none;
}
.hfx-card-dashed-upload-frame rect {
rx: 0.8em;
fill: none;
stroke: color-mix(in srgb, var(--c) 50%, transparent);
stroke-width: 1.5;
stroke-dasharray: 1.5 1;
transition: stroke 0.3s ease;
}
.hfx-card-dashed-upload:hover .hfx-card-dashed-upload-frame rect {
stroke: var(--c);
animation: hfx-card-dashed-upload-march 1s linear infinite;
}
.hfx-card-dashed-upload-icon {
display: grid;
place-items: center;
width: 2.5em;
height: 2.5em;
margin-bottom: 0.35em;
border-radius: 50%;
background: color-mix(in srgb, var(--c) 14%, transparent);
color: var(--c);
transition: background-color 0.3s ease, color 0.3s ease;
}
.hfx-card-dashed-upload:hover .hfx-card-dashed-upload-icon {
background: var(--c);
color: #fff;
}
.hfx-card-dashed-upload-icon svg {
width: 1.25em;
height: 1.25em;
overflow: visible;
fill: none;
stroke: currentColor;
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
}
.hfx-card-dashed-upload:hover .hfx-card-dashed-upload-arrow {
animation: hfx-card-dashed-upload-bob 0.6s cubic-bezier(0.45, 0, 0.55, 1) infinite alternate;
}
.hfx-card-dashed-upload strong {
font-size: 0.9em;
font-weight: 600;
}
.hfx-card-dashed-upload-hint {
font-size: 0.75em;
opacity: 0.65;
}
.hfx-card-dashed-upload u {
color: var(--c);
font-weight: 600;
text-underline-offset: 0.2em;
}
@keyframes hfx-card-dashed-upload-march {
to { stroke-dashoffset: -2.5; }
}
@keyframes hfx-card-dashed-upload-bob {
from { transform: translateY(0.1em); }
to { transform: translateY(-0.2em); }
}
@media (prefers-reduced-motion: reduce) {
.hfx-card-dashed-upload, .hfx-card-dashed-upload *, .hfx-card-dashed-upload::before, .hfx-card-dashed-upload::after, .hfx-card-dashed-upload *::before, .hfx-card-dashed-upload *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}--hfx-color on the element or a parent.
Currently previewing #3b82f6.--hfx-size. Every other dimension is in
em, so the whole thing scales together. Currently previewing 16px.transition or animation
shorthand. Staggered delays are relative, so they keep their rhythm.