Tag: Create

How to Create Wavy Shapes & Patterns in CSS

The wave is probably one of the most difficult shapes to make in CSS. We always try to approximate it with properties like border-radius and lots of magic numbers until we get something that feels kinda close. And that’s before we even get into wavy patterns, which are more difficult.

“SVG it!” you might say, and you are probably right that it’s a better way to go. But we will see that CSS can make nice waves and the code for it doesn’t have to be all crazy. And guess what? I have an online generator to make it even more trivial!

If you play with the generator, you can see that the CSS it spits out is only two gradients and a CSS mask property — just those two things and we can make any kind of wave shape or pattern. Not to mention that we can easily control the size and the curvature of the waves while we’re at it.

Some of the values may look like “magic numbers” but there’s actually logic behind them and we will dissect the code and discover all the secrets behind creating waves.

This article is a follow-up to a previous one where I built all kinds of different zig-zag, scoped, scalloped, and yes, wavy border borders. I highly recommend checking that article as it uses the same technique we will cover here, but in greater detail.

The math behind waves

Strictly speaking, there isn’t one magic formula behind wavy shapes. Any shape with curves that go up and down can be called a wave, so we are not going to restrict ourselves to complex math. Instead, we will reproduce a wave using the basics of geometry.

Let’s start with a simple example using two circle shapes:

Two gray circles.

We have two circles with the same radius next to each other. Do you see that red line? It covers the top half of the first circle and the bottom half of the second one. Now imagine you take that line and repeat it.

A squiggly red line in the shape of waves.

We already see the wave. Now let’s fill the bottom part (or the top one) to get the following:

Red wave pattern.

Tada! We have a wavy shape, and one that we can control using one variable for the circle radii. This is one of the easiest waves we can make and it’s the one I showed off in this previous article

Let’s add a bit of complexity by taking the first illustration and moving the circles a little:

Two gray circles with two bisecting dashed lines indicating spacing.

We still have two circles with the same radii but they are no longer horizontally aligned. In this case, the red line no longer covers half the area of each circle, but a smaller area instead. This area is limited by the dashed red line. That line crosses the point where both circles meet.

Now take that line and repeat it and you get another wave, a smoother one.

A red squiggly line.
A red wave pattern.

I think you get the idea. By controlling the position and size of the circles, we can create any wave we want. We can even create variables for them, which I will call P and S, respectively.

You have probably noticed that, in the online generator, we control the wave using two inputs. They map to the above variables. S is the “Size of the wave” and P is the “curvature of the wave”.

I am defining P as P = m*S where m is the variable you adjust when updating the curvature of the wave. This allows us to always have the same curvature, even if we update S.

m can be any value between 0 and 2. 0 will give us the first particular case where both circles are aligned horizontally. 2 is a kind of maximum value. We can go bigger, but after a few tests I found that anything above 2 produces bad, flat shapes.

Let’s not forget the radius of our circle! That can also be defined using S and P like this:

R = sqrt(P² + S²)/2

When P is equal to 0, we will have R = S/2.

We have everything to start converting all of this into gradients in CSS!

Creating gradients

Our waves use circles, and when talking about circles we talk about radial gradients. And since two circles define our wave, we will logically be using two radial gradients.

We will start with the particular case where P is equal to 0. Here is the illustration of the first gradient:

This gradient creates the first curvature while filling in the entire bottom area —the “water” of the wave so to speak.

