Deluxe Photo Decorations: One Magical Item | CSS Tricks

As the title says, we will decorate the pictures! There are a bunch of other articles that talk about this, but what we’re covering here is very different because it’s more of a challenge. the challenge? Decorate a photo using only <img> tag and nothing more.

That’s right, no extra markup, no divs, no pseudo-elements. Only one sign.

Seems tough, doesn’t it? But by the end of this article – and the others that make up this little series – I’ll prove that CSS is powerful enough to give us amazing, amazing results despite the limited work with a single element.

luxury photo decoration series

  • Single Item Magic – you are here
  • Advanced Hover Masks and Effects (Coming October 21 )
  • Complex outlines and animations (Coming October 28 )

Let’s start with our first example

Before looking into the code, let’s list the possibilities of designing a file <img> Without any additional items or dummy items. we can use borderAnd the box-shadowAnd the outlineand of course, background. It may seem strange to add a background to a photo because we can’t see it because it will be behind the photo – but the trick is to create space Around photo using padding wow border Then we draw our background inside that space.

I think you know what will happen next since you spoke background, truly? yes, gradients! All the decorations we will be making are based on a lot of color gradients. If you’ve been following me for a while, I guess this probably wouldn’t come as a surprise to you at all. 😁

Let’s go back to our first example:

img {
  --s: 10px; /* control the size */
  padding: var(--s);
  border: calc(2 * var(--s)) solid #0000;
  outline: 1px solid #000;
  outline-offset: calc(-1 * var(--s));
  background: conic-gradient(from 90deg at 1px 1px, #0000 25%, #000 0);

we define padding and transparency border using the variable --s To create an area around our image equal to three times this variable.

Why do we use both padding And the border Instead of one or the other? We can get it using only one of them but I need this group for my gradient, by default the initial value of background-clip he is border-box And the background-origin is equal to padding-box.

Here is a step-by-step explanation to understand the logic:

At first, we don’t have any borders on the image, so our gradient will create two parts with it 1px of thickness. (I use 3px In this particular demo so it’s easy to see.) We add a colored border and the gradient still gives us the same result within the padding area (due to background-origin) But it is repeated beyond the borders. If we make the border color transparent, we can use iteration and get the frame we want.

The outline In the demo it has negative compensation. This creates a square shape at the top of the gradient. This is it! We added a beautiful decoration to our photo using one gradient and outline. We could have used more gradients! But I always try to keep my code as simple as possible and I’ve found that adding a file outline Better this way.

Here’s just the gradient solution as I’m just using it padding to determine the space. Still the same result but with a more complex formula.

Let’s try another idea:

For this, I took the previous example by removing a file outlineand applied a clip-path To cut the gradient on each side. The clip-path The value is lengthy and a bit confusing but here is an illustration to better see its points:

I think you got the main idea. We’ll combine backgrounds, outlines, clipping, and some masking to achieve different types of motifs. We’ll also be looking at some great hovering animations as an added bonus! What we’ve looked at so far is just a small overview of what’s coming!

corner frame only

This one takes four ramps. Each gradient covers one corner, and when scrolling, we expand it to create an entire frame around the image. Let’s break down the code for one of the gradients:

--b: 5px; /* border thickness */
background: conic-gradient(from 90deg at top var(--b) left var(--b), #0000 90deg, darkblue 0) 0 0;
background-size: 50px 50px; 
background-repeat: no-repeat;

We will draw a gradient with a size equal to 50px 50px put it in the upper left corner (0 0). As for gradient composition, here’s a step-by-step illustration showing how I came to this result.

We tend to think that gradients are only good for transitioning between two colors. But in fact, we can do a lot with them! They are especially useful when it comes to creating different shapes. The trick is to make sure there are extreme stops between colors – like in the example above – rather than smooth transitions:

#0000 25%, darkblue 0

This is basically saying: “Fill the gradient with a transparent color until 25% from the area, then fill in the remaining space darkblue.

You might be scratching your head above 0 the value. It’s a simple hack to simplify the syntax. In fact, we should use this to pause hard between colors:

#0000 25%, darkblue 25%

This makes more sense! Transparent color ends at 25% And the darkblue It starts exactly where transparency ends, stopping off hard. If we replace the second with 0The browser will do the job for us, so it’s a slightly more efficient way to do it.

Somewhere in the specs, it says:

If a color stop or transition tip has a position less than the specified position of any stop or transition tip before it in the list, set its position to be equal to the largest position selected for any stop or transition tip before it.

0 It is always smaller than any other value, so the browser will always convert it to the largest value that comes before it in the declaration. In our case, this number is 25%.

Now, we apply the same logic to all corners and end up with the following code:

img {
  --b: 5px; /* border thickness */
  --c: #0000 90deg, darkblue 0; /* define the color here */
  padding: 10px;
    conic-gradient(from 90deg  at top    var(--b) left  var(--b), var(--c)) 0 0,
    conic-gradient(from 180deg at top    var(--b) right var(--b), var(--c)) 100% 0,
    conic-gradient(from 0deg   at bottom var(--b) left  var(--b), var(--c)) 0 100%,
    conic-gradient(from -90deg at bottom var(--b) right var(--b), var(--c)) 100% 100%;
  background-size: 50px 50px; /* adjust border length here */
  background-repeat: no-repeat;

I’ve introduced CSS variables to avoid some redundancy because all gradients use the same color configuration.

For the scroll effect, all I do is increase the size of the gradients to create the full frame:

img:hover {
  background-size: 51% 51%;

Yes it is 51% instead of 50% This creates little overlap and avoids potential gaps.

Let’s try another idea using the same technique:

This time we use only two gradients, but with more complex animations. First, we update the position of each gradient, and then we increase their sizes to create the full frame. I’ve also introduced more variables to improve control over color, size, thickness, and even the gap between the image and the frame.

img {
  --b: 8px;  /* border thickness*/
  --s: 60px; /* size of the corner*/
  --g: 14px; /* the gap*/
  --c: #EDC951; 

  padding: calc(var(--b) + var(--g));
    conic-gradient(from  90deg at top    var(--b) left  var(--b), #0000 25%, var(--c) 0),
    conic-gradient(from -90deg at bottom var(--b) right var(--b), #0000 25%, var(--c) 0);
    var(--_p, 0%) var(--_p, 0%),
    calc(100% - var(--_p, 0%)) calc(100% - var(--_p, 0%));
  background-size: var(--s) var(--s);
  background-repeat: no-repeat;
    background-position .3s var(--_i,.3s), 
    background-size .3s calc(.3s - var(--_i, .3s));
img:hover {
  background-size: calc(100% - var(--g)) calc(100% - var(--g));
  --_p: calc(var(--g) / 2);
  --_i: 0s;

why do you do the --_i And the --_p Variables have an underscore in their names? Underscores are part of the naming convention I use to consider “internal” variables used to improve code. It’s nothing special but I want to make a difference between the variables we set to control the frame (like --bAnd the --cetc.) and the ones I use to make the code shorter.

The code may seem confusing and not easy to understand, but I wrote a three-part series where I detail the technique. I highly recommend reading at least the first article to understand how I got to the above code.

Here is an illustration to better understand the different values:

View the same image of two classic cars three times to illustrate the CSS variables used in the code.

Frame detection

Let’s try another type of animation where we reveal the full frame on hover:

Fabulous, isn’t it? And if you look closely, you will notice that the lines disappear in the opposite direction when you take out the mouse which makes the effect even more noticeable! I used a similar effect in a previous article.

But this time, instead of covering all the elements, I am covering only a small part by defining a height To get something like this:

This is the upper bound of our frame. We repeat the same process on each side of the image and have our scroll effect:

img {
  --b: 10px; /* the border thickness*/
  --g: 5px; /* the gap on hover */
  --c: #8A9B0F; 

  padding: calc(var(--g) + var(--b));
  --_g: no-repeat linear-gradient(var(--c) 0 0);
    var(--_g) var(--_i, 0%) 0,
    var(--_g) 100% var(--_i, 0%),
    var(--_g) calc(100% - var(--_i, 0%)) 100%,
    var(--_g) 0 calc(100% - var(--_i, 0%));
  background-size: var(--_i, 0%) var(--b),var(--b) var(--_i, 0%);
  transition: .4s, background-position 0s;
  cursor: pointer;
img:hover {
  --_i: 100%;

As you can see, I apply the same gradient four times and each has a different location to cover only one side at a time.

another one? Let’s go!

This sounds a bit complicated and really requires some imagination to understand how two conical gradients pull off this kind of magic. Here’s a demo to illustrate one of the gradients:

The pseudo element simulates a color gradient. It is initially out of sight, and when hovering, we first reposition it to get the top edge of the frame. Then we increase the height to get the right edge. The shape of the gradient is similar to the one we used in the last section: two sections to cover two sides.

But why did you make Gradient View 200%? You Think 100% It would be enough, wouldn’t it?

100% It should be enough but I won’t be able to move the gradient as I want if I keep it equal to its width 100%. This is another weird how background-position Works. I cover this in a previous article. I’ve also posted an answer on Stack Overflow to deal with this. I know reading is a lot, but it is really worth your time.

Now that we’ve explained the gradient logic, the second one is easy because it does exactly the same thing, but covers the left and bottom edges instead. All we have to do is swap some values ​​and we’re done:

img {
  --c: #8A9B0F; /* the border color */
  --b: 10px; /* the border thickness*/
  --g: 5px;  /* the gap */

  padding: calc(var(--g) + var(--b));
  --_g: #0000 25%, var(--c) 0;
    conic-gradient(from 180deg at top    var(--b) right var(--b), var(--_g))
     var(--_i, 200%) 0 / 200% var(--_i, var(--b))  no-repeat,
    conic-gradient(            at bottom var(--b) left  var(--b), var(--_g))
     0 var(--_i, 200%) / var(--_i, var(--b)) 200%  no-repeat;
  transition: .3s, background-position .3s .3s;
  cursor: pointer;
img:hover {
  --_i: 100%;
  transition: .3s, background-size .3s .3s;

As you can see, both gradients are almost identical. I simply toggle the size and location values.

Frame rotation

This time we will not draw a frame around our image, but rather adjust the appearance of the current image.

You may be asking how can I convert a straight line to an angled line. No, magic is different than that. This is just an illusion we get after combining simple animations of four gradients.

Let’s see how the top gradient animation works:

I simply update the position of the recurring gradient. Nothing fancy yet! Let’s do the same for the right side:

Are you starting to see the trick? Both gradients intersect at the corner to create the illusion that the straight line is changed to an angle. Let’s remove the outline and hide the excess to see it better:

Now, we add two gradients to cover the remaining edges and we’re done:

img {
  --g: 4px; /* the gap */
  --b: 12px; /* border thickness*/
  --c: #669706; /* the color */

  padding: calc(var(--g) + var(--b));
  --_c: #0000 0 25%, var(--c) 0 50%;
  --_g1: repeating-linear-gradient(90deg ,var(--_c)) repeat-x;
  --_g2: repeating-linear-gradient(180deg,var(--_c)) repeat-y;
    var(--_g1) var(--_p, 25%) 0, 
    var(--_g2) 0 var(--_p, 125%),
    var(--_g1) var(--_p, 125%) 100%, 
    var(--_g2) 100% var(--_p, 25%);
  background-size: 200% var(--b), var(--b) 200%;
  transition: .3s;
img:hover {
  --_p: 75%;

If we take this code and modify it a bit, we can get another cool animation:

Can you figure out the logic in this example? This is your homework! The code may look intimidating but it uses the same logic as the previous examples we looked at. Try to isolate each color gradient and imagine how it moves.


That’s a lot of gradients in one article!

Surely I have warned you! But if the challenge is to decorate an image without additional elements and pseudo-elements, we are left with only a few possibilities and gradients are the most powerful option.

Don’t worry if you’re a bit lost in some of the explanations. I always recommend some of my old articles as I go into more detail with some concepts we’ve recycled for this challenge.

I’m leaving with one last demo to stop you until the next article in this series. This time, I’m using radial-gradient() To create another funny hover effect. I’ll let you dissect the code to see how it works. Ask me questions in the comments if you get stuck!

luxury photo decoration series

  • Single Item Magic – you are here
  • Advanced Hover Masks and Effects (Coming October 21 )
  • Complex outlines and animations (Coming October 28 )

Related posts

Leave a Comment