Tag: Building

Building an Images Gallery using PixiJS and WebGL

Sometimes, we have to go a little further than HTML, CSS and JavaScript to create the UI we need, and instead use other resources, like SVG, WebGL, canvas and others.

For example, the most amazing effects can be created with WebGL, because it’s a JavaScript API designed to render interactive 2D and 3D graphics within any compatible web browser, allowing GPU-accelerated image processing.

That said, working with WebGL can be very complex. As such, there’s a variety of libraries that to make it relatively easier, such as PixiJSThree.js, and Babylon.js, among others.We’re going to work with a specific one of those, PixiJS, to create a gallery of random images inspired by this fragment of a Dribbble shot by Zhenya Rynzhuk.

This looks hard, but you actually don’t need to have advanced knowledge of WebGL or even PixiJS to follow along, though some basic knowledge of Javascript (ES6) will come in handy. You might even want to start by getting familiar with  the basic concept of fragment shaders used in WebGL, with The Book of Shaders as a good starting point.

With that, let’s dig into using PixiJS to create this WebGL effect!

Initial setup

Here’s what we’ll need to get started:

  1. Add the PixiJS library as a script in the HTML.
  2. Have a <canvas> element (or add it dynamically from Javascript), to render the application.
  3. Initialize the application with new PIXI.Application(options).

See, nothing too crazy yet. Here’s the JavaScript we can use as a boilerplate:

// Get canvas view const view = document.querySelector('.view') let width, height, app  // Set dimensions function initDimensions () {   width = window.innerWidth   height = window.innerHeight }  // Init the PixiJS Application function initApp () {   // Create a PixiJS Application, using the view (canvas) provided   app = new PIXI.Application({ view })   // Resizes renderer view in CSS pixels to allow for resolutions other than 1   app.renderer.autoDensity = true   // Resize the view to match viewport dimensions   app.renderer.resize(width, height) }  // Init everything function init () {   initDimensions()   initApp() }  // Initial call init()

When executing this code, the only thing that we will see is a black screen as well as a message like this in the if we open up the console:
PixiJS 5.0.2 - WebGL 2 - http://www.pixijs.com/.

We are ready to start drawing on the canvas using PixiJS and WebGL!

Creating the grid background with a WebGL Shader

Next we will create a background that contains a grid, which will allow us to clearly visualize the distortion effect we’re after. But first, we must know what a shader is and how it works.I recommended The Book of Shaders earlier as a starting point to learn about them and this is those concepts will come into play. If you have not done it yet, I strongly recommend that you review that material, and only then continue here.

We are going to create a fragment shader that prints a grid background on the screen:

// It is required to set the float precision for fragment shaders in OpenGL ES // More info here: https://stackoverflow.com/a/28540641/4908989 #ifdef GL_ES precision mediump float; #endif  // This function returns 1 if `coord` correspond to a grid line, 0 otherwise float isGridLine (vec2 coord) {   vec2 pixelsPerGrid = vec2(50.0, 50.0);   vec2 gridCoords = fract(coord / pixelsPerGrid);   vec2 gridPixelCoords = gridCoords * pixelsPerGrid;   vec2 gridLine = step(gridPixelCoords, vec2(1.0));   float isGridLine = max(gridLine.x, gridLine.y);   return isGridLine; }  // Main function void main () {   // Coordinates for the current pixel   vec2 coord = gl_FragCoord.xy;   // Set `color` to black   vec3 color = vec3(0.0);   // If it is a grid line, change blue channel to 0.3   color.b = isGridLine(coord) * 0.3;   // Assing the final rgba color to `gl_FragColor`   gl_FragColor = vec4(color, 1.0); }

This code is drawn from a demo on Shadertoy,  which is a great source of inspiration and resources for shaders.

In order to use this shader, we must first load the code from the file it is in and — only after it has been loaded correctly— we will initialize the app.

// Loaded resources will be here const resources = PIXI.Loader.shared.resources  // Load resources, then init the app PIXI.Loader.shared.add([   'shaders/backgroundFragment.glsl' ]).load(init)

Now, for our shader to work where we can see the result, we will add a new element (an empty Sprite) to the stage, which we will use to define a filter. This is the way PixiJS lets us execute custom shaders like the one we just created.

// Init the gridded background function initBackground () {   // Create a new empty Sprite and define its size   background = new PIXI.Sprite()   background.width = width   background.height = height   // Get the code for the fragment shader from the loaded resources   const backgroundFragmentShader = resources['shaders/backgroundFragment.glsl'].data   // Create a new Filter using the fragment shader   // We don't need a custom vertex shader, so we set it as `undefined`   const backgroundFilter = new PIXI.Filter(undefined, backgroundFragmentShader)   // Assign the filter to the background Sprite   background.filters = [backgroundFilter]   // Add the background to the stage   app.stage.addChild(background) }

And now we see the gridded background with blue lines. Look closely because the lines are a little faint against the dark background color.

The distortion effect 

Our background is now ready, so let’s see how we can add the desired effect (Cubic Lens Distortion) to the whole stage, including the background and any other element that we add later, like images. For this, need to create a new filter and add it to the stage. Yes, we can also define filters that affect the entire stage of PixiJS!

This time, we have based the code of our shader on this awesome Shadertoy demo that implements the distortion effectusing different configurable parameters.

#ifdef GL_ES precision mediump float; #endif  // Uniforms from Javascript uniform vec2 uResolution; uniform float uPointerDown;  // The texture is defined by PixiJS varying vec2 vTextureCoord; uniform sampler2D uSampler;  // Function used to get the distortion effect vec2 computeUV (vec2 uv, float k, float kcube) {   vec2 t = uv - 0.5;   float r2 = t.x * t.x + t.y * t.y;   float f = 0.0;   if (kcube == 0.0) {     f = 1.0 + r2 * k;   } else {     f = 1.0 + r2 * (k + kcube * sqrt(r2));   }   vec2 nUv = f * t + 0.5;   nUv.y = 1.0 - nUv.y;   return nUv; }  void main () {   // Normalized coordinates   vec2 uv = gl_FragCoord.xy / uResolution.xy;    // Settings for the effect   // Multiplied by `uPointerDown`, a value between 0 and 1   float k = -1.0 * uPointerDown;   float kcube = 0.5 * uPointerDown;   float offset = 0.02 * uPointerDown;      // Get each channel's color using the texture provided by PixiJS   // and the `computeUV` function   float red = texture2D(uSampler, computeUV(uv, k + offset, kcube)).r;   float green = texture2D(uSampler, computeUV(uv, k, kcube)).g;   float blue = texture2D(uSampler, computeUV(uv, k - offset, kcube)).b;      // Assing the final rgba color to `gl_FragColor`   gl_FragColor = vec4(red, green, blue, 1.0); }

We are using two uniforms this time. Uniforms are variables that we pass to the shader via JavaScript:

  • uResolution: This is a JavaScript object thaincludes {x: width, y: height}. This uniform allows us to normalize the coordinates of each pixel in the range [0, 1].
  • uPointerDown: This is a float in the range [0, 1], which allows us to animate the distortion effect, increasing its intensity proportionally.

Let’s see the code that we have to add to our JavaScript to see the distortion effect caused by our new shader:

// Target for pointer. If down, value is 1, else value is 0 // Here we set it to 1 to see the effect, but initially it will be 0 let pointerDownTarget = 1 let uniforms  // Set initial values for uniforms function initUniforms () {   uniforms = {     uResolution: new PIXI.Point(width, height),     uPointerDown: pointerDownTarget   } }  // Set the distortion filter for the entire stage const stageFragmentShader = resources['shaders/stageFragment.glsl'].data const stageFilter = new PIXI.Filter(undefined, stageFragmentShader, uniforms) app.stage.filters = [stageFilter]

We can already enjoy our distortion effect!

This effect is static at the moment, so it’s not terribly fun just yet. Next, we’ll see how we can make the effect dynamically respond to pointer events.

Listening to pointer events

PixiJS makes it’s surprisingly simple to listen to events, even multiple events that respond equally to mouse and touch interactions. In this case, we want our animation to work just as well on desktop as on a mobile device, so we must listen to the events corresponding to both platforms.

PixiJs provides an interactive attribute that lets us do just that. We apply it to an element and start listening to events with an API similar to jQuery:

// Start listening events function initEvents () {   // Make stage interactive, so it can listen to events   app.stage.interactive = true    // Pointer & touch events are normalized into   // the `pointer*` events for handling different events   app.stage     .on('pointerdown', onPointerDown)     .on('pointerup', onPointerUp)     .on('pointerupoutside', onPointerUp)     .on('pointermove', onPointerMove) }

From here, we will start using a third uniform (uPointerDiff), which will allow us to explore the image gallery using drag and drop. Its value will be equal to the translation of the scene as we explore the gallery. Below is the code corresponding to each of the event handling functions:

// On pointer down, save coordinates and set pointerDownTarget function onPointerDown (e) {   console.log('down')   const { x, y } = e.data.global   pointerDownTarget = 1   pointerStart.set(x, y)   pointerDiffStart = uniforms.uPointerDiff.clone() }  // On pointer up, set pointerDownTarget function onPointerUp () {   console.log('up')   pointerDownTarget = 0 }  // On pointer move, calculate coordinates diff function onPointerMove (e) {   const { x, y } = e.data.global   if (pointerDownTarget) {     console.log('dragging')     diffX = pointerDiffStart.x + (x - pointerStart.x)     diffY = pointerDiffStart.y + (y - pointerStart.y)   } }

We still will not see any animation if we look at our work, but we can start to see how the messages that we have defined in each event handler function are correctly printed in the console.

Let’s now turn to implementing our animations!

Animating the distortion effect and the drag and drop functionality

The first thing we need to start an animation with PixiJS (or any canvas-based animation) is an animation loop. It usually consists of a function that is called continuously, using requestAnimationFrame, which in each call renders the graphics on the canvas element, thus producing the desired animation.

We can implement our own animation loop in PixiJS, or we can use the utilities included in the library. In this case, we will use the add method of app.ticker, which allows us to pass a function that will be executed in each frame. At the end of the init function we will add this:

// Animation loop // Code here will be executed on every animation frame app.ticker.add(() => {   // Multiply the values by a coefficient to get a smooth animation   uniforms.uPointerDown += (pointerDownTarget - uniforms.uPointerDown) * 0.075   uniforms.uPointerDiff.x += (diffX - uniforms.uPointerDiff.x) * 0.2   uniforms.uPointerDiff.y += (diffY - uniforms.uPointerDiff.y) * 0.2 })

Meanwhile, in the Filter constructor for the background, we will pass the uniforms in the stage filter. This allows us to simulate the translation effect of the background with this tiny modification in the corresponding shader:

