I'm working on creating a react button component with the following animation:
I found some helpful code online and started off by playing around with plain html/css just to get a feel for it and resulted with the below code:
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: sans-serif;
background-color: black;
height: 100dvh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.button {
position: relative;
width: 200px;
height: 40px;
background-color: #083a65;
border: 0;
color: white;
font-size: 18px;
border-radius: 10px;
cursor: pointer;
max-width: 100%;
}
button svg {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
}
button rect {
animation: button-border 2s linear both;
}
button path {
animation: button-border 1s linear both;
}
@keyframes button-border {
to {
stroke-dashoffset: 0;
}
}
<button
type="button"
class="button"
>
My button
<svg
viewBox="0 0 200 40"
fill="none"
preserveAspectRatio="none"
>
<path
d="M100,1
h90
q9.5,0 9.5,9.5
v19
q0,9.5 -9.5,9.5
h-90
"
stroke="red"
stroke-dasharray="460"
stroke-dashoffset="460"
stroke-width="1"
vectorEffect="non-scaling-stroke"
/>
<path
d="M100,1
h-90
q-9.5,0 -9.5,9.5
v19
q0,9.5 9.5,9.5
h90
"
stroke="red"
stroke-dasharray="460"
stroke-dashoffset="460"
stroke-width="1"
vectorEffect="non-scaling-stroke"
/ >
</svg>
</button>
Overall this achieved my goal but I found that I had the following issue. This code worked if the button width was 200px and if the height was 40px (the same size as the viewport). When I have a different button size though, the border effectively scales as desired but so does the border radius (i.e. The entire border is simply stretched out). I'd like to keep the border radius the same pixel amount even when the SVG is being scaled to a different button size. How can I achieve this?
If you make the SVG to fill the button, it will scale the whole SVG which will stretch the border. Instead, you could modify the SVG viewbox and path to be fit for the size you need. Here are a few examples.
Since you are using React, if you know the height and width, you'd be able to build it based on that using props. (If it needs to work for an unknown height and width, you could use JavaScript to calculate the width of the text and set the attributes in runtime based on that.)
[Note: I also moved the background color to the SVG, made the button background transparent, and set the SVG to -1 z-index to appear behind the text.]
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: sans-serif;
background-color: black;
height: 100dvh;
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
}
section {
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
gap: 1em;
}
.button {
position: relative;
background-color: transparent;
border: 0;
color: white;
font-size: 18px;
border-radius: 10px;
cursor: pointer;
max-width: 100%;
}
button svg {
position: absolute;
left: 0;
top: 0;
z-index: -1;
}
button rect {
animation: button-border 2s linear both;
}
button path {
animation: button-border 1s linear both;
}
@keyframes button-border {
to {
stroke-dashoffset: 0;
}
}
<section>
<button
type="button"
class="button"
style="width: 200px; height: 40px"
>
My Button
<svg viewBox="0 0 200 40" fill="none" preserveAspectRatio="none">
<rect x="1" y="1" width="198" height="38" fill="#083a65" rx="9.5" ry="9.5" />
<path
d="M100,1
h90
q9.5,0 9.5,9.5
v19
q0,9.5 -9.5,9.5
h-90
"
stroke="red"
stroke-dasharray="460"
stroke-dashoffset="460"
stroke-width="1"
vectorEffect="non-scaling-stroke"
/>
<path
d="M100,1
h-90
q-9.5,0 -9.5,9.5
v19
q0,9.5 9.5,9.5
h90
"
stroke="red"
stroke-dasharray="460"
stroke-dashoffset="460"
stroke-width="1"
vectorEffect="non-scaling-stroke"
/>
</svg>
</button>
<button
type="button"
class="button"
style="width: 400px; height: 40px"
>
My Button with Longer Text
<svg viewBox="0 0 400 40" fill="none" preserveAspectRatio="none">
<rect x="1" y="1" width="398" height="38" fill="#083a65" rx="9.5" ry="9.5" />
<path
d="M200,1
h190
q9.5,0 9.5,9.5
v19
q0,9.5 -9.5,9.5
h-190
"
stroke="red"
stroke-dasharray="860"
stroke-dashoffset="860"
stroke-width="1"
vectorEffect="non-scaling-stroke"
/>
<path
d="M200,1
h-190
q-9.5,0 -9.5,9.5
v19
q0,9.5 9.5,9.5
h190
"
stroke="red"
stroke-dasharray="860"
stroke-dashoffset="860"
stroke-width="1"
vectorEffect="non-scaling-stroke"
/>
</svg>
</button>
<button
type="button"
class="button"
style="width: 400px; height: 120px"
>
My Button with Longer Text
<svg viewBox="0 0 400 120" fill="none" preserveAspectRatio="none">
<rect x="1" y="1" width="398" height="118" fill="#083a65" rx="9.5" ry="9.5" />
<path
d="M200,1
h190
q9.5,0 9.5,9.5
v99
q0,9.5 -9.5,9.5
h-190
"
stroke="red"
stroke-dasharray="1020"
stroke-dashoffset="1020"
stroke-width="1"
vectorEffect="non-scaling-stroke"
/>
<path
d="M200,1
h-190
q-9.5,0 -9.5,9.5
v99
q0,9.5 9.5,9.5
h190
"
stroke="red"
stroke-dasharray="1020"
stroke-dashoffset="1020"
stroke-width="1"
vectorEffect="non-scaling-stroke"
/>
</svg>
</button>
</section>
In General:
<button
type="button"
class="button"
style="width: {{width}}px; height: {{height}}px"
>
My Button with Longer Text
<svg viewBox="0 0 {{width}} {{height}}" fill="none" preserveAspectRatio="none">
<rect x="1" y="1" width="{{width - 2}}" height="{{height - 2}}" fill="#083a65" rx="9.5" ry="9.5" />
<path
d="M{{width / 2}},1
h{{width / 2 - 10}}
q9.5,0 9.5,9.5
v{{height - 9.5*2 - 2}}
q0,9.5 -9.5,9.5
h-{{width / 2 - 10}}
"
stroke="red"
stroke-dasharray="{{height * 2 + width * 2 - 20}}"
stroke-dashoffset="{{height * 2 + width * 2 - 20}}"
stroke-width="1"
vectorEffect="non-scaling-stroke"
/>
<path
d="M{{width / 2}},1
h-{{width / 2 - 10}}
q-9.5,0 -9.5,9.5
v{{height - 9.5*2 - 2}}
q0,9.5 9.5,9.5
h{{width / 2 - 10}}
"
stroke="red"
stroke-dasharray="{{height * 2 + width * 2 - 20}}"
stroke-dashoffset="{{height * 2 + width * 2 - 20}}"
stroke-width="1"
vectorEffect="non-scaling-stroke"
/>
</svg>
</button>