.wave {   --size: 50px;    mask: radial-gradient(var(--size) at 50% 0%, #0000 99%, red 101%)      50% var(--size)/calc(4 * var(--size)) 100% repeat-x; }

The --size variable defines the radius and the size of the radial gradient. If we compare it with the S variable, then it’s equal to S/2.

Now let’s add the second gradient:

The second gradient is nothing but a circle to complete our wave:

radial-gradient(var(--size) at 50% var(--size), blue 99%, #0000 101%)    calc(50% - 2*var(--size)) 0/calc(4 * var(--size)) 100%

If you check the previous article you will see that I am simply repeating what I already did there.

I followed both articles but the gradient configurations are not the same.

That’s because we can reach the same result using different gradient configurations. You will notice a slight difference in the alignment if you compare both configurations, but the trick is the same. This can be confusing if you are unfamiliar with gradients, but don’t worry. With some practice, you get used to them and you will find by yourself that different syntax can lead to the same result.

Here is the full code for our first wave:

.wave {   --size: 50px;    mask:     radial-gradient(var(--size) at 50% var(--size),#000 99%, #0000 101%)        calc(50% - 2*var(--size)) 0/calc(4 * var(--size)) 100%,     radial-gradient(var(--size) at 50% 0px, #0000 99%, #000 101%)        50% var(--size)/calc(4 * var(--size)) 100% repeat-x; }

Now let’s take this code and adjust it to where we introduce a variable that makes this fully reusable for creating any wave we want. As we saw in the previous section, the main trick is to move the circles so they are no more aligned so let’s update the position of each one. We will move the first one up and the second down.

Our code will look like this:

.wave {   --size: 50px;   --p: 25px;    mask:     radial-gradient(var(--size) at 50% calc(var(--size) + var(--p)), #000 99%, #0000 101%)        calc(50% - 2*var(--size)) 0/calc(4 * var(--size)) 100%,     radial-gradient(var(--size) at 50% calc(-1*var(--p)), #0000 99%, #000 101%)        50% var(--size) / calc(4 * var(--size)) 100% repeat-x; }

I have introduced a new --p variable that’s used it to define the center position of each circle. The first gradient is using 50% calc(-1*var(--p)), so its center moves up while the second one is using calc(var(--size) + var(--p)) to move it down.

A demo is worth a thousand words:

The circles are neither aligned nor touch one another. We spaced them far apart without changing their radii, so we lost our wave. But we can fix things up by using the same math we used earlier to calculate the new radius. Remember that R = sqrt(P² + S²)/2. In our case, --size is equal to S/2; the same for --p which is also equal to P/2 since we are moving both circles. So, the distance between their center points is double the value of --p for this:

R = sqrt(var(--size) * var(--size) + var(--p) * var(--p))

That gives us a result of 55.9px.

Our wave is back! Let’s plug that equation into our CSS:

.wave {   --size: 50px;   --p: 25px;   --R: sqrt(var(--p) * var(--p) + var(--size)*var(--size));    mask:     radial-gradient(var(--R) at 50% calc(var(--size) + var(--p)), #000 99%, #0000 101%)        calc(50% - 2*var(--size)) 0 / calc(4 * var(--size)) 100%,     radial-gradient(var(--R) at 50% calc(-1*var(--p)), #0000 99%, #000 101%)        50% var(--size)/calc(4 * var(--size)) 100% repeat-x; }

This is valid CSS code. sqrt() is part of the specification, but at the time I’m writing this, there is no browser support for it. That means we need a sprinkle of JavaScript or Sass to calculate that value until we get broader sqrt() support.

This is pretty darn cool: all it takes is two gradients to get a cool wave that you can apply to any element using the mask property. No more trial and error — all you need is to update two variables and you’re good to go!

Reversing the wave

What if we want the waves going the other direction, where we’re filling in the “sky” instead of the “water”. Believe it or not, all we have to do is to update two values:

.wave {   --size: 50px;   --p: 25px;   --R: sqrt(var(--p) * var(--p) + var(--size) * var(--size));    mask:     radial-gradient(var(--R) at 50% calc(100% - (var(--size) + var(--p))), #000 99%, #0000 101%)       calc(50% - 2 * var(--size)) 0/calc(4 * var(--size)) 100%,     radial-gradient(var(--R) at 50% calc(100% + var(--p)), #0000 99%, #000 101%)        50% calc(100% - var(--size)) / calc(4 * var(--size)) 100% repeat-x; }

All I did there is add an offset equal to 100%, highlighted above. Here’s the result:

We can consider a more friendly syntax using keyword values to make it even easier:

.wave {   --size: 50px;   --p: 25px;   --R: sqrt(var(--p)*var(--p) + var(--size) * var(--size));    mask:     radial-gradient(var(--R) at left 50% bottom calc(var(--size) + var(--p)), #000 99%, #0000 101%)        calc(50% - 2 * var(--size)) 0/calc(4 * var(--size)) 100%,     radial-gradient(var(--R) at left 50% bottom calc(-1 * var(--p)), #0000 99%, #000 101%)        left 50% bottom var(--size) / calc(4 * var(--size)) 100% repeat-x; }

We’re using the left and bottom keywords to specify the sides and the offset. By default, the browser defaults to left and top — that’s why we use 100% to move the element to the bottom. In reality, we are moving it from the top by 100%, so it’s really the same as saying bottom. Much easier to read than math!

With this updated syntax, all we have to do is to swap bottom for top — or vice versa — to change the direction of the wave.

And if you want to get both top and bottom waves, we combine all the gradients in a single declaration:

.wave {   --size: 50px;   --p: 25px;   --R: sqrt(var(--p)*var(--p) + var(--size)*var(--size));    mask:     /* Gradient 1 */     radial-gradient(var(--R) at left 50% bottom calc(var(--size) + var(--p)), #000 99%, #0000 101%)        left calc(50% - 2*var(--size)) bottom 0 / calc(4 * var(--size)) 51% repeat-x,     /* Gradient 2 */     radial-gradient(var(--R) at left 50% bottom calc(-1 * var(--p)), #0000 99%, #000 101%)        left 50% bottom var(--size) / calc(4 * var(--size)) calc(51% - var(--size)) repeat-x,     /* Gradient 3 */     radial-gradient(var(--R) at left 50% top calc(var(--size) + var(--p)), #000 99%, #0000 101%)        left calc(50% - 2 * var(--size)) top 0 / calc(4 * var(--size)) 51% repeat-x,     /* Gradient 4 */     radial-gradient(var(--R) at left 50% top calc(-1 * var(--p)), #0000 99%, #000 101%)        left 50% top var(--size) / calc(4 * var(--size)) calc(51% - var(--size)) repeat-x; }

If you check the code, you will see that in addition to combining all the gradients, I have also reduced their height from 100% to 51% so that they both cover half of the element. Yes, 51%. We need that little extra percent for a small overlap that avoid gaps.

What about the left and right sides?

It’s your homework! Take what we did with the top and bottom sides and try to update the values to get the right and left values. Don’t worry, it’s easy and the only thing you need to do is to swap values.

If you have trouble, you can always use the online generator to check the code and visualize the result.

Wavy lines

Earlier, we made our first wave using a red line then filled the bottom portion of the element. How about that wavy line? That’s a wave too! Even better is if we can control its thickness with a variable so we can reuse it. Let’s do it!

We are not going to start from scratch but rather take the previous code and update it. The first thing to do is to update the color stops of the gradients. Both gradients start from a transparent color to an opaque one, or vice versa. To simulate a line or border, we need to start from transparent, go to opaque, then back to transparent again:

#0000 calc(99% - var(--b)), #000 calc(101% - var(--b)) 99%, #0000 101%

I think you already guessed that the --b variable is what we’re using to control the line thickness. Let’s apply this to our gradients:

Yeah, the result is far from a wavy line. But looking closely, we can see that one gradient is correctly creating the bottom curvature. So, all we really need to do is rectify the second gradient. Instead of keeping a full circle, let’s make partial one like the other gradient.

Still far, but we have both curvatures we need! If you check the code, you will see that we have two identical gradients. The only difference is their positioning:

.wave {   --size: 50px;   --b: 10px;   --p: 25px;   --R: sqrt(var(--p)*var(--p) + var(--size)*var(--size));    --_g: #0000 calc(99% - var(--b)), #000 calc(101% - var(--b)) 99%, #0000 101%;   mask:     radial-gradient(var(--R) at left 50% bottom calc(-1*var(--p)), var(--_g))        calc(50% - 2*var(--size)) 0/calc(4*var(--size)) 100%,     radial-gradient(var(--R) at left 50% top    calc(-1*var(--p)), var(--_g))        50% var(--size)/calc(4*var(--size)) 100%; }

Now we need to adjust the size and position for the final shape. We no longer need the gradient to be full-height, so we can replace 100% with this:

/* Size plus thickness */ calc(var(--size) + var(--b))

There is no mathematical logic behind this value. It only needs to be big enough for the curvature. We will see its effect on the pattern in just a bit. In the meantime, let’s also update the position to vertically center the gradients:

.wave {   --size: 50px;   --b: 10px;   --p: 25px;   --R: sqrt(var(--p)*var(--p) + var(--size)*var(--size));    --_g: #0000 calc(99% - var(--b)), #000 calc(101% - var(--b)) 99%, #0000 101%;     mask:     radial-gradient(var(--R) at left 50% bottom calc(-1*var(--p)), var(--_g))        calc(50% - 2*var(--size)) 50%/calc(4 * var(--size)) calc(var(--size) + var(--b)) no-repeat,     radial-gradient(var(--R) at left 50% top calc(-1 * var(--p)), var(--_g)) 50%       50%/calc(4 * var(--size)) calc(var(--size) + var(--b)) no-repeat; }

Still not quite there:

One gradient needs to move a bit down and the other a bit up. Both need to move by half of their height.

We are almost there! We need a small fix for the radius to have a perfect overlap. Both lines need to offset by half the border (--b) thickness:

We got it! A perfect wavy line that we can easily adjust by controlling a few variables:

.wave {   --size: 50px;   --b: 10px;   --p: 25px;   --R: calc(sqrt(var(--p) * var(--p) + var(--size) * var(--size)) + var(--b) / 2);    --_g: #0000 calc(99% - var(--b)), #000 calc(101% - var(--b)) 99%, #0000 101%;   mask:     radial-gradient(var(--R) at left 50% bottom calc(-1 * var(--p)), var(--_g))       calc(50% - 2*var(--size)) calc(50% - var(--size)/2 - var(--b)/2) / calc(4 * var(--size)) calc(var(--size) + var(--b)) repeat-x,     radial-gradient(var(--R) at left 50% top calc(-1*var(--p)),var(--_g))       50%  calc(50% + var(--size)/2 + var(--b)/2) / calc(4 * var(--size)) calc(var(--size) + var(--b)) repeat-x; }

I know that the logic takes a bit to grasp. That’s fine and as I said, creating a wavy shape in CSS is not easy, not to mention the tricky math behind it. That’s why the online generator is a lifesaver — you can easily get the final code even if you don’t fully understand the logic behind it.

Wavy patterns

We can make a pattern from the wavy line we just created!

Oh no, the code of the pattern will be even more difficult to understand!

Not at all! We already have the code. All we need to do is to remove repeat-x from what we already have, and tada. 🎉

A nice wavy pattern. Remember the equation I said we’d revisit?

/* Size plus thickness */ calc(var(--size) + var(--b))

Well, this is what controls the distance between the lines in the pattern. We can make a variable out of it, but there’s no need for more complexity. I’m not even using a variable for that in the generator. Maybe I’ll change that later.

Here is the same pattern going in a different direction:

I am providing you with the code in that demo, but I’d for you to dissect it and understand what changes I made to make that happen.

Simplifying the code

In all the previous demos, we always define the --size and --p independently. But do you recall how I mentioned earlier that the online generator evaluates P as equal to m*S, where m controls the curvature of the wave? By defining a fixed multiplier, we can work with one particular wave and the code can become easier. This is what we will need in most cases: a specific wavy shape and a variable to control its size.

Let’s update our code and introduce the m variable:

.wave {   --size: 50px;   --R: calc(var(--size) * sqrt(var(--m) * var(--m) + 1));    mask:     radial-gradient(var(--R) at 50% calc(var(--size) * (1 + var(--m))), #000 99%, #0000 101%)        calc(50% - 2*var(--size)) 0/calc(4 * var(--size)) 100%,     radial-gradient(var(--R) at 50% calc(-1 * var(--size) * var(--m)), #0000 99%, #000 101%)        50% var(--size) / calc(4 * var(--size)) 100% repeat-x;   }

As you can see, we no longer need the --p variable. I replaced it with var(--m)*var(--size), and optimized some of the math accordingly. Now, If we want to work with a particular wavy shape, we can omit the --m variable and replace it with a fixed value. Let’s try .8 for example.

--size: 50px; --R: calc(var(--size) * 1.28);  mask:   radial-gradient(var(--R) at 50% calc(1.8 * var(--size)), #000 99%, #0000 101%)      calc(50% - 2*var(--size)) 0/calc(4 * var(--size)) 100%,   radial-gradient(var(--R) at 50% calc(-.8 * var(--size)), #0000 99%, #000 101%)      50% var(--size) / calc(4 * var(--size)) 100% repeat-x;

See how the code is easier now? Only one variable to control your wave, plus you no more need to rely on sqrt() which has no browser support!

You can apply the same logic to all the demos we saw even for the wavy lines and the pattern. I started with a detailed mathmatical explanation and gave the generic code, but you may find yourself needing easier code in a real use case. This is what I am doing all the time. I rarely use the generic code, but I always consider a simplified version especially that, in most of the cases, I am using some known values that don’t need to be stored as variables. (Spoiler alert: I will be sharing a few examples at the end!)

Limitations to this approach

Mathematically, the code we made should give us perfect wavy shapes and patterns, but in reality, we will face some strange results. So, yes, this method has its limitations. For example, the online generator is capable of producing poor results, especially with wavy lines. Part of the issue is due to a particular combination of values where the result gets scrambled, like using a big value for the border thickness compared to the size:

For the other cases, it’s the issue related to some rounding that will results in misalignment and gaps between the waves:

That said, I still think the method we covered remains a good one because it produces smooth waves in most cases, and we can easily avoid the bad results by playing with different values until we get it perfect.

Wrapping up

I hope that after this article, you will no more to fumble around with trial and error to build a wavy shape or pattern. In addition to the online generator, you have all the math secrets behind creating any kind of wave you want!

The article ends here but now you have a powerful tool to create fancy designs that use wavy shapes. Here’s inspiration to get you started…

What about you? Use my online generator (or write the code manually if you already learned all the math by heart) and show me your creations! Let’s have a good collection in the comment section.


How to Create Wavy Shapes & Patterns in CSS originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

CSS-Tricks

, , ,

How to Create Block Theme Patterns in WordPress 6.0

Block patterns, also frequently referred to as sections, were introduced in WordPress 5.5 to allow users to build and share predefined block layouts in the pattern directory. The directory is the home of a wide range of curated patterns designed by the WordPress community. These patterns are available in simple copy and paste format, require no coding knowledge and thus are a big time saver for users.

Despite many articles on patterns, there is a lack of comprehensive and up-to-date articles on pattern creation covering the latest enhanced features. This article aims to fill the gap with a focus on the recent enhanced features like creating patterns without registration and offer an up-to-date step-by-step guide to create and use them in block themes for novices and experienced authors.

Since the launch of WordPress 5.9 and the Twenty Twenty-Two (TT2) default theme, the use of block patterns in block themes has proliferated. I have been a big fan of block patterns and have created and used them in my block themes.

The new WordPress 6.0 offers three major patterns feature enhancements to authors:

  • Allowing pattern registration through /patterns folder (similar to /parts, /templates, and /styles registration).
  • Registering patterns from the public patterns directory using the theme.json.
  • Adding patterns that can be offered to the user when creating a new page.

In an introductory Exploring WordPress 6.0 video, Automattic product liaison Ann McCathy highlights some newly enhanced patterns features (starting at 15:00) and discusses future patterns enhancement plans — which include patterns as sectioning elements, providing options to pick pattern on page creation, integrating pattern directory search, and more.

Prerequisites

The article assumes that readers have basic knowledge of WordPress full site editing (FSE) interface and block themes. The Block Editor Handbook and Full Site Editing website provide the most up-to-date tutorial guides to learn all FSE features, including block themes and patterns discussed in this article.

Section 1: Evolving approaches to creating block patterns

The initial approach to creating block patterns required the use of block pattern API either as a custom plugin or directly registered in the functions.php file to bundle with a block theme. The newly launched WordPress 6.0 introduced several new and enhanced features working with patterns and themes, including pattern registration via a /patterns folder and a page creation pattern modal.

For background, let’s first briefly overview how the pattern registration workflow evolved from using the register pattern API to directly loading without registration.

Use case example 1: Twenty Twenty-One

The default Twenty Twenty-One theme (TT1) and TT1 Blocks theme (a sibling of TT1) showcase how block patterns can be registered in the theme’s functions.php. In the TT1 Blocks experimental-theme, this single block-pattern.php file containing eight block patterns is added to the functions.php as an include as shown here.

A custom block pattern needs to be registered using the register_block_pattern function, which receives two arguments — title (name of the patterns) and properties (an array describing properties of the pattern).

Here is an example of registering a simple “Hello World” paragraph pattern from this Theme Shaper article:

register_block_pattern(     'my-plugin/hello-world',     array(         'title'   => __( 'Hello World', 'my-plugin' ),         'content' => "<!-- wp:paragraph -->n<p>Hello World</p>n<!-- /wp:paragraph -->",     ) );

After registration, the register_block_pattern() function should be called from a handler attached to the init hook as described here.

 function my_plugin_register_my_patterns() {     register_block_pattern( ... );   }    add_action( 'init', 'my_plugin_register_my_patterns' );

Once block patterns are registered they are visible in the block editor. More detailed documentation is found in this Block Pattern Registration.

Block pattern properties

In addition to required title and content properties, the block editor handbook lists the following optional pattern properties:

  • title (required): A human-readable title for the pattern.
  • content (required): Block HTML Markup for the pattern.
  • description (optional): A visually hidden text used to describe the pattern in the inserter. A description is optional but it is strongly encouraged when the title does not fully describe what the pattern does. The description will help users discover the pattern while searching.
  • categories (optional): An array of registered pattern categories used to group block patterns. Block patterns can be shown on multiple categories. A category must be registered separately in order to be used here.
  • keywords (optional): An array of aliases or keywords that help users discover the pattern while searching.
  • viewportWidth (optional): An integer specifying the intended width of the pattern to allow for a scaled preview of the pattern in the inserter.
  • blockTypes (optional): An array of block types that the pattern is intended to be used with. Each value needs to be the declared block’s name.
  • inserter (optional): By default, all patterns will appear in the inserter. To hide a pattern so that it can only be inserted programmatically, set the inserter to false.

The following is an example of a quote pattern plugin code snippets taken from the WordPress blog.

/* Plugin Name: Quote Pattern Example Plugin */  register_block_pattern(     'my-plugin/my-quote-pattern',      array(       'title'       => __( 'Quote with Avatar', 'my-plugin' ),       'categories'  => array( 'text' ),       'description' => _x( 'A big quote with an avatar".', 'Block pattern description', 'my-plugin' ),       'content'     => '<!-- wp:group --><div class="wp-block-group"><div class="wp-block-group__inner-container"><!-- wp:separator {"className":"is-style-default"} --><hr class="wp-block-separator is-style-default"/><!-- /wp:separator --><!-- wp:image {"align":"center","id":553,"width":150,"height":150,"sizeSlug":"large","linkDestination":"none","className":"is-style-rounded"} --><div class="wp-block-image is-style-rounded"><figure class="aligncenter size-large is-resized"><img src="https://blockpatterndesigns.mystagingwebsite.com/wp-content/uploads/2021/02/StockSnap_HQR8BJFZID-1.jpg" alt="" class="wp-image-553" width="150" height="150"/></figure></div><!-- /wp:image --><!-- wp:quote {"align":"center","className":"is-style-large"} --><blockquote class="wp-block-quote has-text-align-center is-style-large"><p>"Contributing makes me feel like I'm being useful to the planet."</p><cite>— Anna Wong, <em>Volunteer</em></cite></blockquote><!-- /wp:quote --><!-- wp:separator {"className":"is-style-default"} --><hr class="wp-block-separator is-style-default"/><!-- /wp:separator --></div></div><!-- /wp:group -->',       ) );

Using patterns in a template file

Once patterns are created, they can be used in a theme template file with the following block markup:

<!-- wp:pattern {"slug":"prefix/pattern-slug"} /-->

An example from this GitHub repository shows the use of “footer-with-background” pattern slug with “tt2gopher” prefix in TT2 Gopher blocks theme.

Early adopters of block themes and Gutenberg plugin took advantage of patterns in classic themes as well. The default Twenty Twenty and my favorite Eksell themes (a demo site here) are good examples that showcase how pattern features can be added to classic themes.

Use case example 2: Twenty Twenty-Two

If a theme includes only a few patterns, the development and maintenance are fairly manageable. However, if a block theme includes many patterns, like in TT2 theme, then the pattern.php file becomes very large and hard to read. The default TT2 theme, which bundles more than 60 patterns, showcases a refactored pattern registration workflow structure that is easier to read and maintain.

Taking examples from the TT2 theme, let’s briefly discuss how this simplified workflow works.

2.1: Registering Patterns Categories

For demonstration purposes, I created a TT2 child theme and set it up on my local test site with some dummy content. Following TT2, I registered footer-with-background and added to the following pattern categories array list in its block-patterns.php file.

/** * Registers block patterns and categories. */ function twentytwentytwo_register_block_patterns() { 	$ block_pattern_categories = array( 		'footer'   => array( 'label' => __( 'Footers', 'twentytwentytwo' ) ), 		'header'   => array( 'label' => __( 'Headers', 'twentytwentytwo' ) ), 		'pages'    => array( 'label' => __( 'Pages', 'twentytwentytwo' ) ),                 // ... 	);  	/** 	 * Filters the theme block pattern categories. 	 */ 	$ block_pattern_categories = apply_filters( 'twentytwentytwo_block_pattern_categories', $ block_pattern_categories );  	foreach ( $ block_pattern_categories as $ name => $ properties ) { 		if ( ! WP_Block_Pattern_Categories_Registry::get_instance()->is_registered( $ name ) ) { 			register_block_pattern_category( $ name, $ properties ); 		} 	}  	$ block_patterns = array( 		'footer-default', 		'footer-dark', 		'footer-with-background', 		//... 		'header-default', 		'header-large-dark', 		'header-small-dark', 		'hidden-404', 		'hidden-bird', 		//... 	);  	/** 	 * Filters the theme block patterns. 	 */ 	$ block_patterns = apply_filters( 'twentytwentytwo_block_patterns', $ block_patterns );  	foreach ( $ block_patterns as $ block_pattern ) { 		$ pattern_file = get_theme_file_path( '/inc/patterns/' . $ block_pattern . '.php' );  		register_block_pattern( 			'twentytwentytwo/' . $ block_pattern, 			require $ pattern_file 		); 	} } add_action( 'init', 'twentytwentytwo_register_block_patterns', 9 );

In this code example, each pattern listed in the $ block_patterns = array() is called by foreach() function which requires a patterns directory file with the listed pattern name in the array which we will add in the next step.

2.2: Adding a pattern file to the /inc/patterns folder

Next, we should have all the listed patterns files in the $ block_patterns = array(). Here is an example of one of the pattern files, footer-with-background.php:

/**  * Dark footer wtih title and citation  */ return array( 	'title'      => __( 'Footer with background', 'twentytwentytwo' ), 	'categories' => array( 'footer' ), 	'blockTypes' => array( 'core/template-part/footer' ),   'content'    => '<!-- wp:group {"align":"full","style":{"elements":{"link":{"color":{"text":"var:preset|color|background"}}},"spacing":{"padding":{"top":"var(--wp--custom--spacing--small, 1.25rem)","bottom":"var(--wp--custom--spacing--small, 1.25rem)"}}},"backgroundColor":"background-header","textColor":"background","layout":{"inherit":true}} -->       <div class="wp-block-group alignfull has-background-color has-background-header-background-color has-text-color has-background has-link-color" style="padding-top:var(--wp--custom--spacing--small, 1.25rem);padding-bottom:var(--wp--custom--spacing--small, 1.25rem)"><!-- wp:paragraph {"align":"center"} -->       <p class="has-text-align-center">' .       sprintf(         /* Translators: WordPress link. */         esc_html__( 'Proudly powered by %s', 'twentytwentytwo' ),         '<a href="' . esc_url( __( 'https://wordpress.org', 'twentytwentytwo' ) ) . '" rel="nofollow">WordPress</a> | a modified TT2 theme.'       ) . '</p>       <!-- /wp:paragraph --></div>           <!-- /wp:group -->', );

Let’s reference the pattern in the footer.html template part:

<!-- wp:template-part {"slug":"footer"} /-->

This is similar to adding heading or footer parts in a template file.

The patterns can similarly be added to the parts/footer.html template by modifying it to refer to slug of the theme’s pattern file (footer-with-background):

<!-- wp:pattern {"slug":"twentytwentytwo/footer-with-background"} /-->

Now, if we visit the patterns inserter of the block editor, the Footer with background should be available for our use:

Screenshot of the pattern for Footer with background.

The following screenshot shows the newly created footer with background pattern on the front-end.

Screenshot of the footer background as it appears rendered on the site.

Now that patterns have become the integral part of block theme, many patterns are bundled in block themes — like Quadrat, Seedlet, Mayland, Zoologist, Geologist — following the workflow discussed above. Here is an example of the Quadrat theme /inc/patterns folder with a block-pattern registration file and list of files with content source and required pattern header within return array() function.

Section 2: Creating and loading patterns without registration

Please note that this feature requires the installation of WordPress 6.0 or Gutenberg plugin 13.0 or above in your site.

This new WordPress 6.0 feature allows pattern registration via standard files and folders – /patterns, bringing similar conventions like /parts, /templates, and /styles.

The process, as also described in this WP Tavern article, involves the following three steps:

  • creating a patterns folder at the theme’s root
  • adding plugin style pattern header
  • pattern source content

A typical pattern header markup, taken from the article is shown below:

<?php /** * Title: A Pattern Title * Slug: namespace/slug * Description: A human-friendly description. * Viewport Width: 1024 * Categories: comma, separated, values * Keywords: comma, separated, values * Block Types: comma, separated, values * Inserter: yes|no */ ?>

As described in the previous section, only Title and Slug fields are required and other fields are optional.

Referencing examples from recently released themes, I refactored pattern registration in this TT2 Gopher Blocks demo theme, prepared for a previous article on the CSS-Tricks.

In the following steps, let’s explore how a footer-with-background.php pattern registered with PHP and used in a footer.html template is refactored.

2.1: Create a /patterns folder at the root of the theme

The first step is to create a /patterns folder at TT2 Gopher theme’s root and move the footer-with-background.php pattern file to /patterns folder and refactor.

2.2: Add pattern data to the file header

Next, create the following pattern header registration fields.

<?php /** * Title: Footer with background * Slug: tt2gopher/footer-with-background * Categories: tt2gopher-footer * Viewport Width: 1280 * Block Types: core/parts/footer * Inserter: yes */ ?> <!-- some-block-content /-->

A pattern file has a top title field written as PHP comments. Followed by the block-content written in HTML format.

2.3: Add Pattern Content to the file

For the content section, let’s copy the code snippets within single quotes (e.g., '...') from the content section of the footer-with-background and replace the <!-- some-block-content /-->:

<!-- wp:group {"align":"full","style":{"elements":{"link":{"color":{"text":"var:preset|color|foreground"}}},"spacing":{"padding":{"top":"35px","bottom":"30px"}}},"backgroundColor":"background-header","textColor":"foreground","className":"has-foreground","layout":{"inherit":true}} -->     <div class="wp-block-group alignfull has-foreground has-foreground-color has-background-header-background-color has-text-color has-background has-link-color" style="padding-top:35px;padding-bottom:30px"><!-- wp:paragraph {"align":"center","fontSize":"small"} -->     <p class="has-text-align-center has-small-font-size">Powered by WordPress | TT2 Gopher, a modified TT2 theme</p>     <!-- /wp:paragraph --></div> <!-- /wp:group -->

The entire code snippet of the patterns/footer-with-background.php file can be viewed here on the GitHub.

Please note that the /inc/patterns and block-patterns.php are extras, not required in WordPress 6.0, and included only for demo purposes.

2.4: Referencing the patterns slug in the template

Adding the above refactored footer-with-background.php pattern to footer.html template is exactly the same as described in the previous section (Section 1, 2.2).

Now, if we view the site’s footer part in a block editor or front-end of our site in a browser, the footer section is displayed.

Pattern categories and types Registration (optional)

To categorize block patterns, the pattern categories and types should be registered in theme’s functions.php file.

Let’s consider an example of registering block pattern categories from the TT2 Gopher theme.

After the registration, the patterns are displayed in the pattern inserter together with the core default patterns. To add theme specific category labels in the patterns inserter, we should modify the previous snippets by adding theme namespace:

/** * Registers block categories, and type. */  function tt2gopher_register_block_pattern_categories() {  $ block_pattern_categories = array(   'tt2gopher-header' => array( 'label' => __( 'TT2 Gopher - Headers', 'tt2gopher' ) ),   'tt2gopher-footer' => array( 'label' => __( 'TT2 Gopher - Footers', 'tt2gopher' ) ),   'tt2gopher-page' => array( 'label' => __( 'TT2 Gopher - Page', 'tt2gopher' ) ),   // ... );  /** * Filters the theme block pattern categories. */ $ block_pattern_categories = apply_filters( 'tt2gopher_block_pattern_categories', $ block_pattern_categories );  foreach ( $ block_pattern_categories as $ name => $ properties ) {   if ( ! WP_Block_Pattern_Categories_Registry::get_instance()->is_registered( $ name ) ) {     register_block_pattern_category( $ name, $ properties );   } } add_action( 'init', 'tt2gopher_register_block_pattern_categories', 9 );

The footer-with-background pattern is visible in the patterns inserted with its preview (if selected):

Screenshot showing pattern category displayed in patterns inserter (left panel) and corresponding default footer pattern displayed in the editor (right panel).

This process greatly simplifies creating and displaying block patterns in block themes. It is available in WordPress 6.0 without using the Gutenberg plugin.

Examples of themes without patterns registration

Early adopters have already started using this feature in their block themes. A few examples of the themes, that are available from the theme directory, that load patterns without registration are listed below:

Section 3: Creating and using patterns with low-code

The official patterns directory contains community-contributed creative designs, which can be copied and customized as desired to create content. Using patterns with a block editor has never been so easier!

Any patterns from the ever-growing directory can also be added to block themes just by simple “copy and paste” or include in the theme.json file by referring to their directory pattern slug. Next, I will go through briefly how easily this can be accomplished with very limited coding.

Adding and customizing patterns from patterns directory

3.1: Copy pattern from directory into a page

Here, I am using this footer section pattern by FirstWebGeek from the patterns directory. Copied the pattern by selecting the “Copy Pattern” button and directly pasted it in a new page.

3.2: Make desired customizations

I made only a few changes to the color of the fonts and button background. Then copied the entire code from the code editor over to a clipboard.

Screenshot showing modified pattern (left panel) and corresponding code in code editor (right panel)

If you are not familiar with using the code editor, go to options (with three dots, top right), click the Code editor button, and copy the entire code from here.

3.3: Create a new file in /patterns folder

First, let’s create a new /patterns/footer-pattern-test.php file and add the required pattern header section. Then paste the entire code (step 3, above). The pattern is categorized in the footer area (lines: 5), we can view the newly added in the pattern inserter.

<?php  /**  * Title: Footer pattern from patterns library  * Slug: tt2gopher/footer-pattern-test  * Categories: tt2gopher-footer  * Viewport Width: 1280  * Block Types: core/template-part/footer  * Inserter: yes  */ ?>  <!-- wp:group {"align":"full","style":{"spacing":{"padding":{"top":"100px","bottom":"70px","right":"30px","left":"30px"}}},"backgroundColor":"black","layout":{"contentSize":"1280px"}} --> <div class="wp-block-group alignfull has-black-background-color has-background" style="padding-top:100px;padding-right:30px;padding-bottom:70px;padding-left:30px"><!-- wp:columns --> <div class="wp-block-columns"><!-- wp:column --> <div class="wp-block-column"><!-- wp:heading {"style":{"typography":{"fontStyle":"normal","fontWeight":"700","textTransform":"uppercase"}},"textColor":"cyan-bluish-gray"} --> <h2 class="has-cyan-bluish-gray-color has-text-color" style="font-style:normal;font-weight:700;text-transform:uppercase">lorem</h2> <!-- /wp:heading -->  <!-- wp:paragraph {"style":{"typography":{"fontSize":"16px"}},"textColor":"cyan-bluish-gray"} --> <p class="has-cyan-bluish-gray-color has-text-color" style="font-size:16px">One of the main benefits of using Lorem Ipsum is that it can be easily generated, and it takes the pressure off designers to create meaningful text. Instead, they can focus on crafting the best website data.</p> <!-- /wp:paragraph -->  <!-- wp:social-links {"iconColor":"vivid-cyan-blue","iconColorValue":"#0693e3","openInNewTab":true,"className":"is-style-logos-only","style":{"spacing":{"blockGap":{"top":"15px","left":"15px"}}}} --> <ul class="wp-block-social-links has-icon-color is-style-logos-only"><!-- wp:social-link {"url":"#","service":"facebook"} /-->  <!-- wp:social-link {"url":"#","service":"twitter"} /-->  <!-- wp:social-link {"url":"#","service":"instagram"} /-->  <!-- wp:social-link {"url":"#","service":"linkedin"} /--></ul> <!-- /wp:social-links --></div> <!-- /wp:column -->  <!-- wp:column --> <div class="wp-block-column"><!-- wp:heading {"level":4,"style":{"typography":{"textTransform":"capitalize","fontStyle":"normal","fontWeight":"700","fontSize":"30px"}},"textColor":"cyan-bluish-gray"} --> <h4 class="has-cyan-bluish-gray-color has-text-color" style="font-size:30px;font-style:normal;font-weight:700;text-transform:capitalize">Contact Us</h4> <!-- /wp:heading -->  <!-- wp:paragraph {"style":{"typography":{"fontSize":"16px","lineHeight":"1.2"}},"textColor":"cyan-bluish-gray"} --> <p class="has-cyan-bluish-gray-color has-text-color" style="font-size:16px;line-height:1.2">123 BD Lorem, Ipsum<br><br>+123-456-7890</p> <!-- /wp:paragraph -->  <!-- wp:paragraph {"style":{"typography":{"fontSize":"16px","lineHeight":"1"}},"textColor":"cyan-bluish-gray"} --> <p class="has-cyan-bluish-gray-color has-text-color" style="font-size:16px;line-height:1">sample@gmail.com</p> <!-- /wp:paragraph -->  <!-- wp:paragraph {"style":{"typography":{"fontSize":"16px","lineHeight":"1"}},"textColor":"cyan-bluish-gray"} --> <p class="has-cyan-bluish-gray-color has-text-color" style="font-size:16px;line-height:1">Opening Hours: 10:00 - 18:00</p> <!-- /wp:paragraph --></div> <!-- /wp:column -->  <!-- wp:column --> <div class="wp-block-column"><!-- wp:heading {"level":4,"style":{"typography":{"fontSize":"30px","fontStyle":"normal","fontWeight":"700","textTransform":"capitalize"}},"textColor":"cyan-bluish-gray"} --> <h4 class="has-cyan-bluish-gray-color has-text-color" style="font-size:30px;font-style:normal;font-weight:700;text-transform:capitalize">Newsletter</h4> <!-- /wp:heading -->  <!-- wp:paragraph {"style":{"typography":{"fontSize":"16px"}},"textColor":"cyan-bluish-gray"} --> <p class="has-cyan-bluish-gray-color has-text-color" style="font-size:16px">Lorem ipsum dolor sit amet, consectetur ut labore et dolore magna aliqua ipsum dolor sit</p> <!-- /wp:paragraph -->  <!-- wp:search {"label":"","placeholder":"Enter Your Email...","buttonText":"Subscribe","buttonPosition":"button-inside","style":{"border":{"width":"1px"}},"borderColor":"tertiary","backgroundColor":"background-header","textColor":"background"} /--></div> <!-- /wp:column --></div> <!-- /wp:columns --></div> <!-- /wp:group -->

3.4: View the new pattern in the inserter

To view the newly added Footer pattern from patterns library pattern, go to any post or page and select the inserter icon (blue plus symbol, top left), and then select “TT2 Gopher – Footer” categories. The newly added pattern is shown on the left panel, together with other footer patterns and its preview on the right (if selected):

Screenshot showing new footer pattern (left panel) and its preview (right panel).

Registering patterns directly in theme.json file

In WordPress 6.0, it is possible to register any desired patterns from the pattern directory with theme.json file with the following syntax. The 6.0 dev note states, “the patterns field is an array of [pattern slugs] from the Pattern Directory. Pattern slugs can be extracted by the [URL] in single pattern view at the Pattern Directory.”

{     "version": 2,     "patterns": ["short-text", "patterns-slug"] }

This short WordPress 6.0 features video demonstrates how patterns are registered in the /patterns folder (at 3:53) and registering the desired patterns from the pattern director in a theme.json file (at 3:13).

Then, the registered pattern is available in the patterns inserter search box, which is then available for use just like theme-bundled patterns library.

{   "version": 2,   "patterns": [ "footer-from-directory", "footer-section-design-with-3-column-description-social-media-contact-and-newsletter" ] }

In this example, the pattern slug footer-section-design-with-3-column-description-social-media-contact-and-newsletter from the earlier example is registered via theme.json.

Page creation pattern model

As part of “building with patterns” initiatives, WordPress 6.0 offers a pattern modal option to theme authors to add page layout patterns into block theme, allowing site users to select page layout patterns (e.g., an about page, a contact page, a team page, etc.) while creating a page. The following is an example taken from the dev note:

register_block_pattern(     'my-plugin/about-page',     array(         'title'      => __( 'About page', 'my-plugin' ),         'blockTypes' => array( 'core/post-content' ),         'content'    => '<!-- wp:paragraph {"backgroundColor":"black","textColor":"white"} -->         <p class="has-white-color has-black-background-color has-text-color has-background">Write you about page here, feel free to use any block</p>         <!-- /wp:paragraph -->',     ) );

This feature is currently limited to Page Post Type only and not for “Posts Post Type”, yet.

The page creation pattern modal can also be disabled completely by removing the post-content block type of all the patterns. An example sample code is available here.

You can follow and participate in GitHub’s discussion from the links listed under the resource section below.

Using patterns directory to build page

Patterns from the directory can also be used to create the desired post or page layout, similar to page builders. The GutenbergHub team has created an experimental online page builder app using patterns directly from the directory (introductory video). Then the codes from the app can be copied and pasted in a site, which greatly simplifies the building complex page layout process without coding.

In this short video, Jamie Marsland demonstrates (at 1:30) how the app can be used to create an entire page layout similar to page builder using desired page sections of the directory.

Wrapping up

Patterns allow users to recreate their commonly used content layout (e.g., hero page, call out, etc.) in any page and lower the barriers to presenting content in styles, which were previously not possible without coding skills. Just like the plugins and themes directories, the new patterns directory offers users options to use a wide range of patterns of their choices from the pattern directory, and write and display content in style.

Indeed, block patterns will change everything and surely this is a game changer feature in the WordPress theme landscape. When the full potential of building with patterns effort becomes available, this is going to change the way we design block themes and create beautiful content even with low-code knowledge. For many creative designers, the patterns directory may also provide an appropriate avenue to showcase their creativity.


Resources

WordPress 6.0

Creating patterns

Patterns enhancement (GitHub)

Blog articles


How to Create Block Theme Patterns in WordPress 6.0 originally published on CSS-Tricks. You should get the newsletter.

CSS-Tricks

, , , ,
[Top]

Let’s Create a Tiny Programming Language

By now, you are probably familiar with one or more programming languages. But have you ever wondered how you could create your own programming language? And by that, I mean:

A programming language is any set of rules that convert strings to various kinds of machine code output.

In short, a programming language is just a set of predefined rules. And to make them useful, you need something that understands those rules. And those things are compilers, interpreters, etc. So we can simply define some rules, then, to make it work, we can use any existing programming language to make a program that can understand those rules, which will be our interpreter.

Compiler

A compiler converts codes into machine code that the processor can execute (e.g. C++ compiler).

Interpreter

An interpreter goes through the program line by line and executes each command.

Want to give it a try? Let’s create a super simple programming language together that outputs magenta-colored output in the console. We’ll call it Magenta.

Screenshot of terminal output in color magenta.
Our simple programming language creates a codes variable that contains text that gets printed to the console… in magenta, of course.

Setting up our programming language

I am going to use Node.js but you can use any language to follow along, the concept will remain the same. Let me start by creating an index.js file and set things up.

class Magenta {   constructor(codes) {     this.codes = codes   }   run() {     console.log(this.codes)   } }  // For now, we are storing codes in a string variable called `codes` // Later, we will read codes from a file const codes =  `print "hello world" print "hello again"` const magenta = new Magenta(codes) magenta.run()

What we’re doing here is declaring a class called Magenta. That class defines and initiates an object that is responsible for logging text to the console with whatever text we provide it via a codes variable. And, for the time being, we’ve defined that codes variable directly in the file with a couple of “hello” messages.

Screenshot of terminal output.
If we were to run this code we would get the text stored in codes logged in the console.

OK, now we need to create a what’s called a Lexer.

What is a Lexer?

OK, let’s talks about the English language for a second. Take the following phrase:

How are you?

Here, “How” is an adverb, “are” is a verb, and “you” is a pronoun. We also have a question mark (“?”) at the end. We can divide any sentence or phrase like this into many grammatical components in JavaScript. Another way we can distinguish these parts is to divide them into small tokens. The program that divides the text into tokens is our Lexer.

Diagram showing command going through a lexer.

Since our language is very tiny, it only has two types of tokens, each with a value:

  1. keyword
  2. string

We could’ve used a regular expression to extract tokes from the codes string but the performance will be very slow. A better approach is to loop through each character of the code string and grab tokens. So, let’s create a tokenize method in our Magenta class — which will be our Lexer.

Full code
class Magenta {   constructor(codes) {     this.codes = codes   }   tokenize() {     const length = this.codes.length     // pos keeps track of current position/index     let pos = 0     let tokens = []     const BUILT_IN_KEYWORDS = ["print"]     // allowed characters for variable/keyword     const varChars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_'     while (pos < length) {       let currentChar = this.codes[pos]       // if current char is space or newline,  continue       if (currentChar === " " || currentChar === "n") {         pos++         continue       } else if (currentChar === '"') {         // if current char is " then we have a string         let res = ""         pos++         // while next char is not " or n and we are not at the end of the code         while (this.codes[pos] !== '"' && this.codes[pos] !== 'n' && pos < length) {           // adding the char to the string           res += this.codes[pos]           pos++         }         // if the loop ended because of the end of the code and we didn't find the closing "         if (this.codes[pos] !== '"') {           return {             error: `Unterminated string`           }         }         pos++         // adding the string to the tokens         tokens.push({           type: "string",           value: res         })       } else if (varChars.includes(currentChar)) { arater         let res = currentChar         pos++         // while the next char is a valid variable/keyword charater         while (varChars.includes(this.codes[pos]) && pos < length) {           // adding the char to the string           res += this.codes[pos]           pos++         }         // if the keyword is not a built in keyword         if (!BUILT_IN_KEYWORDS.includes(res)) {           return {             error: `Unexpected token $ {res}`           }         }         // adding the keyword to the tokens         tokens.push({           type: "keyword",           value: res         })       } else { // we have a invalid character in our code         return {           error: `Unexpected character $ {this.codes[pos]}`         }       }     }     // returning the tokens     return {       error: false,       tokens     }   }   run() {     const {       tokens,       error     } = this.tokenize()     if (error) {       console.log(error)       return     }     console.log(tokens)   } }

If we run this in a terminal with node index.js, we should see a list of tokens printed in the console.

Screenshot of code.
Great stuff!

Defining rules and syntaxes

We want to see if the order of our codes matches some sort of rule or syntax. But first we need to define what those rules and syntaxes are. Since our language is so tiny, it only has one simple syntax which is a print keyword followed by a string.

keyword:print string

So let’s create a parse method that loops through our tokens and see if we have a valid syntax formed. If so, it will take necessary actions.

class Magenta {   constructor(codes) {     this.codes = codes   }   tokenize(){     /* previous codes for tokenizer */   }   parse(tokens){     const len = tokens.length     let pos = 0     while(pos < len) {       const token = tokens[pos]       // if token is a print keyword       if(token.type === "keyword" && token.value === "print") {         // if the next token doesn't exist         if(!tokens[pos + 1]) {           return console.log("Unexpected end of line, expected string")         }         // check if the next token is a string         let isString = tokens[pos + 1].type === "string"         // if the next token is not a string         if(!isString) {           return console.log(`Unexpected token $ {tokens[pos + 1].type}, expected string`)         }         // if we reach this point, we have valid syntax         // so we can print the string         console.log('x1b[35m%sx1b[0m', tokens[pos + 1].value)         // we add 2 because we also check the token after print keyword         pos += 2       } else{ // if we didn't match any rules         return console.log(`Unexpected token $ {token.type}`)       }     }   }   run(){     const {tokens, error} = this.tokenize()     if(error){       console.log(error)       return     }     this.parse(tokens)   } }

And would you look at that — we already have a working language!

Screenshot of terminal output.

Okay but having codes in a string variable is not that fun. So lets put our Magenta codes in a file called code.m. That way we can keep our magenta codes separate from the compiler logic. We are using .m as file extension to indicate that this file contains code for our language.

Let’s read the code from that file:

// importing file system module const fs = require('fs') //importing path module for convenient path joining const path = require('path') class Magenta{   constructor(codes){     this.codes = codes   }   tokenize(){     /* previous codes for tokenizer */  }   parse(tokens){     /* previous codes for parse method */  }   run(){     /* previous codes for run method */   } }  // Reading code.m file // Some text editors use rn for new line instead of n, so we are removing r const codes = fs.readFileSync(path.join(__dirname, 'code.m'), 'utf8').toString().replace(/r/g, &quot;&quot;) const magenta = new Magenta(codes) magenta.run()

Go create a programming language!

And with that, we have successfully created a tiny Programming Language from scratch. See, a programming language can be as simple as something that accomplishes one specific thing. Sure, it’s unlikely that a language like Magenta here will ever be useful enough to be part of a popular framework or anything, but now you see what it takes to make one.

The sky is really the limit. If you want dive in a little deeper, try following along with this video I made going over a more advanced example. This is video I have also shown hoe you can add variables to your language also.


Let’s Create a Tiny Programming Language originally published on CSS-Tricks. You should get the newsletter.

CSS-Tricks

, , , ,
[Top]

How to Create a Browser Extension

I’ll bet you are using browser extensions right now. Some of them are extremely popular and useful, like ad blockers, password managers, and PDF viewers. These extensions (or “add-ons”) are not limited to those purposes — you can do a lot more with them! In this article, I will give you an introduction on how to create one. Ultimately, we’ll make it work in multiple browsers.

What we’re making

We’re making an extension called “Transcribers of Reddit” and it’s going to improve Reddit’s accessibility by moving specific comments to the top of the comment section and adding aria- attributes for screen readers. We will also take our extension a little further with options for adding borders and backgrounds to comments for better text contrast.

The whole idea is that you’ll get a nice introduction for how to develop a browser extension. We will start by creating the extension for Chromium-based browsers (e.g. Google Chrome, Microsoft Edge, Brave, etc.). In a future post we will port the extension to work with Firefox, as well as Safari which recently added support for Web Extensions in both the MacOS and iOS versions of the browser.

Ready? Let’s take this one step at a time.

Create a working directory

Before anything else, we need a working space for our project. All we really need is to create a folder and give it a name (which I’m calling transcribers-of-reddit). Then, create another folder inside that one named src for our source code.

Define the entry point

The entry point is a file that contains general information about the extension (i.e. extension name, description, etc.) and defines permissions or scripts to execute.

Our entry point can be a manifest.json file located in the src folder we just created. In it, let’s add the following three properties:

{   "manifest_version": 3,   "name": "Transcribers of Reddit",   "version": "1.0" }

The manifest_version is similar to version in npm or Node. It defines what APIs are available (or not). We’re going to work on the bleeding edge and use the latest version, 3 (also known as as mv3).

The second property is name and it specifies our extension name. This name is what’s displayed everywhere our extension appears, like Chrome Web Store and the chrome://extensions page in the Chrome browser.

Then there’s version. It labels the extension with a version number. Keep in mind that this property (in contrast to manifest_version) is a string that can only contain numbers and dots (e.g. 1.3.5).

More manifest.json information

There’s actually a lot more we can add to help add context to our extension. For example, we can provide a description that explains what the extension does. It’s a good idea to provide these sorts of things, as it gives users a better idea of what they’re getting into when they use it.

In this case, we’re not only adding a description, but supplying icons and a web address that Chrome Web Store points to on the extension’s page.

{   "description": "Reddit made accessible for disabled users.",   "icons": {     "16": "images/logo/16.png",     "48": "images/logo/48.png",     "128": "images/logo/128.png"   },   "homepage_url": "https://lars.koelker.dev/extensions/tor/" }
  • The description is displayed on Chrome’s management page (chrome://extensions) and should be brief, less than 132 characters.
  • The icons are used in lots of places. As the docs state, it’s best to provide three versions of the same icon in different resolutions, preferably as a PNG file. Feel free to use the ones in the GitHub repository for this example.
  • The homepage_url can be used to connect your website with the extension. A button including the link will be displayed when clicking on “More details” on the management page.
Our opened extension card inside the extension management page.

Setting permissions

One major advantage extensions have is that their APIs allow you to interact directly with the browser. But we have to explicitly give the extension those permissions, which also goes inside the manifest.json file.

 {   "manifest_version": 3,   "name": "Transcribers of Reddit",   "version": "1.0",   "description": "Reddit made accessible for disabled users.",   "icons": {     "16": "images/logo/16.png",     "48": "images/logo/48.png",     "128": "images/logo/128.png"   },   "homepage_url": "https://lars.koelker.dev/extensions/tor/",    "permissions": [     "storage",     "webNavigation"   ] }

What did we just give this extension permission to? First, storage. We want this extension to be able to save the user’s settings, so we need to access the browser’s web storage to hold them. For example, if the user wants red borders on the comments, then we’ll save that for next time rather than making them set it again.

We also gave the extension permission to look at how the user navigated to the current screen. Reddit is a single-page application (SPA) which means it doesn’t trigger a page refresh. We need to “catch” this interaction, as Reddit will only load the comments of a post if we click on it. So, that’s why we’re tapping into webNavigation.

We’ll get to executing code on a page later as it requires a whole new entry inside manifest.json.

/explanation Depending on which permissions are allowed, the browser might display a warning to the user to accept the permissions. It’s only certain ones, though, and Chrome has a nice outline of them.

Managing translations

Browser extensions have a built-in internalization (i18n) API. It allows you to manage translations for multiple languages (full list). To use the API, we have to define our translations and default language right in the manifest.json file:

"default_locale": "en"

This sets English as the language. In the event that a browser is set to any other language that isn’t supported, the extension will fall back to the default locale (en in this example).

Our translations are defined inside the _locales directory. Let’s create another folder in there each language you want to support. Each subdirectory gets its own messages.json file.

src   └─ _locales      └─ en         └─ messages.json      └─ fr         └─ messages.json

A translation file consists of multiple parts:

  • Translation key (“id”): This key is used to reference the translation.
  • Message: The actual translation content
  • Description (optional): Describes the translation (I wouldn’t use them, they just bloat up the file and your translation key should be descriptive enough)
  • Placeholders (optional): Can be used to insert dynamic content inside a translation

Here’s an example that pulls all that together:

{   "userGreeting": { // Translation key ("id")     "message": "Good $ daytime$ , $ user$ !" // Translation     "description": "User Greeting", // Optional description for translators     "placeholders": { // Optional placeholders       "daytime": { // As referenced inside the message         "content": "$ 1",         "example": "morning" // Example value for our content       },       "user": {          "content": "$ 1",         "example": "Lars"       }     }   } }

Using placeholders is a bit more challenging. At first we need to define the placeholder inside the message. A placeholder needs to be wrapped in between $ characters. Afterwards, we have to add our placeholder to the “placeholder list.” This is a bit unintuitive, but Chrome wants to know what value should be inserted for our placeholders. We (obviously) want to use a dynamic value here, so we use the special content value $ 1 which references our inserted value.

The example property is optional. It can be used to give translators a hint what value the placeholder could be (but is not actually displayed).

We need to define the following translations for our extension. Copy and paste them into the messages.json file. Feel free to add more languages (e.g. if you speak German, add a de folder inside _locales, and so on).

{   "name": {     "message": "Transcribers of Reddit"   },   "description": {     "message": "Accessible image descriptions for subreddits."   },   "popupManageSettings": {     "message": "Manage settings"   },   "optionsPageTitle": {     "message": "Settings"   },   "sectionGeneral": {     "message": "General settings"   },   "settingBorder": {     "message": "Show comment border"   },   "settingBackground": {     "message": "Show comment background"   } }

You might be wondering why we registered the permissions when there is no sign of an i18n permission, right? Chrome is a bit weird in that regard, as you don’t need to register every permission. Some (e.g. chrome.i18n) don’t require an entry inside the manifest. Other permissions require an entry but won’t be displayed to the user when installing the extension. Some other permissions are “hybrid” (e.g. chrome.runtime), meaning some of their functions can be used without declaring a permission—but other functions of the same API require one entry in the manifest. You’ll want to take a look at the documentation for a solid overview of the differences.

Using translations inside the manifest

The first thing our end user will see is either the entry inside the Chrome Web Store or the extension overview page. We need to adjust our manifest file to make sure everything os translated.

{   // Update these entries   "name": "__MSG_name__",   "description": "__MSG_description__" }

Applying this syntax uses the corresponding translation in our messages.json file (e.g. _MSG_name_ uses the name translation).

Using translations in HTML pages

Applying translations in an HTML file takes a little JavaScript.

chrome.i18n.getMessage('name');

That code returns our defined translation (which is Transcribers of Reddit). Placeholders can be done in a similar way.

chrome.i18n.getMessage('userGreeting', {   daytime: 'morning',   user: 'Lars' });

It would be a pain in the butt to apply translations to all elements this way. But we can write a little script that performs the translation based on a data- attribute. So, let’s create a new js folder inside the src directory, then add a new util.js file in it.

src   └─ js      └─ util.js

This gets the job done:

const i18n = document.querySelectorAll("[data-intl]"); i18n.forEach(msg => {   msg.innerHTML = chrome.i18n.getMessage(msg.dataset.intl); });  chrome.i18n.getAcceptLanguages(languages => {   document.documentElement.lang = languages[0]; });

Once that script is added to an HTML page, we can add the data-intl attribute to an element to set its content. The document language will also be set based on the user language.

<!-- Before JS execution --> <html>   <body>     <button data-intl="popupManageSettings"></button>   </body> </html>

<!-- After JS execution --> <html lang="en">   <body>     <button data-intl="popupManageSettings">Manage settings</button>   </body> </html>

Adding a pop-up and options page

Before we dive into actual programming, we we need to create two pages:

  1. An options page that contains user settings
  2. A pop-up page that opens when interacting with the extension icon right next to our address bar. This page can be used for various scenarios (e.g. for displaying stats or quick settings).
The options page containg our settings.

The pop-up containg a link to the options page.

Here’s an outline of the folders and files we need in order to make the pages:

src   ├─ css  |    └─ paintBucket.css  ├─ popup  |    ├─ popup.html  |    ├─ popup.css  |    └─ popup.js  └─ options       ├─ options.html       ├─ options.css       └─ options.js

The .css files contain plain CSS, nothing more and nothing less. I won’t into detail because I know most of you reading this are already fully aware of how CSS works. You can copy and paste the styles from the GitHub repository for this project.

Note that the pop-up is not a tab and that its size depends on the content in it. If you want to use a fixed popup size, you can set the width and height properties on the html element.

Creating the pop-up

Here’s an HTML skeleton that links up the CSS and JavaScript files and adds a headline and button inside the <body>.

<!doctype html> <html lang="en">   <head>     <meta charset="UTF-8">     <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">     <meta http-equiv="X-UA-Compatible" content="ie=edge">     <title data-intl="name"></title>      <link rel="stylesheet" href="../css/paintBucket.css">     <link rel="stylesheet" href="popup.css">      <!-- Our "translation" script -->     <script src="../js/util.js" defer></script>     <script src="popup.js" defer></script>   </head>   <body>     <h1 id="title"></h1>     <button data-intl="popupManageSettings"></button>   </body> </html>

The h1 contains the extension name and version; the button is used to open the options page. The headline will not be filled with a translation (because it lacks a data-intl attribute), and the button doesn’t have any click handler yet, so we need to populate our popup.js file:

const title = document.getElementById('title'); const settingsBtn = document.querySelector('button'); const manifest = chrome.runtime.getManifest();  title.textContent = `$ {manifest.name} ($ {manifest.version})`;  settingsBtn.addEventListener('click', () => {   chrome.runtime.openOptionsPage(); });

This script first looks for the manifest file. Chrome offers the runtime API which contains the getManifest method (this specific method does not require the runtime permission). It returns our manifest.json as a JSON object. After we populate the title with the extension name and version, we can add an event listener to the settings button. If the user interacts with it, we will open the options page using chrome.runtime.openOptionsPage() (again no permission entry needed).

The pop-up page is now finished, but the extension doesn’t know it exists yet. We have to register the pop-up by appending the following property to the manifest.json file.

"action": {   "default_popup": "popup/popup.html",   "default_icon": {     "16": "images/logo/16.png",     "48": "images/logo/48.png",     "128": "images/logo/128.png"   } },

Creating the options page

Creating this page follows a pretty similar process as what we just completed. First, we populate our options.html file. Here’s some markup we can use:

<!doctype html> <html lang="en"> <head>   <meta charset="UTF-8">   <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">   <meta http-equiv="X-UA-Compatible" content="ie=edge">   <title data-intl="name"></title>    <link rel="stylesheet" href="../css/paintBucket.css">   <link rel="stylesheet" href="options.css">    <!-- Our "translation" script -->   <script src="../js/util.js" defer></script>   <script src="options.js" defer></script> </head> <body>   <header>     <h1>       <!-- Icon provided by feathericons.com -->       <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" role="presentation">         <circle cx="12" cy="12" r="3"></circle>         <path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path>       </svg>       <span data-intl="optionsPageTitle"></span>     </h1>   </header>    <main>     <section id="generalOptions">       <h2 data-intl="sectionGeneral"></h2>        <div id="generalOptionsWrapper"></div>     </section>   </main>    <footer>     <p>Transcribers of Reddit extension by <a href="https://lars.koelker.dev" target="_blank">lars.koelker.dev</a>.</p>     <p>Reddit is a registered trademark of Reddit, Inc. This extension is not endorsed or affiliated with Reddit, Inc. in any way.</p>   </footer> </body> </html>

There are no actual options yet (just their wrappers). We need to write the script for the options page. First, we define variables to access our wrappers and default settings inside options.js. “Freezing” our default settings prevents us from accidentally modifying them later.

const defaultSettings = Object.freeze({   border: false,   background: false, }); const generalSection = document.getElementById('generalOptionsWrapper'); 

Next, we need to load the saved settings. We can use the (previously registered) storage API for that. Specifically, we need to define if we want to store the data locally (chrome.storage.local) or sync settings through all devices the end user is logged in to (chrome.storage.sync). Let’s go with local storage for this project.

Retrieving values needs to be done with the get method. It accepts two arguments:

  1. The entries we want to load
  2. A callback containing the values

Our entries can either be a string (e.g. like settings below) or an array of entries (useful if we want to load multiple entries). The argument inside the callback function contains an object of all entries we previously defined in { settings: ... }:

chrome.storage.local.get('settings', ({ settings }) => {   const options = settings ?? defaultSettings; // Fall back to default if settings are not defined   if (!settings) {     chrome.storage.local.set({      settings: defaultSettings,     });  }    // Create and display options   const generalOptions = Object.keys(options).filter(x => !x.startsWith('advanced'));      generalOptions.forEach(option => createOption(option, options, generalSection)); });

To render the options, we also need to create a createOption() function.

function createOption(setting, settingsObject, wrapper) {   const settingWrapper = document.createElement("div");   settingWrapper.classList.add("setting-item");   settingWrapper.innerHTML = `   <div class="label-wrapper">     <label for="$ {setting}" id="$ {setting}Desc">       $ {chrome.i18n.getMessage(`setting$ {setting}`)}     </label>   </div>    <input type="checkbox" $ {settingsObject[setting] ? 'checked' : ''} id="$ {setting}" />   <label for="$ {setting}"     tabindex="0"     role="switch"     aria-checked="$ {settingsObject[setting]}"     aria-describedby="$ {setting}-desc"     class="is-switch"   ></label>   `;    const toggleSwitch = settingWrapper.querySelector("label.is-switch");   const input = settingWrapper.querySelector("input");    input.onchange = () => {     toggleSwitch.setAttribute('aria-checked', input.checked);     updateSetting(setting, input.checked);   };    toggleSwitch.onkeydown = e => {     if(e.key === " " || e.key === "Enter") {       e.preventDefault();       toggleSwitch.click();     }   }    wrapper.appendChild(settingWrapper); }

Inside the onchange event listener of our switch (aká radio button) we call the function updateSetting. This method will write the updated value of our radio button inside the storage.

To accomplish this, we will make use of the set function. It has two arguments: The entry we want to overwrite and an (optional) callback (which we don’t use in our case). As our settings entry is not a boolean or a string but an object containing different settings, we use the spread operator () and only overwrite our actual key (setting) inside the settings object.

function updateSetting(key, value) {   chrome.storage.local.get('settings', ({ settings }) => {     chrome.storage.local.set({       settings: {         ...settings,         [key]: value       }     })   }); }

Once again, we need to “inform” the extension about our options page by appending the following entry to the manifest.json:

"options_ui": {   "open_in_tab": true,   "page": "options/options.html" },

Depending on your use case you can also force the options dialog to open as a popup by setting open_in_tab to false.

Installing the extension for development

Now that we’ve successfully set up the manifest file and have added both the pop-up and options page to the mix, we can install our extension to check if our pages actually work. Navigate to chrome://extensions and enable “Developer mode.” Three buttons will appear. Click the one labeled “Load unpacked” and select the src folder of your extension to load it up.

The extension should now be successfully installed and our “Transcribers of Reddit” tile should be on the page.

We can already interact with our extension. Click on the puzzle piece (🧩) icon right next to the browser’s address bar and click on the newly-added “Transcribers of Reddit” extension. You should now be greeted by a small pop-up with the button to open the options page.

Lovely, right? It might look a bit different on your device, as I have dark mode enabled in these screenshots.

If you enable the “Show comment background” and “Show comment border” settings, then reload the page, the state will persist because we’re saving it in the browser’s local storage.

Adding the content script

OK, so we can already trigger the pop-up and interact with the extension settings, but the extension doesn’t do anything particularly useful yet. To give it some life, we will add a content script.

Add a file called comment.js inside the js directory and make sure to define it in the manifest.json file:

"content_scripts": [   {     "matches": [ "*://www.reddit.com/*" ],     "js": [ "js/comment.js" ]   } ],

The content_scripts is made up of two parts:

  • matches: This array holds URLs that tell the browser where we want our content scripts to run. Being an extension for Reddit and all, we want this to run on any page matching ://www.redit.com/*, where the asterisk is a wild card to match anything after the top-level domain.
  • js: This array contains the actual content scripts.

Content scripts can’t interact with other (normal) JavaScripts. This means if a website’s scripts defines a variable or function, we can’t access it. For example:

// script_on_website.js const username = 'Lars';  // content_script.js console.log(username); // Error: username is not defined

Now let’s start writing our content script. First, we add some constants to comment.js. These constants contain RegEx expressions and selectors that will be used later on. The CommentUtils is used to determine whether or not a post contains a “tor comment,” or if comment wrappers exists.

const messageTypes = Object.freeze({   COMMENT_PAGE: 'comment_page',   SUBREDDIT_PAGE: 'subreddit_page',   MAIN_PAGE: 'main_page',   OTHER_PAGE: 'other_page', });  const Selectors = Object.freeze({   commentWrapper: 'div[style*="--commentswrapper-gradient-color"] > div, div[style*="max-height: unset"] > div',   torComment: 'div[data-tor-comment]',   postContent: 'div[data-test-id="post-content"]' });  const UrlRegex = Object.freeze({   commentPage: //r/.*/comments/.*/,   subredditPage: //r/.*// });  const CommentUtils = Object.freeze({   isTorComment: (comment) => comment.querySelector('[data-test-id="comment"]') ? comment.querySelector('[data-test-id="comment"]').textContent.includes('m a human volunteer content transcriber for Reddit') : false,   torCommentsExist: () => !!document.querySelector(Selectors.torComment),   commentWrapperExists: () => !!document.querySelector('[data-reddit-comment-wrapper="true"]') });

Next, we check whether or not a user directly opens a comment page (“post”), then perform a RegEx check and update the directPage variable. This case occurs when a user directly opens the URL (e.g. by typing it into the address bar or clicking on<a> element on another page, like Twitter).

let directPage = false; if (UrlRegex.commentPage.test(window.location.href)) {   directPage = true;   moveComments(); }

Besides opening a page directly, a user normally interacts with the SPA. To catch this case, we can add a message listener to our comment.js file by using the runtime API.

chrome.runtime.onMessage.addListener(msg => {   if (msg.type === messageTypes.COMMENT_PAGE) {     waitForComment(moveComments);   } });

All we need now are the functions. Let’s create a moveComments() function. It moves the special “tor comment” to the start of the comment section. It also conditionally applies a background color and border (if borders are enabled in the settings) to the comment. For this, we call the storage API and load the settings entry:

function moveComments() {   if (CommentUtils.commentWrapperExists()) {     return;   }    const wrapper = document.querySelector(Selectors.commentWrapper);   let comments = wrapper.querySelectorAll(`$ {Selectors.commentWrapper} > div`);   const postContent = document.querySelector(Selectors.postContent);    wrapper.dataset.redditCommentWrapper = 'true';   wrapper.style.flexDirection = 'column';   wrapper.style.display = 'flex';    if (directPage) {     comments = document.querySelectorAll("[data-reddit-comment-wrapper='true'] > div");   }    chrome.storage.local.get('settings', ({ settings }) => { // HIGHLIGHT 18     comments.forEach(comment => {       if (CommentUtils.isTorComment(comment)) {         comment.dataset.torComment = 'true';         if (settings.background) {           comment.style.backgroundColor = 'var(--newCommunityTheme-buttonAlpha05)';         }         if (settings.border) {           comment.style.outline = '2px solid red';         }         comment.style.order = "-1";         applyWaiAria(postContent, comment);       }     });   }) }

The applyWaiAria() function is called inside the moveComments() function—it adds aria- attributes. The other function creates a unique identifier for use with the aria- attributes.

function applyWaiAria(postContent, comment) {   const postMedia = postContent.querySelector('img[class*="ImageBox-image"], video');   const commentId = uuidv4();    if (!postMedia) {     return;   }    comment.setAttribute('id', commentId);   postMedia.setAttribute('aria-describedby', commentId); }  function uuidv4() {   return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {     var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);     return v.toString(16);   }); }

The following function waits for the comments to load and calls the callback parameter if it finds the comment wrapper.

function waitForComment(callback) {   const config = { childList: true, subtree: true };   const observer = new MutationObserver(mutations => {     for (const mutation of mutations) {       if (document.querySelector(Selectors.commentWrapper)) {         callback();         observer.disconnect();         clearTimeout(timeout);         break;       }     }   });    observer.observe(document.documentElement, config);   const timeout = startObservingTimeout(observer, 10); }  function startObservingTimeout(observer, seconds) {   return setTimeout(() => {     observer.disconnect();   }, 1000 * seconds); }

Adding a service worker

Remember when we added a listener for messages inside the content script? This listener isn’t currently receiving messages. We need to send it to the content script ourselves. For this purpose we need to register a service worker.

We have to register our service worker inside the manifest.json by appending the following code to it:

"background": {   "service_worker": "sw.js" }

Don’t forget to create the sw.js file inside the src directory (service workers always need to be created in the extension’s root directory, src.

Now, let’s create some constants for the message and page types:

const messageTypes = Object.freeze({   COMMENT_PAGE: 'comment_page',   SUBREDDIT_PAGE: 'subreddit_page',   MAIN_PAGE: 'main_page',   OTHER_PAGE: 'other_page', });  const UrlRegex = Object.freeze({   commentPage: //r/.*/comments/.*/,   subredditPage: //r/.*// });  const Utils = Object.freeze({   getPageType: (url) => {     if (new URL(url).pathname === '/') {       return messageTypes.MAIN_PAGE;     } else if (UrlRegex.commentPage.test(url)) {       return messageTypes.COMMENT_PAGE;     } else if (UrlRegex.subredditPage.test(url)) {       return messageTypes.SUBREDDIT_PAGE;     }      return messageTypes.OTHER_PAGE;   } });

We can add the service worker’s actual content. We do this with an event listener on the history state (onHistoryStateUpdated) that detects when a page has been updated with the History API (which is commonly used in SPAs to navigate without a page refresh). Inside this listener, we query the active tab and extract its tabId. Then we send a message to our content script containing the page type and URL.

chrome.webNavigation.onHistoryStateUpdated.addListener(async ({ url }) => {   const [{ id: tabId }] = await chrome.tabs.query({ active: true, currentWindow: true });    chrome.tabs.sendMessage(tabId, {     type: Utils.getPageType(url),     url   }); });

All done!

We’re finished! Navigate to Chrome’s extension management page ( chrome://extensions) and hit the reload icon on the unpacked extension. If you open a Reddit post that contains a “Transcribers of Reddit” comment with an image transcription (like this one), it will be moved to the start of the comment section and be highlighted as long as we’ve enabled it in the extension settings.

The “Transcribers of Reddit” extension highlights a particular comment by moving it to the top of the Reddit thread’s comment list and giving it a bright red border

Conclusion

Was that as hard as you thought it would be? It’s definitely a lot more straightforward than I thought before digging in. After setting up the manifest.json and creating any page files and assets we need, all we’re really doing is writing HTML, CSS, and JavaScript like normal.

If you ever find yourself stuck along the way, the Chrome API documentation is a great resource to get back on track.

Once again, here’s the GitHub repo with all of the code we walked through in this article. Read it, use it, and let me know what you think of it!


How to Create a Browser Extension originally published on CSS-Tricks

CSS-Tricks

, ,
[Top]

How to Create an Animated Chart of Nested Squares Using Masks

We have many well-known chart types: bar, donut, line, pie, you name it. All popular chart libraries support these. Then there are the chart types that do not even have a name. Check out this dreamt-up chart with stacked (nested) squares that can help visualize relative sizes, or how different values compare to one another:

What we’re making

Without any interactivity, creating this design is fairly straightforward. One way to do it is is to stack elements (e.g. SVG <rect> elements, or even HTML divs) in decreasing sizes, where all of their bottom-left corners touch the same point.

But things get trickier once we introduce some interactivity. Here’s how it should be: When we move our mouse over one of the shapes, we want the others to fade out and move away.

We’ll create these irregular shapes using rectangles and masks — literal <svg> with <rect> and <mask> elements. If you are entirely new to masks, you are in the right place. This is an introductory-level article. If you are more seasoned, then perhaps this cut-out effect is a trick that you can take with you.

Now, before we begin, you may wonder if a better alternative to SVG to using custom shapes. That’s definitely a possibility! But drawing shapes with a <path> can be intimidating, or even get messy. So, we’re working with “easier” elements to get the same shapes and effects.

For example, here’s how we would have to represent the largest blue shape using a <path>.

<svg viewBox="0 0 320 320" width="320" height="320">   <path d="M320 0H0V56H264V320H320V0Z" fill="#264653"/> </svg>

If the 0H0V56… does not make any sense to you, check out “The SVG path Syntax: An Illustrated Guide” for a thorough explanation of the syntax.

The basics of the chart

Given a data set like this:

type DataSetEntry = {   label: string;   value: number; };  type DataSet = DataSetEntry[];  const rawDataSet: DataSet = [   { label: 'Bad', value: 1231 },   { label: 'Beginning', value: 6321 },   { label: 'Developing', value: 10028 },   { label: 'Accomplished', value: 12123 },   { label: 'Exemplary', value: 2120 } ];

…we want to end up with an SVG like this:

<svg viewBox="0 0 320 320" width="320" height="320">   <rect width="320" height="320" y="0" fill="..."></rect>   <rect width="264" height="264" y="56" fill="..."></rect>   <rect width="167" height="167" y="153" fill="..."></rect>   <rect width="56" height="56" y="264" fill="..."></rect>   <rect width="32" height="32" y="288" fill="..."></rect> </svg>

Determining the highest value

It will become apparent in a moment why we need the highest value. We can use the Math.max() to get it. It accepts any number of arguments and returns the highest value in a set.

const dataSetHighestValue: number = Math.max(   ...rawDataSet.map((entry: DataSetEntry) => entry.value) );

Since we have a small dataset, we can just tell that we will get 12123.

Calculating the dimension of the rectangles

If we look at the design, the rectangle representing the highest value (12123) covers the entire area of the chart.

We arbitrarily picked 320 for the SVG dimensions. Since our rectangles are squares, the width and height are equal. How can we make 12123 equal to 320? How about the less “special” values? How big is the 6321 rectangle?

Asked another way, how do we map a number from one range ([0, 12123]) to another one ([0, 320])? Or, in more math-y terms, how do we scale a variable to an interval of [a, b]?

For our purposes, we are going to implement the function like this:

const remapValue = (   value: number,   fromMin: number,   fromMax: number,   toMin: number,   toMax: number ): number => {   return ((value - fromMin) / (fromMax - fromMin)) * (toMax - toMin) + toMin; };  remapValue(1231, 0, 12123, 0, 320); // 32 remapValue(6321, 0, 12123, 0, 320); // 167 remapValue(12123, 0, 12123, 0, 320); // 320

Since we map values to the same range in our code, instead of passing the minimums and maximums over and over, we can create a wrapper function:

const valueRemapper = (   fromMin: number,   fromMax: number,   toMin: number,   toMax: number ) => {   return (value: number): number => {     return remapValue(value, fromMin, fromMax, toMin, toMax);   }; };  const remapDataSetValueToSvgDimension = valueRemapper(   0,   dataSetHighestValue,   0,   svgDimension );

We can use it like this:

remapDataSetValueToSvgDimension(1231); // 32 remapDataSetValueToSvgDimension(6321); // 167 remapDataSetValueToSvgDimension(12123); // 320

Creating and inserting the DOM elements

What remains has to do with DOM manipulation. We have to create the <svg> and the five <rect> elements, set their attributes, and append them to the DOM. We can do all this with the basic createElementNS, setAttribute, and the appendChild functions.

Notice that we are using the createElementNS instead of the more common createElement. This is because we are working with an SVG. HTML and SVG elements have different specs, so they fall under a different namespace URI. It just happens that the createElement conveniently uses the HTML namespace! So, to create an SVG, we have to be this verbose:

document.createElementNS('http://www.w3.org/2000/svg', 'svg') as SVGSVGElement;

Surely, we can create another helper function:

const createSvgNSElement = (element: string): SVGElement => {   return document.createElementNS('http://www.w3.org/2000/svg', element); };

When we are appending the rectangles to the DOM, we have to pay attention to their order. Otherwise, we would have to specify the z-index explicitly. The first rectangle has to be the largest, and the last rectangle has to be the smallest. Best to sort the data before the loop.

const data = rawDataSet.sort(   (a: DataSetEntry, b: DataSetEntry) => b.value - a.value );  data.forEach((d: DataSetEntry, index: number) => {   const rect: SVGRectElement = createSvgNSElement('rect') as SVGRectElement;   const rectDimension: number = remapDataSetValueToSvgDimension(d.value);    rect.setAttribute('width', `$ {rectDimension}`);   rect.setAttribute('height', `$ {rectDimension}`);   rect.setAttribute('y', `$ {svgDimension - rectDimension}`);    svg.appendChild(rect); });

The coordinate system starts from the top-left; that’s where the [0, 0] is. We are always going to draw the rectangles from the left side. The x attribute, which controls the horizontal position, defaults to 0, so we don’t have to set it. The y attribute controls the vertical position.

To give the visual impression that all of the rectangles originate from the same point that touches their bottom-left corners, we have to push the rectangles down so to speak. By how much? The exact amount that the rectangle does not fill. And that value is the difference between the dimension of the chart and the particular rectangle. If we put all the bits together, we end up with this:

We already added the code for the animation to this demo using CSS.

Cutout rectangles

We have to turn our rectangles into irregular shapes that sort of look like the number seven, or the letter L rotated 180 degrees.

If we focus on the “missing parts” then we can see they cutouts of the same rectangles we’re already working with.

We want to hide those cutouts. That’s how we are going to end up with the L-shapes we want.

Masking 101

A mask is something you define and later apply to an element. Typically, the mask is inlined in the <svg> element it belongs to. And, generally, it should have a unique id because we have to reference it in order to apply the mask to an element.

<svg>   <mask id="...">     <!-- ... -->   </mask> </svg>

In the <mask> tag, we put the shapes that serve as the actual masks. We also apply the mask attribute to the elements.

<svg>   <mask id="myCleverlyNamedMask">     <!-- ... -->   </mask>   <rect mask="url(#myCleverlyNamedMask)"></rect> </svg>

That’s not the only way to define or apply a mask, but it’s the most straightforward way for this demo. Let’s do a bit of experimentation before writing any code to generate the masks.

We said that we want to cover the cutout areas that match the sizes of the existing rectangles. If we take the largest element and we apply the previous rectangle as a mask, we end up with this code:

<svg viewBox="0 0 320 320" width="320" height="320">   <mask id="theMask">     <rect width="264" height="264" y="56" fill=""></rect>   </mask>   <rect width="320" height="320" y="0" fill="#264653" mask="url(#theMask)"></rect> </svg>

The element inside the mask needs a fill value. What should that be? We’ll see entirely different results based on the fill value (color) we choose.

The white fill

If we use a white value for the fill, then we get this:

Now, our large rectangle is the same dimension as the masking rectangle. Not exactly what we wanted.

The black fill

If we use a black value instead, then it looks like this:

We don’t see anything. That’s because what is filled with black is what becomes invisible. We control the visibility of masks using white and black fills. The dashed lines are there as a visual aid to reference the dimensions of the invisible area.

The gray fill

Now let’s use something in-between white and black, say gray:

It’s neither fully opaque or solid; it’s transparent. So, now we know we can control the “degree of visibility” here by using something different than white and black values which is a good trick to keep in our back pockets.

The last bit

Here’s what we’ve covered and learned about masks so far:

  • The element inside the <mask> controls the dimension of the masked area.
  • We can make the contents of the masked area visible, invisible, or transparent.

We have only used one shape for the mask, but as with any general purpose HTML tag, we can nest as many child elements in there as we want. In fact, the trick to achieve what we want is using two SVG <rect> elements. We have to stack them one on top of the other:

<svg viewBox="0 0 320 320" width="320" height="320">   <mask id="maskW320">     <rect width="320" height="320" y="0" fill="???"></rect>     <rect width="264" height="264" y="56" fill="???"></rect>   </mask>   <rect width="320" height="320" y="0" fill="#264653" mask="url(#maskW320)"></rect> </svg>

One of our masking rectangles is filled with white; the other is filled with black. Even if we know the rules, let’s try out the possibilities.

<mask id="maskW320">   <rect width="320" height="320" y="0" fill="black"></rect>   <rect width="264" height="264" y="56" fill="white"></rect> </mask>

The <mask> is the dimension of the largest element and the largest element is filled with black. That means everything under that area is invisible. And everything under the smaller rectangle is visible.

Now let’s do flip things where the black rectangle is on top:

<mask id="maskW320">   <rect width="320" height="320" y="0" fill="white"></rect>   <rect width="264" height="264" y="56" fill="black"></rect> </mask>

This is what we want!

Everything under the largest white-filled rectangle is visible, but the smaller black rectangle is on top of it (closer to us on the z-axis), masking that part.

Generating the masks

Now that we know what we have to do, we can create the masks with relative ease. It’s similar to how we generated the colored rectangles in the first place — we create a secondary loop where we create the mask and the two rects.

This time, instead of appending the rects directly to the SVG, we append it to the mask:

data.forEach((d: DataSetEntry, index: number) => {   const mask: SVGMaskElement = createSvgNSElement('mask') as SVGMaskElement;    const rectDimension: number = remapDataSetValueToSvgDimension(d.value);   const rect: SVGRectElement = createSvgNSElement('rect') as SVGRectElement;    rect.setAttribute('width', `$ {rectDimension}`);   // ...setting the rest of the attributes...    mask.setAttribute('id', `maskW$ {rectDimension.toFixed()}`);    mask.appendChild(rect);    // ...creating and setting the attributes for the smaller rectangle...    svg.appendChild(mask); });  data.forEach((d: DataSetEntry, index: number) => {     // ...our code to generate the colored rectangles... });

We could use the index as the mask’s ID, but this seems a more readable option, at least to me:

mask.setAttribute('id', `maskW$ {rectDimension.toFixed()}`); // maskW320, masW240, ...

As for adding the smaller rectangle in the mask, we have easy access the value we need because we previously ordered the rectangle values from highest to lowest. That means the next element in the loop is the smaller rectangle, the one we should reference. And we can do that by its index.

// ...previous part where we created the mask and the rectangle...  const smallerRectIndex = index + 1;  // there's no next one when we are on the smallest if (data[smallerRectIndex] !== undefined) {   const smallerRectDimension: number = remapDataSetValueToSvgDimension(     data[smallerRectIndex].value   );   const smallerRect: SVGRectElement = createSvgNSElement(     'rect'   ) as SVGRectElement;    // ...setting the rectangle attributes...    mask.appendChild(smallerRect); }  svg.appendChild(mask);

What is left is to add the mask attribute to the colored rectangle in our original loop. It should match the format we chose:

rect.setAttribute('mask', `url(#maskW$ {rectDimension.toFixed()})`); // maskW320, maskW240, ...

The final result

And we are done! We’ve successfully made a chart that’s made out of nested squares. It even comes apart on mouse hover. And all it took was some SVG using the <mask> element to draw the cutout area of each square.


The post How to Create an Animated Chart of Nested Squares Using Masks appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , , , ,
[Top]

Create Your Own Automated Social Images With Resoc

There has been a lot of talk about automated social images lately. GitHub has created its own. A WordPress plugin has been acquired by Jetpack. There is definitely interest! People like Ryan Filler and Zach Leatherman have implemented social images on their websites. They had to code a lot of things on their own. But the landscape is changing and tools are available to smooth the process.

In this tutorial, we are going to create our own automated social images with HTML and CSS, integrate them to an Eleventy blog — mostly by configuration — and deploy our site to Netlify.

If you really, really can’t wait, check the result or browse the project!

What are social images again?

In the <head> section of HTML, we insert a few Open Graph markups:

<meta property="og:title" content="The blue sky strategy" /> <meta property="og:description" content="Less clouds, more blue" /> <meta property="og:image" content="/sky-with-clouds.jpg" />

When we share this page on Facebook, we and our friends see this:

LinkedIn, Twitter, WhatsApp, Slack, Discord, iMessage… All these sites behave pretty much the same way: they provide a visual “card” that accompanies the link, giving it more space and context.

Twitter has its own set of markups with its Twitter Cards, but they are very similar. And Twitter falls back to Open Graph when it can’t find them.

It is natural for our pages to have a title and a description. But in the screenshot above, they are quite small compared to the space and attention the picture of sky and clouds gets — not to mention the size of the clickable area. That’s the power of the social image. It’s easy to understand the impact these images can have when a link is shared.

From Level 0 to Level 3

Not all social images are created equal. These are not official terms, but let’s consider numbered “levels” on how impactful these social image cards can be.

Level 0

The most basic social image is no image. The link might be lost in a sea of content with the small area and not much visual.

Level 1

A classic technique is to create a site-wide social image. While this solution might seem to offer a good outcome-to-effort ratio, one could argue this is worse than no image at all. Sure, we get some attention, but the reaction might be negative, especially if people see a lot of links to this website that all look the same. It risks feeling repetitive and unnecessary.

Level 2

The next level is standard in blogs and media sites: the social image of a post. Each post has its own featured image, and they differ from one post to another. This practice is totally legitimate for a news site, where the photo complements the page content. The potential drawback here is that it requires effort to find and create artwork for each and every published post.

That might lead to a bit of laziness. We’ve all been exposed to images that are obviously stock photos. It might get attention, but perhaps not the kind of attention you actually want.

Need an image of an intentionally diverse group of people meeting around a table foe work? There’s a ton of them out there!

Level 3

The final level: per-page, content-rich, meaningful social images. CSS-Tricks is doing just this. The team’s social images are branded. They share the same layout. They mention the post title, along with the author’s name and profile picture, something the regular title and description could not show. They grab attention and are memorable.

The CSS-Tricks social card incorporates information related to the post worth looking at.

There is an obvious requirement with this approach: automation. It is out of question to create unique images for every possible link. Just think of the overhead. We’d need some programmatic solution to help with the heavy lifting.

Let’s start a blog with blog posts that have unique social images

To give ourselves a nice little excuse (and sandbox) to build out unique social images, we’ll put together a quick blog. When I write and publish an article to this blog, I follow a quick two-step process:

  1. Write and publish the article
  2. Post the published URL to my social network accounts

This is when social images must shine. We want to give our blog its best shot at being noticed. But that’s not our only goal. This blog should establish our personal brand. We want our friends, colleagues, and followers to remember us when they see our social posts. We want something that’s repeatable, recognizable, and representative of ourselves.

Creating a blog is a lot or work. Although automated social images are cool, it’s unwise to spend too much time on them. (Chris came to the same conclusion at the end of 2020). So, in the interest of efficiency, we’re making an Eleventy site. Eleventy is a simple static site generator. Instead of starting from scratch, let’s use one of the starter projects. In fact, let’s pick the first one, eleventy-base-blog.

This is just the base template. We’re only using it to make sure we have posts to share.

Visit the eleventy-base-blog GitHub page and use it as a template:

Use eleventy-base-blog as a template

Let’s create the repository, and set a repository name, description. We can make it public or private, it doesn’t matter.

Next, we clone our repository locally, install packages, and run the site:

git clone [your repo URL] cd my-demo-blog ### Or whatever you named it npm install npm run serve

Our site running is running at http://localhost:8080.

Now let’s deploy it. Netlify makes this a super quick (and free!) task. (Oh, and spoiler alert: our social images automation relies on a Netlify Function.)

So, let’s go to Netlify and create an account, that is, if you don’t already have one. Either way, create a new site:

Click the “New site from Git” button to link up the project repo for hosting and deployment.

Go through the process of allowing Netlify to access the blog repository.

Simply leave the default values as they are and click the “Deploy site” button

Netlify deploys our site:

After a minute or so, the blog is deployed:

The site is deployed — we’re all set!

One image template to rule them all

Our social images are going to be based on an image template. To design this template, we are going to use the technologies we already know and love: HTML and CSS. HTML doesn’t turn itself into images auto-magically, but there are tools for this, the most famous being headless Chrome with Puppeteer.

However, instead of building our social image stack ourselves, we use the Resoc Image Template Development Kit. So, from the project root we can run this in the terminal:

npx itdk init resoc-templates/default -m title-description

This command creates a new image template in the resoc-templates/default directory. It also opens up in a new browser window.

The viewer provides a browser preview of the template configuration, as well as UI to change the values.

We could use this template as-is, but that only gets us to Level 2 on “impactful” spectrum. What we need to make this go all the way up to Level 3 and match the CSS-Tricks template is:

  • the page title aligned to the right with a bit of negative space on the left.
  • a footer at the bottom that contains a background gradient made from two colors we are going to use throughout the blog
  • the post author’s name and profile picture

If we head back to the browser, we can see in the Parameters panel of the template viewer that the template expects two parameters: a title and description. That’s just the template we chose when we ran -m title-description in the terminal as we set things up. But we can add more parameters by editing resoc-templates/default/resoc.manifest.json. Specifically, we can remove the second parameter to get:

{   "partials": {     "content": "./content.html.mustache",     "styles": "./styles.css.mustache"   },   "parameters": [     {       "name": "title",       "type": "text",       "demoValue": "A picture is worth a thousand words"     }   ] }

The viewer reflects the change in the browser:

Now the description is gone.

It’s time to design the image itself, which we can do in resoc-templates/default/content.html.mustache:

<div class="wrapper">   <main>     <h1>{{ title }}</h1>   </main>   <footer>     <img src="profil-pic.jpg" />     <h2>Philippe Bernard</h2>   </footer> </div>

That’s just regular HTML. Well, except {{ title }}. This is Mustache, the templating framework Resoc uses to inject parameter values into the template. We can even type some text in the “Title” field to see it working:

Looking at the previews, notice that we’re missing an image, profil-pic.jpg. Copy your best profile picture to resoc-templates/default/profil-pic.jpg:

The profile picture is now set.

It’s time to write the CSS in resoc-templates/default/styles.css.mustache. The point of this post isn’t how to style the template, but here’s what I ended up using:

@import url('https://fonts.googleapis.com/css2?family=Anton&family=Raleway&display=swap');  .wrapper {   display: flex;   flex-direction: column; }  main {   flex: 1;   display: flex;   flex-direction: column;   justify-content: center;   position: relative; }  h1 {   text-align: right;   margin: 2vh 3vw 10vh 20vw;   background: rgb(11,35,238);   background: linear-gradient(90deg, rgba(11,35,238,1) 0%, rgba(246,52,12,1) 100%);   -webkit-text-fill-color: transparent;   -webkit-background-clip: text;   font-family: 'Anton';   font-size: 14vh;   text-transform: uppercase;   text-overflow: ellipsis;   display: -webkit-box;   -webkit-line-clamp: 3;   -webkit-box-orient: vertical; }  h2 {   color: white;   margin: 0;   font-family: 'Raleway';   font-size: 10vh; }  footer {   flex: 0 0;   min-height: 20vh;   display: flex;   align-items: center;   background: rgb(11,35,238);   background: linear-gradient(90deg, rgba(11,35,238,1) 0%, rgba(246,52,12,1) 100%);   padding: 2vh 3vw 2vh 3vw; }  footer img {   width: auto;   height: 100%;   border-radius: 50%;   margin-right: 3vw; }

Most of the sizes rely on vw and vh units to help anticipate the various contexts that the template might be rendered. We are going to follow Facebook’s recommndations, which are 1200×630. Twitter Cards, on the other hand, are sized differently. We could render images in a low resolution, like 600×315, but let’s go with 1200×630 so we we only need to work in pixels.

The viewer renders the Facebook preview at 1200×630 and scales it down to fit the screen. If the preview fulfills your expectations, so will the actual Open Graph images.

So far, the template matches our needs:

What about the image?

There is one little thing to add before we are done with the template. Some of our blog posts will have images, but not all of them. In situations where a post doesn’t have an image, it would be cool to use the image to fill the space on the left.

This is a new template parameter, so we need to update resoc-templates/default/resoc.manifest.json once again:

{   "partials": {     "content": "./content.html.mustache",     "styles": "./styles.css.mustache"   },   "parameters": [     {       "name": "title",       "type": "text",       "demoValue": "A picture is worth a thousand words"     },     {       "name": "sideImage",       "type": "imageUrl",       "demoValue": "https://resoc.io/assets/img/demo/photos/pexels-photo-371589.jpeg"     }   ] }

Let’s declare an additional div in resoc-templates/default/content.html.mustache:

<div class="wrapper">   <main>     {{#sideImage}}     <div class="sideImage"></div>     {{/sideImage}}     <h1>{{ title }}</h1>   </main>   <footer>     <img src="profil-pic.jpg" />     <h2>Philippe Bernard</h2>   </footer> </div>

The new {{#sideImage}} ... {{/sideImage}} syntax is a Mustache section. It’s only present when the sideImage parameter is defined.

We need a little extra CSS to handle the image. Notice that we’re able to use the Mustache syntax here to inset the background-image value for a specific post. Here’s how I approached it in the resoc-templates/default/styles.css.mustache file:

{{#sideImage}} .sideImage {   position: absolute;   width: 100%;   height: 100%;   background-image: url({{{ sideImage }}});   background-repeat: no-repeat;   background-size: auto 150vh;   background-position: -35vw 0vh;   -webkit-mask-image: linear-gradient(45deg, rgba(0,0,0,0.5), transparent 40%); } {{/sideImage}}

Our template looks great!

We commit our template:

git add resoc-templates git commit -m "Resoc image template"

Before we automate the social images, let’s generate one manually, just as a teaser. The viewer provides a command line to generate the corresponding image for our testing purposes:

Copy it, run it from a terminal and open output-image.jpg:

Social images automation

OK, so we created one image via the command line. What should we do now? Call it as many times as there are pages on our blog? This sounds like a boring task, and there is a deeper issue with this approach: time. Even if creating a single image took something like two seconds, we can multiply by the number of pages and we easily see the effort grow and grow.

The original Eleventy blog template is generated almost instantly, but we should wait about a minute for something as marginal as social images? This is not acceptable.

Instead of performing this task at build time, we are going to defer it, lazy style, with a Netlify Function and a Netlify on-demand builder. Actually, we aren’t actually dealing directly with a Netlify Function — an Eleventy plugin is going to handle this for us.

Let’s install that now. We can add the Resoc Social Image plugin for Eleventy, along with its companion Netlify plugin, with this command:

npm install --save-dev @resoc/eleventy-plugin-social-image @resoc/netlify-plugin-social-image

Why two plugins? The first one is dedicated to Eleventy, while the second one is framework-agnostic (for example, it can be used for Next.js).

Edit .eleventy.js at the root of the project so that we’re importing the plugin:

const pluginResoc = require("@resoc/eleventy-plugin-social-image");

Configure it near the top of .eleventy.js, right after the existing eleventyConfig.addPlugin:

eleventyConfig.addPlugin(pluginResoc, {   templatesDir: 'resoc-templates',   patchNetlifyToml: true });

templatesDir is where we stored our image template. patchNetlifyToml is asking the plugin to configure @resoc/netlify-plugin-social-image in netlify.toml for us.

We want all our pages to have automated social images. So, let’s open the master template, _includes/layouts/base.njk, and add this near the top of the file:

{% set socialImageUrl %} {%- resoc   template = "default",   slug = (title or metadata.title) | slug,   values = {     title: title or metadata.title,     sideImage: featuredImage   } -%} {% endset %}

This declares a new variable named socialImageUrl. The content of this variable is provided by the resoc short code, which takes three parameters:

  • The template is the sub directory of our template (it is in resoc-templates/default).
  • The slug is used to build the social image URL (e.g. /social-images/brand-new-post.jpg). We slug-ify the page title to provide a unique and sharable URL.
  • The values are the content, as defined in resoc-templates/default/resoc.manifest.json. title is obvious, because pages already have a title. sideImage is set to a meta named featuredImage, which we are going to define for illustrated pages.

Now we can open up _includes/layouts/base.njk, place our cursor in the <head>, add some new markup to populate all that stuff

<meta name="og:title" content="{{ title or metadata.title }}"/> <meta name="og:description" content="{{ description or metadata.description }}"/> <meta name="og:image" content="{{ socialImageUrl }}"/> <meta name="og:image:width" content="1200"/> <meta name="og:image:height" content="630"/>

The title and description markups are similar to the existing <title> and <meta name="description">. We’re using socialImageUrl as-is for the og:image meta. We also provide the social image dimensions to round things out.

Automated social images are ready!

Let’s deploy this

When we deploy the blog again, all pages will show the text-only version of our template. To see the full version , we assign an image to an existing page. that requires us to edit one of the posts — I created four posts and am editing the fourth one, posts/fourthpost.md — so there’s a featuredImage entry after the existing meta:

--- title: This is my fourth post. description: This is a post on My Blog about touchpoints and circling wagons. date: 2018-09-30 tags: second tag layout: layouts/post.njk featuredImage: https://resoc.io/assets/img/demo/photos/pexels-pixabay-459653.jpg ---

Using an external URL is enough here, but we normally drop images in an img directory with Eleventy and provide the base URL once and for all in _includes/layouts/base.njk.

Build the site again:

npm run build

When running git status, we might notice two modified files in addition to the ones we edited ourselves. In .gitignore, the plugin added resoc-image-data.json. This file stores our social image data used internally by the Netlify plugin, and netlify.toml now contains the Netlify plugin configuration.

Deploy time!

git commit -a -m "Automated social images" git push

Netlify is notified and deploys the site. Once the latest version is online, share the homepage somewhere (e.g. Slack it to yourself or use the Facebook debugger). Here’s how the social card looks for the homepage, which does not contain an image:

This is our text-only card.

And here’s how it looks for a post that does contain an image:

This card sports an image.

Perfect!

Conclusion

So far, automated social images have mostly been a matter of developers willing to explore and play around with lots of different ideas and approaches, some easy and some tough. We kept things relatively simple.

With a few lines of code, we were able to quickly setup automated social images on a blog based on Eleventy and hosted on Netlify. The part we spent the most time on was the image template, but that’s not a problem. With the viewer and Mustache already integrated, we focused on what we know, love, and value: web design.

Hopefully something like the Resoc image template dev kit and its related tools will help make the automated social images go from being a niche hobby into the mainstream.


The post Create Your Own Automated Social Images With Resoc appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , ,
[Top]

How to Create a Contact Form With Next.js and Netlify

We’re going to create a contact form with Next.js and Netlify that displays a confirmation screen and features enhanced spam detection.

Next.js is a powerful React framework for developing performant React applications that scale. By integrating a Next.js site with Netlify’s technology, we can quickly get a working contact form up and running without having to write any server-side code.

Not only is it a relatively fast process to set up forms to be processed by Netlify, but it’s also free to get started (with up to 100 free submissions/per site hosted on Netlify). Form submissions automatically go through Netlify’s built-in spam filter which uses Akismet and there are also options that can be configured to increase the level of spam detection.

Creating the contact form

Within the Next.js application we should create a ContactForm component to render the contact form inside of the contact page. If you’d like for this form to render at /contact, then the ContactForm component below with labels and input fields should be used within the pages/contact.js file.

const ContactForm = (   <form     name="contact-form"     method="POST"     action="contact/?success=true"   >     <label htmlFor="name">Name *</label>     <input       id="name"       name="name"       required       type="text"     />     <label htmlFor="company">Company *</label>     <input id="company" name="company" required type="text" />     <label htmlFor="email">E-mail Address *</label>     <input id="email" type="email" name="email" required />     <label htmlFor="message">Message *</label>     <textarea id="message" name="message" required></textarea>     <button type="submit">Submit</button>   </form> );

The above markup is required to render a form with a field for Name, Company, Email address and message with a submit button. When submitting the form, based on the value of the form’s action, it should redirect to contact/?success=true from /contact. Right now there is not yet a difference between the page’s appearance with and without the success query parameter, but we will update that later.

Our Contact.js file looks like this so far:

import React from "react"; const ContactPage = () => {  const ContactForm = (/* code in above code sample*/)    return (    <div>      <h1>Contact Us</h1>      {ContactForm}    </div>  ); };   export default ContactPage;

Now that we have the basic form set up, the real magic will happen after we add additional information for Netlify to auto-recognize the form during future site deployments. To accomplish this we should update the form to have the attribute data-netlify="true" and a hidden input field that contains the name of our contact form. In Netlify, once we navigate to our site in the dashboard and then click on the “forms” tab  we will be able to view our form responses based on the name that we’ve put in our hidden field. It’s important that if you have multiple forms within a site that they have unique names so that they are recorded properly in Netlify.

<form   method="POST"   name="contact-form"   action="contact/?success=true"   data-netlify="true" > <input type="hidden" name="form-name" value="contact-form" />

After successfully deploying the site to Netlify with the data-netlify attribute and the form-name field  then we can go to the deployed version of the site and fill out the form. Upon submitting the form and navigating to https://app.netlify.com/sites/site-name/forms (where site-name is the name of your site) then our most recent form submission should appear if we have successfully set up the form. 

Redirect to confirmation screen 

In order to improve the user experience, we should add some logic to redirect to a confirmation screen on form submission when the URL changes to /contact/?success=true. There is also the option to redirect to an entirely different page as the action when the form is submitted but using query params we can achieve something similar with the Next Router. We can accomplish this by creating a new variable to determine if the confirmation screen or the form should be visible based on the query parameter. The next/router which is imported with import { useRouter } from "next/router"; can be used to retrieve the current query params. 

const router = useRouter();   const confirmationScreenVisible = router.query?.success && router.query.success === "true";

In our case, the confirmation screen and form can never be visible at the same time; therefore, the following statement can be used to determine if the form is visible or not.

const formVisible = !confirmationScreenVisible; 

To give users the option to resubmit the form, we can add a button to the confirmation screen to reset the form by clearing the query params. Using router.replace (instead of router.push) not only updates the page but replaces the current page in the history to the version without query params. 

<button onClick={() => router.replace("/contact", undefined, { shallow: true })}> Submit Another Response </button>

We can then conditionally render the form based on whether or not the form is visible with:

{formVisible ? ContactForm : ConfirmationMessage}

Putting it all together, we can use the following code to conditionally render the form based on the query params (which are updated when the form is submitted):

import React, { useState } from "react"; import { useRouter } from "next/router";   const ContactPage = () => {  const [submitterName, setSubmitterName] = useState("");  const router = useRouter();  const confirmationScreenVisible =    router.query?.success && router.query.success === "true";  const formVisible = !confirmationScreenVisible;    const ConfirmationMessage = (    <React.Fragment>      <p>        Thank you for submitting this form. Someone should get back to you within 24-48 hours.      </p>        <button onClick={() => router.replace("/contact", undefined, { shallow: true })}> Submit Another Response </button>    </React.Fragment>  );    const ContactForm = (/* code in first code example */);    return (    <div>      <h1>Contact Us</h1> {formVisible ? ContactForm : ConfirmationMessage}    </div>  ); };   export default ContactPage;

Adding a hidden bot field

Now that the core functionality of our form is working, we can add additional spam detection to our form in addition to the base spam detection because Akismet is included with all Netlify Forms by default. We can enable this by adding data-netlify-honeypot="bot-field" to our form.

<form   className="container"   method="POST"   name="contact-form"   action="contact/?success=true"   data-netlify="true"   data-netlify-honeypot="bot-field" >

We also need to create a new hidden paragraph that contains a label named bot-field that contains the input. This field is “visible” to bots, but not humans. When this honeypot form field is filled, Netlify detects a bot and then the submission is flagged as spam.

<p hidden>   <label>     Don’t fill this out: <input name="bot-field" />   </label> </p>

Further customizations

  • We could explore another spam prevention option that Netlify supports by adding reCAPTCHA 2 to a Netlify form.
  • We could update the form to allow uploaded files with input <input type="file">.
  • We could set up notifications for form submissions. That happens over at https://app.netlify.com/sites/[your-site-name]/settings/forms where we can include a custom subject field (which can be hidden) for email notifications.

Full code

The code for the full site code is available over at GitHub.

 Bonus

The following code includes everything we covered as well as the logic for setting a custom subject line with what was submitted in the name field.

import React, { useState } from "react"; import { useRouter } from "next/router";   const ContactPage = () => {  const [submitterName, setSubmitterName] = useState("");  const router = useRouter();  const confirmationScreenVisible =    router.query?.success && router.query.success === "true";  const formVisible = !confirmationScreenVisible;    const ConfirmationMessage = (    <React.Fragment>      <p>        Thank you for submitting this form. Someone should get back to you        within 24-48 hours.      </p>        <button onClick={() => router.replace("/contact", undefined, { shallow: true })}> Submit Another Response </button>    </React.Fragment>  );    const ContactForm = (    <form      className="container"      method="POST"      name="contact-form"      action="contact/?success=true"      data-netlify="true"      data-netlify-honeypot="bot-field"    >      <input        type="hidden"        name="subject"        value={`You've got mail from $ {submitterName}`}      />      <input type="hidden" name="form-name" value="contact-form" />      <p hidden>        <label>          Don’t fill this out: <input name="bot-field" />        </label>      </p>        <label htmlFor="name">Name *</label>      <input        id="name"        name="name"        required        onChange={(e) => setSubmitterName(e.target.value)}        type="text"      />      <label htmlFor="company">Company *</label>      <input id="company" name="company" required type="text" />      <label htmlFor="email">E-mail Address *</label>      <input id="email" type="email" name="email" required />      <label htmlFor="message">Message *</label>      <textarea id="message" name="message" required/>      <button type="submit">Submit</button>    </form>  );    return (    <div>      <h1>Contact Us</h1> {formVisible ? ContactForm : ConfirmationMessage}    </div>  ); };   export default ContactPage;

The post How to Create a Contact Form With Next.js and Netlify appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , ,
[Top]

How to Create CSS Charts With Interesting Shapes, Glyphs and Emoji

Let’s forego the usual circles and bars we typically see used in charts for more eccentric shapes. With online presentations more and more common today, a quick way to spruce up your web slides and make them stand out is to give the charts a shapely makeover 🪄

I’ll show you how to create charts with interesting shapes using glyphs, CSS shapes, and emojis with minimal effort.

Let’s start with a simple example.

Using glyphs

<div id="chart">   <div id="chart-shape">⬠</div>   <div id="chart-value"></div>  </div>
#chart {   width: 300px;    height: 300px;   display: grid;   background: white; } #chart * {   height: inherit;   grid-area: 1 / 1; }

We first give the chart some dimensions and stack the two div inside it by assigning them to the same grid cell. They can be stacked up using any other way, too — with position property, for instance.

Look at the HTML above one more time. One of the divs has a pentagon symbol — the chart shape we want. I added that symbol using the “Emoji and Symbols” keyboard, though it can also be done with the HTML entity value for pentagon, &#x2B20;, inside the div.

The div with the symbol is then styled with CSS font properties as well as a desired chart color. It’s large enough and centered.

#chart-shape {   font: 300px/300px serif;   text-align: center;    color: limegreen; }

To the second div in the HTML contains a conic gradient background image. The percentage of the gradient represents the visual value of the chart. The same div also has mix-blend-mode: screen;.

#chart-value {   background: conic-gradient(transparent 75%, darkseagreen 75%);   mix-blend-mode: screen; }

The mix-blend-mode property blends colors inside an element with its backdrop. The screen blend mode value causes a lighter blend to come through. A lighter green shows through the portion where the darkseagreen colored part of the conic gradient overlaps with the limegreen colored pentagram, while the rest of the darskseagreen gradient disappears against the white backdrop of the chart.

An alternative to adding the chart shape in the HTML is to add it as another background layer in CSS and use background-blend-mode instead of mix-blend-mode. However, the code for a chart shape inside the CSS can be less legible for a quick glance. So it’s up to you to see where it’ll be easier for you to add the chart shape in: HTML or CSS. You’ve both options.

#chart {   width: 300px;    height: 300px;   background:   url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg'><foreignObject width='300px' height='100%'><div xmlns='http://www.w3.org/1999/xhtml' style='font:300px/300px serif;color:limegreen;text-align: center;background:white'>⬠</div></foreignObject></svg>"),   conic-gradient(transparent 75%, darkseagreen 75%);   background-blend-mode: screen; }

The pentagon symbol is added as a background image in addition to the conic gradient. Then, finally, the property-value pair background-blend-mode: screen; kicks in and the result looks same as the previous demo.

The pentagon background image is created by embedding the HTML with the pentagon symbol () into an SVG that is embedded into a data URL.

<!-- Unwrapped SVG code from the Data URL --> <svg xmlns='http://www.w3.org/2000/svg'>   <foreignObject width='300px' height='100%'>     <div xmlns='http://www.w3.org/1999/xhtml'           style='           font:300px/300px serif;           color:limegreen;           text-align: center;           background:white;'>           ⬠     </div>   </foreignObject> </svg>

Which becomes this in CSS:

background: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg'><foreignObject width='300px' height='100%'><div xmlns='http://www.w3.org/1999/xhtml' style='font:300px/300px serif;color:limegreen;text-align: center;background:white'>⬠</div></foreignObject></svg>");

Using CSS shapes

Next, let’s use CSS shapes in place of symbols. CSS shapes are primarily created with the use of border properties. We have a collection of CSS shapes in our archive for your reference.

Here’s a set of properties that can create a simple triangle shape in an element we’ll later add to the SVG, replacing the symbol:

border: 150px solid white;  border-bottom: 300px solid lime;  border-top: unset;

When combined with the conic gradient and the background blend, the result is:

<div id="chart"></div>
#chart {   width: 300px;   height: 300px;   background:   url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg'><foreignObject width='300px' height='100%'><html xmlns='http://www.w3.org/1999/xhtml'><div style='border:150px solid white; border-bottom:300px solid lime; border-top:unset'></div><div style='border:150px solid transparent; border-bottom:300px solid white; border-top:unset; transform:scale(0.8) translateY(-360px);'></div></html></foreignObject></svg>"),   conic-gradient(transparent 75%, darkseagreen 75%);   background-blend-mode: screen; }

To restrict the design to the border, a smaller white triangle was added to the design.

<!-- Unwrapped SVG code from the Data URL --> <svg xmlns='http://www.w3.org/2000/svg'>   <foreignObject width='300px' height='100%'>    <html xmlns='http://www.w3.org/1999/xhtml'>     /* green triangle */     <div style='          border: 150px solid white;           border-bottom: 300px solid lime;           border-top: unset'></div>     /* smaller white triangle */     <div style='          border: 150px solid transparent;           border-bottom: 300px solid white;           border-top: unset;           transform: scale(0.8) translateY(-360px);'></div>    </html>   </foreignObject> </svg>

Which, again, becomes this in CSS:

background: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg'><foreignObject width='300px' height='100%'><html xmlns='http://www.w3.org/1999/xhtml'><div style='border:150px solid white; border-bottom:300px solid lime; border-top:unset'></div><div style='border:150px solid transparent; border-bottom:300px solid white; border-top:unset; transform:scale(0.8) translateY(-360px);'></div></html></foreignObject></svg>");

Using emojis

Will emojis work with this approach to charts? You bet it will! 🥳

A block-colored emoji is fed into the SVG image the same way the HTML symbols are. The block color of the emoji is created by giving it a transparent color value, followed by adding a desired color as text-shadow. I covered this technique in another post.

<div id="chart"></div>
#chart {   width: 300px;    height: 300px;   background: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg'><foreignObject width='300px' height='300px'><body style='margin:0;text-align:center;color:transparent;' xmlns='http://www.w3.org/1999/xhtml'><div style='text-shadow: 0 0 limegreen;font:200px/300px serif;background:white;'>🍏</div><div style='text-shadow:0 0 white;font:170px/300px serif;position:relative;top:-300px;'>🍏</div></body></foreignObject></svg>"),   conic-gradient(transparent 64%, darkseagreen 64%);   background-blend-mode: screen; }

Just as with the last demo, a smaller white apple shape is added at the center to create the border design.

<!-- Unwrapped SVG code from the Data URL --> <svg xmlns='http://www.w3.org/2000/svg'>   <foreignObject width='300px' height='300px'>     <body xmlns='http://www.w3.org/1999/xhtml' style='           margin: 0;           text-align: center;           color:transparent;'>        /* green apple shape */        <div style='             text-shadow: 0 0 limegreen;              font-size: 200px;              background: white;'>🍏</div>        /* smaller white apple shape */        <div style='             text-shadow:0 0 white;              font-size: 170px;              position: relative;              top: -300px;'>🍏</div>     </body>   </foreignObject> </svg>

I added the two divs inside the <body> element so the repeating style properties of the divs are declared only once in the body element. The divs will then automatically inherit those properties.

Chris had the idea to animate the conic gradient — its percent value to be specific — using the CSS @property (supported in Chrome at the time of writing this article), and it just has the most beautiful effect on the design. @property is a CSS at-rule that explicitly defines a custom CSS property. In supported browsers, when a custom property is defined using @property it can be animated.

@property --n {   syntax: '<percentage>';   inherits: true;   initial-value: 30%; } #chart {   width: 300px;    height: 300px;   --n: 30%;  /*declaration for browsers with no @property support */   background:      url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg'><foreignObject width='300px' height='300px'><body style='margin:0;text-align:center;color:transparent;' xmlns='http://www.w3.org/1999/xhtml'><div style='text-shadow: 0 0 limegreen;font:200px/300px serif;background:white;'>🍏</div><div style='text-shadow:0 0 white;font:170px/300px serif;position:relative;top:-300px;'>🍏</div></body></foreignObject></svg>"),     conic-gradient(transparent var(--n), darkseagreen var(--n));   background-blend-mode: screen;   transition: --n 2s ease-in-out	 } #chart:hover { --n: 70%; }

The chart above will change its value on hover. In Chrome, the change will look animated.

And although it won’t be as smooth as CSS animation, you can also try animating the gradient using JavaScript. The following JavaScript will cause a somewhat similar animating effect as the above when the cursor moves over the chart.

const chart = document.querySelector('#chart') chart.onpointerover = ()=>{   var i = 30,       timer = setInterval(()=> {         if (i < 70)           chart.style.setProperty('--n', i++ + '%')         else clearInterval(timer)       }, 10) } chart.onpointerout = ()=>{   var i = 70,       timer = setInterval(()=> {         if (i >= 30)            chart.style.setProperty('--n', i-- + '%')         else clearInterval(timer)       }, 10) }

When trying your own designs, keep in mind how the different blend modes work. I used the screen blend mode in all my demos just to keep things simple. But with different blend modes, and different backdrop colors, you’ll get varying results. So, I recommend going deeper into blend modes if you haven’t already.

Also, if you want to exclude an element’s color from the final result, try isolation: isolate; on the element — the browser will ignore that backdrop color when applying the blend.

And even though there are all kinds of unusual and quirky shapes we can use in any wild colors we want, always be mindful of the legibility by making the chart value large and clear enough to read.


The post How to Create CSS Charts With Interesting Shapes, Glyphs and Emoji appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , , ,
[Top]

Can We Create a “Resize Hack” With Container Queries?

If you follow new developments in CSS, you’ve likely heard of the impending arrival of container queries. We’re going to look at the basics here, but if you’d like another look, check out Una’s “Next Gen CSS: @container” article. After we have a poke at the basics ourselves, we’re going to build something super fun with them: a fresh take on the classic CSS meme featuring Peter Griffin fussing with window blinds. 😉

So, what is a container query? It’s… exactly that. Much like we have media queries for querying things such as the viewport size, a container query allows us to query the size of a container. Based on that, we can then apply different styles to the children of said container.

What does it look like? Well, the exact standards are being worked out. Currently, though, it’s something like this:

.container {   contain: layout size;   /* Or... */   contain: layout inline-size; }  @container (min-width: 768px) {   .child { background: hotpink; } }

The layout keyword turns on layout-containment for an element. inline-size allows users to be more specific about containment. This currently means we can only query the container’s width. With size, we are able to query the container’s height.

Again, we things could still change. At the time of writing, the only way to use container queries (without a polyfill) is behind a flag in Chrome Canary (chrome://flags). I would definitely recommend having a quick read through the drafts over on csswg.org.

The easiest way to start playing would be to whip up a couple quick demos that sport a resizable container element.

Try changing the contain values (in Chrome Canary) and see how the demos respond. These demo uses contain: layout size which doesn’t restrict the axis. When both the height and width of the containers meet certain thresholds, the shirt sizing adjusts in the first demo. The second demo shows how the axes can work individually instead, where the beard changes color, but only when adjusting the horizontal axis.

@container (min-width: 400px) and (min-height: 400px) {   .t-shirt__container {     --size: "L";     --scale: 2;   } }

That’s what you need to know to about container queries for now. It’s really just a few new lines of CSS.

The only thing is: most demos for container queries I’ve seen so far use a pretty standard “card” example to demonstrate the concept. Don’t get me wrong, because cards are a great use case for container queries. A card component is practically the poster child of container queries. Consider a generic card design and how it could get affected when used in different layouts. This is a common problem. Many of us have worked on projects where we wind up making various card variations, all catering to the different layouts that use them.

But cards don‘t inspire much to start playing with container queries. I want to see them pushed to greater limits to do interesting things. I‘ve played with them a little in that t-shirt sizing demo. And I was going to wait until there was better browser support until I started digging in further (I’m a Brave user currently). But then Bramus shared there was a container query polyfill!

And this got me thinking about ways to “hack” container queries.

⚠️ Spoiler alert: My hack didn’t work. It did momentarily, or at least I thought it did. But, this was actually a blessing because it prompted more conversation around container queries.

What was my idea? I wanted to create something sort of like the “Checkbox Hack” but for container queries.

<div class="container">   <div class="container__resizer"></div>   <div class="container__fixed-content"></div> </div>

The idea is that you could have a container with a resizable element inside it, and then another element that gets fixed positioning outside of the container. Resizing containers could trigger container queries and restyle the fixed elements.

.container {   contain: layout size; }  .container__resize {   resize: vertical;   overflow: hidden;   width: 200px;   min-height: 100px;   max-height: 500px; }  .container__fixed-content {   position: fixed;   left: 200%;   top: 0;   background: red; }  @container(min-height: 300px) {   .container__fixed-content {     background: blue;   } }

Try resizing the red box in this demo. It will change the color of the purple box.

Can we debunk a classic CSS meme with container queries?

Seeing this work excited me a bunch. Finally, an opportunity to create a version of the Peter Griffin CSS meme with CSS and debunk it!

You’ve probably seen the meme. It’s a knock on the Cascade and how difficult it is to manage it. I created the demo using cqfill@0.5.0… with my own little touches, of course. 😅

Moving the cord handle, resizes an element which in turn affects the container size. Different container breakpoints would update a CSS variable, --open, from 0 to 1, where 1 is equal to an “open” and 0 is equal to a “closed” state.

@container (min-height: 54px) {   .blinds__blinds {     --open: 0.1;   } } @media --css-container and (min-height: 54px) {   .blinds__blinds {     --open: 0.1;   } } @container (min-height: 58px) {   .blinds__blinds {     --open: 0.2;   } } @media --css-container and (min-height: 58px) {   .blinds__blinds {     --open: 0.2;   } } @container (min-height: 62px) {   .blinds__blinds {     --open: 0.3;   } } @media --css-container and (min-height: 62px) {   .blinds__blinds {     --open: 0.3;   } }

But…. as I mentioned, this hack isn’t possible.

What’s great here is that it prompted conversation around how container queries work. It also highlighted a bug with the container query polyfill which is now fixed. I would love to see this “hack” work though.

Miriam Suzanne has been creating some fantastic content around container queries. The capabilities have been changing a bunch. That’s the risk of living on the bleeding edge. One of her latest articles sums up the current status.

Although my original demo/hack didn’t work, we can still kinda use a “resize” hack to create those blinds. Again, we can query height if we use contain: layout size. Side note: it’s interesting how we’re currently unable to use contain to query a container’s height based on resizing its child elements.

Anyway. Consider this demo:

The arrow rotates as the container is resized. The trick here is to use a container query to update a scoped CSS custom property.

.container {   contain: layout size; }  .arrow {   transform: rotate(var(--rotate, 0deg)); }  @container(min-height: 200px) {   .arrow {     --rotate: 90deg;   } }

We‘ve kinda got a container query trick here then. The drawback with not being able to use the first hack concept is that we can’t go completely 3D. Overflow hidden will stop that. We also need the cord to go beneath the window which means the windowsill would get in the way.

But, we can almost get there.

This demo uses a preprocessor to generate the container query steps. At each step, a scoped custom property gets updated. This reveals Peter and opens the blinds.

The trick here is to scale up the container to make the resize handle bigger. Then I scale down the content to fit back where it’s meant to.


This fun demo “debunking the meme” isn’t 100% there yet, but, we’re getting closer. Container queries are an exciting prospect. And it’ll be interesting to see how they change as browser support evolves. It’ll also be exciting to see how people push the limits with them or use them in different ways.

Who know? The “Resize Hack” might fit in nicely alongside the infamous “Checkbox Hack” one day.


The post Can We Create a “Resize Hack” With Container Queries? appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , ,
[Top]

How to Create Neon Text With CSS

Neon text can add a nice, futuristic touch to any website. I’ve always loved the magic of neon signs, and wanted to recreate them using CSS. I thought I’d share some tips on how to do it! In this article, we’re going to take a look at how to add glowing effects to text. We’ll also take a look at various ways to animate the neon signs, all using CSS and keyframes.

Here’s what we’ll be making:

Adding a glow effect to text

First, let’s make the text glow. This can be done in CSS with the text-shadow property. What’s neat about text-shadow is that we can apply multiple shadows on it just by comma-separating them:

.neonText {   color: #fff;   text-shadow:     0 0 7px #fff,     0 0 10px #fff,     0 0 21px #fff,     0 0 42px #0fa,     0 0 82px #0fa,     0 0 92px #0fa,     0 0 102px #0fa,     0 0 151px #0fa; }

text-shadow requires four values, the first two of which represent the horizontal and vertical position of the shadow, respectively. The third value represents the size of the blur radius while the last value represents the color of the shadow. To increase the size of the glow effect, we would increase the third value, which represents the blur radius. Or, expressed another way:

text-shadow: [x-offset] [y-offset] [blur-radius] [color];

Here’s what we get with that small bit of CSS:

The next thing you might be wondering is what’s up with all of those values? How did I get those and why are there so many? First, we added white glow effects to the outer edges of the text’s letters with a small blur radius.

.neonText {   color: #fff;   text-shadow:     /* White glow */     0 0 7px #fff,     0 0 10px #fff,     0 0 21px #fff, }

The last five values are wider text shadows of a larger blur radius that forms the green glow.

.neonText {   color: #fff;   text-shadow:     /* White glow */     0 0 7px #fff,     0 0 10px #fff,     0 0 21px #fff,     /* Green glow */     0 0 42px #0fa,     0 0 82px #0fa,     0 0 92px #0fa,     0 0 102px #0fa,     0 0 151px #0fa; }

It’d be great if we could accomplish this with fewer than five shadows, but we need all these shadows so that they can be stacked over one another to add more depth to the glow. If we had used a single text-shadow instead, the effect would not have the depth required to make it look realistic.

Go ahead and experiment with various hues and colors as well as blur radius sizes! There’s a huge variety of cool glow effects you can make, so try different variations — you can even mix and match colors where one color blends into another.

The “flickering” effect

One thing you might notice about neon signs is that some of them — particularly older ones — tend to flicker. The light kind of goes in and out. We can do the same sort of thing with CSS animations! Let’s reach for @keyframes to make an animation that flickers the light on and off in quick, seemingly random flashes.

@keyframes flicker {   0%, 18%, 22%, 25%, 53%, 57%, 100% {     text-shadow:       0 0 4px #fff,       0 0 11px #fff,       0 0 19px #fff,       0 0 40px #0fa,       0 0 80px #0fa,       0 0 90px #0fa,       0 0 100px #0fa,       0 0 150px #0fa;   }   20%, 24%, 55% {            text-shadow: none;   } }

That’s really it! We’ve taken the exact same text-shadow property and values we had before, wrapped them in a @keyframes animation called flicker, and chose points in the timeline to apply the shadows, as well as points that completely remove the shadows.

All that’s left is to call the animation where we want the light to flicker. In this particular case, let’s only add it to the <h1> element. Having one part of the entire sign flicker feels a little more realistic than if we applied the flicker to all of the text.

h1 {   animation: flicker 1.5s infinite alternate;      } 

Note that if we did want the entire sign to flicker, then we could technically remove the text-shadow values on the .neonText class, add the animation to it, and let the @keyframes apply the shadows instead.

It’s quite a cool effect, and adds more realism to our neon text! Of course, there are other effects you could try out too, which will also be explored further in this article. For example, how about more of a pulsating animation or a more subtle flicker?

Let’s explore those and other effects!

Pulsating glow

We just got a quick peek at this. It uses keyframes, just as the previous example does, where we specify the size of the blur radius at the start and end of the animation.

We want the size of the blur radius to be smallest at the end of the animation, so we simply decrease the blur radius values for each text-shadow value in the 0% keyframe. This way, the size of the blur gradually ebbs and flows, creating a pulsating effect.

@keyframes pulsate {   100% {     /* Larger blur radius */     text-shadow:       0 0 4px #fff,       0 0 11px #fff,       0 0 19px #fff,       0 0 40px #0fa,       0 0 80px #0fa,       0 0 90px #0fa,       0 0 100px #0fa,       0 0 150px #0fa;   }   0% {     /* Smaller blur radius */     text-shadow:       0 0 2px #fff,       0 0 4px #fff,       0 0 6px #fff,       0 0 10px #0fa,       0 0 45px #0fa,       0 0 55px #0fa,       0 0 70px #0fa,       0 0 80px #0fa;   } }

Once again, we add the animation to some element. We’ll go with <h1> again:

h1 {   animation: pulsate 2.5s infinite alternate;      }

Here it is with it all put together:

Subtle flicker

We can tone things down a bit and make the flickering action super subtle. All we need to do is slightly decrease the size of the blur radius in the 0% keyframe, just not to the extent as seen in the previous example.

@keyframes pulsate {   100% {     /* Larger blur radius */     text-shadow:       0 0 4px #fff,       0 0 11px #fff,       0 0 19px #fff,       0 0 40px #f09,       0 0 80px #f09,       0 0 90px #f09,       0 0 100px #f09,       0 0 150px #f09;   }  0% {     /* A slightly smaller blur radius */     text-shadow:       0 0 4px #fff,       0 0 10px #fff,       0 0 18px #fff,       0 0 38px #f09,       0 0 73px #f09,       0 0 80px #f09,       0 0 94px #f09,       0 0 140px #f09;   } }

Since the flickering is more subtle and the reduction of the blur radius is not as large, we should increase the number of times this animation occurs per second in order to emulate more frequent flickering. This can be done by decreasing the animation’s duration, say to a mere 0.11s:

h1 {   animation: pulsate 0.11s ease-in-out infinite alternate;     }

Using a background image

It would be really neat if our sign was hanging on a wall instead of empty space. Let’s grab a background image for that, maybe some sort of brick texture from Unsplash or something:

body {   background-image: url(wall.jpg); }

Adding a border

One last detail we can add is some sort of circular or rectangular border around the sign. It’s just a nice way to frame the text and make it look like, you know, an actual sign. By adding a shadow to the border, we can give it the same neon effect as the text!

Whatever element is the container for the text is what needs a border. Let’s say we’re only working with an <h1> element. That’s what gets the border. We call the border shorthand property to make a solid white border around the heading, plus a little padding to give the text some room to breathe:

h1 {   border: 0.2rem solid #fff;   padding: 0.4em; }

We can round the corners of the border a bit so things aren’t so sharp by applying a border-radius on the heading. You can use whatever value works best for you to get the exact roundness you want.

h1 {   border: 0.2rem solid #fff;   border-radius: 2rem;   padding: 0.4em; }

The last piece is the glow! Now, text-shadow won’t work for the border here but that’s okay because that’s what the box-shadow property is designed to do. The syntax is extremely similar, so we can even pull exactly what we have for text-shadow and tweak the values slightly:

h1 {   border: 0.2rem solid #fff;   border-radius: 2rem;   padding: 0.4em;   box-shadow: 0 0 .2rem #fff,               0 0 .2rem #fff,               0 0 2rem #bc13fe,               0 0 0.8rem #bc13fe,               0 0 2.8rem #bc13fe,               inset 0 0 1.3rem #bc13fe; }

Notice that inset keyword? That’s something text-shadow is unable to do but adding it to the border’s box-shadow allows us to get some of the glow on both sides of the border for some realistic depth.

What about accessibility?

If users have a preference for reduced motion, we’ll need to accommodate for this using the prefers-reduced-motion media query. This allows us to remove our animation effects in order to make our text more accessible to those with a preference for reduced motion.

For example, we could modify the flashing animation from the Pen above so that users who have prefers-reduced-motion enabled don’t see the animation. Recall that we applied the flashing effect to the <h1> element only, so we’ll switch off the animation for this element:

@media screen and (prefers-reduced-motion) {    h1 {     animation: none;   } }

It’s incredibly important to ensure that users’ preferences are catered for, and making use of this media query is a great way to make the effect more accessible for those with a preference for reduced motion.

Conclusion

Hopefully this has shown you how to create cool neon text for your next project! Make sure to experiment with various fonts, blur radius sizes and colors and don’t forget to try out different animations, too — there’s a world of possibilities out there. And add a comment if you’ve created a neat shadow effect you want to share. Thanks for reading!


The post How to Create Neon Text With CSS appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, ,
[Top]