Breaking Down Jhey's Shooting Star Border

This blogpost was created to try and break down Jhey's shooting star border because I was curious how it worked.

I've tried reading other resources like this but I wasn't very satisfied with the details, so I decided to build up on top of it and break down all the CSS and understand how they were used.

Demo

Spark duration: 3sBorder radius: 999pxSpark color: #ffffffBox shadow color: #333333Backdrop color: #000000Backdrop hover color: #141414

The Basic HTML Needed

This is using React with CSS module styles applied on it


<button style={buttonStyles} className={styles.button}>
<span className={styles.spark} />
<span className={styles.backdrop} />
<span className={styles.text}>Shooting star border</span>
</button>

Setup basic styles

Position relative

Used to set up the button as the parent container for the spark and backdrop to be placed absolutely inside of it

Box shadow

Used to fake the border effect


.button {
padding: 6px 18px;
display: grid;
border-radius: var(--border-radius);
overflow: hidden;
position: relative;
box-shadow: 0 1000px 0 0 var(--box-shadow-color) inset;
}

Short explanation of some uncommon CSS properties

Relative

By using display: relative;, the elements with display: absolute; within it are placed with respect to it.
So, if there is an element that has position: absolute; top: 0; right: 0;
It would be placed at the top right corner of the parent container.

Inset

Inset is a shorthand for top, right, bottom, left properties.
Eg: inset: 1px 2px 3px 4px; is the same as top: 1px; right: 2px; bottom: 3px; left: 4px;
And when used with position: absolute;, you can think of it as having the same width and height as the parent container, but it has a 1px gap on the top, 2px gap on the right, 3px gap on the bottom and 4px gap on the left.
When used in box-shadow, the box-shadow is displayed inside the element, and this is how we fake the border effect.

Box-shadow

We set the offset-x to 0 (1st value), offset-y to 1000px (2nd value), blur-radius to 0 (3rd value) and spread-radius to 0 (4th value).
The fake border effect can only be achieved when the offset-y is >= the height of the button. Thus, we set a high value for it.
It can also be achieved with offset-x >= width of the button.

Masking the Spark

Inset 0

Equivalent to top: 0; right: 0; bottom: 0; left: 0;, this means it has the same width and height as the parent container.

Mask

We use a linear gradient to create a mask for the spark.
You can see how the mask looks like below.

Animation

We animate with the flip keyframes, which rotates the mask 360deg at 2x the spark duration.
We animate it forever with infinite and steps(2, end) makes it so that the animation only has 2 frames, the start and end.


.spark {
position: absolute;
inset: 0;
border-radius: var(--border-radius);
rotate: 0deg;
mask-image: linear-gradient(white, transparent 50%);
animation: flip calc(var(--spark) * 2) infinite steps(2, end);
}
@keyframes flip {
to {
rotate: 360deg;
}
}

Mask

A mask is used as a "cover" to hide parts of an inner element.
We will be using the mask to cover parts of the actual spark (.spark:before) to create the shooting star effect.
Since gradients (linear and conic gradients, etc.) can only be used as background-image, we use mask-image to apply the mask.

::before

::before and ::after are pseudo-elements and they can be thought as spans that are placed before and after the text content.
They must have the content property set in order to work.

The Actual Spark

Size

The width is twice of the width of the .spark element, and the height is the same as the width with aspect-ratio 1.

Inset

The inset is equivalent to top:0; left: 50%;, using auto means the properties(right, bottom) are not set.

Rotate

We rotate from left to top to right, -90deg to 90deg.

Conic-gradient

This acts as the shooting star's trail.


.spark::before {
content: "";
position: absolute;
width: 200%;
aspect-ratio: 1;
inset: 0 auto auto 50%;
z-index: -1;
translate: -50% -15%;
rotate: -90deg;
opacity: 1;
background-image: conic-gradient(
from 0deg,
transparent 0 340deg,
var(--spark-color) 360deg
);
animation: rotate var(--spark) linear infinite both;
}
@keyframes rotate {
to {
transform: rotate(180deg);
}
}

Demo

This is the conic gradient that acts as the shooting star's trail.

And below shows it in action, where the green border is the ::before element, the blue border is the .spark element.
We can see that the mask covers half of the button, and the conic gradient is only shown on the other half.
Note that the flip animation on the .spark element also applies to the ::before element. That is why we are seeing the ::before element rotating and flipping.

button

Last but not least, we have the backdrop with inset 1px.
This allow for a gap of 1px to show the spark at the button's "border".


.backdrop {
position: absolute;
inset: 1px;
background-color: var(--backdrop-color);
border-radius: var(--border-radius);
}
.button:hover .backdrop {
background-color: var(--backdrop-hover-color);
}

Here, the full code is shown below.


.button {
padding: 6px 18px;
display: grid;
border-radius: var(--border-radius);
overflow: hidden;
position: relative;
box-shadow: 0 1000px 0 0 var(--box-shadow-color) inset;
}
.spark {
position: absolute;
inset: 0;
border-radius: var(--border-radius);
rotate: 0deg;
mask-image: linear-gradient(white, transparent 50%);
animation: flip calc(var(--spark) * 2) infinite steps(2, end);
}
@keyframes flip {
to {
rotate: 360deg;
}
}
.spark::before {
content: "";
position: absolute;
width: 200%;
aspect-ratio: 1;
inset: 0 auto auto 50%;
z-index: -1;
translate: -50% -15%;
rotate: -90deg;
opacity: 1;
background-image: conic-gradient(
from 0deg,
transparent 0 340deg,
var(--spark-color) 360deg
);
animation: rotate var(--spark) linear infinite both;
}
@keyframes rotate {
to {
transform: rotate(180deg);
}
}
.backdrop {
position: absolute;
inset: 1px;
background-color: var(--backdrop-color);
border-radius: var(--border-radius);
}
.button:hover .backdrop {
background-color: var(--backdrop-hover-color);
}
.text {
z-index: 1;
color: rgb(203 213 225);
}

Hope you have learnt something from the blog post above!
Feel free to reach out to me if you have any questions or feedbacks!