Tag: Building

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

, , , , , , ,

Building Responsive WordPress Forms

Within the arsenal of every WordPress developer exists a toolbox of plugins used to implement key features on a website. Forms, up until now, have been a point of contention for most developers, given that no form plugins have offered seamless integration with existing website code. Therefore, forms often become an alien chunk of code requiring custom and time-consuming stylization.

Now there’s a solution: WS Form

WS Form is a developer-focused WordPress form plugin, which outputs framework-ready, responsive HTML5 code. It allows you to rapidly create forms using an innovative layout editor and a plethora of development features.

Front-End, Framework-Compatible HTML from a Layout Editor

If you’re developing or implementing a theme using Bootstrap (versions 3 & 4) or Foundation (versions 5, 6 & 6.4+), WS Form will output code that is native to those frameworks. For themes that do not use those frameworks, a fallback framework is included that is fully responsive and easy for developers to style.

The WS Form layout editor allows you to edit your form at any breakpoint. Form elements are dragged and dropped into the form, and all responsive CSS classes are handled for you. For developers wanting additional control, each field type comes with a vast array of settings, including the ability to add your own wrapper and field-level classes.

And within WS Form, time travel is real. The undo history feature allows you to step back to any point in your form development and continue from that point forward.

Introducing the First Form Debug Console

WS Form is the first WordPress form plugin to offer a dedicated debug console for developers.

A time-consuming task, when developing any form, is having to repeatedly populate a form to test it. WS Form is the first WordPress form plugin to offer the ability to automatically populate a form. Simply click “Populate” in the debug console, and the form will be pre-populated with different sample data each time. This dramatically speeds up development time, particularly with larger, multi-tab forms.

The console provides per form instance activity and error logging, as well as the ability to reload a form while still on the same web page.

Extensive HTML5 Input Type Support

WS Form includes settings for all form input types. Settings include everything from default values and placeholder text to custom validation messages and datalists. In addition to elementary HTML5 input types, WS Form offers additional fields, such as reCAPTCHA, signatures, and even e-commerce and payment buttons.

Some HTML5 input types, such as date and color selectors, are still not supported in all web browsers. WS Form overcomes this obstacle by checking for native support, and if unavailable, a suitable alternative component is loaded. You have the option of loading that component from your web server or from a CDN.

See the Field Types

Limitless Conditional Logic

Conditional logic allows you to make a form interactive and improve usability. For example, you could opt to only show shipping address fields if a checkbox is checked, or you could show an error message if a password confirmation does not match.

WS Form comes with an extensive array of options when creating if, then, and else conditions at form, tab, section, and field levels. Furthermore, conditional options are context sensitive, so, for example, color fields allow you to fire behavior if the hue or lightness of that field matches specified conditions. WS Form even allows you to fire actions, such as sending an email or showing a message, if any condition is met. This could be useful for automatically saving a form as a user steps through tabs on a form.

An Ever-Expanding Library of Form Actions

WS Form actions are fired whenever a form is saved or submitted by a user. Actions can also be fired using conditional logic.

The actions include:

  • Sending emails
  • Showing messages (e.g., a thank you message)
  • Running JavaScript
  • Firing a WordPress hook (actions or filters)
  • Initiating WordPress GDPR functionality, such as a data export or erasure request
  • Redirecting

See the Actions

Try it Today

Building a WordPress form in WS Form means you can rapidly prototype and implement forms. With responsive HTML5 code, automatic framework compatibility, and advanced conditional logic, just to name a few of the features, WS Form is changing the way WordPress forms can enhance and empower a website.

Use coupon code CSST20 to receive 20% off any WS Form PRO product!

Try a Demo

The post Building Responsive WordPress Forms appeared first on CSS-Tricks.

CSS-Tricks

, , ,
[Top]

Building Responsive WordPress Forms

(This is a sponsored post.)

Within the arsenal of every WordPress developer exists a toolbox of plugins used to implement key features on a website. Forms, up until now, have been a point of contention for most developers, given that no form plugins have offered seamless integration with existing website code. Therefore, forms often become an alien chunk of code requiring custom and time-consuming stylization.

Now there’s a solution: WS Form

WS Form is a developer-focused WordPress form plugin, which outputs framework-ready, responsive HTML5 code. It allows you to rapidly create forms using an innovative layout editor and a plethora of development features.

