Categories
Uncategorized

CSS math functions min(), max() and clamp()

CSS provides a set of functions for creating numerical expressions to use without having to resort to complex calculations using calc() and variables.

Clamp

My favorite use for these functions is to control font sizing in a dynamic environment. Until I discovered clamp() I couldn’t think of any way to not let the font get smaller or larger than preset values.

The following CSS rule will restrict the size of our paragraph text:

p {
  font-size: clamp(16px, 1.5vw, 24px);
}

The rule means that: If 1.5vw is smaller than 16px then keep it at 16px. Likewise, if 1.5vw is larger than 24px then keep it at 24px and if it’s between 16 and 24px then we keep the 1.5vw value.

Min and Max

We also have min() and max() functions that work by taking one of two values; which value it takes depends on the function we use.

In the first example, we create a container that is 40em or 400px, whichever is smaller at the current screen size.

.container {
  border: 2px solid red;
  margin: 0 auto;
  padding: 0 2em;
  width: min(400px, 40em);
}

This example provides the opposite example. The width will be 400px or 40 em, whatever is larger at the current screen size.

.container2 {
  border: 2px solid red;
  margin: 0 auto;
  padding: 0 2em;
  width: max(400px, 40em);
}

We can also chain multiple min() and max() functions to create more complex values.

This example will calculate the min() value between 5em and 64px and then use that as the value of the outer max() function.

.container {
  width: max(10px, min(64px, 5em));
}

You can also have more than two values for either min() or max() but it becomes harder to reason through the rule so I’d recommend always keeping it down to two final values.

Links and Resources

Categories
Uncategorized

Using Pointer Events

In May 2019 I wrote a post about Pointer Events as an introduction to Pointer Events and how they could be polyfilled for browsers (Safari) that didn’t support them natively.

Almost a year later things have progressed. Safari supports the feature so there’s no need for a polyfill and it’s now practical to use in production.

As a refresher, these are the events, the onEvent handlers and a brief explanation of when they trigger, taken from MDN

Event On Event Handler Description
pointerover onpointerover Fired when a pointer is moved into an element’s hit test boundaries.
pointerenter onpointerenter Fired when a pointer is moved into the hit test boundaries of an element or one of its descendants, including as a result of a pointerdown event from a device that does not support hover (see pointerdown).
pointerdown onpointerdown Fired when a pointer becomes active buttons state.
pointermove onpointermove Fired when a pointer changes coordinates. This event is also used if the change in pointer state can not be reported by other events.
pointerup onpointerup Fired when a pointer is no longer active buttons state.
pointercancel onpointercancel A browser fires this event if it concludes the pointer will no longer be able to generate events (for example the related device is deactived).
pointerout onpointerout Fired for several reasons including: pointer is moved out of the hit test boundaries of an element; firing the pointerup event for a device that does not support hover (see pointerup); after firing the pointercancel event (see pointercancel); when a pen stylus leaves the hover range detectable by the digitizer.
pointerleave onpointerleave Fired when a pointer is moved out of the hit test boundaries of an element. For pen devices, this event is fired when the stylus leaves the hover range detectable by the digitizer.
gotpointercapture ongotpointercapture Fired when an element receives pointer capture.
lostpointercapture onlostpointercapture Fired after pointer capture is released for a pointer.

The idea is to recreate two basic event handlers so they’ll work across devices: click and hover.

First, and naive implementation

Using the following HTML code.

<div class="box">
  <h1>Click Me!</h2>
</div>

We can use the following Javascript code to listen for pointerdown and pointerover.

if (window.PointerEvent) {
  const box = document.querySelector(".box");

  box.addEventListener("pointerdown", (evt) => {
    console.log("Pointer click equivalent");
  });

  box.addEventListener("pointerover", (evt) => {
    console.log("Pointer moved in");
  });
}

We wrap our code on a basic feature detection block to make sure we only use the feature in browsers that support it.

Next, we capture a reference to the HTML object that we want to work with and add the two event listeners.

