loading...
Cover image for Gradient borders with curves and 3d movement in CSS (Nextjs ticket clone)

Gradient borders with curves and 3d movement in CSS (Nextjs ticket clone)

medhatdawoud profile image Medhat Dawoud Updated on ・8 min read

This post has been originally published on my blog

UPDATE: here is a Japanese version of this article

On 27th of October 2020 was the first global user conference of Next.js, I'm so excited about it as a React developer, That's why I've registered immediately after I knew about it, but what happened after I registered was super interesting, I've got a confirmation message from the conf committee with this URL https://nextjs.org/conf/tickets/medhatdawoud which is an interactive ticket, well designed and well animated I'd like to thank the team for designing and developing that, and today we are going to make a clone of it (for learning purposes).

nextjs conf ticket

Challenges

We have quite some challenges here to resolve:

  1. building the ticket itself (✅ will just start with a pre-created one)
  2. implement the gradient borders.
  3. implement the half-circles right and left.
  4. implement the animation according to the cursor move.

Implementation

Let's start with the implementation step by step, hence the final code could be found in this github repo alongside other challenges as well.

1. building the ticket itself

As we agreed earlier, that would be ready one, you can find the whole code in the repo, but this is the HTML:

<div class="ticket-visual_visual" id="ticket">
  <div class="left"></div>
  <div class="right"></div>
  <div class="ticket-visual-wrapper">
    <div class="ticket-visual_profile">
      <div class="ticket-profile_profile">
        <img
          src="https://github.com/medhatdawoud.png"
          alt="medhatdawoud"
          class="ticket-profile_image"
        />
        <div class="ticket-profile_text">
          <p class="ticket-profile_name">Medhat Dawoud</p>
          <p class="ticket-profile_username">
            <span class="ticket-profile_githubIcon">
              <img src="./github.svg" alt="" />
            </span>
            medhatdawoud
          </p>
        </div>
      </div>
      <div class="ticket-event">
        <img src="./event-logos.png" />
      </div>
    </div>
    <div class="ticket-visual_ticket-number-wrapper">
      <div class="ticket-visual_ticket-number">№ 014747</div>
    </div>
  </div>
</div>
Enter fullscreen mode Exit fullscreen mode

Note: event-logos.png is the lower part of the ticket that I took it as a screenshot because that is out of our focus today.

And the CSS is as follow:

:root {
  --size: 1;
  --background: #000;
}

body {
  background: var(--background);
  color: white;
  font-family: Arial, Helvetica, sans-serif;
}

* {
  box-sizing: border-box;
}

.ticket-visual_visual {
  width: 650px;
  height: 320px;
  margin: 100px auto;
  position: relative;
  transition: all 300ms cubic-bezier(0.03, 0.98, 0.53, 0.99) 0s;
  border: 5px solid #fff;
}

.ticket-visual-wrapper {
  width: 100%;
  height: 100%;
}

.ticket-visual_profile {
  padding: calc(39px * var(--size)) calc(155px * var(--size)) calc(
      39px * var(--size)
    ) calc(58px * var(--size));
}

.ticket-profile_text {
  margin: 0;
}

.ticket-profile_profile {
  display: flex;
  flex-direction: row;
}

.ticket-event {
  margin-top: 25px;
  margin-left: -10px;
}

.ticket-profile_image {
  width: calc(82px * var(--size));
  height: calc(82px * var(--size));
  border-radius: 50%;
}

.ticket-profile_name {
  font-size: calc(32px * var(--size));
  margin: 10px 0 5px 20px;
  font-weight: 700;
}

.ticket-profile_username {
  margin: 0 0 5px 20px;
  color: #8a8f98;
  display: flex;
}

.ticket-profile_githubIcon img {
  width: 18px;
  height: 18px;
  margin-right: 5px;
}

.ticket-visual_ticket-number-wrapper {
  position: absolute;
  right: 35px;
  bottom: 0;
}

.ticket-visual_ticket-number {
  transform: rotate(90deg) translateY(calc(100px * var(--size)));
  transform-origin: bottom right;
  font-size: calc(40px * var(--size));
  font-weight: 700;
  text-align: center;
  padding-bottom: 35px;
  width: calc(320px - 10px);
  border-bottom: 2px dashed #333;
}
Enter fullscreen mode Exit fullscreen mode

Now it looks as follow:

raw challenge

2. implementing the gradient borders

The first goto CSS property for making a gradient or even an image as a border is the border-image property, which has great support on all browsers including ie11 as per MDN.

The only problem with using it is that it doesn't support border-radius so we cannot use it, unfortunately, and will make some work-around to implement that.

