from Java, SQL and jOOQ. https://ift.tt/GNYrLSR
via IFTTT
We can make variables in CSS pretty easily:
:root {
--scale: 1;
}
And we can declare them on any element:
.thing {
transform: scale(--scale);
}
Even better for an example like this is applying the variable on a user interaction, say :hover
:
:root {
--scale: 1;
}
.thing {
height: 100px;
transform: scale(--scale);
width: 100px;
}
.thing:hover {
--scale: 3;
}
But if we wanted to use that variable in an animation… nada.
:root {
--scale: 1;
}
@keyframes scale {
from { --scale: 0; }
to { --scale: 3; }
}
/* Nope! */
.thing {
animation: scale .25s ease-in;
height: 100px;
width: 100px;
}
That’s because the variable is recognized as a string and what we need is a number that can be interpolated between two numeric values. That’s where we can call on @property
to not only register the variable as a custom property, but define its syntax as a number:
@property --scale {
syntax: "<number>";
initial-value: 1;
inherits: true;
}
Now we get the animation!
You’re going to want to check browser support since @property
has only landed in Chrome (starting in version 85) as of this writing. If you’re going to use it, probably best to use it where it’s supported. Looks like we can use the properties from @property
to sniff that out via @supports
:
@supports (inherits: true) {
/* Styles for supporting browsers. */
}
Interpolating Numeric CSS Variables originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
Whenever we build simple or complex layouts using CSS Grid, we’re usually positioning items with line numbers. Grid layouts contain grid lines that are automatically indexed with positive and negative line numbers (that is unless we explicitly name them). Positioning items with line numbers is a fine way to lay things out, though CSS Grid has numerous ways to accomplish the same with an undersized cognitive encumbrance. One of those ways is something I like to think of as the “ASCII” method.
The method boils down to using grid-template-areas
to position grid items using custom-named areas at the grid container level rather than line numbers.
When we declare an element as a grid container using display: grid
, the grid container, by default, generates a single-column track and rows that sufficiently hold the grid items. The container’s child elements that participate in the grid layout are converted to grid items, irrespective of their display
property.
For instance, let’s create a grid by explicitly defining columns and rows using the grid-template-columns
and grid-template-rows
properties.
.grid {
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: repeat(3, 200px);
}
This little snippet of CSS creates a 3×2 grid where the grid items take up equal space in the columns, and where the grid contains three rows with a track size of 200px
.
We can define the entire layout with named grid areas using the grid-template-areas
property. According to the spec, the initial value of grid-template-areas
is none
.
grid-template-areas = none | <string>+
<string>+
is listing the group of strings enclosed with a quote. Each string is represented as a cell, and each quoted string is represented as a row. Like this:
grid-template-areas: "head head" "nav main" "foot foot";
The value of grid-template-areas
describes the layout as having four grid areas. They are,
head
nav
main
foot
head
and foot
span two column tracks and one row track. The remaining nav
and main
each span one column track and one row track. The value of grid-template-areas
is a lot like arranging ASCII characters, and as Chris suggested a while back, we can get a visualization of the overall structure of the layout from the CSS itself which is the most trouble-free way to understand it.
OK, so we created our layout with four named grid areas: head
, nav
, main
, foot
.
Now, let’s start to position the grid items against named grid areas instead of line numbers. Specifically, let’s place a header
element into the named grid area head
and specify the named grid area head
in the header
element using the grid-area
property.
Named grid areas in a grid layout are called idents. So, what we just did was create a custom ident named head
that we can use to place items into certain grid tracks.
header { grid-area: head; }
We can other HTML elements using other custom idents:
nav { grid-area: nav; }
main { grid-area: main; }
footer { grid-area: foot; }
According to CSS Grid Layout Module Level 1, all strings must be defined under the following tokens:
head
is a named cell token.In grid-template-area
, every quoted string (the rows) must have the same number of cells and define the complete grid without ignoring any cell.
We can ignore a cell or leave it as an empty cell using the full-stop character (.
)
.grid {
display: grid;
grid-template-areas:
"head head"
"nav main"
"foot .";
}
If that feels visually awkward or imbalanced to you, we can use multiple full-stop characters without any whitespaces separating them:
.grid {
display: grid;
grid-template-areas:
"head head"
"nav main"
"foot ....";
}
A named cell token can span multiple grid cells, But those cells must form a rectangular layout. In other words, we’re unable to create “L” or “T”-shaped layouts, although the spec does hint at support for non-rectangular layouts with disconnected regions in the future.
Line-based placement is where we use the grid-column
and grid-row
properties to position an element on the grid using grid line numbers that are automatically indexed by a number:
.grid-item {
grid-column: 1 / 3; /* start at grid column line 1 and span to line 3 */
}
But grid item line numbers can change if our layout changes at a breakpoint. In those cases, it’s not like we can rely on the same line numbers we used at a specific breakpoint. This is where it takes extra cognitive encumbrance to understand the code.
That’s why I think an ASCII-based approach works best. We can redefine the layout for each breakpoint using grid-template-areas
within the grid container, which offers a convenient visual for how the layout will look directly in the CSS — it’s like self-documented code!
.grid {
grid-template-areas:
"head head"
"nav main"
"foot ...."; /* much easier way to see the grid! */
}
.grid-item {
grid-area: foot; /* much easier to place the item! */
}
We can actually see a grid’s line numbers and grid areas in DevTools. In Firefox, for example, go to the Layout panel. Then, under the Grid tab, locate the “Grid display settings” and enable the “Display line number” and “Display area names” options.
This ASCII approach using named areas requires a lot less effort to visualize and easily find the placement of elements.
Whenever I see a tutorial on named grid areas, the common example is generally some layout pattern containing header
, main
, sidebar
, and footer
areas. I like to think of this as the “universal” use case since it casts such a wide net.
It’s a great example to illustrate how grid-template-areas
works, but a real-life implementation usually involves media queries set to change the layout at certain viewport widths. Rather than having to re-declare grid-area
on each grid item at each breakpoint to re-position everything, we can use grid-template-areas
to “respond” to the breakpoint instead — and get a nice visual of the layout at each breakpoint in the process!
Before defining the layout, let’s assign an ident to each element using the grid-area
property as a base style.
header {
grid-area: head;
}
.left-side {
grid-area: left;
}
main {
grid-area: main;
}
.right-side {
grid-area: right;
}
footer {
grid-area: foot;
}
Now, let’s define the layout again as a base style. We’re going with a mobile-first approach so that things will stack by default:
.grid-container {
display: grid;
grid-template-areas:
"head"
"left"
"main"
"right"
"foot";
}
Each grid item is auto-sized in this configuration — which seems a little bit weird — so we can set min-height: 100vh
on the grid container to give us more room to work with:
.grid-container {
display: grid;
grid-template-areas:
"head"
"left"
"main"
"right"
"foot";
min-height: 100vh;
}
Now let’s say we want the main
element to sit to the right of the stacked left
and right
sidebars when we get to a slightly wider viewport width. We re-declare grid-template-areas
with an updated ASCII layout to get that:
@media (min-width: 800px) {
.parent {
grid-template-columns: 0.5fr 1fr;
grid-template-rows: 100px 1fr 1fr 100px;
grid-template-areas:
"head head"
"left main"
"right main"
"foot foot";
}
}
I tossed some column and row sizing in there purely for aesthetics.
As the browser gets even wider, we may want to change the layout again, so that main
is sandwiched between the left
and right
sidebars. Let’s write the layout visually!
.grid-container {
grid-template-columns: 200px 1fr 200px; /* again, just for sizing */
grid-template-areas:
"head head head"
"left main right"
"left main right"
"foot foot foot";
}
According to the spec, grid-template-areas
automatically generates names for the grid lines created by named grid areas. We call these implicitly-named grid lines because they are named for us for free without any additional work.
Every named grid area gets four implicitly-named grid lines, two in the column direction and two in the row direction, where -start
and -end
are appended to the ident. For example, a grid area named head
gets head-start
and head-end
lines in both directions for a total of four implicitly-named grid lines.
We can use these lines to our advantage! For instance, if we want an element to overlay the main
, left
, and right
areas of our grid. Earlier, we talked about how layouts have to be rectangular — no “T” and “L” shaped layouts allowed. Consequently, we’re unable to use the ASCII visual layout method to place the overlay. We can, however, use our implicit line names using the same grid-area
property on the overlay that we use to position the other elements.
Did you know that grid-area
is a shorthand property, sort of the same way that margin
and padding
are shorthand properties? It takes multiple values the same way, but instead of following a “clockwise” direction like, margin
— which goes in order of margin-block-start
, margin-inline-end
, margin-block-end
, and margin-inline-start
— grid-area
goes like this:
grid-area: block-start / inline-start / block-end / inline-end;
But we’re talking about rows and columns, not block and inline directions, right? Well, they correspond to one another. The row axis corresponds to the block direction, and the column axis corresponds to the inline direction:
grid-area: grid-row-start / grid-column-start / grid-row-end / grid-column-end;
Back to positioning that overlay element as a grid item in our layout. The grid-area
property will be helpful to position the element using our implicitly-named grid lines:
.overlay {
grid-area: left-start / left-start / right-end / main-end;
}
When we focus on layouts like the “universal” use case we just saw, it’s tempting to think of grid areas in terms of one area per element. But it doesn’t have to work like that. We can repeat idents to reserve more space for them in the layout. We saw that when we repeated the head
and foot
idents in the last example:
.grid-container {
grid-template-areas:
"head head head"
"left main right"
"left main right"
"foot foot foot";
}
Notice that main
, left
, and right
are also repeated but in the block direction.
Let’s forget about full page layouts and use named grid areas on a component. Grid is just as good for component layouts as full pages!
Here’s a pretty standard hero component that sports a row of images followed by different blocks of text:
The HTML is pretty simple:
<div class="hero">
<div class="image">
<img src="..." alt="" />
</div>
<div class="text">
<!-- ... -->
</div>
</div>
We could do this for a real fast stacked layout:
.hero {
grid-template-areas:
"image"
"text";
}
But then we have to reach for some padding
, max-width
or whatever to get the text area narrower than the row of images. How about we expand our ASCII layout into a four-column grid instead by repeating our idents on both rows:
.hero {
display: grid;
grid-template-columns: repeat(4, 1fr); /* maintain equal sizing */
grid-template-areas:
"image image image image"
"text text text text";
}
Alright, now we can place our grid items into those named areas:
.hero .image {
grid-area: image;
}
.hero .text {
grid-area: text;
}
So far, so good — both rows take up the entire width. We can use that as our base layout for small screens.
But maybe we want to introduce the narrower text when the viewport reaches a larger width. We can use what we know about the full-stop character to “skip” columns. Let’s have the text
ident skip the first and last columns in this case.
@media (min-width: 800px) {
main {
grid-template-columns: repeat(6, 1fr); /* increase to six columns */
grid-template-areas:
"image image image image image image"
"..... text text text text .....";
}
}
Now we have the spacing we want:
If the layout needs additional tweaking at even larger breakpoints, we can add more columns and go from there:
.hero {
grid-template-columns: repeat(8, 1fr);
grid-template-areas:
"image image image image image image image image"
"..... text text text text text text .....";
}
Dev tool visualization:
Remember when 12-column and 16-column layouts were the big things in CSS frameworks? We can quickly scale up to that and maintain a nice visual ASCII layout in the code:
main {
grid-template-columns: repeat(12, 1fr);
grid-template-areas:
"image image image image image image image image image image image image"
"..... text text text text text text text text text text .....";
}
We’ve looked at one fairly generic example and one relatively straightforward example. We can still get nice ASCII layout visualizations with more complex layouts.
Let’s work up to this:
I’ve split this up into two elements in the HTML, a header
and a main
:
<header>
<div class="logo"> ... </div>
<div class="menu"> ... </div>
</header>
<main>
<div class="image"> ... </div>
<h2> ... </h2>
<div class="image"> ... </div>
<div class="image"> ... </div>
</main>
I think flexbox is more appropriate for the header
since we can space its child elements out easily that way. So, no grid
there:
header {
display: flex;
justify-content: space-between;
/* etc. */
}
But grid
is well-suited for the main
element’s layout. Let’s define the layout and assign the idents to the corresponding elements that we need to position the .text
and three .image
elements. We’ll start with this as our baseline for small screens:
.grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-template-areas:
"image1 image1 ..... image2"
"texts texts texts texts"
"..... image3 image3 .....";
}
You can already see where we’re going with this, right? The layout is visualized for us, and we can drop the grid items into place with the custom idents:
.image:nth-child(1) {
grid-area: image1;
}
.image:nth-last-child(2) {
grid-area: image2;
}
.image:nth-last-child(1) {
grid-area: image3;
}
h2 {
grid-area: texts;
}
That’s our base layout, so let’s venture into a wider breakpoint:
@media (min-width: 800px) {
.grid {
grid-template-columns: repeat(8, 1fr);
grid-template-areas:
". image1 image1 ...... ...... ...... image2 ."
". texts texts texts texts texts image2 ."
". ..... image3 image3 image3 image3 ...... .";
}
}
I bet you know exactly how that will look because the layout is right there in the code!
Same deal if we decide to scale up even further:
.grid {
grid-template-columns: repeat(12, 1fr);
grid-template-areas:
". image1 image1 ..... ..... ..... ..... ..... ..... ..... ..... ."
". texts texts texts texts texts texts texts texts texts image2 ."
". ..... image3 image3 image3 image3 ..... ..... ..... ..... ..... .";
}
Here’s the full demo:
I’m using the “negative margin
hack” to get the first image to overlap the heading.
I’m curious if anyone else is using grid-template-areas
to create named areas for the benefit of having an ASCII visual of the grid layout. Having that as a reference in my CSS code has helped de-mystify some otherwise complex designs that may have been even more complex when dealing with line numbers.
But if nothing else, defining grid layouts this way teaches us some interesting things about CSS Grid that we saw throughout this post:
grid-template-areas
property allows us to create custom idents — or “named areas” — and use them to position grid items using the grid-area
property.grid-template-areas
accepts as values, including named cell tokens, null cell tokens, and trash cell tokens.grid-template-areas
needs the same number of cells. Ignoring a single cell doesn’t create a layout; it is considered a trash token.grid-template-areas
property value by using required whitespaces between named cell tokens while defining the grid layout......
). Otherwise, a single whitespace between null cell tokens creates unnecessary empty cells, resulting in an invalid layout.grid-area
, then re-declaring the layout with grid-template-areas
on the grid container to update the track listing, if needed. There’s no need to touch the grid items.<custom-ident>-start
and <custom-ident>-end
in both the column and row directions.Using Grid Named Areas to Visualize (and Reference) Your Layout originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
Neither do I! And that’s probably because there’s a lot happening in WordPress-land. The evolution towards full-site editing (FSE) introduces frequent changes to the way we build themes and plugins, and at such break-neck speed that the documentation itself is either non-existent or nearly stale upon being published. Heck, the term “full-site editing” might even change.
Tom McFarlin was musing about this in his post titled “Writing Tutorials in These Gutenberg Times”:
I know Gutenberg has been in development for five years and I know that it’s matured a lot over the course of that time. But [t]he number of tutorials explaining how to do something that’s already outdated was absolutely incredible.
The truth is that I wouldn’t know where to start if I was asked to make a new WordPress site. As I see, there are a number of ways to go in this evolving era of WordPress:
I mean, we have so many tools for extending WordPress as a CMS that the front end of a WordPress site may vary from site to site. We can quite literally build an entire custom WordPress site with nothing but some tweaks to the theme.json
file and fiddling around with layouts in the Block Editor.
It’s amazing and dizzying all at once.
It can also be frustrating, and we saw some of the frustration boil over when Matt Mullenweg commented on the recent design updates to the WordPress.org homepage and the amount of time took to complete:
[…] it’s such a basic layout, it’s hard to imagine it taking a single person more than a day on Squarespace, Wix, Webflow, or one of the WP page builders.
(And, yes, someone proved that a nearly identical copy of the design could be created in 20 minutes.)
I think Matt’s comments have more to do with the process and solving the right problems than they are criticizing the approach that was taken. But reading the comments on that post is a nice microcosm of what I believe is an existential dilemma that many WordPress developers — including myself — are feeling after five years of living between “classic” and FSE themes.
I’ll be honest: I feel super out of touch with FSE development. So out of touch that I’ve wondered whether I’ve fallen too far behind and whether I’ll be able to catch up. I know there’s a huge effort to bolster learning (Learn WordPress is a great example of that), but it feels like there’s still something missing — or some sorta disconnect — that’s preventing the community from being on the same page as far as where we are and where we’re heading.
Could it be a lack of communication? Nah, there’s lots of that, not to mention lots of opportunities to attend meetings and view meeting notes. Could it be a lack of stable documentation? That’s legit, at least when I’ve tried seeking information on block development.
Perhaps the biggest shortcoming is the dearth of blog posts that share tips, tricks, and best practices. The WordPress community has always been a vast army of folks who generously share their talents and wisdom. But I think Tom summed it up best when he tweeted:
my sympathy to anyone who duckduckgo’s/googles a tutorial for how to create a gutenberg block and cannot find a single consistent tutorial.
— Tom McFarlin (@tommcfarlin) August 17, 2022
what a mess.
I, for one, would love to be writing about WordPress as much as I have in the “classic” era. But again, there’s that elusive starting point that prevents me from feeling confident about anything I’d say.
Not Sure How to WordPress Anymore? originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
If a utility class only does one thing, chances are you don’t want it to be overridden by any styles coming from elsewhere. One approach is to use !important
to be 100% certain the style will be applied, regardless of specificity conflicts.
The Tailwind config file has an !important
option that will automatically add !important
to every utility class. There’s nothing wrong with using !important
this way, but nowadays there are better ways to handle specificity. Using CSS Cascade Layers we can avoid the heavy-handed approach of using !important
.
Cascade layers allow us to group styles into “layers”. The precedence of a layer always beats the specificity of a selector. Specificity only matters inside each layer. Establishing a sensible layer order helps avoid styling conflicts and specificity wars. That’s what makes CSS Cascade Layers a great tool for managing custom styles alongside styles from third-party frameworks, like Tailwind.
A Tailwind source .css
file usually starts something like this:
@tailwind base;
@tailwind components;
@tailwind utilities;
@tailwind variants;
Let’s take a look at the official Tailwind docs about directives:
Directives are custom Tailwind-specific at-rules you can use in your CSS that offer special functionality for Tailwind CSS projects. Use the
@tailwind
directive to insert Tailwind’sbase
,components
,utilities
andvariants
styles into your CSS.
In the output CSS file that gets built, Tailwind’s CSS reset — known as Preflight — is included first as part of the base styles. The rest of base
consists of CSS variables needed for Tailwind to work. components
is a place for you to add your own custom classes. Any utility classes you’ve used in your markup will appear next. Variants are styles for things like hover and focus states and responsive styles, which will appear last in the generated CSS file.
@layer
directiveConfusingly, Tailwind has its own @layer
syntax. This article is about the CSS standard, but let’s take a quick look at the Tailwind version (which gets compiled away and doesn’t end up in the output CSS). The Tailwind @layer
directive is a way to inject your own extra styles into a specified part of the output CSS file.
For example, to append your own styles to the base
styles, you would do the following:
@layer base {
h1 {
font-size: 30px;
}
}
The components
layer is empty by default — it’s just a place to put your own classes. If you were doing things the Tailwind way, you’d probably use @apply
(although the creator of Tailwind recently advised against it), but you can also write classes the regular way:
@layer components {
.btn-blue {
background-color: blue;
color: white;
}
}
The CSS standard is much more powerful. Let’s get back to that…
@layer
Here’s how we can rewrite this to use the CSS standard @layer
:
@layer tailwind-base, my-custom-styles, tailwind-utilities;
@layer tailwind-base {
@tailwind base;
}
@layer tailwind-utilities {
@tailwind utilities;
@tailwind variants;
}
Unlike the Tailwind directive, these don’t get compiled away. They’re understood by the browser. In fact, DevTools in Edge, Chrome, Safari, and Firefox will even show you any layers you’ve defined.
You can have as many layers as you want — and name them whatever you want — but in this example, all my custom styles are in a single layer (my-custom-styles
). The first line establishes the layer order:
@layer tailwind-base, my-custom-styles, tailwind-utilities;
This needs to be provided upfront. Be sure to include this line before any other code that uses @layer
. The first layer in the list will be the least powerful, and the last layer in the list will be the most powerful. That means tailwind-base
is the least powerful layer and any code in it will be overridden by all the subsequent layers. That also means tailwind-utilities
will always trump any other styles — regardless of source order or specificity. (Utilities and variants could go in separate layers, but the maintainers of Tailwind will ensure variants always trump utilities, so long as you include the variants below the utilities directive.)
Anything that isn’t in a layer will override anything that is in a layer (with the one exception being styles that use !important
). So, you could also opt to leave utilities
and variants
outside of any layer:
@layer tailwind-base, tailwind-components, my-custom-styles;
@layer tailwind-base {
@tailwind base;
}
@layer tailwind-components {
@tailwind components;
}
@tailwind utilities;
@tailwind variants;
What did this actually buy us? There are plenty of times when advanced CSS selectors come in pretty handy. Let’s create a version of :focus-within
that only responds to keyboard focus rather than mouse clicks using the :has
selector (which lands in Chrome 105). This will style a parent element when any of its children receive focus. Tailwind 3.1 introduced custom variants — e.g. <div class="[&:has(:focus-visible)]:outline-red-600">
— but sometimes it’s easier to just write CSS:
@layer tailwind-base, my-custom-styles;
@layer tailwind-base {
@tailwind base;
}
@tailwind utilities;
@layer my-custom-styles {
.radio-container {
padding: 4px 24px;
border: solid 2px rgb(230, 230, 230);
}
.radio-container:has(:focus-visible) {
outline: solid 2px blue;
}
}
Let’s say in just one instance we want to override the outline-color
from blue
to something else. Let’s say the element we’re working with has both the Tailwind class .outline-red-600
and our own .radio-container:has(:focus-visible)
class:
<div class="outline-red-600 radio-container"> ... </div>
Which outline-color
will win?
Ordinarily, the higher specificity of .radio-container:has(:focus-visible)
would mean the Tailwind class has no effect — even if it’s lower in the source order. But, unlike the Tailwind @layer
directive that relies on source order, the CSS standard @layer
overrules specificity.
As a result, we can use complex selectors in our own custom styles but still override them with Tailwind’s utility classes when we need to — without having to resort to heavy-handed !important
usage to get what we want.
Using CSS Cascade Layers to Manage Custom Styles in a Tailwind Project originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
The GOV.UK team recently published “How and why we removed jQuery from GOV.UK“. This was an insightful look at how an organization can assess its tooling and whether something is still the best tool for the job. This is not a nudge to strip libraries out of your current project right now! Many of us may still be supporting legacy projects and browser requirements that prevent this from being a viable option.
Some of the criticism appears to be that the library size argument is negligible on modern network speeds and caching.
GOV.UK posted an update to address this criticism with metrics – “The impact of removing jQuery on our web performance“.
This article also makes the case for improving maintenance. Instead of upgrading disparate outdated versions of code and having to address security updates in a piecemeal approach, removing the dependency reduces this footprint. This is the dream of having the luxury for addressing technical debt.
Previously, GitHub also documented how they incrementally decoupled jQuery from their front-end code. Improving maintenance and developer experience played a role into their decision.
What caught my eye in particular was the link to the documentation on how to remove jQuery. Understanding how to decouple and perform migration steps are maintenance tasks that will continue to come up for websites and it’s reassuring to have a guide from someone that had to do the same.
Further musing on this subject turned up the old chestnuts “You Might Not Need jQuery” (2014), “(Now More Than Ever) You Might Not Need jQuery” (2017), “Is jQuery still relevant? (1)” (2016), and “Is jQuery still relevant? (2)” (2017).
To Shared Link — Permalink on CSS-Tricks
Removing jQuery from GOV.UK originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
Alright, so the last time we checked in, we were using CSS Grid and combining them with CSS clip-path
and mask
techniques to create grids with fancy shapes.
Here’s just one of the fantastic grids we made together:
Ready for the second round? We are still working with CSS Grid, clip-path
, and mask
, but by the end of this article, we’ll end up with different ways to arrange images on the grid, including some rad hover effects that make for an authentic, interactive experience to view pictures.
And guess what? We’re using the same markup that we used last time. Here’s that again:
<div class="gallery">
<img src="..." alt="...">
<img src="..." alt="...">
<img src="..." alt="...">
<img src="..." alt="...">
<!-- as many times as we want -->
</div>
Like the previous article, we only need a container with images inside. Nothing more!
Last time, our grids were, well, typical image grids. Other than the neat shapes we masked them with, they were pretty standard symmetrical grids as far as how we positioned the images inside.
Let’s try nesting an image in the center of the grid:
We start by setting a 2✕2 grid for four images:
.gallery {
--s: 200px; /* controls the image size */
--g: 10px; /* controls the gap between images */
display: grid;
gap: var(--g);
grid-template-columns: repeat(2, auto);
}
.gallery > img {
width: var(--s);
aspect-ratio: 1;
object-fit: cover;
}
Nothing complex yet. The next step is to cut the corner of our images to create the space for the nested image. I already have a detailed article on how to cut corners using clip-path
and mask
. You can also use my online generator to get the CSS for masking corners.
What we need here is to cut out the corners at an angle equal to 90deg
. We can use the same conic-gradient technique from that article to do that:
.gallery > img {
mask: conic-gradient(from var(--_a), #0000 90deg, #000 0);
}
.gallery > img:nth-child(1) { --_a: 90deg; }
.gallery > img:nth-child(2) { --_a: 180deg; }
.gallery > img:nth-child(3) { --_a: 0deg; }
.gallery > img:nth-child(4) { --_a:-90deg; }
We could use the clip-path
method for cutting corners from that same article, but masking with gradients is more suitable here because we have the same configuration for all the images — all we need is a rotation (defined with the variable --_a
) get the effect, so we’re masking from the inside instead of the outside edges.
Now we can place the nested image inside the masked space. First, let’s make sure we have a fifth image element in the HTML:
<div class="gallery">
<img src="..." alt="...">
<img src="..." alt="...">
<img src="..." alt="...">
<img src="..." alt="...">
<img src="..." alt="...">
</div>
We are going to rely on the good ol’ absolute positioning to place it in there:
.gallery > img:nth-child(5) {
position: absolute;
inset: calc(50% - .5*var(--s));
clip-path: inset(calc(var(--g) / 4));
}
The inset
property allows us to place the image at the center using a single declaration. We know the size of the image (defined with the variable --s
), and we know that the container’s size equals 100%. We do some math, and the distance from each edge should be equal to (100% - var(--s))/2
.
You might be wondering why we’re using clip-path
at all here. We’re using it with the nested image to have a consistent gap. If we were to remove it, you would notice that we don’t have the same gap between all the images. This way, we’re cutting a little bit from the fifth image to get the proper spacing around it.
The complete code again:
.gallery {
--s: 200px; /* controls the image size */
--g: 10px; /* controls the gap between images */
display: grid;
gap: var(--g);
grid-template-columns: repeat(2, auto);
position: relative;
}
.gallery > img {
width: var(--s);
aspect-ratio: 1;
object-fit: cover;
mask: conic-gradient(from var(--_a), #0000 90deg, #000 0);
}
.gallery > img:nth-child(1) {--_a: 90deg}
.gallery > img:nth-child(2) {--_a:180deg}
.gallery > img:nth-child(3) {--_a: 0deg}
.gallery > img:nth-child(4) {--_a:-90deg}
.gallery > img:nth-child(5) {
position: absolute;
inset: calc(50% - .5*var(--s));
clip-path: inset(calc(var(--g) / 4));
}
Now, many of you might also be wondering: why all the complex stuff when we can place the last image on the top and add a border to it? That would hide the images underneath the nested image without a mask, right?
That’s true, and we will get the following:
No mask
, no clip-path
. Yes, the code is easy to understand, but there is a little drawback: the border color needs to be the same as the main background to make the illusion perfect. This little drawback is enough for me to make the code more complex in exchange for real transparency independent of the background. I am not saying a border approach is bad or wrong. I would recommend it in most cases where the background is known. But we are here to explore new stuff and, most important, build components that don’t depend on their environment.
Let’s try another shape this time:
This time, we made the nested image a circle instead of a square. That’s an easy task with border-radius
But we need to use a circular cut-out for the other images. This time, though, we will rely on a radial-gradient()
instead of a conic-gradient()
to get that nice rounded look.
.gallery > img {
mask:
radial-gradient(farthest-side at var(--_a),
#0000 calc(50% + var(--g)/2), #000 calc(51% + var(--g)/2));
}
.gallery > img:nth-child(1) { --_a: calc(100% + var(--g)/2) calc(100% + var(--g)/2); }
.gallery > img:nth-child(2) { --_a: calc(0% - var(--g)/2) calc(100% + var(--g)/2); }
.gallery > img:nth-child(3) { --_a: calc(100% + var(--g)/2) calc(0% - var(--g)/2); }
.gallery > img:nth-child(4) { --_a: calc(0% - var(--g)/2) calc(0% - var(--g)/2); }
All the images use the same configuration as the previous example, but we update the center point each time.
The above figure illustrates the center point for each circle. Still, in the actual code, you will notice that I am also accounting for the gap to ensure all the points are at the same position (the center of the grid) to get a continuous circle if we combine them.
Now that we have our layout let’s talk about the hover effect. In case you didn’t notice, a cool hover effect increases the size of the nested image and adjusts everything else accordingly. Increasing the size is a relatively easy task, but updating the gradient is more complicated since, by default, gradients cannot be animated. To overcome this, I will use a font-size
hack to be able to animate the radial gradient.
If you check the code of the gradient, you can see that I am adding 1em
:
mask:
radial-gradient(farthest-side at var(--_a),
#0000 calc(50% + var(--g)/2 + 1em), #000 calc(51% + var(--g)/2 + 1em));
It’s known that em
units are relative to the parent element’s font-size
, so changing the font-size
of the .gallery
will also change the computed em
value — this is the trick we are using. We are animating the font-size
from a value of 0
to a given value and, as a result, the gradient is animated, making the cut-out part larger, following the size of the nested image that is getting bigger.
Here is the code that highlights the parts involved in the hover effect:
.gallery {
--s: 200px; /* controls the image size */
--g: 10px; /* controls the gaps between images */
font-size: 0; /* initially we have 1em = 0 */
transition: .5s;
}
/* we increase the cut-out by 1em */
.gallery > img {
mask:
radial-gradient(farthest-side at var(--_a),
#0000 calc(50% + var(--g)/2 + 1em), #000 calc(51% + var(--g)/2 + 1em));
}
/* we increase the size by 2em */
.gallery > img:nth-child(5) {
width: calc(var(--s) + 2em);
}
/* on hover 1em = S/5 */
.gallery:hover {
font-size: calc(var(--s) / 5);
}
The font-size
trick is helpful if we want to animate gradients or other properties that cannot be animated. Custom properties defined with @property can solve such a problem, but support for it is still lacking at the time of writing.
I discovered the font-size
trick from @SelenIT2 while trying to solve a challenge on Twitter.
Another shape? Let’s go!
This time we clipped the nested image into the shape of a rhombus. I’ll let you dissect the code as an exercise to figure out how we got here. You will notice that the structure is the same as in our examples. The only differences are how we’re using the gradient to create the shape. Dig in and learn!
We can combine what we’ve learned here and in previous articles to make an even more exciting image grid. This time, let’s make all the images in our grid circular and, on hover, expand an image to reveal the entire thing as it covers the rest of the photos.
The HTML and CSS structure of the grid is nothing new from before, so let’s skip that part and focus instead on the circular shape and hover effect we want.
We are going to use clip-path
and its circle()
function to — you guessed it! — cut a circle out of the images.
That figure illustrates the clip-path
used for the first image. The left side shows the image’s initial state, while the right shows the hovered state. You can use this online tool to play and visualize the clip-path
values.
For the other images, we can update the center of the circle (70% 70%
) to get the following code:
.gallery > img:hover {
--_c: 50%; /* same as "50% at 50% 50%" */
}
.gallery > img:nth-child(1) {
clip-path: circle(var(--_c, 55% at 70% 70%));
}
.gallery > img:nth-child(2) {
clip-path: circle(var(--_c, 55% at 30% 70%));
}
.gallery > img:nth-child(3) {
clip-path: circle(var(--_c, 55% at 70% 30%));
}
.gallery > img:nth-child(4) {
clip-path: circle(var(--_c, 55% at 30% 30%));
}
Note how we are defining the clip-path
values as a fallback inside var()
. This way allows us to more easily update the value on hover by setting the value of the --_c
variable. When using circle()
, the default position of the center point is 50% 50%
, so we get to omit that for more concise code. That’s why you see that we are only setting 50%
instead of 50% at 50% 50%
.
Then we increase the size of our image on hover to the overall size of the grid so we can cover the other images. We also ensure the z-index
has a higher value on the hovered image, so it is the top one in our stacking context.
.gallery {
--s: 200px; /* controls the image size */
--g: 8px; /* controls the gap between images */
display: grid;
grid: auto-flow var(--s) / repeat(2, var(--s));
gap: var(--g);
}
.gallery > img {
width: 100%;
aspect-ratio: 1;
cursor: pointer;
z-index: 0;
transition: .25s, z-index 0s .25s;
}
.gallery > img:hover {
--_c: 50%; /* change the center point on hover */
width: calc(200% + var(--g));
z-index: 1;
transition: .4s, z-index 0s;
}
.gallery > img:nth-child(1){
clip-path: circle(var(--_c, 55% at 70% 70%));
place-self: start;
}
.gallery > img:nth-child(2){
clip-path: circle(var(--_c, 55% at 30% 70%));
place-self: start end;
}
.gallery > img:nth-child(3){
clip-path: circle(var(--_c, 55% at 70% 30%));
place-self: end start;
}
.gallery > img:nth-child(4){
clip-path: circle(var(--_c, 55% at 30% 30%));
place-self: end;
}
What’s going on with the
place-self
property? Why do we need it and why does each image have a specific value?
Do you remember the issue we had in the previous article when creating the grid of puzzle pieces? We increased the size of the images to create an overflow, but the overflow of some images was incorrect. We fixed them using the place-self
property.
Same issue here. We are increasing the size of the images so each one overflows its grid cells. But if we do nothing, all of them will overflow on the right and bottom sides of the grid. What we need is:
To get that, we need to place each image correctly using the place-self
property.
In case you are not familiar with place-self
, it’s the shorthand for justify-self
and align-self
to place the element horizontally and vertically. When it takes one value, both alignments use that same value.
In a previous article, I created a cool zoom effect that applies to a grid of images where we can control everything: number of rows, number of columns, sizes, scale factor, etc.
A particular case was the classic expanding panels, where we only have one row and a full-width container.
We will take this example and combine it with shapes!
Before we continue, I highly recommend reading my other article to understand how the tricks we’re about to cover work. Check that out, and we’ll continue here to focus on creating the panel shapes.
First, let’s start by simplifying the code and removing some variables
We only need one row and the number of columns should adjust based on the number of images. That means we no longer need variables for the number of rows (--n
) and columns (--m
) but we need to use grid-auto-flow: column
, allowing the grid to auto-generate columns as we add new images. We will consider a fixed height for our container; by default, it will be full-width.
Let’s clip the images into a slanted shape:
Once again, each image is contained in its grid cell, so there’s more space between the images than we’d like:
We need to increase the width of the images to create an overlap. We replace min-width:
100%
with min-width: calc(100% + var(--s))
, where --s
is a new variable that controls the shape.
Now we need to fix the first and last images, so they sort of bleed off the page without gaps. In other words, we can remove the slant from the left side of the first image and the slant from the right side of the last image. We need a new clip-path
specifically for those two images.
We also need to rectify the overflow. By default, all the images will overflow on both sides, but for the first one, we need an overflow on the right side while we need a left overflow for the last image.
.gallery > img:first-child {
min-width: calc(100% + var(--s)/2);
place-self: start;
clip-path: polygon(0 0,100% 0,calc(100% - var(--s)) 100%,0 100%);
}
.gallery > img:last-child {
min-width: calc(100% + var(--s)/2);
place-self: end;
clip-path: polygon(var(--s) 0,100% 0,100% 100%,0 100%);
}
The final result is a nice expanding panel of slanted images!
We can add as many images as you want, and the grid will adjust automatically. Plus, we only need to control one value to control the shape!
We could have made this same layout with flexbox since we are dealing with a single row of elements. Here is my implementation.
Sure, slanted images are cool, but what about a zig-zag pattern? I already teased this one at the end of the last article.
All I’m doing here is replacing clip-path
with mask
… and guess what? I already have a detailed article on creating that zig-zag shape — not to mention an online generator to get the code. See how all everything comes together?
The trickiest part here is to make sure the zig-zags are perfectly aligned, and for this, we need to add an offset for every :nth-child(odd)
image element.
.gallery > img {
mask:
conic-gradient(from -135deg at right, #0000, #000 1deg 89deg, #0000 90deg)
100% calc(50% + var(--_p, 0%))/51% calc(2*var(--s)) repeat-y,
conic-gradient(from 45deg at left, #0000, #000 1deg 89deg, #0000 90deg)
0% calc(50% + var(--_p, 0%))/51% calc(2*var(--s)) repeat-y;
}
/* we add an offset to the odd elements */
.gallery > img:nth-child(odd) {
--_p: var(--s);
}
.gallery > img:first-child {
mask:
conic-gradient(from -135deg at right, #0000, #000 1deg 89deg, #0000 90deg)
0 calc(50% + var(--_p, 0%))/100% calc(2*var(--s));
}
.gallery > img:last-child {
mask:
conic-gradient(from 45deg at left, #0000, #000 1deg 89deg, #0000 90deg)
0 calc(50% + var(--_p, 0%)) /100% calc(2*var(--s));
}
Note the use of the --_p
variable, which will fall back to 0%
but will be equal to --_s
for the odd images.
Here is a demo that illustrates the issue. Hover to see how the offset — defined by --_p
— is fixing the alignment.
Also, notice how we use a different mask for the first and last image as we did in the previous example. We only need a zig-zag on the right side of the first image and the left side of the last image.
And why not rounded sides? Let’s do it!
I know that the code may look scary and tough to understand, but all that’s going on is a combination of different tricks we’ve covered in this and other articles I’ve already shared. In this case, I use the same code structure as the zig-zag and the slanted shapes. Compare it with those examples, and you will find no difference! Those are the same tricks in my previous article about the zoom effect. Then, I am using my other writing and my online generator to get the code for the mask that creates those rounded shapes.
If you recall what we did for the zig-zag, we had used the same mask for all the images but then had to add an offset to the odd images to create a perfect overlap. In this case, we need a different mask for the odd-numbered images.
The first mask:
mask:
linear-gradient(-90deg,#0000 calc(2*var(--s)),#000 0) var(--s),
radial-gradient(var(--s),#000 98%,#0000) 50% / calc(2*var(--s)) calc(1.8*var(--s)) space repeat;
The second one:
mask:
radial-gradient(calc(var(--s) + var(--g)) at calc(var(--s) + var(--g)) 50%,#0000 98% ,#000)
calc(50% - var(--s) - var(--g)) / 100% calc(1.8*var(--s))
The only effort I did here is update the second mask to include the gap variable (--g
) to create that space between the images.
The final touch is to fix the first and last image. Like all the previous examples, the first image needs a straight left edge while the last one needs a straight right edge.
For the first image, we always know the mask it needs to have, which is the following:
.gallery > img:first-child {
mask:
radial-gradient(calc(var(--s) + var(--g)) at right, #0000 98%, #000) 50% / 100% calc(1.8 * var(--s));
}
For the last image, it depends on the number of elements, so it matters if that element is :nth-child(odd)
or :nth-child(even)
.
.gallery > img:last-child:nth-child(even) {
mask:
linear-gradient(to right,#0000 var(--s),#000 0),
radial-gradient(var(--s),#000 98%,#0000) left / calc(2*var(--s)) calc(1.8*var(--s)) repeat-y
}
.gallery > img:last-child:nth-child(odd) {
mask:
radial-gradient(calc(var(--s) + var(--g)) at left,#0000 98%,#000) 50% / 100% calc(1.8*var(--s))
}
That’s all! Three different layouts but the same CSS tricks each time:
And here is a big demo with all of them together. All you need is to add a class to activate the layout you want to see.
And here is the one with the Flexbox implementation
Oof, we are done! I know there are many CSS tricks and examples between this article and the last one, not to mention all of the other tricks I’ve referenced here from other articles I’ve written. It took me time to put everything together, and you don’t have to understand everything at once. One reading will give you a good overview of all the layouts, but you may need to read the article more than once and focus on each example to grasp all the tricks.
Did you notice that we didn’t touch the HTML at all other than perhaps the number of images in the markup? All the layouts we made share the same HTML code, which is nothing but a list of images.
Before I end, I will leave you with one last example. It’s a “versus” between two anime characters with a cool hover effect.
What about you? Can you create something based on what you have learned? It doesn’t need to be complex — imagine something cool or funny like I did with that anime matchup. It can be a good exercise for you, and we may end with an excellent collection in the comment section.
CSS Grid and Custom Shapes, Part 2 originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.