csscss-animationsbackground-imageborderborder-radius

Border radius input animation


I'm looking to create an animated border input similar to the one shown in this Dribbble post: https://dribbble.com/shots/20266537-Login-page.

Currently, I have an idea using a background image, but it doesn't support border radius:

@keyframes bg {
    0% {
      background-size: 0% 3px, 3px 0, 0 3px, 3px 0;
      background-position: 70px -2px, calc(100% + 2px) -1px, 100% 100%,
        -2px 100%;
    }
    20% {
      background-size: 90% 3px, 3px 0, 0 3px, 3px 0;
      background-position: 70px -2px, calc(100% + 2px) -1px, 100% 100% -2px 100%;
    }
    40% { 
      background-size: 90% 3px, 3px calc(100% + 2px), 0 3px, 3px 0;
      background-position: 70px -2px, calc(100% + 2px) -1px,
        calc(100% + 1px) calc(100% + 2px), -2px 100%;
    }
    60% {
      background-size: 90% 3px, 3px calc(100% + 2px), calc(100% + 2px) 3px,
        3px 0;
      background-position: 70px -2px, calc(100% + 2px) -1px,
        calc(100% + 1px) calc(100% + 2px), -2px 100%;
    }
    100% {
      background-size: 100% 3px, 3px calc(100% + 2px), calc(100% + 2px) 3px,
        3px calc(100% + 2px);
      background-position: 0 -2px, calc(100% + 2px) -1px,
        calc(100% + 1px) calc(100% + 2px), -2px 100%;
/*        border: 2px solid #82c0fe; */
    }
}

.box {
    position:relative;
    border-radius:10px;
      border: 1px solid #ccc;
    width: 25%;
    margin: 2rem auto;  
    padding: 2em;
    background-repeat: no-repeat;
    background-image:   linear-gradient(to right, #5D26C1, #a17fe0, #59C173),
                        linear-gradient(to bottom, #5D26C1, #a17fe0, #59C173),
                        linear-gradient(to right,#5D26C1, #a17fe0, #59C173),
                        linear-gradient(to bottom, #5D26C1, #a17fe0, #59C173);
    animation: bg 2s forwards 1; 
}  
<div class="box"></div>

I would appreciate any assistance or alternative ideas you may have to achieve the desired effect. Thank you!


Solution

  • Consider using an <svg>:

    div {
      position: relative;
    }
    
    input {
      border: 0;
      width: 300px;
      height: 50px;
    }
    input:focus {
      outline: 2px solid transparent;
    }
    
    svg {
      position: absolute;
      left: 0;
      top: 0;
      pointer-events: none;
    }
    
    .focus {
      transition: stroke-dashoffset 1s;
    }
    
    input:focus + * .focus {
      stroke-dashoffset: 0;
    }
    <div>
      <input type="text" />
      <svg width=300 height=50 viewBox="0 0 300 50">
        <defs>
          <linearGradient id="gradient" gradientTransform="rotate(10)">
            <stop offset="5%" stop-color="blue" />
            <stop offset="95%" stop-color="purple" />
          </linearGradient>
          <mask id=border>
            <rect
              x=1
              y=1
              rx=10
              ry=10
              stroke-width=2
              stroke=#ccc
              width=298
              height=48
              fill=none
              pathLength=1
              stroke-dasharray=1
              stroke-dashoffset=-.08
              id=border
            />
          </mask>
        </defs>
        <rect
          x=1
          y=1
          rx=10
          ry=10
          stroke-width=2
          stroke=#ccc
          width=298
          height=48
          fill=none
          mask=url(#border)
        />
        <rect
          x=1
          y=1
          rx=10
          ry=10
          stroke-width=2
          stroke="url(#gradient)"
          width=298
          height=48
          fill=none
          mask=url(#border)
          pathLength=1
          stroke-dasharray=1
          stroke-dashoffset=.92
          class="focus"
        />
      </svg>

    Explanation

    The <svg> should be the same size as the input:

    input {
      …
      width: 300px;
      height: 50px;
    }
    
    <svg width=300 height=50 viewBox="0 0 300 50">
      …
    </svg>
    

    Have the <svg> positioned on top of the <input>:

    div {
      position: relative;
    }
    
    …
    
    svg {
      position: absolute;
      left: 0;
      top: 0;
      pointer-events: none;
    }
    
    <div>
      <input type="text" />
      <svg …>
    

    Use a stroked <rect> for the inactive border state:

    <svg …>
      <rect
        x=1
        y=1
        rx=10
        ry=10
        stroke-width=2
        stroke=#ccc
        width=298
        height=48
        fill=none
      />
    </svg>
    

    Add another rectangle on top for the gradient stroke:

    <svg …>
      <defs>
        <linearGradient id="gradient" gradientTransform="rotate(10)">
          <stop offset="5%" stop-color="blue" />
          <stop offset="95%" stop-color="purple" />
        </linearGradient>
      </defs>
      <rect
        x=1
        y=1
        rx=10
        ry=10
        stroke-width=2
        stroke=#ccc
        width=298
        height=48
        fill=none
      />
      <rect
        x=1
        y=1
        rx=10
        ry=10
        stroke-width=2
        stroke="url(#gradient)"
        width=298
        height=48
        fill=none
      />
    </svg>
    

    Add a mask to both <rect>s for the gap

    <svg …>
      <defs>
        …
        <mask id=border>
          <rect
            x=1
            y=1
            rx=10
            ry=10
            stroke-width=2
            stroke=#ccc
            width=298
            height=48
            fill=none
            pathLength=1
            stroke-dasharray=1
            stroke-dashoffset=-.08
            id=border
          />
        </mask>
      </defs>
      <rect
        …
        mask=url(#border)
      />
      <rect
        …
        mask=url(#border)
        pathLength=1
      />
    </svg>
    

    Add the stroke animation on focus by having a stroke length with dashes, where the dash is the length of the <rect>. Have the gap of the dash be displayed and then transition it such that the "filled-in" portion of the stroke dash becomes visible.

    .focus {
      transition: stroke-dashoffset 1s;
    }
    
    input:focus + * .focus {
      stroke-dashoffset: 0;
    }
    
    <svg …>
      …
      <rect
        …
        pathLength=1
        stroke-dasharray=1
        stroke-dashoffset=.92
        class="focus"
      />
    </svg>