The idea is mainly to use a div inside another div, let's call them parent div and child div, you can easily add an image or gradient color in our case as background to the parent div, then give a solid color to the child div for example solid black in our case, then give the parent div padding of the width of the border you want, in our case 5px, and technically what the padding is doing is to put a space between the border and the content inside the element, so it will press the child div with 5px from all directions, and that will enable 5px to show from the parent div as if they are a border to the child div.

well, let's implement that, we have a parent child which is .ticket-visual_visual then we can give it a background with the desired gradient border colors, after getting the 4 colors from the main conf website and create them as custom properties as follow:

:root {
  // rest of variable
  --color1: #d25778;
  --color2: #ec585c;
  --color3: #e7d155;
  --color4: #56a8c6;
}

.ticket-visual_visual {
  // other code here
  background: linear-gradient(
    to right,
    var(--color1),
    var(--color2),
    var(--color3),
    var(--color4)
  );
}
Enter fullscreen mode Exit fullscreen mode

Notice using linear-gradient the first param is to right as we needed to have that gradient from left to right.

Now we need to make the child div with solid background as we agreed, the child div here is .ticket-visual-wrapper, so let's give it a background:

.ticket-visual-wrapper {
  background: var(--background); // --background is #000
}
Enter fullscreen mode Exit fullscreen mode

Now we have made it with that work-around for a gradient border, let's now try to give them border radius:

.ticket-visual_visual {
  // other styles
  background: linear-gradient(
    to right,
    var(--color1),
    var(--color2),
    var(--color3),
    var(--color4)
  );
  border-radius: 20px;
}

.ticket-visual-wrapper {
  // other styles
  background: var(--background);
  border-radius: 15px;
}
Enter fullscreen mode Exit fullscreen mode

and the current result should be:

border gradient with curves

Well, we reached a good stage, for now, we have made a curved border with gradient color.

3. implementing the half-circles right and left

Note that there are several different ways to reach the same result that might be better as well than this solution feel free to write you suggestion in a comment :)

With the same idea, we used before we need to use pseudo-elements of the parent div as parent elements and for the child div as child elements.

so basically will use :before and :after pseudo-elements as follow:

.ticket-visual_visual:before {
  content: "";
  display: block;
  position: absolute;
  top: 130px;
  left: -30px;
  width: 60px;
  height: 60px;
  border-radius: 50%;
  background: var(--color1);
  z-index: 2;
}

.ticket-visual_visual:after {
  content: "";
  display: block;
  position: absolute;
  top: 130px;
  right: -30px;
  width: 60px;
  height: 60px;
  border-radius: 50%;
  background: var(--color4);
  z-index: 2;
}
Enter fullscreen mode Exit fullscreen mode

As you can notice, we treat them as divs and positioned them in the middle left and right of the card, also give both of them the extremes of the gradient colors, the left one takes the first color --color1 as background and the right one takes --color4 as background, so the result now should be as follow:

Then we need to add a child circle for each of them with a solid color (black), let's add pseudo-elements for the .ticket-visual-wrapper as well, but first let's add position: relative to it:

.ticket-visual-wrapper {
  width: 100%;
  height: 100%;
  background: var(--background);
  border-radius: 15px;
  position: relative;
}

.ticket-visual-wrapper:before {
  content: "";
  display: block;
  position: absolute;
  top: 130px;
  left: -30px;
  width: 50px;
  height: 50px;
  border-radius: 50%;
  background: var(--background);
  z-index: 3;
}

.ticket-visual-wrapper:after {
  content: "";
  display: block;
  position: absolute;
  top: 130px;
  right: -30px;
  width: 50px;
  height: 50px;
  border-radius: 50%;
  background: var(--background);
  z-index: 3;
}
Enter fullscreen mode Exit fullscreen mode

As you see, we made 2 smaller circles 50px X 50px then the parent ones 60px X 60px and the background here for both are the color of the background --background which is black, the last notice here is that I give them z-index: 3 to make them get elevated on top of the parent pseudo-elements.

The current result:

The only remaining thing is to hide the outer halves of the circles, TBW I found that having something like a cover for them could be a good solution, so I decided to add 2 divs that could be used as covers inside .ticket-visual_visual as follow:

<div class="left"></div>
<div class="right"></div>
Enter fullscreen mode Exit fullscreen mode

and in CSS as they are inside a position: relative div, by giving them position: absolute they will be positioned well:

.left {
  position: absolute;
  top: 110px;
  left: -50px;
  width: 50px;
  height: 100px;
  background: var(--background);
  z-index: 4;
}