Most of the time this will be OK as the behavior we want is similar across pointing devices

Take two

But there are times when we may want to do things differently based on what type of device is accessing the element and how it’s doing it.

For example, it’s different to click on a button with your finger than with a pen.

The pointerover event remains the same as that one doesn’t need to know the type of pointing device that we used.

We change pointerdown to a switch statement where we test for different types of pointer devices and take appropriate action based on the type of device.

We use a switch statement to match the type of pointer in use: mouse, pen, or touch.

const box = document.querySelector(".box");

if (window.PointerEvent) {
  box.addEventListener("pointerover", (evt) => {
    console.log("Pointer moved in");
  });

  box.addEventListener("pointerdown", (evt) => {
    switch(evt.pointerType) {
      case "mouse":
        console.log('mouse input detected');
        break;
      case "pen":
        console.log('pen/stylus input detected');
        break;
      case "touch":
        console.log('touch input detected');
        break;
      default:
        console.log('pointerType is empty or could not be detected');
    }
  });
}

Refining the code

The last portion of this post will cover some refinements that we can do to the script to improve performance.

First, we create external functions for each of the events that we want to handle.


function handlePointerOver(evt) { console.log("Pointer moved in"); } function handlePenInput(evt) { console.log("pen/stylus input detected"); } function handleTouchInput(evt) { console.log("touch input detected"); } function handleMouseInput(evt) { console.log("mouse input detected"); }

Then we reference the functions from inside the event handlers. This way we make the code more modular.

if (window.PointerEvent) {
  box.addEventListener("pointerover", (evt) => {
    handlePointerEvent(evt);
  });

  box.addEventListener("pointerdown", (evt) => {
    console.log("Pointer is down");
    switch (evt.pointerType) {
      case "mouse":
        handleMouseInput(evt);
        break;
      case "pen":
        handlePenInput(evt);
        break;
      case "touch":
        handleTouchInput(evt);
        break;
      default:
        console.log("pointerType could not be detected");
    }
  });
}

We can further refine the touch event handler by using multi-touch interactions as explained in MDN’s Multi-touch interaction

Categories
Uncategorized

Creating a color library

The idea is to use Dudley Storey’s The new defaults to automate adding the colors on the list as a set of CSS Custom Properties using both Javascript and CSS properties.

We will use Node and its experimental ESM module support. At its most basic, the script consists of three sections.

The first section imports the fs methods from the fs module.

Note:

It is important to note that, in Node 12.x.x, the ESM module is still experimental so you need to run node with the experimental-modules and you will get a warning whenever you run the code.
This warning does not show up when you run your code in Node 14.x.x

import * as fs from 'fs'

const new_defaults = [
  // whites
  ['white', '#fffefc'],
  ['pearl', '#fbfcf7'],
  ['alabaster', '#fefaf0'],
  ['snow', '#f4fefd'],
  ['ivory', '#fef7e5'],
  ['cream', '#fffbda'],
  ['eggshell', '#fef9e3'],
  // Array cut down for readability
];

Once we have the colors that we want to work with we’ll create three functions to address different types of conversion to custom properties.

The first one will create the current version of CSS custom properties.

It’s a 4 step process

  1. Create the Writeable Stream
  2. Write the opening of the CSS rule
  3. Loop through the newDefaults array and use the values to build a CSS custom property
  4. Write the closing of the CSS rule
  5. We call the function to execute the code.
export function generateCustomProperty() {
  const writer = fs.createWriteStream('new-default-props.css'); // 1

  writer.write(':root { \n') // 2
  newDefaults.forEach((color) => writer.write(`\t--color-${color[0]}: ${color[1]};\n`)) // 3
  writer.write('}\n'); // 4

}
generateCustomProperty(); // 5

The second function will generate @property style declarations for the color custom properties.

generateCSSProperty is similar to generateCustomProperty but it uses a different way to declare the properties using Houdini APIs.

