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).
Challenges
We have quite some challenges here to resolve:
- building the ticket itself (✅ will just start with a pre-created one)
- implement the gradient borders.
- implement the half-circles right and left.
- implement the animation according to the cursor move.
Here are 5 other ways to implement that, you can check them after finishing this article
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>
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;
}
Now it looks as follow:
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)
);
}
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
}
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;
}
and the current result should be:
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;
}
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;
}
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>
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;
}
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
})
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 }
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
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)`
})
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:
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 👋
Top comments (10)
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...
Glad you liked it, good suggestion, I got it from several ppl also, Thanks
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 🤷♂️
Great Post!
Very cool, thanks for sharing!
I would remove
left
andright
and just addoverflow: hidden;
to.ticket-visual_visual
. Works for me: codepen.io/elalemanyo/full/RwRJzEjVery cool, thanks for sharing. Wish there was a demo on the page or original blog post............ demands interaction.
Nice!
Could you let me know what's the syntax theme from the code snapshots? Thanks.
It was
NightOwl
Nice. Interesting the way to solve the border.
Thanks for share!
Nice one, that will cut a bit of code 👏