.right {
  position: absolute;
  top: 110px;
  right: -50px;
  width: 50px;
  height: 100px;
  background: var(--background);
  z-index: 4;
}
Enter fullscreen mode Exit fullscreen mode

giving them background black, and z-index: 4 to be on to and cover the halves of the circles, the final result is:

Now the design is complete like the one implemented in the conf website.

4. implementing the animation according to the cursor move

Now is the time for a bit of JavaScript, we simply need to calculate a variable with is the position of the cursor (mouse) every time we move so we can add a listener to the mousemove event.

window.addEventListener("mousemove", e => {
  // some code to run every time a user moves the mouse cursor
})
Enter fullscreen mode Exit fullscreen mode

I decided to add that in an inline script tag in the same HTML file because it doesn't require a separate file.

before the listening we need to select the ticker element and get its bounding rect as well, to calculate the center point of the ticket element as follow:

const ticketElm = document.getElementById("ticket")
const { x, y, width, height } = ticketElm.getBoundingClientRect()
const centerPoint = { x: x + width / 2, y: y + height / 2 }
Enter fullscreen mode Exit fullscreen mode

then inside the mousemove event lister we need to add some code to transform that ticket, simply we can add some calculations for the degree that we will use for rotation as follow:

const degreeX = (e.clientY - centerPoint.y) * 0.008
const degreeY = (e.clientX - centerPoint.x) * -0.008
Enter fullscreen mode Exit fullscreen mode

Note that this calculation means: we get the difference between the current mouse position and the center point we calculated earlier, then multiply them by a very small number 0.008, I got it by trying and error until I feel that fit best.

Then we can use these calculated degrees to make the transformation:

window.addEventListener("mousemove", e => {
  const degreeX = (e.clientY - centerPoint.y) * 0.008
  const degreeY = (e.clientX - centerPoint.x) * -0.008

  ticketElm.style.transform = `perspective(1000px) rotateX(${degreeX}deg) rotateY(${degreeY}deg)`
})
Enter fullscreen mode Exit fullscreen mode

at line 5 you can find that we simply set the perspective of the element to 1000px which is a big number to make it move very smooth without rotation, also we used the rotation of x and y based on the calculated degrees.

Then the final result will be:

3d animate on hover

And, we're done here, you might notice some shiny gradient on moving the mouse, but that's for you for homework to make the ticket look glossy, please let me know if you did.

Conclusion

I've enjoyed writing this article, and I hope you enjoyed reading it as well: we've learned from it multiple things or at least I hope so:

  • How to work-around and make a gradient border with border-radius in place
  • How to implement a half-circle with a gradient border
  • How to use perspective in implementing a 3D animation
  • How to think about the calculation of the variable
  • All code is on Github go check it out, fork, clone, and do your homework 😉.

Finally, feel free to share it or discuss it with me on Twitter if you want any help, or follow and let's be friends.

If you understand Arabic, here is an explanation step by step in an Arabic tutorial:
https://youtu.be/BfAydRvM-vk

Tot ziens 👋

Discussion

pic
Editor guide
Collapse
conorluddy profile image
Conor

Nice tut! If you needed a left/right div anyways for the covers I personally just would have used them for the actual circles instead, and then used their own pseudo-elements to cover the halves (if that makes sense).

I believe you could also use css-clip to hide the halves you don't want to see, but I've never used it so I might be wrong...

Collapse
medhatdawoud profile image
Medhat Dawoud Author

Glad you liked it, good suggestion, I got it from several ppl also, Thanks

Collapse
auxnon profile image
Nick McAvoy

You could always just host the content in a padded transparent div and directly map underneath it a filled svg of a ticket or whatever you want to it's box boundaries via small onResize event. Messier but itd have the option to overlay any content not just solid color, and wilder shape possibilities 🤷‍♂️

Collapse
gregjacobs profile image
Greg

Great Post!

Collapse
elalemanyo profile image
Alemaño

Very cool, thanks for sharing!
I would remove left and right and just add overflow: hidden; to .ticket-visual_visual. Works for me: codepen.io/elalemanyo/full/RwRJzEj

Collapse
rw3iss profile image
Ryan Weiss

Very cool, thanks for sharing. Wish there was a demo on the page or original blog post............ demands interaction.

Collapse
spaceinvadev profile image
Mauricio Paternina

Nice!

Could you let me know what's the syntax theme from the code snapshots? Thanks.

Collapse
medhatdawoud profile image
Medhat Dawoud Author

It was NightOwl

Collapse
luiyit profile image
Luiyit Hernandez

Nice. Interesting the way to solve the border.
Thanks for share!

Collapse
medhatdawoud profile image
Medhat Dawoud Author

Nice one, that will cut a bit of code 👏