export function generateCSSProperty() {
  const writer = fs.createWriteStream('new-defaults.css');

  writer.write(':root { \n')
  newDefaults.forEach((color) => writer.write(`@property --color-${color[0]} {
    syntax: "<color>";
    initialValue: "${color[1]}";
    inherits: true;\n}\n\n`))
  writer.write('}\n');
  writer.end();
}

generateCSSProperty();

The final function generates JavaScript-based CSS.registerProperty declarations for the list of New Default colors. The syntax is almost identical to CSS Property declarations discussed earlier in the post.

export function generateJSProps() {
  const writer = fs.createWriteStream('new-defaults.js');

  newDefaults.forEach((color) =>
  writer.write(`window.CSS.registerProperty({
    name: '--color-${color[0]}',
    syntax: '</color><color>',
    inherits: true,
    initialValue: '${color[1]}',
  });\n\n`))

  writer.end();
}

Conclusion

So which one to use? As with many things on the web stack it depends on what browsers you need to support and whether you’re writing styles on CSS or Javascript.

I’m partial to CSS @property declarations but they are just now being implemented in Chromium-based browsers so it’ll be a while before they are available on stable channels.

Another thing to consider is how does CSS in JS handle custom properties. I am not familiar enough with those tools to tell.

Categories
Uncategorized

@property rule

CSS Properties and Values API Level 1 provides a CSS equivalent solution to CSS.registerProperty, the @property at-rule.

Unfortunately, it is not implemented by any browser yet.

The reason why I’m writing about it now is that Chrome filed an Intent to Ship the feature.

What problem are Houdini Custom Properties trying to solve?

Custom properties are treated as strings, so developers have to jump through hoops to make them work and have to do things that are not necessarily intuitive to create the effects they need.

:root {
  --base-font-size: 16;
  --base-text-color: '#aaa';
}

body {
  font-size: calc(var(--base-font-size) * 1px);
  color: var(--base-text-color);
}

Another thing to consider is that, because all custom properties are treated as strings they not animatable, and cannot be validated since the validator has no way of knowing what is the actual value of the property it’s parsing.

Houdini Custom Properties

Houdini makes it easier to deal with the drawbacks of custom properties and provides, in my opinion, a better developer experience.

The first version of Houdini Custom Properties is written in JavaScript and it provides additional information about the properties that make them easier to work with.