Front-End, Framework-Compatible HTML from a Layout Editor

If you’re developing or implementing a theme using Bootstrap (versions 3 & 4) or Foundation (versions 5, 6 & 6.4+), WS Form will output code that is native to those frameworks. For themes that do not use those frameworks, a fallback framework is included that is fully responsive and easy for developers to style.

The WS Form layout editor allows you to edit your form at any breakpoint. Form elements are dragged and dropped into the form, and all responsive CSS classes are handled for you. For developers wanting additional control, each field type comes with a vast array of settings, including the ability to add your own wrapper and field-level classes.

And within WS Form, time travel is real. The undo history feature allows you to step back to any point in your form development and continue from that point forward.

Introducing the First Form Debug Console

WS Form is the first WordPress form plugin to offer a dedicated debug console for developers.

A time-consuming task, when developing any form, is having to repeatedly populate a form to test it. WS Form is the first WordPress form plugin to offer the ability to automatically populate a form. Simply click “Populate” in the debug console, and the form will be pre-populated with different sample data each time. This dramatically speeds up development time, particularly with larger, multi-tab forms.

The console provides per form instance activity and error logging, as well as the ability to reload a form while still on the same web page.

Extensive HTML5 Input Type Support

WS Form includes settings for all form input types. Settings include everything from default values and placeholder text to custom validation messages and datalists. In addition to elementary HTML5 input types, WS Form offers additional fields, such as reCAPTCHA, signatures, and even e-commerce and payment buttons.

Some HTML5 input types, such as date and color selectors, are still not supported in all web browsers. WS Form overcomes this obstacle by checking for native support, and if unavailable, a suitable alternative component is loaded. You have the option of loading that component from your web server or from a CDN.

See the Field Types

Limitless Conditional Logic

Conditional logic allows you to make a form interactive and improve usability. For example, you could opt to only show shipping address fields if a checkbox is checked, or you could show an error message if a password confirmation does not match.

WS Form comes with an extensive array of options when creating if, then, and else conditions at form, tab, section, and field levels. Furthermore, conditional options are context sensitive, so, for example, color fields allow you to fire behavior if the hue or lightness of that field matches specified conditions. WS Form even allows you to fire actions, such as sending an email or showing a message, if any condition is met. This could be useful for automatically saving a form as a user steps through tabs on a form.

An Ever-Expanding Library of Form Actions

WS Form actions are fired whenever a form is saved or submitted by a user. Actions can also be fired using conditional logic.

The actions include:

  • Sending emails
  • Showing messages (e.g., a thank you message)
  • Running JavaScript
  • Firing a WordPress hook (actions or filters)
  • Initiating WordPress GDPR functionality, such as a data export or erasure request
  • Redirecting

See the Actions

Try it Today

Building a WordPress form in WS Form means you can rapidly prototype and implement forms. With responsive HTML5 code, automatic framework compatibility, and advanced conditional logic, just to name a few of the features, WS Form is changing the way WordPress forms can enhance and empower a website.

Use coupon code CSST20 to receive 20% off any WS Form PRO product!

Try a Demo

Direct Link to ArticlePermalink

The post Building Responsive WordPress Forms appeared first on CSS-Tricks.

CSS-Tricks

, , ,
[Top]

Building a Donut Chart with Vue and SVG

Mmm… forbidden donut.”

– Homer Simpson

I recently needed to make a donut chart for a reporting dashboard at work. The mock-up that I got looked something like this:

My chart had a few basic requirements. It needed to:

  • Dynamically calculate its segments based on an arbitrary set of values
  • Have labels
  • Scale well across all screen sizes and devices
  • Be cross-browser compatible back to Internet Explorer 11
  • Be accessible
  • Be reusable across my work’s Vue.js front end

I also wanted something that I could animate later if I needed to. All of this sounded like a job for SVG.

SVGs are accessible out-of-the-box (the W3C has a whole section on this) and can be made more accessible through additional input. And, because they’re powered by data, they’re a perfect candidate for dynamic visualization.

There are plenty of articles on the topic, including two by Chris (here and here) and a super recent one by Burke Holland. I didn’t use D3 for this project because the application didn’t need the overhead of that library.

I created the chart as a Vue component for my project, but you could just as easily do this with vanilla JavaScript, HTML, and CSS.