uniform vec2 uPointerDiff;  void main () {   // Coordinates minus the `uPointerDiff` value   vec2 coord = gl_FragCoord.xy - uPointerDiff;    // ... more code here ... }

And now we can see the distortion effect in action, including the drag and drop functionality for the gridd background. Play with it!

Randomly generate a masonry grid layout

To make our UI more interesting, we can randomly generate the sizing and dimensions of the grid cells. That is, each image can have different dimensions, creating a kind of masonry layout.

Let’s use Unsplash Source, which will allow us to get random images from Unsplash and define the dimensions we want. This will facilitate the task of creating a random masonry layout, since the images can have any dimension that we want, and therefore, generate the layout beforehand.

To achieve this, we will use an algorithm that executes the following steps:

  1. We will start with a list of rectangles.
  2. We will select the first rectangle in the list divide it into two rectangles with random dimensions, as long as both rectangles have dimensions equal to or greater than the minimum established limit. We’ll add a check to make sure it’s possible and, if it is, add both resulting rectangles to the list.
  3. If the list is empty, we will finish executing. If not, we’ll go back to step two.

I think you’ll get a much better understanding of how the algorithm works in this next demo. Use the buttons to see how it runs: Next  will execute step two, All will execute the entire algorithm, and Reset will reset to step one.

Drawing solid rectangles

Now that we can properly generate our random grid layout, we will use the list of rectangles generated by the algorithm to draw solid rectangles in our PixiJS application. That way, we can see if it works and make adjustments before adding the images using the Unsplash Source API.

To draw those rectangles, we will generate a random grid layout that is five times bigger than the viewport and position it in the center of the stage. That allows us to move with some freedom to any direction in the gallery.

// Variables and settings for grid const gridSize = 50 const gridMin = 3 let gridColumnsCount, gridRowsCount, gridColumns, gridRows, grid let widthRest, heightRest, centerX, centerY, rects  // Initialize the random grid layout function initGrid () {   // Getting columns   gridColumnsCount = Math.ceil(width / gridSize)   // Getting rows   gridRowsCount = Math.ceil(height / gridSize)   // Make the grid 5 times bigger than viewport   gridColumns = gridColumnsCount * 5   gridRows = gridRowsCount * 5   // Create a new Grid instance with our settings   grid = new Grid(gridSize, gridColumns, gridRows, gridMin)   // Calculate the center position for the grid in the viewport   widthRest = Math.ceil(gridColumnsCount * gridSize - width)   heightRest = Math.ceil(gridRowsCount * gridSize - height)   centerX = (gridColumns * gridSize / 2) - (gridColumnsCount * gridSize / 2)   centerY = (gridRows * gridSize / 2) - (gridRowsCount * gridSize / 2)   // Generate the list of rects   rects = grid.generateRects() }

So far, we have generated the list of rectangles. To add them to the stage, it is convenient to create a container, since then we can add the images to the same container and facilitate the movement when we drag the gallery.

Creating a container in PixiJS is like this:

let container  // Initialize a Container element for solid rectangles and images function initContainer () {   container = new PIXI.Container()   app.stage.addChild(container) }

Now we can now add the rectangles to the container so they can be displayed on the screen.

// Padding for rects and images const imagePadding = 20  // Add solid rectangles and images // So far, we will only add rectangles function initRectsAndImages () {   // Create a new Graphics element to draw solid rectangles   const graphics = new PIXI.Graphics()   // Select the color for rectangles   graphics.beginFill(0xAA22CC)   // Loop over each rect in the list   rects.forEach(rect => {     // Draw the rectangle     graphics.drawRect(       rect.x * gridSize,       rect.y * gridSize,       rect.w * gridSize - imagePadding,       rect.h * gridSize - imagePadding     )   })   // Ends the fill action   graphics.endFill()   // Add the graphics (with all drawn rects) to the container   container.addChild(graphics) }

Note that we have added to the calculations a padding (imagePadding) for each rectangle. In this way the images will have some space among them.

Finally, in the animation loop, we need to add the following code to properly define the position for the container:

// Set position for the container container.x = uniforms.uPointerDiff.x - centerX container.y = uniforms.uPointerDiff.y - centerY

And now we get the following result:

But there are still some details to fix, like defining limits for the drag and drop feature. Let’s add this to the onPointerMove event handler, where we effectively check the limits according to the size of the grid we have calculated:

diffX = diffX > 0 ? Math.min(diffX, centerX + imagePadding) : Math.max(diffX, -(centerX + widthRest)) diffY = diffY > 0 ? Math.min(diffY, centerY + imagePadding) : Math.max(diffY, -(centerY + heightRest))

Another small detail that makes things more refined is to add an offset to the grid background. That keeps the blue grid lines in tact. We just have to add the desired offset (imagePadding / 2 in our case) to the background shader this way:

// Coordinates minus the `uPointerDiff` value, and plus an offset vec2 coord = gl_FragCoord.xy - uPointerDiff + vec2(10.0);

And we will get the final design for our random grid layout:

Adding images from Unsplash Source

We have our layout ready, so we are all set to add images to it. To add an image in PixiJS, we need a Sprite, which defines the image as a Texture of it. There are multiple ways of doing this. In our case, we will first create an empty Sprite for each image and, only when the Sprite is inside the viewport, we will load the image, create the Texture and add it to the Sprite. Sound like a lot? We’ll go through it step-by-step.

To create the empty sprites, we will modify the initRectsAndImages function. Please pay attention to the comments for a better understanding:

// For the list of images let images = []  // Add solid rectangles and images function initRectsAndImages () {   // Create a new Graphics element to draw solid rectangles   const graphics = new PIXI.Graphics()   // Select the color for rectangles   graphics.beginFill(0x000000)   // Loop over each rect in the list   rects.forEach(rect => {     // Create a new Sprite element for each image     const image = new PIXI.Sprite()     // Set image's position and size     image.x = rect.x * gridSize     image.y = rect.y * gridSize     image.width = rect.w * gridSize - imagePadding     image.height = rect.h * gridSize - imagePadding     // Set it's alpha to 0, so it is not visible initially     image.alpha = 0     // Add image to the list     images.push(image)     // Draw the rectangle     graphics.drawRect(image.x, image.y, image.width, image.height)   })   // Ends the fill action   graphics.endFill()   // Add the graphics (with all drawn rects) to the container   container.addChild(graphics)   // Add all image's Sprites to the container   images.forEach(image => {     container.addChild(image)   }) }

So far, we only have empty sprites. Next, we will create a function that’s responsible for downloading an image and assigning it as Texture to the corresponding Sprite. This function will only be called if the Sprite is inside the viewport so that the image only downloads when necessary.

On the other hand, if the gallery is dragged and a Sprite is no longer inside the viewport during the course of the download, that request may be aborted, since we are going to use an AbortController (more on this on MDN). In this way, we will cancel the unnecessary requests as we drag the gallery, giving priority to the requests corresponding to the sprites that are inside the viewport at every moment.

Let’s see the code to land the ideas a little better:

// To store image's URL and avoid duplicates let imagesUrls = {}  // Load texture for an image, giving its index function loadTextureForImage (index) {   // Get image Sprite   const image = images[index]   // Set the url to get a random image from Unsplash Source, given image dimensions   const url = `https://source.unsplash.com/random/$ {image.width}x$ {image.height}`   // Get the corresponding rect, to store more data needed (it is a normal Object)   const rect = rects[index]   // Create a new AbortController, to abort fetch if needed   const { signal } = rect.controller = new AbortController()   // Fetch the image   fetch(url, { signal }).then(response => {     // Get image URL, and if it was downloaded before, load another image     // Otherwise, save image URL and set the texture     const id = response.url.split('?')[0]     if (imagesUrls[id]) {       loadTextureForImage(index)     } else {       imagesUrls[id] = true       image.texture = PIXI.Texture.from(response.url)       rect.loaded = true     }   }).catch(() => {     // Catch errors silently, for not showing the following error message if it is aborted:     // AbortError: The operation was aborted.   }) }

Now we need to call the loadTextureForImage function for each image whose corresponding Sprite is intersecting with the viewport. In addition, we will cancel the fetch requests that are no longer needed, and we will add an alpha transition when the rectangles enter or leave the viewport.

// Check if rects intersects with the viewport // and loads corresponding image function checkRectsAndImages () {   // Loop over rects   rects.forEach((rect, index) => {     // Get corresponding image     const image = images[index]     // Check if the rect intersects with the viewport     if (rectIntersectsWithViewport(rect)) {       // If rect just has been discovered       // start loading image       if (!rect.discovered) {         rect.discovered = true         loadTextureForImage(index)       }       // If image is loaded, increase alpha if possible       if (rect.loaded && image.alpha < 1) {         image.alpha += 0.01       }     } else { // The rect is not intersecting       // If the rect was discovered before, but the       // image is not loaded yet, abort the fetch       if (rect.discovered && !rect.loaded) {         rect.discovered = false         rect.controller.abort()       }       // Decrease alpha if possible       if (image.alpha > 0) {         image.alpha -= 0.01       }     }   }) }

And the function that verifies if a rectangle is intersecting with the viewport is the following:

// Check if a rect intersects the viewport function rectIntersectsWithViewport (rect) {   return (     rect.x * gridSize + container.x <= width &&     0 <= (rect.x + rect.w) * gridSize + container.x &&     rect.y * gridSize + container.y <= height &&     0 <= (rect.y + rect.h) * gridSize + container.y   ) }

Last, we have to add the checkRectsAndImages function to the animation loop:

// Animation loop app.ticker.add(() => {   // ... more code here ...    // Check rects and load/cancel images as needded   checkRectsAndImages() })

Our animation is nearly ready!

Handling changes in viewport size

When initializing the application, we resized the renderer so that it occupies the whole viewport, but if the viewport changes its size for any reason (for example, the user rotates their mobile device), we should re-adjust the dimensions and restart the application.

// On resize, reinit the app (clean and init) // But first debounce the calls, so we don't call init too often let resizeTimer function onResize () {   if (resizeTimer) clearTimeout(resizeTimer)   resizeTimer = setTimeout(() => {     clean()     init()   }, 200) } // Listen to resize event window.addEventListener('resize', onResize)

The clean function will clean any residuals of the animation that we were executing before the viewport changed its dimensions:

// Clean the current Application function clean () {   // Stop the current animation   app.ticker.stop()   // Remove event listeners   app.stage     .off('pointerdown', onPointerDown)     .off('pointerup', onPointerUp)     .off('pointerupoutside', onPointerUp)     .off('pointermove', onPointerMove)   // Abort all fetch calls in progress   rects.forEach(rect => {     if (rect.discovered && !rect.loaded) {       rect.controller.abort()     }   }) }

In this way, our application will respond properly to the dimensions of the viewport, no matter how it changes. This gives us the full and final result of our work!

Some final thoughts

Thanks for taking this journey with me! We walked through a lot but we learned a lot of concepts along the way and walked out with a pretty neat piece of UI. You can check the code on GitHub, or play with demos on CodePen.

If you have worked with WebGL before (with or without using other libraries), I hope you saw how nice it is working with PixiJS. It abstracts the complexity associated with the WebGL world in a great way, allowing us to focus on what we want to do rather than the technical details to make it work.

Bottom line is that PixiJS brings the world of WebGL closer for front-end developers to grasp, opening up a lot of possibilities beyond HTML, CSS and JavaScript.

The post Building an Images Gallery using PixiJS and WebGL appeared first on CSS-Tricks.

CSS-Tricks

, , , , ,

Building the Web We Want

On the Microsoft Edge team, we’re committed to an open web and helping to drive innovation forward, which is why we’ve kicked off a new initiative in collaboration with Google, Mozilla, Samsung Internet, Igalia and — most importantly — the web community, called The Web We Want.

The Web We Want is an open initiative for web developers and designers (or anyone who builds things for the web) to tell browser vendors what we should focus on building or fixing. The overarching question we’re asking is this: If you had a magic wand and could change anything about the web platform, what would it be? What are problems you’ve encountered with the web that you need to hack around or what tools need to be improved or built to help you do your job better? What’s something you think you should be able to do natively but can’t? These are just a few questions to get you thinking about submissions to the initiative.

Some of the submissions we’ve had so far span from specific feature requests to broader problems with the web platform:

This is just a snapshot of the feedback we’re getting. The whole list is available for you to browse.

Why should I submit to the Web We Want? 

This is an opportunity to make your voice heard and help us determine where the future of the web platform is headed. Once you’ve submitted your problem or feature, we’ll determine if your want is something that browser vendors can tackle directly or if it’s something that needs more scoping and is suited for Web Standards discussions.

This is an opportunity for all of us to take a step back and reassess where the future of the web is heading and figure out where the gaps are that make building for the web difficult today.

How can I participate?

There are a few different ways you can participate and there are a few different components to the Web We Want. First, think about what you think browser vendors should go fix and submit your ideas at webwewant.fyi. And that could be all that you want to do, which is great! We want any and all feedback about the platform and we have folks from different browsers constantly watching what’s being submitted. 

There’s a second, optional aspect to the Web We Want, which is a great way to get involved in the web community and dip your toes in the public speaking pool. We’ve been running the Web We Want as a community-focused panel session at conferences around the world. 

If you submit and are attending one of the events we’ll be at, your submission could be picked to be presented in a 3-5 minute lightning pitch to a panel of judges during the session, like an episode of Shark Tank but for tech. The live sessions we run are a way to get feedback and opinions from industry experts like Rachel AndrewJen SimmonsMiriam Suzanne, and Marcy Sutton.

Even if you’re not attending one of the events in person, you can still participate! We want to be mindful that not everyone can attend conferences so if you your submission is selected and you indicate you’re not attending an event,you’ll have an opportunity to record your lightning talk ahead of time and we’ll include it in the live session. 

The live session culminates with the judges deliberating and deciding on the most pressing “want” pitched during the session. We also have a community voting aspect that allows the event audience to rank what they think is the most pressing thing for browser vendors (or standards bodies) to work on. Even if you’re not attending an event, you can still vote for your favorite “wants” on the website as well as by sharing them on social media. 

So far we’ve run sessions at An Event Apart DC, Smashing Conf, and View Source, and the community participation and panel discussion of wants has been fantastic. If you run a meetup or a conference and are interested in facilitating this session, reach out to Stephanie Stimac or Aaron Gustafson

The Web We Want

We’re at a point in the web platform where browser vendors and standards bodies are eager to know what it is web developers and designers need in the platform. We need to know where to invest resources. Hop over to the Web We Want and let us know what you think the web platform needs so that we can shape the future of the web together. 

The post Building the Web We Want appeared first on CSS-Tricks.

CSS-Tricks

,
[Top]

Building an accessible autocomplete control

Here’s a great in-depth post from Adam Silver about his journey to create an autocomplete field that’s as accessible as possible. There are so many edge cases to consider! There are old browsers and their peculiar quirks, there are accessibility best practices for screen readers, and not to mention dealing with the component design when there’s no JavaScript, etc.

Adam offers a warning before he begins:

[…] I’ve been looking at ways to let users enter a destination country. Unfortunately, native HTML form controls just aren’t good enough for this type of interaction. And so we need to build a custom autocomplete control from scratch. A word of warning though: this is one of the hardest UI components I’ve ever had to make—they’re just way harder than they look.

I also just bought Adam’s book, Form Design Patterns, and this post now makes me extra excited to read it.

Direct Link to ArticlePermalink

The post Building an accessible autocomplete control appeared first on CSS-Tricks.

CSS-Tricks

, , ,
[Top]

Building Multi-Directional Layouts

There are some new features in CSS that can assist us with building layouts for different directions and languages with ease. This article is about CSS logical properties and values (e.g. margin-inline-start).  These are a W3C working draft that still going under heavy editing, but have shipped in many browsers. I want to talk about this because I’ve been using these properties for a little while and have seen a significant boost to my workflow after switching to them.

I’ll talk about the specifications and how can you use it today in your work. I live in Egypt where we use Arabic as a primary language. Arabic is written from right to left, which means Arabic websites look like a mirror image of an English version. Most of the websites we create are bilingual, which means we provide a stylesheet specific for each direction. We do that by flipping values and properties of almost everything! I will not talk in details about this part but you can talk a quick look about a past article I wrote on the topic. 

It starts with declaring the dir attribute on the HTML tag. 

 <html dir="rtl">

This attribute accepts one of two values: ltr (which is the default value if none is specified) and rtl. According to its value, the browser starts to paint the elements following a specific algorithm. Text will be written with respect to the direction and punctuations will be placed in their correct location. Some elements, like tables, will have their direction switched (for example, a <td> starting from the right in rtl). Thankfully, some new specifications, like CSS Grid, and flexbox follow a similar approach to the table. That means we don’t have to change the order of anything because the browser will take care of it!

HTML5 introduced a new auto value for the dir attribute. It will check for the first character within the element and, if it belongs to a language that is written from left-to-right (like Latin characters), the element will have an ltr direction and vice versa. The W3C urges authors to avoid relying on this value to determine text direction and use a server-side solution instead. 

An interesting use case for the auto value is when you’re unsure about the direction of the content, such user-generated content, like a comment thread. I see a lot of people contributing to discussions in Arabic websites in English. The support for auto is pretty good except, for Internet Explorer and Edge.

Introducing the :dir() pseudo-class

The :dir() pseudo-class is a new selector modifier that selects an element by evaluating its direction value. It works like this:

/* Select all paragraphs that have their computed direction value set to rtl */ p:dir(rtl) {   font-size: 16px; /* Sometimes Arabic glyphs need a size boost to feel right. */ } 
 /* Select all paragraphs that have their computed direction value set to ltr */ p:dir(ltr) {   font-size: 14px; }

The beauty of this selector is that it’s the first one to select elements based on a computed value. Whether the direction of the element was inherited from the HTML dir attribute or set using CSS like html:lang("ar") { direction: rtl; }, the selector will match the element. Even better, if you have the direction of the element set to auto, it will still correctly the element based on its content!

<style>   p {     direction: auto;   }   p:dir(ltr) {     background: green;   }   p:dir(rtl) {     background: red;   } </style> 
 <!-- The following paragraph will have a green background --> <p>This is a paragraph that starts with a latin character</p> <!-- The following paragraph will have a red background --> <p>هذا النص يستخدم حروف عربية</p>

Sadly, the support for :dir() isn’t great and limited only to Firefox.

This browser support data is from Caniuse, which has more detail. A number indicates that browser supports the feature at that version and up.

Desktop

Chrome Opera Firefox IE Edge Safari
No No 17* No No No

Mobile / Tablet

iOS Safari Opera Mobile Opera Mini Android Android Chrome Android Firefox
No No No No No 68

Even if the browser support was great, the selector only allows you to target elements and manually apply certain styles to them. That means that we still should flip the values for everything (like margins, paddings, floats, positions, etc.) which doesn’t really enhance our workflow or reduce the effort to produce multi-directional layouts.

Introducing CSS logical properties and values

As defined by the W3C, logical properties and values provide us with the ability to control layout through logical, rather than physical, direction and dimension mappings. Let’s skip the technical jargon and jump directly to the details. These provide us with new properties and values that will evaluate differently according to certain conditions.

Logical values

Let’s say we have a paragraph that we want to align in a direction that’s opposite to the natural direction of the language. Let’s say this is in English that follows the ltr direction. We would do something like this:

<article>   <p class="opposite">     Lorem ipsum dolor sit amis ..   </p> </article>

And the CSS file would be like this:

.opposite {   text-align: right; }

To provide the opposite for the rtl version, we would override the selector by targeting the <html> tag with the dir attribute, or simply provide a different file for the rtl version, like this:

html[dir="rtl"] .opposite {   text-align : left; }

Logical properties and values were created to solve this problem. Why don’t we use values that evaluates to the correct context instead of using left and right? In an ltr element, the value left means the beginning or the start of the element while on the rtl element, the value right means the start! It’s simple, right?

So instead of what we wrote before, we can use:

.opposite {   text-align: end; }

And that’s it! If the element’s computed direction is ltr, the text would be aligned right; and the computed direction would be opposite for the rtl elements. So, instead of using left and right values for text-align, we can simply replace it with start and end. This is a lot easier and more convenient than our previous options.

Logical properties

What we just looked at were logical values, so let’s turn now to logical properties. Logical properties are new properties that have the same idea; they evaluate differently according to the direction of the element. Take a look at margin as an example. Previously, we wanted to add some space toward the start of the paragraph. We can do so in the ltr document by using:

article img {   margin-left: 15px; }

Now, in the case of the rtl version, we will need to add the margin to the opposite direction in addition to resetting the left value:

html[dir="rtl"] article img {   margin-left: 0;   margin-right: 15px; }

We can do better with logical properties. Consider the following:

article img {   margin-inline-start: 15px; }

The -inline-start part evaluates to the beginning of the horizontal axis of the image. In the case of ltr, that means left, and in the case of rtl, that means right.

The start and end are probably obvious by now, but what is with the word inline and why do we need it? To understand it, we need to talk about something called CSS writing modes. Jen Simmons wrote an excellent article on that topic. I won’t regurgitate everything explained there, but the bottom line is that we can use writing modes to define the writing direction. Some languages, like the Chinese, Korean, and Japanese, can be written vertically from top to bottom. CSS writing modes allow us to control that flow. Take a look at the following paragraph:

You can clearly identify the top, bottom, left and right edges of the block. What will happen if we change the direction of the paragraph using CSS writing modes to flow vertically?

When we talk about the “top” of this rotated paragraph, do we mean the where the content begins or what was the right edge when it wasn’t rotated? Identifying the four directions becomes confusing. Let’s look at them from a different perspective. In “normal” writing conditions, the vertical axis will have the suffix -block and the horizontal axis will have the suffix -inline, both followed by start or end:

And if we rotate it, it should be like this:

Since we are talking about normal horizontal layout, we will be using -inline-start and -inline-end, but it is good to know about the other properties as well. We can also write logical shorthand values by using the logical keyword. Consider the following:

article img {   margin: logical 10px 20px 30px 40px; }

The computed value of the image’s margin will be the following:

article img {   margin-block-start: 10px;   margin-inline-start: 20px;   margin-block-end: 30px;   margin-inline-end: 40px; }

The logical keyword is an experimental feature at this point in time, which means it may change or it may be replaced by the time the spec becomes official. There’s an open discussion on the topic that you can follow in the meantime.

Logical properties also allow us to apply values to a certain axis, meaning we have margin-inline and margin-block for the horizontal and vertical axises, respectively.

Property Logical Property
margin-top margin-block-start
margin-left margin-inline-start
margin-right margin-inline-end
margin-bottom margin-block-end

Just like with the logical margin properties, we also have logical padding properties which follow the same rules as the margin.

Property Logical Property
padding-top padding-block-start
padding-left padding-inline-start
padding-right padding-inline-end
padding-bottom padding-block-end

Logical positioning properties

In the previous examples we were able to modify the meaning of the property by appending suffixes, but what about the positions? The properties names changed completely from what we know now as top, right, bottom, and left.

.element {   position: absolute;   inset-block-start: 0;  /* evaluates to top */   inset-block-end: 0;    /* evaluates to bottom */   inset-inline-start: 0; /* evaluates to left in ltr and right in rtl */   inset-inline-end: 0;   /* evaluates to right in ltr and left in rtl */ }

Learning new properties and values can be hard but, hey, we get a shorthand property called inset to make it a little easier:

/* Shorthand FTW! */ .element {   position: absolute;   inset: logical 10px 20px 30px 40px; } 
 /* It evaluates to this */ .element {   position: absolute;   inset-block-start: 10px;   inset-inline-start: 20px;   inset-block-end: 30px;   inset-inline-end: 40px; }

inset supports both inset-block and inset-inline just like margin and padding.

Property Logical Property
top inset-block-start
left inset-inline-start
right inset-inline-end
bottom inset-block-end

Logical border properties

Border properties can also become logical by appending the -inline-start and -block-start.

Property Logical Property
border-top{-size|style|color} border-block-start{-size|style|color}
border-left{-size|style|color} border-inline-start{-size|style|color}
border-right{-size|style|color} border-inline-end{-size|style|color}
border-bottom{-size|style|color} border-block-end{-size|style|color}

In her deep dive on logical properties and values, Rachel Andrew includes an excellent demo that demonstrates logical border properties and how they respond to different writing modes.

How can we start using all this today?

We can start using all the magic of logical properties and value today, thanks to the power of PostCSS! Jonathan Neal wrote this lovely PostCSS plugin that enables us to write logically and compile the code to something today’s browsers will understand. The plugin works in three stages:

  • It translates the new syntax to existing standards that unsupported browsers will recognize, using the :dir pseudo-class to create output to ltr and rtl.
  • It uses another one of Neal’s plugins to translate :dir to an attribute selector, like this:
 .element:dir(ltr) {    ...  }  [dir="ltr"] .element {    ...  }
  • It uses the postcss-nested plugin to transform nested selectors to one-line selectors, the same way other CSS preprocessors do.

PostCSS works with any workflow. You can try it with Grunt, Gulp, and webpack.


I will close by saying I have seen a lot of benefits since making the shift to logical properties and values. Sure, building multi-directional layouts takes time. There’s the learning curve, the addition of more properties to write, and of course, testing. Our previous methods for creating multi-directional layouts were either taking care of both directions in development or working on one direction at a time — neither of which is all that suitable for big projects. With logical properties and values you write your code once and it works for both directions without any consideration.

References

The post Building Multi-Directional Layouts appeared first on CSS-Tricks.

CSS-Tricks

, ,
[Top]

How Building in the Open Can Change Our Industry

I have to admit, I’m a developer who hasn’t built a website. When I first read Chris’s question, I sat in silence for at least a minute. Which technical topic did I want to discuss? A new library, programming language or best practice? Nothing, in particular, came to mind. Is that because I’m a new developer?

I’ve been coding for about one year now and got my first job a month ago. Even though I’ve been coding for that time, I wouldn’t say I’ve built a website. I’ve contributed to a couple of open-source projects whose output was websites, but I’ve spent a lot of time practicing technical tests in order to get into the industry and now I’m writing Kotlin for the Guardian Newspaper’s Android application.

After a couple minutes thinking about this question, I realized I wanted to write about who gets to build websites and how and where we choose to build them in order to welcome new people. I’ve spent this year giving conference talks on this topic because I have first-hand knowledge of what it’s like to become a developer with little time and money. It’s not easy being on the “outside” trying to get into our industry. How can we make it easier for new people to join us here? How can we welcome under-represented groups to the table? In 2020 you can make a huge difference to our industry by welcoming new developers, especially those from under-represented groups.

It’s been five years since the most well-known tech companies first released diversity reports, revealing that workforces were overwhelmingly white or Asian men. Despite their business successes, though, none of these big tech companies have made much progress in diversifying their workforces.

In 2014, Apple, one of the largest tech companies by revenue, had 20% women in its technical staff. This increased to only 23% in 2018 (Apple). At Google, the share of US technical employees who are black was 2.0% in 2014 and only rose to 2.8% in 2018 (Google). At Facebook in the US, there were 3% Hispanic technical staff in 2014. Last year there were 3.1% (Facebook).

Continuing our homogenous engineering community is a risk. We are less likely to build products best for our diverse user groups. For example, there have been numerous reports of facial recognition systems misidentifying black people. A US Government study found a top-performing system misidentified black people 5-10x more than white people. In addition, “according to a 2011 study by the National Institute of Standards and Technologies (Nist), facial recognition software is actually more accurate on Asian faces when it’s created by firms in Asian countries, suggesting that who makes the software strongly affects how it works” (Guardian 2017).

Thankfully, there are a number of things you can do in 2020 to contribute to a more diverse engineering community. Building websites in the open, in ways that welcome new people, can have a hugely positive impact on our industry and on the websites that we as an engineering industry produce.

First, how can building open source websites help us welcome new people? You can help with this by being a great open-source citizen and upholding best practices by giving positive and constructive code reviews, keeping documentation up to date and useful, offering help to new contributors and remote pairing with them if at all possible. Some amazing developers like Suz Hinton (@noopkat) live stream their open source contributions, which is phenomenal.

GitHub’s research has shown that documentation is highly valued, but frequently overlooked. As a new contributor, I really value clear and useful documentation, especially on installation, raising a PR and where to get help if I get stuck. Mozilla found that the most significant barrier to engaging in on-ramping others is unclear communications and unfriendly community. Using positive language in your documentation can really encourage first-time contributors to your project. For example, by expressly indicating that you welcome new contributors with instructions on how they can get involved.

We want to create an engineering community highly attractive to under-represented groups. One of the ways that we can do this is by supporting new developers to get a foothold into the profession. There are so many ways we can do this!

The first thing we can do with new developers is to help them by pairing and giving code reviews. Open source projects are perfect for this! By giving constructive code reviews you can help newbies level up their coding skills. You can also set up a Slack channel for your repo where you can answer technical questions. I have met superstar open source heroes who spent time pairing with me, which, if available to you, is an amazing way to help new people.

The greatest challenge I faced when teaching myself to code was finding a job. You can give new developers an insight into your company or your day to day working life by writing an article on Medium, posting a Twitter thread, or making a YouTube video. Some developers offer their own office hours which they do on a live stream or some offer a few one-off mentoring calls with new developers.

Who we welcome into the room to build websites with us says something about who we are and our values. In 2020, let’s welcome new people, especially those from under-represented groups to join us. We’ve discussed how amazing open-source projects are for this and how we can practically contribute to supporting new people. Let’s challenge ourselves to support at least one person from an under-represented group trying to get into the engineering industry in 2020. Together we can change who has the privilege to build the web.

The post How Building in the Open Can Change Our Industry appeared first on CSS-Tricks.

CSS-Tricks

, , ,
[Top]

We asked web developers we admire: “What about building websites has you interested this year?”

For the first time ever here on CSS-Tricks, we’re going to do an end-of-year series of posts. Like an Advent calendar riff, only look at us, we’re beating the Advent calendar rush! We’ll be publishing several articles a day from a variety of web developers we look up to, where gave them all the same prompt:

What about building websites has you interested this year?

We’re aiming for a bit of self-reflection and real honesty. As in, not what you think you should care about or hot takes on current trends, but something that has quite literally got you thinking. Our hope is that all put together, the series paints an interesting picture of where we are and where we’re going in the web development industry.

We didn’t ask people for their future predictions directly. Our hope is that the future is glimpsable through seeing what is commanding developer attention. I mention that as this series take some inspiration from NeimanLab’s series that run each year (e.g. 2019, 2018, 2017…) which directly asks for people’s predictions about journalism. Maybe we’ll try that one year!

Automattic has a been a wonderful partner to us for a while now, and so I’m using this series as another way to thank them for that. Automattic are the makers of WordPress.com and big contributors to WordPress itself, which this site runs on, and make premium plugins like WooCommerce and Jetpack, which we also use.

Stay tuned here on the blog for all the wonderful thoughts from developers we’ll be publishing this week (hey even RSS is still cool I heard) or bookmark the homepage for the series.

The post We asked web developers we admire: “What about building websites has you interested this year?” appeared first on CSS-Tricks.

CSS-Tricks

, , , , , , , , ,
[Top]

Let Mavo Shine in Building Interactive Web Applications

As you could guess from the title, this tutorial is dedicated to Mavo: a new, approachable way to create complex, reactive, persistent web applications just by writing HTML and CSS, without a single line of JavaScript and no server backend.

The app we will build together is a flashcard study app for foreign language learners. It is a fully-fledged CRUD application that lets you:

  • Create, delete, update flashcards and re-arrange them via drag and drop.
  • Import and export flashcards.
  • Evaluate how well you’ve learned the word on a flashcard.

Here is what our finished application looks like:

In the tutorial, I will guide you through the entire process of building the app.

At the end of some steps, I provide suggestions for you to experiment with Mavo—to learn a bit more—and to make some enhancements to the application we are building.

Are you ready? Let’s get started! 😀

Static template

See the Pen
01. Static template
by Dmitry Sharabin (@dsharabin)
on CodePen.

In order to illustrate how Mavo enhances standard HTML, we will first create a purely static HTML page and then use Mavo to turn this static HTML into a fully-functional web application.

Assume we have the following HTML code inside <body>:

<header>   <h1>Flashcards</h1> </header>  <main> <article>   <p>Word or phrase</div>   <p>Translation</div> </article> </main>

In that code, the <article> element represents a single flashcard.

Let’s add some styling in order to make our HTML look more like an actual flashcards app. You can take a look at the source code and play with it here.

Getting started with Mavo

Right now, all we have is a static template. It’s time to add functionality, so it can actually work like a flashcard application. Here comes Mavo!

In order to use Mavo, we first need to include its JavaScript and CSS files in our page’s <head> section:

<head>   ...   <script src="https://get.mavo.io/mavo.min.js"></script>   <link rel="stylesheet" href="https://get.mavo.io/mavo.css">   ... </head>

Defining a Mavo app

To enable Mavo functionality on an HTML structure, we must use the mv-app attribute on the element that contains our Mavo app (which may even be the <body> or <html> element, that’s fine!). Its value is an ID for our app that should be unique in the page.

Considering that the <main> element is representing our Mavo app, let’s add the mv-app attribute to it and give our app the ID “flashcards”:

<main mv-app="flashcards">   ... </main>

The property attribute

See the Pen
02. The property attribute
by Dmitry Sharabin (@dsharabin)
on CodePen.

It’s time to tell Mavo what elements of our app are important, i.e., which elements we want to be editable and be saved.

Now we have two such elements, and they are the <p> elements. Let’s add the property attribute to those elements to tell Mavo they contain data. Elements with the property attribute are called properties.

Keep in mind that the value of the property attribute should describe the element, similarly to an id or class attribute:

...    <p property="source">Word or phrase</div>   <p property="translation">Translation</div>  ...

Notice any changes in our app? The Mavo toolbar with an Edit button appeared at the top of the page. The Edit button lets the user switch between read and edit modes. Now our app is in read mode. That means we can’t edit the data on the page.

Now lets us switch to edit mode by clicking the Edit button. What has changed? The text of the Edit button becomes Editing to indicate that we are in edit mode. If you hover over the paragraphs, Mavo communicates that you can click to edit them by highlighting them in yellow. Go ahead! Click the text and edit it. Wow! We can change the content of the page right in place!

💻 Let’s get our hands dirty!

Assume that in addition to the word and its translation, the flashcard should have an example of the word’s usage in a sentence. Enhance the app by adding the appropriate elements to the flashcard.

The mv-multiple attribute

See the Pen
03. The mv-multiple attribute
by Dmitry Sharabin (@dsharabin)
on CodePen.

At this point, we have only one flashcard in our app. That’s not very useful! For a working flashcard application, we need the ability to add, delete, and rearrange flashcards. How can we do that? We could create more flashcards by adding more <article> elements to our code, but then how does an end user create and delete flashcards?

Fortunately, Mavo has something to offer that makes this a breeze: the mv-multiple attribute, which tells Mavo that certain elements can be multiplied. It converts the element it’s used on to an editable collection of items and generates (customizable) UI for adding, deleting, and rearranging items.

Let’s use the mv-multiple attribute in our app to convert our lonely flashcard into a collection of flashcards:

<article property="flashcard" mv-multiple>   ...     </article>

Now switch the app to edit mode. Note that below the flashcard, there is now an Add flashcard button. Let’s give it a try: create a few flashcards with the help of that button. Now we can dynamically add new elements right in the app, even though there are no corresponding elements in the HTML. But that is not all!

Try hovering over a flashcard and notice the three buttons that appear near its top right corner for adding, deleting and rearranging elements via a drag and drop handle. And by hovering over any item bar button, we can understand which flashcard they correspond: Mavo highlights it. Isn’t that amazing?

The mv-storage attribute

See the Pen
04. The mv-storage attribute
by Dmitry Sharabin (@dsharabin)
on CodePen.

Now that we have the basic UI in place, let’s try the following:

  • Switch to edit mode (if you haven’t already done so).
  • Edit the first flashcard’s source word and translation. Add a couple more flashcards too.
  • Switch the app back to read mode.
  • And finally… refresh the page.

What?! Where did our data go? Wasn’t Mavo supposed to save it? What happened?

Actually, we never told Mavo if or where to store our data!

To do so, we need to use the mv-storage attribute. What options do we have? Well, Mavo opens great possibilities for us, and Mavo plugins open up even more!

In our application, we are going to store the data in the browser’s localStorage, which is one of the simplest options available, so it’s good for our first Mavo app. We just need to add the attribute mv-storage with the value local to the element with the mv-app attribute (also called the Mavo root).

<main mv-app="flashcards" mv-storage="local">   ... </main>

Have a look at the Mavo toolbar. Notice something? Another button appeared—the Save button.

Try to edit the app data one more time. Note that the Save button is now highlighted. Hover over the Save button, and Mavo will highlight the properties with the unsaved data. Isn’t that cool?

Click the Save button and refresh the page (there is no need to switch to read mode before refreshing the page). Is your data still there? Great! We are one step closer to our goal—a fully-fledged flashcard application.

The mv-autosave attribute

Now we have to click the Save button every time we need our data to be saved? That may be safer, to prevent destroying valuable data, but it can often be inconvenient. Can we just save the data automatically? Sure! To save the data automatically every time it is changed, we can use the mv-autosave attribute on our Mavo root. Its value is the number of seconds to throttle saving by. Let’s add mv-autosave="3" to the root element of our app:

<main mv-app="flashcard" mv-storage="local" mv-autosave="3">   ... </main>

Change the data one more time and have a look at the Save button. See? In the beginning, it was highlighted but after 3 seconds–it is not. All our data is now saved automatically!

So, now the main part of our app would look like that:

<main mv-app="flashcards" mv-storage="local" mv-autosave="3">   <article property="flashcard" mv-multiple>     <p property="source">Word or phrase</div>     <p property="translation">Translation</div>   </article> </main>
💻 Let’s get our hands dirty!

We are almost done with the alpha version of our app. Now it’s your turn to make the app even better. No worries, you have all the knowledge you need.

Enhance the app so as flashcards could be organized by end users in different groups related to various topics, e.g., the users could gather all the flashcards corresponding to clothing in one group, all the flashcards associated with kitchen utensils in the other one, etc.

💡 Hints!

There are many ways to achieve that goal, and it’s up to you to decide what to follow. However, I’d like you to think about some questions before proceeding:

  1. What HTML element would you use as a grouping element? It would be convenient for the users if they could see the name of the group of flashcards (topic name) and could collapse the group up to the title.
  2. What Mavo attribute(s) are you going to add to that element, if any? Will the element be a property or a collection?
  3. Will end users be able to add new topics, delete and rearrange them, change the title of a topic and move flashcards between different topics?

What if you decide not to organize flashcards in groups, but instead just label them with tags that correspond to various topics? Well, that is perfectly fine. The solution with tags is also appropriate. For the sake of practice, try to accomplish that approach too.

The mv-bar attribute

See the Pen
05. The mv-bar attribute
by Dmitry Sharabin (@dsharabin)
on CodePen.

As our app stores the data locally, by default, the users of the app won’t be able to share their cards with other users. Wouldn’t it be great if we would let them export their flashcards and import somebody else’s flashcards? Thankfully, these features are already implemented in Mavo, and we can very easily add them to our app!

The mv-bar attribute controls which buttons are going to appear in the toolbar, if any. It’s typically specified on the Mavo root (an element with the mv-app attribute). Buttons are represented by their ids (which are very logical): edit, import, export, etc.

As we only want to add a few buttons to the default set, we can use the so-called relative syntax, which allows us to add and remove buttons from the default set without having to explicitly list everything out. All we need is to start the mv-bar attribute’s value with the with keyword.

By doing that, we would get the following:

<main mv-app="flashcards"       mv-storage="local"       mv-autosave="3"       mv-bar="with import export">       ... </main>
💻 Let’s get our hands dirty!

Give those features a try: add some flashcards, try to export them in a file. Then delete existing flashcards and import the flashcards from the previously exported file.

Expressions and MavoScript

See the Pen
06. Expressions and MavoScript
by Dmitry Sharabin (@dsharabin)
on CodePen.

Let’s now add some statistics to our app, such as the number of flashcards! Sounds interesting? I hoped so. 😀

To do that, we need to learn something new about Mavo.

We can dynamically refer to the value of any property we have defined, anywhere in our Mavo app (including in HTML attributes), by putting its name inside brackets, like this: [propertyName]. This is an example of a simple expression, which allows us to dynamically calculate things, reactively as they change.

Now let’s experiment and add a [source] expression inside the flashcard property, e.g., between two properties: the source and the translation.

...   <p property="source">Word or phrase</div>   [source]   <p property="translation">Translation</div> ...

What has changed in our app? The value of the flashcard source property is now shown on the page twice.

Switch to edit mode and try to change the value of the source property. Can you see that? The page content updates while you are changing the property value! That’s why I said earlier that Mavo lets us develop reactive web applications.

That’s indeed cool, but unfortunately, in our case, it’s not really helpful: we can’t use this expression to count the number of flashcards—we would always have only one value.

What if we put the [source] expression outside the flashcard property? We will have something like that:

...   [source]   <article property="flashcard" mv-multiple>     ...   </article> ...

How does this differ from the previous case? To see the difference add some flashcards if you haven’t done so yet. Now instead of one value we have a list of comma separated values: the source property of all flashcards. That’s exactly we were looking for: the number of items in the list corresponds the number of flashcards in the app.

Makes sense? Well, yes, but it wouldn’t it be more logical if we would count the number of flashcards, not the number of values of its source property? After all, a flashcard added exists even before we fill in its source or translation. I suggest you do the following: let’s substitute the [source] expression with [flashcard]:

...   [flashcard]   <article property="flashcard" mv-multiple>     ...   </article> ...

Noticed the difference? We still have a list, but its values are not simple values but objects, i.e., complex values containing all data that pertains to each flashcard. The good news is that the number of these objects is equal to the number of flashcards, since there is one for each flashcard, even when it’s completely empty. So, right now we have an object for each flashcard, but how do we count them and display the total count?

Now let’s get familiar with the MavoScript functions and find the one that would let us count the number of flashcards. Remember, we have a list of flashcards, so we need to find a function that would let us count the number of items in a list. And here it is—the count() function does exactly that!

But how can we use functions in expressions? Are there any rules we need to be aware of? Well, yes, there is a couple:

  1. Expressions are denoted by brackets.
  2. Do not nest brackets.

Let’s try using the count() function to count the number of flashcards:

... [count(flashcard)] <article property="flashcard" mv-multiple>   ... </article> ...

And that’s exactly what we were aiming for—now we have some statistics in our app! Isn’t that cool?

💻 Let’s get our hands dirty!

I hope you’ve already warmed up and ready to continue experimenting with Mavo.

Improve the application so that the statistics are displayed not only for the total number of flashcards in the app but also for the number of flashcards in each topic separately if there are any topics.

💡Hint!
Want to filter a list based on some criteria? The where operator will help.

The self-evaluation feature

We already have an application that lets us create, edit and store multiple flashcards. But how do we keep track of which ones we have already learned and which ones we need to practice more? Any respectable flashcards application needs a self-evaluation feature. Let’s investigate how we can add that!

Suppose in our app we have two buttons for the self-evaluation: the Bad and the Good. What exactly do we want to happen every time an end user clicks the buttons? Well, the idea is rather simple:

  • Clicking the Bad button would indicate the user hasn’t learned the word yet and we want our app to move the corresponding flashcard to the beginning of the list so they could see it as soon as possible after launching the app.
  • Clicking the Good button would indicate the user has learned the word and the corresponding flashcard needs to move to the end of the list to let them work with other flashcards which they haven’t learned yet.

“Are you sure we can do that without JavaScript?” you may ask. Yep! Mavo is extremely powerful and is able to equip us with all the tools we need!

Now when we know what we are going to implement, let’s set the UI in place first and then move on to the next step. Our markup would look something like this:

... <article property="flashcard" mv-multiple>   ...   <section>     <h2>Evaluate Yourself</h2>     <button>Bad</button>     <button>Good</button>   </section> </article> ...

The mv-action attribute

See the Pen
07. The mv-action attribute
by Dmitry Sharabin (@dsharabin)
on CodePen.

Mavo actions allow us to create our very own controls that modify data in custom ways when the user interacts with them. Sounds promising right?

To define a custom action we need to use the mv-action attribute on an appropriate element inside our Mavo app. The action is performed every time the element is clicked. That’s exactly what we were looking for.

The value of the mv-action attribute is an expression. We can use any of the expression functions and syntax that MavoScript provides to us, as well as a few more to facilitate data manipulation, such as add(), set(), move(), or delete(). It is important to note that unlike normal expressions which are evaluated reactively, these expressions are only evaluated each time the action is triggered.

So, we need to move flashcards inside the collection, and Mavo has an appropriate function that lets us do that—the move() function. Its first argument refers to the item we are moving, and the second is its position in the collection. Bear in mind that elements of the collection are numbered starting from 0.

Let’s implement the first point of the outline we discussed earlier: while self-evaluating, an end user clicks the Bad button and the corresponding flashcard moves to the beginning of the collection, i.e., it becomes the first one. So in the code, we have:

... <article property="flashcard" mv-multiple>   ...   <button mv-action="move(flashcard, 0)">Bad</button>   ... </article> ...

Pay attention that in the mv-action attribute we refer to the flashcard property inside the property itself, since we want to deal with the current flashcard.

If we try to implement the second point of the outline, we will face a problem. Can you suggest what problem exactly will it be?

Let’s remember that if an end user clicks the Good button the corresponding flashcard moves to the end of the collection, i.e., it becomes the last one. To make a flashcard last in the collection we need to know the number of items in it.

Thankfully, a bit earlier we’ve already solved that task and implemented the corresponding feature. But could we use that solution to solve our current problem? Unfortunately, we can’t: as we already know, we can refer to the collection of flashcards by its name (and evaluate its size) only outside the flashcard property. But in our case, we need to do that inside it: the Good button for which we need to write an expression is inside the flashcard property.

What should we do then? I’m glad you ask. Mavo has the solution.

Using the meta element to hold intermediate values

See the Pen
08. The <meta> element
by Dmitry Sharabin (@dsharabin)
on CodePen.

So, on the one hand, we know that the [count(flashcards)] expression gives us the number of flashcards if it is evaluated outside the flashcard property. On the other hand, we need to use that value inside the flashcard property.

To solve that dilemma, we need to evaluate the number of flashcards outside the flashcard property and somehow hold the result to be able to use it elsewhere in the app, precisely inside the flashcard property. For cases like that, in Mavo, there are so-called computed properties.

To hold an intermediate result so we can refer to it, we need an HTML element in our code. It is recommended to use the <meta> element for that purpose, like so: <meta property="propertyName" content="[expression]">. The advantage of using this element is that it is hidden outside edit mode, both semantically and visually.

Now let’s add the flashcardCount computed property in our app. Remember, we must place it outside the flashcard property, but then we can refer to it from anywhere:

... <meta property="flashcardCount" content="[count(flashcard)]"> <article property="flashcard" mv-multiple>     ... </article> ...

Only one step left to finish the implementation of the self-evaluation feature: if an end user clicks the Good button the corresponding flashcard moves to the end of the collection, i.e., it becomes the last one. Let’s add the relevant action in the code of our app:

... <meta property="flashcardCount" content="[count(flashcard)]"> <article property="flashcard" mv-multiple>   ...   <button mv-action="move(flashcard, flashcardCount)">Good</button> </article> ...

We are done! Congratulations! 😀

💻 Let’s get our hands dirty!

There is another way to solve that task: with the help of the $ all special property. The $ all property represents a collection itself if it is placed inside the collection. So there is no need to use any computed property in this case. Try to implement that solution on your own.

There is only one more tiny thing left that we need to fix. Remember the part where we added some stats to our app? Remember the expression we built to evaluate the number of flashcards in the app: [count(flashcard)]? Instead, we can (and should) now use the computed property we defined. Make the appropriate changes in the app.

Takeaways

So what have we learned so far? Let’s recap. In order to turn any static HTML page into a Mavo app we need to:

  1. Include the Mavo JavaScript and CSS files in the page’s <head> section.
  2. Add the mv-app attribute to the Mavo root element.
  3. Tell Mavo what elements of our app are important by adding the property attribute to them.
  4. Place the mv-multiple attribute on the element that will be multiplied and converted into a collection.
  5. Tell Mavo where to store our data by adding mv-storage attribute to the Mavo root.
  6. Decide whether Mavo should save our data automatically or not. If yes, add the mv-autosave attribute to the Mavo root.

    We also know that:

  7. The Mavo toolbar is fully-customizable. The mv-bar attribute controls which buttons are going to appear there.
  8. Expressions let us present the current value of properties in other elements and perform computations. An expression value (and type) vary depending on the place the expression takes in the code. Mavo’s expression syntax is called MavoScript.
  9. Custom actions allow creating controls that modify data in custom ways. To define a custom action set the mv-action attribute on an appropriate element inside a Mavo app
  10. Properties whose values are expressions are called computed properties. To hold an intermediate result to be able to refer to it elsewhere in the app, it is recommended to use the <meta> element.

Instead of an epilogue

So we built our app. Is it already perfect? Of course not, nothing is! There are so many things that can be improved, and so many features that can be added (with the help of Mavo, we can even make our app multilingual!). Go ahead, enhance it more, don’t hesitate to try something new!

What we’ve learned so far about Mavo is just the tip of the iceberg, and there is so much more. I encourage you to give it a closer look, by reading the documentation, by examining examples (on the Mavo site, or on CodePen: made by Lea Verou and a few made by myself), and by creating new stuff! Good luck! 😉

Acknowledgments

I want to thank two great people. First of all, my huge thanks go to Lea Verou, who not only inspired me to write this tutorial (and helped me make it happen) but also inspires me all the time by the way she makes the world of web development a better place. I’ve never met such a gifted human being, and I am happy having an opportunity to make some stuff with her!

I also thank James Moore. The examples he uses in his course “Functional Programming for Beginners with JavaScript” on Udemy pushed me to make my very own version of a flashcard study app. He is a wonderful teacher!

The post Let Mavo Shine in Building Interactive Web Applications appeared first on CSS-Tricks.

CSS-Tricks

, , , ,
[Top]

Building a Conference Schedule with CSS Grid

It’s hard to beat the feeling of finding a perfect use for a new technology. You can read every handy primer under the sun and ooh-and-ahh at flashy demos, but the first time you use it on your own project… that’s when things really click.

I gained a new appreciation for CSS Grid when building a flexible layout for a conference schedule. The needs of the project aligned perfectly with grid’s strengths: a two-dimensional (vertical and horizontal) layout with complex placement of child elements. In the process of building a proof of concept, I found a few techniques that made the code highly readable and outright fun to work with.

The resulting demo included some interesting uses of CSS Grid features and forced me to grapple with some details of grid you don’t run into in every day.

Before we get started, it might be a good idea to keep another tab open with the CSS-Tricks guide to CSS Grid to reference the concepts we cover throughout the post.

Defining our layout requirements

I set out to create the following layout thanks to WordCamp, the hundreds of WordPress-focused conferences that happen around the world each year. These varied events range in size and format, yet all use the same schedule layout tool.

A schedule with 4 tracks of presentations (columns) and 6 times slots. The length of sessions vary, and some talks span multiple tracks.
The final layout we’ll build.

I helped schedule a couple WordCamps and styled a WordCamp website, so I knew the shortcomings of the existing HTML table layout. If your schedule didn’t fit in a uniform grid, well…¯_(ツ)_/¯

Setting out to find a better way, I started by listing the layout requirements:

  • Sessions of variable length (limited to set time increments)
    Imagine back-to-back one-hour talks in three rooms alongside a two-hour workshop in another.
  • Sessions spanning one or more “Tracks”
    Tracks are usually associated with a specific room in the venue. In the case of my local WordCamp in Seattle, the venue can literally remove a wall to combine two rooms!
  • Schedule can include empty space
    A last-minute cancellation or extra-short session creates gaps in a schedule.
  • Design is easy to customize with CSS
    WordCamp websites allow theming only via CSS.
  • Layout can be automatically generated from CMS content
    Since we’re building a layout from structured session data on thousands of websites, we can’t rely on any HTML or CSS that’s too clever or bespoke.

Getting started: Solid HTML

Before I write any CSS, I always start with rock-solid HTML.

The top-level <div> will have a class of .schedule and serve as the grid parent. Each unique start time gets its own heading followed by all sessions starting at that time. The markup for each session isn’t very important, but make sure that seeing the layout isn’t required to understand when and where a session happens. (You’ll see why in a moment.)

<h2>Conference Schedule</h2> <div class="schedule">    <h3 class="time-slot">8:00am</h3>   <div class="session session-1 track-1">     <h4 class="session-title"><a href="#">Session Title</a></h4>     <span class="session-time">8:00am - 9:00am</span>     <span class="session-track">Track 1</span>     <span class="session-presenter">Presenter Name</span>   </div>   <!-- Sessions 2, 3, 4 -->    <h3 class="time-slot">9:00am</h3>   <div class="session session-5 track-1">     <h4 class="session-title"><a href="#">Session Title</a></h4>     <span class="session-time">9:00am - 10:00am</span>     <span class="session-track">Track 1</span>     <span class="session-presenter">Presenter Name</span>   </div>   <!-- Sessions 6, 7, 8 -->    <!-- etc... -->  </div> <!-- end .schedule -->

Mobile layout and grid fallback complete!

Adding in a bit of up-to-you CSS to make things pretty, our mobile layout and fallback for browsers that don’t support CSS Grid is already complete!

Here’s how mine looks with the colors I used:

People on phones, zooming in their browsers, or even using Internet Explorer will have no problem finding their favorite sessions at this conference!

Adding the grid layout

Now for the actual CSS Grid part!

My ah-ha moment when building this came from reading Robin’s article here on CSS-Tricks, “Making a Bar Chart with CSS Grid.” TL;DR: One grid row represents 1% of the chart’s height, so a bar spans the same number of rows as the percentage it represents.

.chart {   display: grid;   grid-template-rows: repeat(100, 1fr); /* 1 row = 1%! */ }  .fifty-percent-bar {   grid-row: 51 / 101; /* 101 - 51 = 50 => 50% */ }

That helped me realize that grid is perfect for any layout tied to some regular incremental unit. In the case of a schedule, that unit is time! For this demo, we’ll use increments of 30 minutes, but do whatever makes sense for you. (Just watch out for the Chrome bug that limits Grid layouts to 1000 rows.)

The first version I tried used similar syntax to the bar chart in Robin’s and some basic math to place the sessions. We’re using eight rows because there are eight 30-minute increments between 8 a.m. and 12 p.m. Remember that implicit grid line numbers start at one (not zero), so the grid rows are numbered one through nine.

A 5 row grid with numbered lines, 1-6.
A rudimentary version of a single column schedule with numbered grid lines. (View Demo: Numbered Gridlines vs. Named Gridlines)
.schedule {   display: grid;   grid-template-rows: repeat(8, 1fr); }  .session-1 {   grid-row: 1 / 3; /* 8am - 9am, 3 - 1 = 2 30-minute increment */ }  .session-2 {   grid-row: 3 / 6; /* 9am - 10:30am, 6-3 = 3 30-minute increments */ }

The problem with this technique is that placing items on a grid with a lot of rows is very abstract and confusing. (This issue added a ton of complexity to Robin’s bar chart demo, too.)

This is where named grid lines come to the rescue! Instead of relying on grid line numbers, we can give each line a predictable name based on the corresponding time of day.

A 5 row grid with rows named for the time. Example: time-0800
By using named grid lines for the schedule, it becomes much easier to place a session. (View Demo: Numbered Gridlines vs. Named Gridlines)
.schedule {   display: grid;   grid-template-rows:     [time-0800] 1fr     [time-0830] 1fr     [time-0900] 1fr     [time-0930] 1fr;     /* etc...         Note: Use 24-hour time for line names */ }  .session-1 {   grid-row: time-0800 / time-0900; }  .session-2 {   grid-row: time-0900 / time-1030; }

That is gloriously easy to understand. There is no complicated math to figure out how many rows there are before and after a session starts or ends. Even better, we can generate grid line names and session layout styles with information stored in WordPress. Throw a start and end time at the grid, and you’re good to go!

Since the schedule has multiple tracks, we’ll need a column for each one. The tracks work in a similar way to the times, using named track lines for each grid column line. There’s also an extra first column for the start time headings.

.schedule { /* continued */   grid-template-columns:     [times] 4em     [track-1-start] 1fr     [track-1-end track-2-start] 1fr     [track-2-end track-3-start] 1fr     [track-3-end track-4-start] 1fr     [track-4-end]; }

Here though, we take named grid lines one step further. Each line gets two names: one for the track it starts and one for the track it ends. This isn’t strictly necessary, but it makes the code much clearer, especially when a session spans more than one column.

With the time- and track-based grid lines defined, we can now place any session we want just from knowing it’s time and track!

.session-8 {   grid-row: time-1030 / time-1100;   grid-column: track-2-start / track-3-end; /* spanning two tracks! */ }

Putting that all together, we get some lengthy but extremely readable code that is a real joy to work with.

@media screen and (min-width: 700px) {   .schedule {     display: grid;     grid-gap: 1em;     grid-template-rows:       [tracks] auto /* Foreshadowing! */       [time-0800] 1fr       [time-0830] 1fr       [time-0900] 1fr       [time-0930] 1fr       [time-1000] 1fr       [time-1030] 1fr       [time-1100] 1fr       [time-1130] 1fr       [time-1200] 1fr;     grid-template-columns:       [times] 4em       [track-1-start] 1fr       [track-1-end track-2-start] 1fr       [track-2-end track-3-start] 1fr       [track-3-end track-4-start] 1fr       [track-4-end];   }    .time-slot {     grid-column: times;   } }
<div class="session session-1 track-1" style="grid-column: track-1; grid-row: time-0800 / time-0900;">   <!-- details --> </div> <div class="session session-2 track-2" style="grid-column: track-2; grid-row: time-0800 / time-0900">   <!-- details --> </div> <!-- etc... -->

The final code uses inline styles for session placement which feels right to me. If you don’t like this and are working with more modern browsers, you could pass the line names to CSS via CSS variables.

Quick note: using fr units versus the auto value for row heights

One detail worth noting is the use of the fr unit for defining row heights.

When using 1fr to determine row heights, all rows have the same height. That height is determined by the content of the tallest row in the schedule. (I had to read the W3C spec for fr to figure this out!) That produces a beautiful schedule where height is proportional to time, but can also lead to a very tall layout.

A grid with 5 rows of equal height. The height matches the tallest row containing 3 lines of text.
Using 1fr produces equal-height rows determined by the tallest row in the grid. (View Demo: Grid row heights 1fr vs auto)

For example, if your schedule grid has 15-minute increments from 7 a.m. to 6 p.m., that’s a total of 48 grid rows. In that case, you probably want to use auto for your row height because the schedule is much more compact with each grid row’s height determined by its content.

A grid with 5 rows of unequal height. Each row contains its content.
Using auto makes each row the height of its content. (View Demo: Grid row heights 1fr vs auto)

A word about accessibility

There are real concerns about the accessibility of certain CSS Grid techniques. Specifically, the ability to change the order of information visually in ways that don’t match the source order causes problems for people using keyboard navigation.

This layout uses that ability to arbitrarily place items on a grid, so some caution is warranted. However, because the heading and source order align with the visualization of start times, this seems like a safe use to me.

If you’re inspired to do something similar, carefully consider accessibility. It makes sense to order information by time in this case, but I can imagine a legitimate case for TAB order to go down columns rather than across rows. (Modifying this demo to do that shouldn’t be too hard!)

Whatever you do, always consider accessibility.

Adding sticky track names

Finally, it’s time to add in the track names that look like table headers at the top of each column. Since a session’s track is already visible, I chose to hide the “headers” from assistive technology with aria-hidden="true".

The track names go in the first grid row, conveniently named “tracks.” As long as you don’t have any weird overflow issues, position: sticky keeps those in view while you scroll.

<span class="track-slot" aria-hidden="true" style="grid-column: track-1;">Track 1</span> <span class="track-slot" aria-hidden="true" style="grid-column: track-2;">Track 2</span> <span class="track-slot" aria-hidden="true" style="grid-column: track-3;">Track 3</span> <span class="track-slot" aria-hidden="true" style="grid-column: track-4;">Track 4</span>
.track-slot {   display: none; /* only visible with Grid layout */ }  @supports( display:grid ) {   @media screen and (min-width:700px) {         .track-slot {       grid-row: tracks;       display: block;       position: sticky;       top: 0;       z-index: 1000;       background-color: rgba(255,255,255,.9);     }   } }

It’s a slick little finishing touch to the final demo. ✨

The result

Here’s how things look with everything we’ve covered put together!

See the Pen
Conference Schedule with CSS Grid
by Mark Root-Wiley (@mrwweb)
on CodePen.

We’re just getting started

This schedule is definitely the most satisfying use of CSS Grid I’ve ever made. I love how “data-driven” and semantic the line naming is, and the accessibility and CMS requirements fit in perfectly without any inconvenience.

The only question left for me is what other types of “data-driven” grids are out there to build? I’ve seen some great calendar layouts and here’s a Monopoly board layout. What about a football field, a timeline, restaurant tables, or theater seats? What else?

The post Building a Conference Schedule with CSS Grid appeared first on CSS-Tricks.

CSS-Tricks

, , ,
[Top]

A Practical Use Case for Vue Render Functions: Building a Design System Typography Grid

This post covers how I built a typography grid for a design system using Vue render functions. Here’s the demo and the code. I used render functions because they allow you to create HTML with a greater level of control than regular Vue templates, yet surprisingly I couldn’t find very much when I web searched around for real-life, non-tutorial applications of them. I’m hoping this post will fill that void and provide a helpful and practical use case on using Vue render functions.

I’ve always found render functions to be a little out-of-character for Vue. While the rest of the framework emphasizes simplicity and separation of concerns, render functions are a strange and often difficult-to-read mix of HTML and JavaScript.

For example, to display:

<div class="container">   <p class="my-awesome-class">Some cool text</p> </div>

…you need:

render(createElement) {   return createElement("div", { class: "container" }, [     createElement("p", { class: "my-awesome-class" }, "Some cool text")   ]) }

I suspect that this syntax turns some people off, since ease-of-use is a key reason to reach for Vue in the first place. This is a shame because render functions and functional components are capable of some pretty cool, powerful stuff. In the spirit of demonstrating their value, here’s how they solved an actual business problem for me.

Quick disclaimer: It will be super helpful to have the demo open in another tab to reference throughout this post.

Defining criteria for a design system

My team wanted to include a page in our VuePress-powered design system showcasing different typography options. This is part of a mockup that I got from our designer.

A screenshot of the typographic design system. There are four columns where the first shows the style name with the rendered style, second is the element or class, third shows the properties that make the styles, and fourth is the defined usage.

And here’s a sample of some of the corresponding CSS:

h1, h2, h3, h4, h5, h6 {   font-family: "balboa", sans-serif;   font-weight: 300;   margin: 0; }  h4 {   font-size: calc(1rem - 2px); }  .body-text {   font-family: "proxima-nova", sans-serif; }  .body-text--lg {   font-size: calc(1rem + 4px); }  .body-text--md {   font-size: 1rem; }  .body-text--bold {   font-weight: 700; }  .body-text--semibold {   font-weight: 600; }

Headings are targeted with tag names. Other items use class names, and there are separate classes for weight and size.

Before writing any code, I created some ground rules:

  • Since this is really a data visualization, the data should be stored in a separate file.
  • Headings should use semantic heading tags (e.g. <h1>, <h2>, etc.) instead of having to rely on a class.
  • Body content should use paragraph (<p>) tags with the class name (e.g. <p class="body-text--lg">).
  • Content types that have variations should be grouped together by wrapping them in the root paragraph tag, or corresponding root element, without a styling class. Children should be wrapped with <span> and the class name.
<p>   <span class="body-text--lg">Thing 1</span>   <span class="body-text--lg">Thing 2</span> </p>
  • Any content that’s not demonstrating special styling should use a paragraph tag with the correct class name and <span> for any child nodes.
<p class="body-text--semibold">   <span>Thing 1</span>   <span>Thing 2</span> </p>
  • Class names should only need to be written once for each cell that’s demonstrating styling.

Why render functions make sense

I considered a few options before starting:

Hardcoding

I like hardcoding when appropriate, but writing my HTML by hand would have meant typing out different combinations of the markup, which seemed unpleasant and repetitive. It also meant that data couldn’t be kept in a separate file, so I ruled out this approach.

Here’s what I mean:

<div class="row">   <h1>Heading 1</h1>   <p class="body-text body-text--md body-text--semibold">h1</p>   <p class="body-text body-text--md body-text--semibold">Balboa Light, 30px</p>   <p class="group body-text body-text--md body-text--semibold">     <span>Product title (once on a page)</span>     <span>Illustration headline</span>   </p> </div>

Using a traditional Vue template

This would normally be the go-to option. However, consider the following:

See the Pen
Different Styles Example
by Salomone Baquis (@soluhmin)
on CodePen.

In the first column, we have:

– An <h1>> tag rendered as-is.
– A <p> tag that groups some <span> children with text, each with a class (but no special class on the <p> tag).
– A <p> tag with a class and no children.

The result would have meant many instances of v-if and v-if-else, which I knew would get confusing fast. I also disliked all of that conditional logic inside the markup.

Because of these reasons, I chose render functions. Render functions use JavaScript to conditionally create child nodes based on all of the criteria that’s been defined, which seemed perfect for this situation.

Data model

As I mentioned earlier, I’d like to keep typography data in a separate JSON file so I can easily make changes later without touching markup. Here’s the raw data.

Each object in the file represents a different row.

{   "text": "Heading 1",   "element": "h1", // Root wrapping element.   "properties": "Balboa Light, 30px", // Third column text.   "usage": ["Product title (once on a page)", "Illustration headline"] // Fourth column text. Each item is a child node.  }

The object above renders the following HTML:

<div class="row">   <h1>Heading 1</h1>   <p class="body-text body-text--md body-text--semibold">h1</p>   <p class="body-text body-text--md body-text--semibold">Balboa Light, 30px</p>   <p class="group body-text body-text--md body-text--semibold">     <span>Product title (once on a page)</span>     <span>Illustration headline</span>   </p> </div>

Let’s look at a more involved example. Arrays represent groups of children. A classes object can store classes. The base property contains classes that are common to every node in the cell grouping. Each class in variants is applied to a different item in the grouping.

{   "text": "Body Text - Large",   "element": "p",   "classes": {     "base": "body-text body-text--lg", // Applied to every child node     "variants": ["body-text--bold", "body-text--regular"] // Looped through, one class applied to each example. Each item in the array is its own node.    },   "properties": "Proxima Nova Bold and Regular, 20px",   "usage": ["Large button title", "Form label", "Large modal text"] }

Here’s how that renders:

<div class="row">   <!-- Column 1 -->   <p class="group">     <span class="body-text body-text--lg body-text--bold">Body Text - Large</span>     <span class="body-text body-text--lg body-text--regular">Body Text - Large</span>   </p>   <!-- Column 2 -->   <p class="group body-text body-text--md body-text--semibold">     <span>body-text body-text--lg body-text--bold</span>     <span>body-text body-text--lg body-text--regular</span>   </p>   <!-- Column 3 -->   <p class="body-text body-text--md body-text--semibold">Proxima Nova Bold and Regular, 20px</p>   <!-- Column 4 -->   <p class="group body-text body-text--md body-text--semibold">     <span>Large button title</span>     <span>Form label</span>     <span>Large modal text</span>   </p> </div>

The basic setup

We have a parent component, TypographyTable.vue, which contains the markup for the wrapper table element, and a child component, TypographyRow.vue, which creates a row and contains our render function.

I loop through the row component, passing the row data as props.

<template>   <section>     <!-- Headers hardcoded for simplicity -->     <div class="row">       <p class="body-text body-text--lg-bold heading">Hierarchy</p>       <p class="body-text body-text--lg-bold heading">Element/Class</p>       <p class="body-text body-text--lg-bold heading">Properties</p>       <p class="body-text body-text--lg-bold heading">Usage</p>     </div>       <!-- Loop and pass our data as props to each row -->     <typography-row       v-for="(rowData, index) in $ options.typographyData"       :key="index"       :row-data="rowData"     />   </section> </template> <script> import TypographyData from "@/data/typography.json"; import TypographyRow from "./TypographyRow"; export default {   // Our data is static so we don't need to make it reactive   typographyData: TypographyData,   name: "TypographyTable",   components: {     TypographyRow   } }; </script>

One neat thing to point out: the typography data can be a property on the Vue instance and be accessed using $ options.typographyData since it doesn’t change and doesn’t need to be reactive. (Hat tip to Anton Kosykh.)

Making a functional component

The TypographyRow component that passes data is a functional component. Functional components are stateless and instanceless, which means that they have no this and don’t have access to any Vue lifecycle methods.

The empty starting component looks like this:

// No <template> <script> export default {   name: "TypographyRow",   functional: true, // This property makes the component functional   props: {     rowData: { // A prop with row data       type: Object     }   },   render(createElement, { props }) {     // Markup gets rendered here   } } </script>

The render method takes a context argument, which has a props property that’s de-structured and used as the second argument.

The first argument is createElement, which is a function that tells Vue what nodes to create. For brevity and convention, I’ll be abbreviating createElement as h. You can read about why I do that in Sarah’s post.

h takes three arguments:

  1. An HTML tag (e.g. div)
  2. A data object with template attributes (e.g. { class: 'something'})
  3. Text strings (if we’re just adding text) or child nodes built using h
render(h, { props }) {   return h("div", { class: "example-class }, "Here's my example text") }

OK, so to recap where we are at this point, we’ve covered creating:

  • a file with the data that’s going to be used in my visualization;
  • a regular Vue component where I’m importing the full data file; and
  • the beginning of a functional component that will display each row.

To create each row, the data from the JSON file needs to be passed into arguments for h. This could be done all at once, but that involves a lot of conditional logic and is confusing.

Instead, I decided to do it in two parts:

  1. Transform the data into a predictable format.
  2. Render the transformed data.

Transforming the common data

I wanted my data in a format that would match the arguments for h, but before doing this, I wrote out how I wanted things structured:

// One cell {   tag: "", // HTML tag of current level   cellClass: "", // Class of current level, null if no class exists for that level   text: "", // Text to be displayed    children: [] // Children each follow this data model, empty array if no child nodes }

Each object represents one cell, with four cells making up each row (an array).

// One row [ { cell1 }, { cell2 }, { cell3 }, { cell4 } ]

The entry point would be a function like:

function createRow(data) { // Pass in the full row data and construct each cell   let { text, element, classes = null, properties, usage } = data;   let row = [];   row[0] = createCellData(data) // Transform our data using some shared function   row[1] = createCellData(data)   row[2] = createCellData(data)   row[3] = createCellData(data)    return row; }

Let’s take another look at our mockup.

The first column has styling variations, but the rest seem to follow the same pattern, so let’s start with those.

Again, the desired model for each cell is:

{   tag: "",   cellClass: "",    text: "",    children: [] }

This gives us a tree-like structure for each cell since some cells have groups of children. Let’s use two functions to create the cells.

  • createNode takes each of our desired properties as arguments.
  • createCell wraps around createNode so that we can check if the text that we’re passing in is an array. If it is, we build up an array of child nodes.
// Model for each cell function createCellData(tag, text) {   let children;   // Base classes that get applied to every root cell tag   const nodeClass = "body-text body-text--md body-text--semibold";   // If the text that we're passing in as an array, create child elements that are wrapped in spans.    if (Array.isArray(text)) {     children = text.map(child => createNode("span", null, child, children));   }   return createNode(tag, nodeClass, text, children); } // Model for each node function createNode(tag, nodeClass, text, children = []) {   return {     tag: tag,     cellClass: nodeClass,     text: children.length ? null : text,     children: children   }; }

Now, we can do something like:

function createRow(data) {   let { text, element, classes = null, properties, usage } = data;   let row = [];   row[0] = ""   row[1] = createCellData("p", ?????) // Need to pass in class names as text    row[2] = createCellData("p", properties) // Third column   row[3] = createCellData("p", usage) // Fourth column    return row; }

We pass properties and usage to the third and fourth columns as text arguments. However, the second column is a little different; there, we’re displaying the class names, which are stored in the data file like:

"classes": {   "base": "body-text body-text--lg",   "variants": ["body-text--bold", "body-text--regular"] },

Additionally, remember that headings don’t have classes, so we want to show the heading tag names for those rows (e.g. h1, h2, etc.).

Let’s create some helper functions to parse this data into a format that we can use for our text argument.

// Pass in the base tag and class names as arguments function displayClasses(element, classes) {   // If there are no classes, return the base tag (appropriate for headings)   return getClasses(classes) ? getClasses(classes) : element; }  // Return the node class as a string (if there's one class), an array (if there are multiple classes), or null (if there are none.)  // Ex. "body-text body-text--sm" or ["body-text body-text--sm body-text--bold", "body-text body-text--sm body-text--italic"] function getClasses(classes) {   if (classes) {     const { base, variants = null } = classes;     if (variants) {       // Concatenate each variant with the base classes       return variants.map(variant => base.concat(`$ {variant}`));     }     return base;   }   return classes; }

Now we can do this:

function createRow(data) {   let { text, element, classes = null, properties, usage } = data;   let row = [];   row[0] = ""   row[1] = createCellData("p", displayClasses(element, classes)) // Second column   row[2] = createCellData("p", properties) // Third column   row[3] = createCellData("p", usage) // Fourth column    return row; }

Transforming the demo data

This leaves the first column that demonstrates the styles. This column is different because we’re applying new tags and classes to each cell instead of using the class combination used by the rest of the columns:

<p class="body-text body-text--md body-text--semibold">

Rather than try to do this in createCellData or createNodeData, let’s make another function to sit on top of these base transformation functions and handle some of the new logic.

function createDemoCellData(data) {   let children;   const classes = getClasses(data.classes);   // In cases where we're showing off multiple classes, we need to create children and apply each class to each child.   if (Array.isArray(classes)) {     children = classes.map(child =>       // We can use "data.text" since each node in a cell grouping has the same text       createNode("span", child, data.text, children)     );   }   // Handle cases where we only have one class   if (typeof classes === "string") {     return createNode("p", classes, data.text, children);   }   // Handle cases where we have no classes (ie. headings)   return createNode(data.element, null, data.text, children); }

Now we have the row data in a normalized format that we can pass to our render function:

function createRow(data) {   let { text, element, classes = null, properties, usage } = data   let row = []   row[0] = createDemoCellData(data)   row[1] = createCellData("p", displayClasses(element, classes))   row[2] = createCellData("p", properties)   row[3] = createCellData("p", usage)    return row }

Rendering the data

Here’s how we actually render the data to display:

// Access our data in the "props" object const rowData = props.rowData;  // Pass it into our entry transformation function const row = createRow(rowData);  // Create a root "div" node and handle each cell return h("div", { class: "row" }, row.map(cell => renderCells(cell)));  // Traverse cell values function renderCells(data) {    // Handle cells with multiple child nodes   if (data.children.length) {     return renderCell(       data.tag, // Use the base cell tag       { // Attributes in here         class: {           group: true, // Add a class of "group" since there are multiple nodes           [data.cellClass]: data.cellClass // If the cell class isn't null, apply it to the node         }       },       // The node content       data.children.map(child => {         return renderCell(           child.tag,           { class: child.cellClass },           child.text         );       })     );   }    // If there are no children, render the base cell   return renderCell(data.tag, { class: data.cellClass }, data.text); }  // A wrapper function around "h" to improve readability function renderCell(tag, classArgs, text) {   return h(tag, classArgs, text); }

And we get our final product! Here’s the source code again.

Wrapping up

It’s worth pointing out that this approach represents an experimental way of addressing a relatively trivial problem. I’m sure many people will argue that this solution is needlessly complicated and over-engineered. I’d probably agree.

Despite the up-front cost, however, the data is now fully separated from the presentation. Now, if my design team adds or removes rows, I don’t have to dig into messy HTML — I just update a couple of properties in the JSON file.

Is it worth it? Like everything else in programming, I guess it depends. I will say that this comic strip was in the back of my mind as I worked on this:

A three-panel comic strip. First panel is a stick figure at a dinner table asking to pass the salt. Second panel is the same figure with no dialogue. Third panel is another figure saying he's building a system to pass the condiments and that it will save time in the long run. First figure says it's already been 20 minutes.
Source: https://xkcd.com/974

Maybe that’s an answer. I’d love to hear all of your (constructive) thoughts and suggestions, or if you’ve tried other ways of going about a similar task.

The post A Practical Use Case for Vue Render Functions: Building a Design System Typography Grid appeared first on CSS-Tricks.

CSS-Tricks

, , , , , , , ,
[Top]

Responsive Designs and CSS Custom Properties: Building a Flexible Grid System

Last time, we looked at a few possible approaches for declaring and using CSS custom properties in responsive designs. In this article, we’ll take a closer look at CSS variables and how to use them in reusable components and modules. We will learn how to make our variables optional and set fallback values.

As an example, we will build a simple grid system based on flexbox. Grid systems play a vital role in responsive designs. However, building a grid system that is flexible and lightweight at the same time can be a tricky task. Let’s see what the common approaches towards grid systems are and how CSS custom properties can help us build them.

Article Series:

  1. Defining Variables and Breakpoints
  2. Building a Flexible Grid System (This Post)

A simple CSS grid system

Let’s start with a 12-column grid system:

.container { 	max-width: 960px; 	margin: 0 auto; 	display: flex; }  .col-1 { flex-basis: 8.333%; } .col-2 { flex-basis: 16.666%; } .col-3 { flex-basis: 25%; } .col-4 { flex-basis: 33.333%; } .col-5 { flex-basis: 41.666%; } .col-6 { flex-basis: 50%; } /* and so on up to 12... */

See the Pen
#5 Building responsive features with CSS custom properties
by Mikołaj (@mikolajdobrucki)
on CodePen.

There’s quite a lot of repetition and hard-coded values here. Not to mention how many more will be generated once we add more breakpoints, offset classes, etc.

Building a grid system with Sass

To make our grid example more readable and maintainable, let’s use Sass to preprocess our CSS:

$ columns: 12; // Number of columns in the grid system  .container { 	display: flex; 	flex-wrap: wrap; 	margin: 0 auto; 	max-width: 960px; }  @for $ width from 1 through $ columns { 	.col-#{$ width} { 		flex-basis: $ width / $ columns * 100%; 	}   }

See the Pen
#6 Building responsive features with CSS custom properties
by Mikołaj (@mikolajdobrucki)
on CodePen.

This is definitely much easier to work with. As we develop our grid further and, let’s say, would like to change it from 12 columns to 16 columns, all we have to do is to update a single variable (in comparison to dozens of classes and values). But… as long as our Sass is shorter and more maintainable now, the compiled code is identical to the first example. We are still going to end up with a massive amount of code in the final CSS file. Let’s explore what happens if we try to replace the Sass variables with CSS custom properties instead.

Building a grid system with CSS custom properties

Before we start playing with CSS custom properties, let’s start with some HTML first. Here’s the layout we’re aiming for:

It consists of three elements: a header, a content section and a sidebar. Let’s create markup for this view, giving each of the elements a unique semantic class (header, content, sidebar) and a column class which indicates that this element is a part of a grid system:

<div class="container"> 	<header class="header column"> 		header 	</header> 	<main class="content column"> 		content 	</main> 	<aside class="sidebar column"> 		sidebar 	</aside> </div>

Our grid system, as before, is based on a 12-column layout. You can envision it as an overlay covering our content areas:

So .header takes all 12 columns, .content takes eight columns (66.(6)% of the total width) and .sidebar takes four columns (33.(3)% of the total width). In our CSS, we would like to be able to control the width of each section by changing a single custom property:

.header { 	--width: 12; }  .content { 	--width: 8; }  .sidebar { 	--width: 4; }

To make it work, all we need to do is write a rule for the .column class. Lucky for us, most of the work is already done! We can re-use the Sass from the previous chapter and replace the Sass variables with CSS custom properties:

.container { 	display: flex; 	flex-wrap: wrap; 	margin: 0 auto; 	max-width: 960px; }  .column { 	--columns: 12; /* Number of columns in the grid system */ 	--width: 0; /* Default width of the element */  	flex-basis: calc(var(--width) / var(--columns) * 100%); }

Notice two important changes here:

  1. The --columns variable is now declared inside of the .column rule. The reason is that this variable is not supposed to be used outside of the scope of this class.
  2. The math equation we perform in the flex-basis property is now enclosed within a calc() function. Math calculations that are written in Sass are compiled by the preprocessor and don’t need additional syntax. calc(), on the other hand, lets us perform math calculations in live CSS. The equation always needs to be wrapped within a calc() function.

On a very basic level, that’s it! We’ve just built a 12-column grid system with CSS custom properties. Congratulations! We could call it a day and happily finish this article right now, but… we usually need a grid system that is a bit more sophisticated. And this is when things are getting really interesting.

See the Pen
#8 Building responsive features with CSS custom properties
by Mikołaj (@mikolajdobrucki)
on CodePen.

Adding a breakpoint to the grid

Most times, we need layouts to look different on various screen sizes. Let’s say that in our case we want the layout to remain as it is on a large viewport (e.g. desktop) but have all three elements become full-width on smaller screens (e.g. mobile).

So, in this case, we would like our variables to look as follows:

.header { 	--width-mobile: 12; }  .content { 	--width-mobile: 12; 	--width-tablet: 8; /* Tablet and larger */ }  .sidebar { 	--width-mobile: 12; 	--width-tablet: 4; /* Tablet and larger */ }

.content and .sidebar each hold two variables now. The first variable (--width-mobile) is a number of columns an element should take by default, and the second one (--width-tablet) is the number of columns an element should take on larger screens. The .header element doesn’t change; it always takes the full width. On larger screens, the header should simply inherit the width it has on mobile.

Now, let’s update our .column class.

CSS variables and fallback

To make the mobile version work as expected, we need to alter the .column class as follows:

.column { 	--columns: 12; /* Number of columns in the grid system */ 	--width: var(--width-mobile, 0); /* Default width of the element */ 	 	flex-basis: calc(var(--width) / var(--columns) * 100%); }

Basically, we replace the value of the --width variable with --width-mobile. Notice that the var() function takes two arguments now. The first of them is a default value. It says: “If a --width-mobile variable exists in a given scope, assign its value to the --width variable.” The second argument is a fallback. In other words: “If a --width-mobile variable is not declared in a given scope, assign this fallback value to the --width variable.” We set this fallback to prepare for a scenario where some grid elements won’t have a specified width.

For example, our .header element has a declared --width-mobile variable which means the --width variable will be equal to it and the flex-basis property of this element will compute to 100%:

.header { 	--width-mobile: 12; }  .column { 	--columns: 12; 	--width: var(--width-mobile, 0); /* 12, takes the value of --width-mobile */ 	 	flex-basis: calc(var(--width) / var(--columns) * 100%); /* 12 ÷ 12 × 100% = 100% */ }

If we remove the --width-mobile variable from the .header rule, then the --width variable will use a fallback value:

.header { 	/* Nothing here... */ }  .column { 	--columns: 12; 	--width: var(--width-mobile, 0); /* 0, takes the the fallback value */ 	 	flex-basis: calc(var(--width) / var(--columns) * 100%); /* 0 ÷ 12 × 100% = 0% */ }

Now, as we understand how to set fallback for CSS custom properties, we can create a breakpoint, by adding a media query to our code:

.column { 	--columns: 12; /* Number of columns in the grid system */ 	--width: var(--width-mobile, 0); /* Default width of the element */ 	 	flex-basis: calc(var(--width) / var(--columns) * 100%); }  @media (min-width: 576px) { 	.column { 		--width: var(--width-tablet); /* Width of the element on tablet and up */ 	} }

This works exactly as expected, but only for the content and sidebar, i.e. for the elements that have specified both --width-mobile and --width-tablet. Why?

The media query we created applies to all .column elements, even those that don’t have a --width-tablet variable declared in their scope. What happens if we use a variable that is not declared? The reference to the undeclared variable in a var() function is then considered invalid at computed-value time, i.e. invalid at the time a user agent is trying to compute it in the context of a given declaration.

Ideally, in such a case, we would like the --width: var(--width-tablet); declaration to be ignored and the previous declaration of --width: var(--width-mobile, 0); to be used instead. But this is not how custom properties work! In fact, the invalid --width-tablet variable will still be used in the flex-basis declaration. A property that contains an invalid var() function always computes to its initial value. So, as flex-basis: calc(var(--width) / var(--columns) * 100%); contains an invalid var() function the whole property will compute to auto (the initial value for flex-basis).

What else we can do then? Set a fallback! As we learned before, a var() function containing a reference to the undeclared variable, computes to its fallback value, as long as it’s specified. So, in this case, we can just set a fallback to the --width-tablet variable:

.column { 	--columns: 12; /* Number of columns in the grid system */ 	--width: var(--width-mobile, 0); /* Default width of the element */ 	 	flex-basis: calc(var(--width) / var(--columns) * 100%); }  @media (min-width: 576px) { 	.column { 		--width: var(--width-tablet, var(--width-mobile, 0)); 	} }

See the Pen
#9 Building responsive features with CSS custom properties
by Mikołaj (@mikolajdobrucki)
on CodePen.

This will create a chain of fallback values, making the --width property use --width-tablet when available, then --width-mobile if --width-tablet is not declared, and eventually, 0 if neither of the variables is declared. This approach allows us to perform numerous combinations:

.section-1 { 	/* Flexible on all resolutions */ }  .section-2 { 	/* Full-width on mobile, half of the container's width on tablet and up */ 	--width-mobile: 12; 	--width-tablet: 6; } 	 .section-3 { 	/* Full-width on all resolutions */ 	--width-mobile: 12; } 	 .section-4 { 	/* Flexible on mobile, 25% of the container's width on tablet and up */ 	--width-tablet: 3; }

One more thing we can do here is convert the default 0 value to yet another variable so we avoid repetition. It makes the code a bit longer but easier to update:

.column { 	--columns: 12; /* Number of columns in the grid system */ 	--width-default: 0; /* Default width, makes it flexible */ 	--width: var(--width-mobile, var(--width-default)); /* Width of the element */ 	 	flex-basis: calc(var(--width) / var(--columns) * 100%); }  @media (min-width: 576px) { 	.column { 		--width: var(--width-tablet, var(--width-mobile, var(--width-default))); 	} }

See the Pen
#10 Building responsive features with CSS custom properties
by Mikołaj (@mikolajdobrucki)
on CodePen.

Now, we have a fully functional, flexible grid! How about adding some more breakpoints?

Adding more breakpoints

Our grid is already quite powerful but we often need more than one breakpoint. Fortunately, adding more breakpoints to our code couldn’t be easier. All we have to do is to re-use the code we already have and add one variable more:

.column { 	--columns: 12; /* Number of columns in the grid system */ 	--width-default: 0; /* Default width, makes it flexible */ 	--width: var(--width-mobile, var(--width-default)); /* Width of the element */ 	 	flex-basis: calc(var(--width) / var(--columns) * 100%); }  @media (min-width: 576px) { 	.column { 		--width: var(--width-tablet, var(--width-mobile, var(--width-default))); 	} }  @media (min-width: 768px) { 	.column { 		--width: var(--width-desktop, var(--width-tablet, var(--width-mobile, var(--width-default)))); 	} }

See the Pen
#11 Building responsive features with CSS custom properties
by Mikołaj (@mikolajdobrucki)
on CodePen.

Reducing fallback chains

One thing that doesn’t look that great in our code is that feedback chains are getting longer and longer with every breakpoint. If we’d like to tackle this issue, we can change our approach to something like this:

.column { 	--columns: 12; /* Number of columns in the grid system */ 	--width: var(--width-mobile, 0); /* Width of the element */ 	 	flex-basis: calc(var(--width) / var(--columns) * 100%); }  @media (min-width: 576px) { 	.column { 		--width-tablet: var(--width-mobile); 		--width: var(--width-tablet); 	} }  @media (min-width: 768px) { 	.column { 		--width-desktop: var(--width-tablet); 		--width: var(--width-desktop); 	} }

See the Pen
#12 Building responsive features with CSS custom properties
by Mikołaj (@mikolajdobrucki)
on CodePen.

This code is doing exactly the same job but in a bit different way. Instead of creating a full fallback chain for each breakpoint, we set a value of each variable to the variable from the previous breakpoint as a default value.

Why so complicated?

It looks like we’ve done quite a lot of work to complete a relatively simple task. Why? The main answer is: to make the rest of our code simpler and more maintainable. In fact, we could build the same layout by using the techniques described in the previous part of this article:

.container { 	display: flex; 	flex-wrap: wrap; 	margin: 0 auto; 	max-width: 960px; }  .column { 	--columns: 12; /* Number of columns in the grid system */ 	--width: 0; /* Default width of the element */  	flex-basis: calc(var(--width) / var(--columns) * 100%); }  .header { 	--width: 12; }  .content { 	--width: 12; }  .sidebar { 	--width: 12; }  @media (min-width: 576px) { 	.content { 		--width: 6; 	} 	 	.sidebar { 		--width: 6; 	} }  @media (min-width: 768px) { 	.content { 		--width: 8; 	} 	 	.sidebar { 		--width: 4; 	} }

In a small project, this approach could work perfectly well. For the more complex solutions, I would suggest considering a more scalable solution though.

Why should I bother anyway?

If the presented code is doing a very similar job to what we can accomplish with preprocessors such as Sass, why should we bother at all? Are custom properties any better? The answer, as usually, is: it depends. An advantage of using Sass is better browser support. However, using custom properties has a few perks too:

  1. It’s plain CSS. In other words, it’s a more standardized, dependable solution, independent from any third parties. No compiling, no package versions, no weird issues. It just works (apart from the browsers where it just doesn’t work).
  2. It’s easier to debug. That’s a questionable one, as one may argue that Sass provides feedback through console messages and CSS does not. However, you can’t view and debug preprocessed code directly in a browser, whilst working with CSS variables, all the code is available (and live!) directly in DevTools.
  3. It’s more maintainable. Custom properties allow us to do things simply impossible with any preprocessor. It allows us to make our variables more contextual and, therefore, more maintainable. Plus, they are selectable by JavaScript, something Sass variables are not.
  4. It’s more flexible. Notice, that the grid system we’ve built is extremely flexible. Would you like to use a 12-column grid on one page and a 15-column grid on another? Be my guest—it’s a matter of a single variable. The same code can be used on both pages. A preprocessor would require generating code for two separate grid systems.
  5. It takes less space. As long as the weight of CSS files is usually not the main bottleneck of page load performance, it still goes without saying that we should aim to optimize CSS files when possible. To give a better image of how much can be saved, I made a little experiment. I took the grid system from Bootstrap and rebuilt it from scratch with custom properties. The results are as follows: the basic configuration of the Bootstrap grid generates over 54KB of CSS whilst a similar grid made with custom properties is a mere 3KB. That’s a 94% difference! What is more, adding more columns to the Bootstrap grid makes the file even bigger. With CSS variables, we can use as many columns as we want without affecting the file size at all.

The files can be compressed to minimize the difference a bit. The gzipped Bootstrap grid takes 6.4KB in comparison to 0.9KB for the custom properties grid. This is still an 86% difference!

Performance of CSS variables

Summing up, using CSS custom properties has a lot of advantages. But, if we are making the browser do all the calculations that had been done by preprocessors, are we negatively affecting the performance of our site? It’s true that using custom properties and calc() functions will use more computing power. However, in cases similar to the examples we discussed in this article, the difference will usually be unnoticeable. If you’d like to learn more about this topic, I would recommend reading this excellent article by Lisi Linhart.

Not only grid systems

After all, understanding the ins and outs of custom properties may not be as easy as it seems. It will definitely take time, but it’s worth it. CSS variables can be a huge help when working on reusable components, design systems, theming and customizable solutions. Knowing how to deal with fallback values and undeclared variables may turn out to be very handy then.

Thanks for reading and good luck on your own journey with CSS custom properties!

The post Responsive Designs and CSS Custom Properties: Building a Flexible Grid System appeared first on CSS-Tricks.

CSS-Tricks

, , , , , , ,
[Top]