The items that we must have on each custom property declaration:

  • name: the name of the custom property, including the two dashes (–)
  • syntax: one or more of the valid syntax names available
    • An optional multiplier (either +, #) immediately after the syntax name
    • The separator character (|) between the values. This is only required if there is more than one value on the syntax
  • initial value: The default value for the property. This includes the type of value we’re using
  • inherits: Whether the property will propagate its value down the tree

The first example will be used for font sizing and can take either a length or percentage value using the <length-percentage> shorthand syntax. The default value is 16px and it will inherit down the tree unless the CSS author overrides it.

CSS.registerProperty({
  name: "--base-font-size",
  syntax: "<length-percentage>",
  initialValue: "16px",
  inherits: true
});

The second example is a color custom property that uses a three-digit RGB color as the default value but can use any color syntax allowed in CSS Color Module Level 3 and CSS Color Module Level 4 (currently a working draft). This property will not inherit down the tree.

CSS.registerProperty({
  name: "--my-color",
  syntax: "<color>",
  initialValue: "#333",
  inherits: false
});

Houdini Custom Properties in CSS

One of the main reasons why I’ve refrained from using Houdini Custom Properties is that, until now, they must be defined in Javascript and they add a script, either inline or external, to each page of the site you use them on.

The new CSS @property at-rule takes the same values as the JavaScript version and needs to be placed in one stylesheet linked to the pages.

The equivalent CSS @property version of the JavaScript examples are shown below.

@property --base-font-size {
  syntax: "<length-percentage>";
  initialValue: "16px";
  inherits: true;
}

@property --base-text-color {
  syntax: "<color>";
  initialValue: "#333";
  inherits: false;
}

The advantages of using Houdini-style custom properties are that we can animate and validate them without having to jump through the hoops we have to today.

The downside is that we have to write more code (either CSS or JavaScript) and still provide a fallback for browsers that don’t support custom properties or Houdini APIs yet.

Categories
Uncategorized

Gutenberg as design systems (part 3)

Blocks are awesome and provide a graphical way to create content but sharing them is not as intuitive as I would like to be, at least not yet.

This post will discuss how to use plugins to share both custom blocks, variations, and combined blocks and variations.

Custom Blocks

In the plugin’s index.php we create a block category where we can place all our blocks.

The comment at the top of the page contains the information that will appear on the plugins page.

The first functions rivendellweb_blocks_block_category defines a callback function that creates a category for the blocks we create. We then use the block_categories filter with the rivendellweb_blocks_block_category callback function.

Next, we include the blocks that we have created using require example-0x/index.php where the x represents a different directory containing a custom block.

<?php

/**
 * Plugin Name: Rivendellweb Blocks
 * Plugin URI: https://github.com/WordPress/rivendellweb-blocks
 * Description: Rivendellweb blocks collection.
 * Version: 0.0.1
 * Author: Carlos Araya
 *
 * @package rivendellweb-blocks
 */

if ( ! defined( 'ABSPATH' ) ) {
  exit;
}

function rivendellweb_blocks_block_category( $categories, $post ) {
  if ( $post->post_type !== 'post' ) {
      return $categories;
  }
  return array_merge(
    $categories,
    array(
      array(
        'slug' => 'rivendellweb-blocks',
        'title' => __( 'Rivendellweb Blocks', 'rivendellweb-blocks' ),
        'icon'  => 'wordpress',
      ),
    )
  );
}
add_filter( 'block_categories', 'rivendellweb_blocks_block_category', 10, 2);


include 'example-01/index.php';
include 'example-02/index.php';
include 'example-03/index.php';
include 'example-04/index.php';
include 'example-05/index.php';
include 'example-06/index.php';
include 'example-07/index.php';

If the blocks are valid and the individual build processes succeeded, then the plugin will add seven blocks to Gutenberg running on the server.

The full example is on GitHub at rivendellweb-blocks and has been tested with Gutenberg 7.9.1 and with Github Gutenberg.

Block Variations

As discussed earlier, variations allow you to change the look of a block without having to code a brand new version. The plugin that holds these variations has three files:

  • A PHP file that makes the package into a plugin and links to scripts and styles
  • A Javascript files with the variation definitions
  • A CSS files containing the variations’ styles

The plugin’s index.php has the plugin boilerplate comment and two action + callback instances: one to enqueue the scrip and one to enqueue the stylesheet.

<?php

/**
 * Plugin Name: Rivendellweb variations
 * Plugin URI: https://github.com/WordPress/rivendellweb-variations
 * Description: Rivendellweb blocks variations.
 * Version: 0.0.1
 * Author: Carlos Araya
 *
 * @package rivendellweb-blocks
 */

if ( ! defined( 'ABSPATH' ) ) {
  exit;
}

function rivendellweb_enqueue_variations() {
    wp_enqueue_script(
        'rivendellweb-script',
        plugins_url( './src/block-variations.js', __FILE__ ),
        array( 'wp-blocks', 'wp-dom-ready', 'wp-edit-post' ),
        filemtime( plugin_dir_path( __FILE__ ) . 'build/block-variations.js' )
    );
}
add_action( 'enqueue_block_editor_assets', 'rivendellweb_enqueue_variations' );

function rivendellweb_variation_styles() {
    wp_enqueue_style(
        'rivendellweb_variations_css',
        plugins_url( './src/block-variations.css', __FILE__ ) );
}
add_action( 'enqueue_block_assets', 'rivendellweb_variation_styles' );

The full example is on GitHub at rivendellweb-variations and has been tested with Gutenberg 7.9.1 and with Github Gutenberg.

Both Together

Whether to combine the two plugins discussed in this post into a single plugin is the reader’s choice. I chose not to because I believe that each plugin should do one thing only and do it well.