Here’s the finished product:

See the Pen Vue Donut Chart – Final Version by Salomone Baquis (@soluhmin) on CodePen.

Reinventing the wheel circle

Like any self-respecting developer, the first thing I did was Google to see if someone else had already made this. Then, like same said developer, I scrapped the pre-built solution in favor of my own.

The top hit for “SVG donut chart” is this article, which describes how to use stroke-dasharray and stroke-dashoffset to draw multiple overlaid circles and create the illusion of a single segmented circle (more on this shortly).

I really like the overlay concept, but found recalculating both stroke-dasharray and stroke-dashoffset values confusing. Why not set one fixed stroke-dasharrary value and then rotate each circle with a transform? I also needed to add labels to each segment, which wasn’t covered in the tutorial.

Drawing a line

Before we can create a dynamic donut chart, we first need to understand how SVG line drawing works. If you haven’t read Jake Archibald’s excellent Animated Line Drawing in SVG. Chris also has a good overview.

Those articles provide most of the context you’ll need, but briefly, SVG has two presentation attributes: stroke-dasharray and stroke-dashoffset.

stroke-dasharray defines an array of dashes and gaps used to paint the outline of a shape. It can take zero, one, or two values. The first value defines the dash length; the second defines the gap length.

stroke-dashoffset, on the other hand, defines where the set of dashes and gaps begins. If the stroke-dasharray and the stroke-dashoffset values are the length of the line and equal, the entire line is visible because we’re telling the offset (where the dash-array starts) to begin at the end of the line. If the stroke-dasharray is the length of the line, but the stroke-dashoffset is 0, then the line is invisible because we’re offsetting the rendered part of the dash by its entire length.

Chris’ example demonstrates this nicely:

See the Pen Basic Example of SVG Line Drawing, Backward and Forward by Chris Coyier (@chriscoyier) on CodePen.

How we’ll build the chart

To create the donut chart’s segments, we’ll make a separate circle for each one, overlay the circles on top of one another, then use stroke, stroke-dasharray, and stroke-dashoffset to show only part of the stroke of each circle. We’ll then rotate each visible part into the correct position, creating the illusion of a single shape. As we do this, we’ll also calculate the coordinates for the text labels.

Here’s an example demonstrating these rotations and overlays:

See the Pen Circle Overlays by Salomone Baquis (@soluhmin) on CodePen.

Basic setup

Let’s start by setting up our structure. I’m using x-template for demo purposes, but I’d recommend creating a single file component for production.

<div id="app">   <donut-chart></donut-chart> </div> <script type="text/x-template" id="donutTemplate">   <svg height="160" width="160" viewBox="0 0 160 160">     <g v-for="(value, index) in initialValues">       <circle :cx="cx" :cy="cy" :r="radius" fill="transparent" :stroke="colors[index]" :stroke-width="strokeWidth" ></circle>       <text></text>     </g>   </svg> </script>
Vue.component('donutChart', {   template: '#donutTemplate',   props: ["initialValues"],   data() {     return {       chartData: [],       colors: ["#6495ED", "goldenrod", "#cd5c5c", "thistle", "lightgray"],       cx: 80,       cy: 80,                             radius: 60,       sortedValues: [],       strokeWidth: 30,         }   }   }) new Vue({   el: "#app",   data() {     return {       values: [230, 308, 520, 130, 200]     }   }, });

With this, we:

  • Create our Vue instance and our donut chart component, then tell our donut component to expect some values (our dataset) as props
  • Establish our basic SVG shapes: <circle> for the segments and <text> for the labels, with the basic dimensions, stroke width, and colors defined
  • Wrap these shapes in a <g> element, which groups them together
  • Add a v-for loop to the g> element, which we’ll use to iterate through each value that the component receives
  • Create an empty sortedValues array, which we’ll use to hold a sorted version of our data
  • Create an empty chartData array, which will contain our main positioning data

Circle length

Our stroke-dasharray should be the length of the entire circle, giving us an easy baseline number which we can use to calculate each stroke-dashoffset value. Recall that the length of a circle is its circumference and the formula for circumference is 2πr (you remember this, right?).

We can make this a computed property in our component.

computed: {   circumference() {     return 2 * Math.PI * this.radius   } }

…and bind the value to our template markup.

