• Variable Aspect Ratio Card With Conic Gradients Meeting Along the Diagonal
• Originally written by Ana Tudor
• Translation from: The Gold Project
• Translator: Hoarfroster

I recently had an interesting problem — I needed to implement a card with a variable aspect ratio (determined by the user), and the aspect ratio was defined in the custom attribute — Ratio. Cards with specific aspect ratios are a classic CSS problem that has become easier to solve in recent years, especially with aspect-ratios, but the tricky part here is that we need to add a cone gradient to each card along the diagonal intersection, as shown here:

Aspect ratio cards set by the user.

The challenge here is that it is easier to make abrupt changes along the diagonal of a variable aspect ratio box with linear-gradient(), for example using a direction like “to the top left” that varies with aspect ratio, But conic-gradient(), which requires an Angle or a percentage to show how far it goes in a full circle, is not easy to construct.

Check out this guide to see how tapering works.

# Simple solution

The CSS specification now includes trigonometric and inverse trigonometric functions, which can help us here – the Angle between a diagonal and a vertical line is the aspect ratio atan(var(–ratio)) Where the tangent to the Angle formed by the diagonal to the vertical line is the width exceeding the height — exactly our aspect ratio).

The Angle between a diagonal and a vertical line.

Writing it in code, we have:

``````--ratio: 3/ 2;
aspect-ratio: var(--ratio);
--angle: atan(var(--ratio));
background:
/* below the diagonal */
conic-gradient(from var(--angle) at 0 100%.# 319197.#ff7a18.#af002d  calc(90deg - var(--angle)), transparent 0%),
/* above the diagonal */
conic-gradient(from calc(.5turn + var(--angle)) at 100% 0.#ff7a18.#af002d.# 319197 calc(90deg - var(--angle)));
Copy the code``````

However, no browser currently implements trigonometric and inverse trigonometric functions, so this simple solution will have to be left to the future.

# JavaScript solutions

We can of course calculate — Angle using the –ratio value in JavaScript.

``````let angle = Math.atan(1 / ratio.split('/').map(c= > +c.trim()).reduce((a, c) = > c / a, 1));
document.body.style.setProperty('--angle'.`The \${+ (180 * angle / Math.PI).toFixed(2)}deg`)
Copy the code``````

But what if JavaScript doesn’t work? What if we really need a pure CSS solution? Okay, it’s a bit of a hassle, but we can still do it!

# Hacky CSS solution

This is an idea I got from the particularity of SVG tween, and to be honest, I found it very frustrating when I first saw it.

Let’s say we have a 50% gradient from bottom to top (because in CSS, this is a 0° gradient). Now, assuming we have the same gradient in SVG, we change the Angle of the two gradients to the same value.

In CSS, this is:

``````linear-gradient(45deg.var(--stop-list));
Copy the code``````

In SVG, we have:

``````<linearGradient id='g' y1='100%' x2='0%' y2='0%'
Copy the code``````

As shown below, these two do not give us the same result. While the CSS gradient is actually at 45°, rotating the same 45° SVG gradient along the diagonal has a noticeable transition between orange and red, even though our box is not square, so the diagonal is not at 45°!

`45 °`CSS and SVG Gradients (example).

This is because our SVG gradient is drawn inside a 1×1 square box and rotated 45°, which causes it to suddenly change from orange to red along the diagonal of the square. The square is then stretched to fit the rectangle, which basically changes the Angle of the diagonal.

Note that SVG gradient distortion occurs only if we don’t change the gradientUnits property of the linearGradient from its default value, objectBoundingBox, to userSpaceOnUse.

## The basic idea

We can’t use SVG here because it only has linear and radial tween, not cone tween. However, we can place CSS tapered gradients in a square box and make them intersect diagonally using a 45° Angle:

``````aspect-ratio: 1/ 1;
width: 19em;
background: /* Below the diagonal */conic-gradient(from 45deg at 0 100%.# 319197.#ff7a18.#af002d 45deg, transparent 0%), /* above the diagonal */conic-gradient(from calc(.5turn + 45deg) at 100% 0.#ff7a18.#af002d.# 319197 45deg);
Copy the code``````

We can then stretch the box using scaling transform — the trick is that the/in 3/2 is a delimiter when used as an aspec-ratio value, but is parsed as a division in calc() :

``````--ratio: 3/ 2;
transform: scaley(calc(1/ (var(--ratio))));
Copy the code``````

We can see this by changing the value of ratio in the embedded edgible code below, so that the two tapered gradiens always intersect diagonally: CodePen

Note that this demo is only applicable to browsers that support aspect-ratio. This property is supported out of the box in Chrome 88+, but Firefox still needs to set the layout.css.aspect-ratif. enabled flag to true in about:config. If you’re using Safari… Okay, I’m sorry!

Enable flags in Firefox.

## The problems with this approach and how to solve them

However, scaling the actual.card element is rarely a good idea. For my use case, the cards are on the grid and setting directional proportions on them messes up the layout (the grid cells are still square, even though we’ve scaled the.card element in them). They also have text content oddly stretched by the scaley() function.

Problems with scaling actual cards (example)

The solution is to provide the required aspect-ratio for the actual card and create our background by placing the absolutely positioned ::before after the text content (z-index: -1). This pseudo-element gets the width of its.card parent element and is initially square. We also set the previous orientation zoom and cone gradient. Note that since we’re absolutely positioned ::before is aligned to the top edge of its.card parent, we should also scale it relative to this edge (transform-Origin needs to have a value of 0 along the Y-axis, while the X-axis value doesn’t matter, it can be anything).

``````body {
--ratio: 3/ 2;
/* Other decorative layout styles */
}

.card {
position: relative;
aspect-ratio: var(--ratio);

&::before {
position: absolute;
z-index: -1; /* Move below the text */

aspect-ratio: 1/ 1; /* Make the card square */
width: 100%;

/* Let it scale to its top edge */
transform-origin: 0 0;
/* Use the Transform to give him the specified scaling ratio */
transform: scaley(calc(1 / (var(--ratio))));
/* Set background */
background: /* gradient(from45deg at 0 100%.# 319197.#af002d.#ff7a18 45deg, transparent 0%), */ gradient(from calc(.5turn + 45deg) at 100% 0.#ff7a18.#af002d.# 319197 45deg);
content: ' '; }}Copy the code``````

Note that in this example, we have changed the style from native CSS to use SCSS.

This is much better because as you can see in the embed below, it’s also editable, so we can modify the –ratio and see how everything fits perfectly when we change its value.

CodePen for code reference and effect preview.

## Inside margin problem

Since we didn’t put any padding on the card, the text might go all the way to the edge, or even slightly beyond the edge, because it’s tilted a little bit.

The lack of`padding`Can cause problems.

That shouldn’t be too hard to fix, right? We just added a padding, right? Well, when we did that, we found that the layout failed!

add`padding`It spoils the layout. (example)

This is because the aspect-ratio we set on the.card element is the aspect ratio of the.card box specified by box-sizing. Since we didn’t explicitly set any box-sizing value, its current value is the default which is content-box. Adding the same value of padding around this box will give us a padding-box with a different aspect ratio, so that it no longer overlapped with the other elements’ ::before.

To better understand this, assume that our aspect-ratio is 4/1 and the width of the content-box is 16rem (256px). This means that the height of the content-box is a quarter of this width, which works out to 4rem (64px). So the content-box is a rectangle 16rem× 4REM (256px×64px).

Now suppose we add a 1rem (16px) padding along each edge. Now, the width of the padding-box is 18rem (288px, as shown in the animated GIF above) — this computes a width of 16rem (256px) for the Content-box, Plus 1rem on the left and 1REM on the right from the padding. Similarly, the height of the padding-box is 6REM (96px) — calculate the height of the content-box, which is 4REM (64px), plus 1REM (16px) of the top and 1REM of the padding at the bottom.

This means that the padding-box is an 18rem×6rem (288px×96px) rectangle, and since 18 = 3… 6, it has an aspect ratio of 3/1, rather than the 4/1 value we set for the aspect-ratio attribute! Also, the width of the ::before pseudo-element is equal to the width of its parent, the padding-box (which we calculate to be 18rem or 288px), and its aspect ratio (set by scaling) is still 4/1, so its visual height is calculated to be 4.5rem (72px). This explains why the background created with this pseudo-element — shrunk vertically to an 18rem×4.5rem (288px×72px) rectangle — is now bigger than the actual card — an 18rem×6rem (288px×72px) rectangle with padding 96px) rectangle — make it smaller.

So, it looks like the solution is pretty simple — we just need to set box-sizing to a border-box to solve our problem, because that will apply aspect-ratio to the box (the same as the padding-box when we don’t have a border).

Sure enough, that would solve the problem… But only for Firefox!

Shows the differences between Chromium (above) and Firefox (below).

The text should be vertically centered because we gave the.card elements a grid layout and placed place-content: Center on them. However, this does not happen in Chromium browsers. This becomes even more obvious when we delete the last declaration — somehow the cells in the card grid also get a 3/1 aspect ratio and overflow the card’s content-box:

use`place-content: center`To check the card grid. (example)

Fortunately, this is a known Chromium bug and should be fixed in the next few months.

In the meantime, what we can do is remove the box-sizing, padding, and place-content declarations from the.card element, and move the child elements (or use the ::after pseudo-elements — I mean, if it only takes one line of code, but we’re lazy. Of course, if we want the text to remain selectable, the actual child elements would be a better idea) and make it a grid with padding.

``````.card {
/* As before, subtract box-sizing, place-content, and margin to define the last two children we moved */

&__content {
place-content: center;

CodePen for code reference and effect preview.

Let’s say we also want our cards to have rounded corners. Since the background we created with a directional transform like Scaley on the ::before pseudo-element will also distort the rounded corners, the easiest way to do this is to set a border-RADIUS on the actual.card element. And use overflow: hidden to cut out everything except the card.

Nonuniform scaling distorts rounded corners. (example)

However, if at some point we want our.card’s other children to be visible outside of it, this becomes a problem. So, what we’re going to do is set the border-RADIUS directly on the ::before pseudo-element that created the background of the card and scale the transform along the y-reversal direction on this border-RADIUS:

``````\$r:.5rem;

.card {
/* Same as before */

&::before {