<svg height="160" width="160" viewBox="0 0 160 160">   <g v-for="(value, index) in initialValues">     <circle :cx="cx" :cy="cy" :r="radius" fill="transparent" :stroke="colors[index]" :stroke-width="strokeWidth" :stroke-dasharray="circumference" ></circle>     <text></text>   </g> </svg>

In the initial mockup, we saw that the segments went from largest to smallest. We can make another computed property to sort these. We’ll store the sorted version inside the sortedValues array.

sortInitialValues() {   return this.sortedValues = this.initialValues.sort((a,b) => b-a) }

Finally, in order for these sorted values to be available to Vue before the chart gets rendered, we’ll want to reference this computed property from the mounted() lifecycle hook.

mounted() {   this.sortInitialValues                 }

Right now, our chart look like this:

See the Pen Donut Chart – No Segments by Salomone Baquis (@soluhmin) on CodePen.

No segments. Just a solid-colored donut. Like HTML, SVG elements are rendered in the order that they appear in the markup. The color that appears is the stroke color of the last circle in the SVG. Because we haven’t added any stroke-dashoffset values yet, each circle’s stroke goes all the way around. Let’s fix this by creating segments.

Creating segments

To get each of the circle segments, we’ll need to:

  1. Calculate the percentage of each data value from the total data values that we pass in
  2. Multiply this percentage by the circumference to get the length of the visible stroke
  3. Subtract this length from the circumference to get the stroke-offset

It sounds more complicated than it is. Let’s start with some helper functions. We first need to total up our data values. We can use a computed property to do this.

dataTotal() {   return this.sortedValues.reduce((acc, val) => acc + val) },

To calculate the percentage of each data value, we’ll need to pass in values from the v-for loop that we created earlier, which means that we’ll need to add a method.

methods: {   dataPercentage(dataVal) {     return dataVal / this.dataTotal   } },

We now have enough information to calculate our stroke-offset values, which will establish our circle segments.

Again, we want to: (a) multiply our data percentage by the circle circumference to get the length of the visible stroke, and (b) subtract this length from the circumference to get the stroke-offset.

Here’s the method to get our stroke-offsets:

calculateStrokeDashOffset(dataVal, circumference) {   const strokeDiff = this.dataPercentage(dataVal) * circumference   return circumference - strokeDiff },

…which we bind to our circle in the HTML with:

:stroke-dashoffset="calculateStrokeDashOffset(value, circumference)"

And voilà! We should have something like this:

See the Pen Donut Chart – No Rotations by Salomone Baquis (@soluhmin) on CodePen.

Rotating segments

Now the fun part. All of segments begin at 3 o’clock, which is the default starting point for SVG circles. To get them in the right place, we need to rotate each segment to its correct position.

We can do this by finding each segment’s ratio out of 360 degrees and then offset that amount by the total degrees that came before it.

First, let’s add a data property to keep track of the offset:

angleOffset: -90,

Then our calculation (this is a computed property):

calculateChartData() {   this.sortedValues.forEach((dataVal, index) => {     const data = {       degrees: this.angleOffset,     }     this.chartData.push(data)     this.angleOffset = this.dataPercentage(dataVal) * 360 + this.angleOffset   }) },

Each loop creates a new object with a “degrees” property, pushes that into our chartValues array that we created earlier, and then updates the angleOffset for the next loop.

But wait, what’s up with the -90 value?

Well, looking back at our original mockup, the first segment is shown at the 12 o’clock position, or -90 degrees from the starting point. By setting our angleOffset at -90, we ensure that our largest donut segment starts from the top.

To rotate these segments in the HTML, we’ll use the transform presentation attribute with the rotate function. Let’s create another computed property so that we can return a nice, formatted string.

returnCircleTransformValue(index) {   return rotate($ {this.chartData[index].degrees}, $ {this.cx}, $ {this.cy})` },

The rotate function takes three arguments: an angle of rotation and x and y coordinates around which the angle rotates. If we don’t supply cx and cy coordinates, then our segments will rotate around the entire SVG coordinate system.

Next, we bind this to our circle markup.

:transform="returnCircleTransformValue(index)"

And, since we need to do all of these calculations before the chart is rendered, we’ll add our calculateChartData computed property in the mounted hook:

mounted() {   this.sortInitialValues   this.calculateChartData }

Finally, if we want that sweet, sweet gap between each segment, we can subtract two from the circumference and use this as our new stroke-dasharray.

adjustedCircumference() {   return this.circumference - 2 },
:stroke-dasharray="adjustedCircumference"

Segments, baby!

See the Pen Donut Chart – Segments Only by Salomone Baquis (@soluhmin) on CodePen.

Labels

We have our segments, but now we need to create labels. This means that we need to place our <text> elements with x and y coordinates at different points along the circle. You might suspect that this requires math. Sadly, you are correct.

Fortunately, this isn’t the kind of math where we need to apply Real Concepts; this is more the kind where we Google formulas and don’t ask too many questions.

According to the Internet, the formulas to calculate x and y points along a circle are:

x = r cos(t) + a y = r sin(t) + b

…where r is the radius, t is the angle, and a and b are the x and y center point offsets.

We already have most of this: we know our radius, we know how to calculate our segment angles, and we know our center offset values (cx and cy).

There’s one catch, though: in those formulas, t is in *radians*. We’re working in degrees, which means that we need to do some conversions. Again, a quick search turns up a formula:

radians = degrees * (π / 180)

…which we can represent in a method:

degreesToRadians(angle) {   return angle * (Math.PI / 180) },

We now have enough information to calculate our x and y text coordinates:

calculateTextCoords(dataVal, angleOffset) {   const angle = (this.dataPercentage(dataVal) * 360) / 2 + angleOffset   const radians = this.degreesToRadians(angle)    const textCoords = {     x: (this.radius * Math.cos(radians) + this.cx),     y: (this.radius * Math.sin(radians) + this.cy)   }   return textCoords },

First, we calculate the angle of our segment by multiplying the ratio of our data value by 360; however, we actually want half of this because our text labels are in the middle of the segment rather than the end. We need to add the angle offset like we did when we created the segments.

Our calculateTextCoords method can now be used in the calculateChartData computed property:

calculateChartData() {   this.sortedValues.forEach((dataVal, index) => {     const { x, y } = this.calculateTextCoords(dataVal, this.angleOffset)             const data = {       degrees: this.angleOffset,       textX: x,       textY: y     }     this.chartData.push(data)     this.angleOffset = this.dataPercentage(dataVal) * 360 + this.angleOffset   }) },

Let’s also add a method to return the label string:

percentageLabel(dataVal) {   return `$ {Math.round(this.dataPercentage(dataVal) * 100)}%` },

And, in the markup:

<text :x="chartData[index].textX" :y="chartData[index].textY">{{ percentageLabel(value) }}</text>

Now we’ve got labels:

See the Pen Donut Chart – Unformatted Labels by Salomone Baquis (@soluhmin) on CodePen.

Blech, so off-center. We can fix this with the text-anchor presentation attribute. Depending on your font and font-size, you may want to adjust the positioning as well. Check out dx and dy for this.

Revamped text element:

<text text-anchor="middle" dy="3px" :x="chartData[index].textX" :y="chartData[index].textY">{{ percentageLabel(value) }}</text>

Hmm, it seems that if we have small percentages, the labels go outside of the segments. Let’s add a method to check for this.

segmentBigEnough(dataVal) {   return Math.round(this.dataPercentage(dataVal) * 100) > 5 }
<text v-if="segmentBigEnough(value)" text-anchor="middle" dy="3px" :x="chartData[index].textX" :y="chartData[index].textY">{{ percentageLabel(value) }}</text>

Now, we’ll only add labels to segments larger than 5%.

And we’re done! We now have a reusable donut chart component that can accept any set of values and create segments. Super cool!

The finished product:

See the Pen Vue Donut Chart – Final Version by Salomone Baquis (@soluhmin) on CodePen.

Next steps

There are lots of ways that we can modify or improve this now that it’s built. For example:

  • Adding elements to enhance accessibility, such as <title> and <desc> tags, aria-labels, and aria role attributes.
  • Creating animations with CSS or libraries like Greensock to create eye-catching effects when the chart comes into view.
  • Playing with color schemes.

I’d love to hear what you think about this implementation and other experiences you’ve had with SVG charts. Share in the comments!

The post Building a Donut Chart with Vue and SVG appeared first on CSS-Tricks.

CSS-Tricks

, ,
[Top]