Tag: Tool

Make Your Own Dev Tool

Amber Wilson on making bookmarklets to help yo-self. She shows off one that injects an accessibility script — I like this approach, as it means you don’t have to maintain the bookmarklet, just the script it links to). Another example runs some code contained right in the link. The result is literally a bookmark in your browser you can click to do something that is useful to you on any site.

Well, I say “any” site, but what I mean is “sites that don’t have a Content Security Policy (CSP)” which is capable of totally blocking inline scripts (and that’s probably the best thing a CSP can do). That’s wonderful for security, but completely stops bookmarklets. The answer is browser extensions. The story with those is getting better as browsers converge on a standard format.

Browser extensions are much harder to write. Someone should make a browser extension that allows you to create arbitrary bookmarklet-ish snippets to run. I found a few attempts at this in a quick search, but nothing that looks particularly nice. Another thought: DevTools has snippets.

Direct Link to ArticlePermalink


The post Make Your Own Dev Tool appeared first on CSS-Tricks.

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

CSS-Tricks

Learn Z-Index Using a Visualization Tool

There are some neat interactive demos in here from Thiru Manikandan. There are a couple of very tricky things with z-index that never fail to confuse. In addition to things like requiring positioning and source order, the trickiest are the stacking contexts and parent/child relationships. z-index isn’t a flat playing field. Even if you put z-index: 2147483644¹ on an element, it’s possible nothing will happen because that element might be inside a parent element with its own stacking context and a lower z-index than a sibling or some higher-up level DOM element.

  1. Just three shy of the maximum 2147483647. LOLZ. Hat tip to Dan Danney who mentioned seeing that in the wild recently.

Direct Link to ArticlePermalink

The post Learn Z-Index Using a Visualization Tool appeared first on CSS-Tricks.

CSS-Tricks

, , , ,
[Top]

The Best Design System Tool is Slack

There’s a series questions I have struggled with for as long as I can remember. The questions have to do with how design systems work: Where should we document things? Do we make a separate app? Do we use a third-party tool to document our components? How should that tie into Figma or Sketch? What about written documentation? Should we invest a lot of time into making a giant Polaris-like wiki of how to build things?

The issue with all these tools and links and repositories is that it can become increasingly difficult to remember where to go for what kind of information. Designers should go here and engineers should go there — unless, of course, you’re an iOS engineer, then you need this special resource instead. It can be overwhelming and confusing for everyone that doesn’t live within the orbit of design systems drama and is just trying to ship a feature on time.

After years of struggling with these questions, I think my current advice to my past (and current) self is this: meet the people where they are. And where are most people asking questions about design systems, whether that’s a color variable or a component or a design pattern?

In Slack!

The other day I thought it would be neat to set up some Slackbot custom responses to do a rather simple thing. When someone types color me into a channel, I all the color variables and their hex values are pasted. That way, no one needs to learn a new tool or bookmark yet another link.

Here’s how it works.

We first have to open up the settings of the organization you’re in and click the “Customize” item in this dropdown:

That pops open a new tab with the “Customize your Workspace” settings. If you select “Slackbot” from the options, then you can then see all of the custom responses that have been set up already. From there, we can create a new response like this:

That n is what breaks things onto a new line so that I can now test it out in a chat with myself once I’ve saved this:

Because this takes up so much darn space, I also made separate answers for each color, like blue and purple. But all of this has me wondering: how else can we use Slack — or whatever chat app or communication tool — to extend the cause of good design systems work?

I bet there’s a ton of other things we can do to improve our lives within tools like this and make design systems work even easier.

The post The Best Design System Tool is Slack appeared first on CSS-Tricks.

CSS-Tricks

, , , ,
[Top]

Build a Node.js Tool to Record and Compare Google Lighthouse Reports

In this tutorial, I’ll show you step by step how to create a simple tool in Node.js to run Google Lighthouse audits via the command line, save the reports they generate in JSON format and then compare them so web performance can be monitored as the website grows and develops.

I’m hopeful this can serve as a good introduction for any developer interested in learning about how to work with Google Lighthouse programmatically.

But first, for the uninitiated…

What is Google Lighthouse?

Google Lighthouse is one of the best-automated tools available on a web developer’s utility belt. It allows you to quickly audit a website in a number of key areas which together can form a measure of its overall quality. These are:

  • Performance
  • Accessibility
  • Best Practices
  • SEO
  • Progressive Web App

Once the audit is complete, a report is then generated on what your website does well… and not so well, with the latter intending to serve as an indicator for what your next steps should be to improve the page.

Here’s what a full report looks like. 

Along with other general diagnostics and web performance metrics, a really useful feature of the report is that each of the key areas is aggregated into color-coded scores between 0-100.

Not only does this allow developers to quickly gauge the quality of a website without further analysis, but it also allows non-technical folk such as stakeholders or clients to understand as well.

For example, this means, it’s much easier to share the win with Heather from marketing after spending time improving website accessibility as she’s more able to appreciate the effort after seeing the Lighthouse accessibility score go up 50 points into the green.

But equally, Simon the project manager may not understand what Speed Index or First Contentful Paint means, but when he sees the Lighthouse report showing website performance score knee deep in the red, he knows you still have work to do.

If you’re in Chrome or the latest version of Edge, you can run a Lighthouse audit for yourself right now using DevTools. Here’s how:

You can also run a Lighthouse audit online via PageSpeed Insights or through popular performance tools, such as WebPageTest.

However, today, we’re only interested in Lighthouse as a Node module, as this allows us to use the tool programmatically to audit, record and compare web performance metrics.

Let’s find out how.

Setup

First off, if you don’t already have it, you’re going to need Node.js. There are a million different ways to install it. I use the Homebrew package manager, but you can also download an installer straight from the Node.js website if you prefer. This tutorial was written with Node.js v10.17.0 in mind, but will very likely work just fine on the most versions released in the last few years.

You’re also going to need Chrome installed, as that’s how we’ll be running the Lighthouse audits.

Next, create a new directory for the project and then cd into it in the console. Then run npm init to begin to create a package.json file. At this point, I’d recommend just bashing the Enter key over and over to skip as much of this as possible until the file is created.

Now, let’s create a new file in the project directory. I called mine lh.js, but feel free to call it whatever you want. This will contain all of JavaScript for the tool. Open it in your text editor of choice, and for now, write a console.log statement.

console.log('Hello world');

Then in the console, make sure your CWD (current working directory) is your project directory and run node lh.js, substituting my file name for whatever you’ve used.

You should see:

$  node lh.js Hello world

If not, then check your Node installation is working and you’re definitely in the correct project directory.

Now that’s out of the way, we can move on to developing the tool itself.

Opening Chrome with Node.js

Let’s install our project’s first dependency: Lighthouse itself.

npm install lighthouse --save-dev

This creates a node_modules directory that contains all of the package’s files. If you’re using Git, the only thing you’ll want to do with this is add it to your .gitignore file.

In lh.js, you’ll next want to delete the test console.log() and import the Lighthouse module so you can use it in your code. Like so:

const lighthouse = require('lighthouse');

Below it, you’ll also need to import a module called chrome-launcher, which is one of Lighthouse’s dependencies and allows Node to launch Chrome by itself so the audit can be run.

const lighthouse = require('lighthouse'); const chromeLauncher = require('chrome-launcher');

Now that we have access to these two modules, let’s create a simple script which just opens Chrome, runs a Lighthouse audit, and then prints the report to the console.

Create a new function that accepts a URL as a parameter. Because we’ll be running this using Node.js, we’re able to safely use ES6 syntax as we don’t have to worry about those pesky Internet Explorer users.

const launchChrome = (url) => {  }

Within the function, the first thing we need to do is open Chrome using the chrome-launcher module we imported and send it to whatever argument is passed through the url parameter. 

We can do this using its launch() method and its startingUrl option.

const launchChrome = url => {   chromeLauncher.launch({     startingUrl: url   }); };

Calling the function below and passing a URL of your choice results in Chrome being opened at the URL when the Node script is run.

launchChrome('https://www.lukeharrison.dev');

The launch function actually returns a promise, which allows us to access an object containing a few useful methods and properties.

For example, using the code below, we can open Chrome, print the object to the console, and then close Chrome three seconds later using its kill() method.

const launchChrome = url => {   chromeLauncher     .launch({       startingUrl: url     })     .then(chrome => {       console.log(chrome);       setTimeout(() => chrome.kill(), 3000);     }); };  launchChrome("https://www.lukeharrison.dev");

Now that we’ve got Chrome figured out, let’s move on to Lighthouse.

Running Lighthouse programmatically

First off, let’s rename our launchChrome() function to something more reflective of its final functionality: launchChromeAndRunLighthouse(). With the hard part out of the way, we can now use the Lighthouse module we imported earlier in the tutorial.

In the Chrome launcher’s then function, which only executes once the browser is open, we’ll pass Lighthouse the function’s url argument and trigger an audit of this website.

const launchChromeAndRunLighthouse = url => {   chromeLauncher     .launch({       startingUrl: url     })     .then(chrome => {       const opts = {         port: chrome.port       };       lighthouse(url, opts);     }); };  launchChromeAndRunLighthouse("https://www.lukeharrison.dev");

To link the lighthouse instance to our Chrome browser window, we have to pass its port along with the URL.

If you were to run this script now, you will hit an error in the console:

(node:47714) UnhandledPromiseRejectionWarning: Error: You probably have multiple tabs open to the same origin.

To fix this, we just need to remove the startingUrl option from Chrome Launcher and let Lighthouse handle URL navigation from here on out.

const launchChromeAndRunLighthouse = url => {   chromeLauncher.launch().then(chrome => {     const opts = {       port: chrome.port     };     lighthouse(url, opts);   }); };

If you were to execute this code, you’ll notice that something definitely seems to be happening. We just aren’t getting any feedback in the console to confirm the Lighthouse audit has definitely run, nor is the Chrome instance closing by itself like before.

Thankfully, the lighthouse() function returns a promise which lets us access the audit results.

Let’s kill Chrome and then print those results to the terminal in JSON format via the report property of the results object.

const launchChromeAndRunLighthouse = url => {   chromeLauncher.launch().then(chrome => {     const opts = {       port: chrome.port     };     lighthouse(url, opts).then(results => {       chrome.kill();       console.log(results.report);     });   }); };

While the console isn’t the best way to display these results, if you were to copy them to your clipboard and visit the Lighthouse Report Viewer, pasting here will show the report in all of its glory.

At this point, it’s important to tidy up the code a little to make the launchChromeAndRunLighthouse() function return the report once it’s finished executing. This allows us to process the report later without resulting in a messy pyramid of JavaScript.

const lighthouse = require("lighthouse"); const chromeLauncher = require("chrome-launcher");  const launchChromeAndRunLighthouse = url => {   return chromeLauncher.launch().then(chrome => {     const opts = {       port: chrome.port     };     return lighthouse(url, opts).then(results => {       return chrome.kill().then(() => results.report);     });   }); };  launchChromeAndRunLighthouse("https://www.lukeharrison.dev").then(results => {   console.log(results); });

One thing you may have noticed is that our tool is only able to audit a single website at the moment. Let’s change this so you can pass the URL as an argument via the command line.

To take the pain out of working with command-line arguments, we’ll handle them with a package called yargs.

npm install --save-dev yargs

Then import it at the top of your script along with Chrome Launcher and Lighthouse. We only need its argv function here.

const lighthouse = require('lighthouse'); const chromeLauncher = require('chrome-launcher'); const argv = require('yargs').argv;

This means if you were to pass a command line argument in the terminal like so:

node lh.js --url https://www.google.co.uk

…you can access the argument in the script like so:

const url = argv.url // https://www.google.co.uk

Let’s edit our script to pass the command line URL argument to the function’s url parameter. It’s important to add a little safety net via the if statement and error message in case no argument is passed.

if (argv.url) {   launchChromeAndRunLighthouse(argv.url).then(results => {     console.log(results);   }); } else {   throw "You haven't passed a URL to Lighthouse"; }

Tada! We have a tool that launches Chrome and runs a Lighthouse audit programmatically before printing the report to the terminal in JSON format.

Saving Lighthouse reports

Having the report printed to the console isn’t very useful as you can’t easily read its contents, nor are they aren’t saved for future use. In this section of the tutorial, we’ll change this behavior so each report is saved into its own JSON file.

To stop reports from different websites getting mixed up, we’ll organize them like so:

  • lukeharrison.dev
    • 2020-01-31T18:18:12.648Z.json
    • 2020-01-31T19:10:24.110Z.json
  • cnn.com
    • 2020-01-14T22:15:10.396Z.json
  • lh.js

We’ll name the reports with a timestamp indicating when the date/time the report was generated. This will mean no two report file names will ever be the same, and it’ll help us easily distinguish between reports.

There is one issue with Windows that requires our attention: the colon (:) is an illegal character for file names. To mitigate this issue, we’ll replace any colons with underscores (_), so a typical report filename will look like:

  • 2020-01-31T18_18_12.648Z.json

Creating the directory

First, we need to manipulate the command line URL argument so we can use it for the directory name.

This involves more than just removing the www, as it needs to account for audits run on web pages which don’t sit at the root (eg: www.foo.com/bar), as the slashes are invalid characters for directory names. 

For these URLs, we’ll replace the invalid characters with underscores again. That way, if you run an audit on https://www.foo.com/bar, the resulting directory name containing the report would be foo.com_bar.

To make dealing with URLs easier, we’ll use a native Node.js module called url. This can be imported like any other package and without having to add it to thepackage.json and pull it via npm.

const lighthouse = require('lighthouse'); const chromeLauncher = require('chrome-launcher'); const argv = require('yargs').argv; const url = require('url');

Next, let’s use it to instantiate a new URL object.

if (argv.url) {   const urlObj = new URL(argv.url);    launchChromeAndRunLighthouse(argv.url).then(results => {     console.log(results);   }); }

If you were to print urlObj to the console, you would see lots of useful URL data we can use.

$  node lh.js --url https://www.foo.com/bar URL {   href: 'https://www.foo.com/bar',   origin: 'https://www.foo.com',   protocol: 'https:',   username: '',   password: '',   host: 'www.foo.com',   hostname: 'www.foo.com',   port: '',   pathname: '/bar',   search: '',   searchParams: URLSearchParams {},   hash: '' }

Create a new variable called dirName, and use the string replace() method on the host property of our URL to get rid of the www in addition to the https protocol:

const urlObj = new URL(argv.url); let dirName = urlObj.host.replace('www.','');

We’ve used let here, which unlike const can be reassigned, as we’ll need to update the reference if the URL has a pathname, to replace slashes with underscores. This can be done with a regular expression pattern, and looks like this:

const urlObj = new URL(argv.url); let dirName = urlObj.host.replace("www.", ""); if (urlObj.pathname !== "/") {   dirName = dirName + urlObj.pathname.replace(///g, "_"); }

Now we can create the directory itself. This can be done through the use of another native Node.js module called fs (short for “file system”).

const lighthouse = require('lighthouse'); const chromeLauncher = require('chrome-launcher'); const argv = require('yargs').argv; const url = require('url'); const fs = require('fs');

We can use its mkdir() method to create a directory, but first have to use its existsSync() method to check if the directory already exists, as Node.js would otherwise throw an error:

const urlObj = new URL(argv.url); let dirName = urlObj.host.replace("www.", ""); if (urlObj.pathname !== "/") {   dirName = dirName + urlObj.pathname.replace(///g, "_"); } if (!fs.existsSync(dirName)) {   fs.mkdirSync(dirName); }

Testing the script at the point should result in a new directory being created. Passing https://www.bbc.co.uk/news as the URL argument would result in a directory named bbc.co.uk_news.

Saving the report

In the then function for launchChromeAndRunLighthouse(), we want to replace the existing console.log with logic to write the report to disk. This can be done using the fs module’s writeFile() method.

launchChromeAndRunLighthouse(argv.url).then(results => {   fs.writeFile("report.json", results, err => {     if (err) throw err;   }); });

The first parameter represents the file name, the second is the content of the file and the third is a callback containing an error object should something go wrong during the write process. This would create a new file called report.json containing the returning Lighthouse report JSON object.

We still need to send it to the correct directory, with a timestamp as its file name. The former is simple — we pass the dirName variable we created earlier, like so:

launchChromeAndRunLighthouse(argv.url).then(results => {   fs.writeFile(`$ {dirName}/report.json`, results, err => {     if (err) throw err;   }); });

The latter though requires us to somehow retrieve a timestamp of when the report was generated. Thankfully, the report itself captures this as a data point, and is stored as the fetchTime property. 

We just need to remember to swap any colons (:) for underscores (_) so it plays nice with the Windows file system.

launchChromeAndRunLighthouse(argv.url).then(results => {   fs.writeFile(     `$ {dirName}/$ {results["fetchTime"].replace(/:/g, "_")}.json`,     results,     err => {       if (err) throw err;     }   ); }); 

If you were to run this now, rather than a timestamped.json filename, instead you would likely see an error similar to:

UnhandledPromiseRejectionWarning: TypeError: Cannot read property 'replace' of undefined

This is happening because Lighthouse is currently returning the report in JSON format, rather than an object consumable by JavaScript.

Thankfully, instead of parsing the JSON ourselves, we can just ask Lighthouse to return the report as a regular JavaScript object instead.

This requires editing the below line from:

return chrome.kill().then(() => results.report);

…to:

return chrome.kill().then(() => results.lhr);

Now, if you rerun the script, the file will be named correctly. However, when opened, it’s only content will unfortunately be…

[object Object]

This is because we’ve now got the opposite problem as before. We’re trying to render a JavaScript object without stringifying it into a JSON object first.

The solution is simple. To avoid having to waste resources on parsing or stringifying this huge object, we can return both types from Lighthouse:

return lighthouse(url, opts).then(results => {   return chrome.kill().then(() => {     return {       js: results.lhr,       json: results.report     };   }); });

Then we can modify the writeFile instance to this:

fs.writeFile(   `$ {dirName}/$ {results.js["fetchTime"].replace(/:/g, "_")}.json`,   results.json,   err => {     if (err) throw err;   } );

Sorted! On completion of the Lighthouse audit, our tool should now save the report to a file with a unique timestamped filename in a directory named after the website URL.

This means reports are now much more efficiently organized and won’t override each other no matter how many reports are saved.

Comparing Lighthouse reports

During everyday development, when I’m focused on improving performance, the ability to very quickly compare reports directly in the console and see if I’m headed in the right direction could be extremely useful. With this in mind, the requirements of this compare functionality ought to be:

  1. If a previous report already exists for the same website when a Lighthouse audit is complete, automatically perform a comparison against it and show any changes to key performance metrics.
  2. I should also be able to compare key performance metrics from any two reports, from any two websites, without having to generate a new Lighthouse report which I may not need.

What parts of a report should be compared? These are the numerical key performance metrics collected as part of any Lighthouse report. They provide insight into the objective and perceived performance of a website.

In addition, Lighthouse also collects other metrics that aren’t listed in this part of the report but are still in an appropriate format to be included in the comparison. These are:

  • Time to first byte – Time To First Byte identifies the time at which your server sends a response.
  • Total blocking time – Sum of all time periods between FCP and Time to Interactive, when task length exceeded 50ms, expressed in milliseconds.
  • Estimated input latency – Estimated Input Latency is an estimate of how long your app takes to respond to user input, in milliseconds, during the busiest 5s window of page load. If your latency is higher than 50ms, users may perceive your app as laggy.

How should the metric comparison be output to the console? We’ll create a simple percentage-based comparison using the old and new metrics to see how they’ve changed from report to report.

To allow for quick scanning, we’ll also color-code individual metrics depending on if they’re faster, slower or unchanged.

We’ll aim for this output:

First Contentful Paint is 0.49% slower First Meaningful Paint is 0.47% slower Speed Index is 12.92% slower Estimated Input Latency is the same Total Blocking Time is 85.71% faster Max Potential First Input Delay is 10.53% faster Time to first byte is 19.89% slower First CPU Idle is 0.47% slower Time to Interactive is 0.02% slower

Compare the new report against the previous report

Let’s get started by creating a new function called compareReports() just below our launchChromeAndRunLighthouse() function, which will contain all the comparison logic. We’ll give it two parameters —from and to  — to accept the two reports used for the comparison.

For now, as a placeholder, we’ll just print out some data from each report to the console to validate that it’s receiving them correctly.

const compareReports = (from, to) => {   console.log(from["finalUrl"] + " " + from["fetchTime"]);   console.log(to["finalUrl"] + " " + to["fetchTime"]); };

As this comparison would begin after the creation of a new report, the logic to execute this function should sit in the then function for launchChromeAndRunLighthouse().

If, for example, you have 30 reports sitting in a directory, we need to determine which one is the most recent and set it as the previous report which the new one will be compared against. Thankfully, we already decided to use a timestamp as the filename for a report, so this gives us something to work with.

First off, we need to collect any existing reports. To make this process easy, we’ll install a new dependency called glob, which allows for pattern matching when searching for files. This is critical because we can’t predict how many reports will exist or what they’ll be called.

Install it like any other dependency:

npm install glob --save-dev

Then import it at the top of the file the same way as usual:

const lighthouse = require('lighthouse'); const chromeLauncher = require('chrome-launcher'); const argv = require('yargs').argv; const url = require('url'); const fs = require('fs'); const glob = require('glob');

We’ll use glob to collect all of the reports in the directory, which we already know the name of via the dirName variable. It’s important to set its sync option to true as we don’t want JavaScript execution to continue until we know how many other reports exist.

launchChromeAndRunLighthouse(argv.url).then(results => {   const prevReports = glob(`$ {dirName}/*.json`, {     sync: true   });    // et al  });

This process returns an array of paths. So if the report directory looked like this:

  • lukeharrison.dev
    • 2020-01-31T10_18_12.648Z.json
    • 2020-01-31T10_18_24.110Z.json

…then the resulting array would look like this:

[  'lukeharrison.dev/2020-01-31T10_18_12.648Z.json',  'lukeharrison.dev/2020-01-31T10_18_24.110Z.json' ]

Because we can only perform a comparison if a previous report exists, let’s use this array as a conditional for the comparison logic:

const prevReports = glob(`$ {dirName}/*.json`, {   sync: true });  if (prevReports.length) { }

We have a list of report file paths and we need to compare their timestamped filenames to determine which one is the most recent.

This means we first need to collect a list of all the file names, trim any irrelevant data such as directory names, and taking care to replace the underscores (_) back with colons (:) to turn them back into valid dates again. The easiest way to do this is using path, another Node.js native module.

const path = require('path');

Passing the path as an argument to its parse method, like so:

path.parse('lukeharrison.dev/2020-01-31T10_18_24.110Z.json');

Returns this useful object:

{   root: '',   dir: 'lukeharrison.dev',   base: '2020-01-31T10_18_24.110Z.json',   ext: '.json',   name: '2020-01-31T10_18_24.110Z' }

Therefore, to get a list of all the timestamp file names, we can do this:

if (prevReports.length) {   dates = [];   for (report in prevReports) {     dates.push(       new Date(path.parse(prevReports[report]).name.replace(/_/g, ":"))     );   } }

Which again if our directory looked like:

  • lukeharrison.dev
    • 2020-01-31T10_18_12.648Z.json
    • 2020-01-31T10_18_24.110Z.json

Would result in:

[  '2020-01-31T10:18:12.648Z',  '2020-01-31T10:18:24.110Z' ]

A useful thing about dates is that they’re inherently comparable by default:

const alpha = new Date('2020-01-31'); const bravo = new Date('2020-02-15');  console.log(alpha > bravo); // false console.log(bravo > alpha); // true

So by using a reduce function, we can reduce our array of dates down until only the most recent remains:

dates = []; for (report in prevReports) {   dates.push(new Date(path.parse(prevReports[report]).name.replace(/_/g, ":"))); } const max = dates.reduce(function(a, b) {   return Math.max(a, b); });

If you were to print the contents of max to the console, it would throw up a UNIX timestamp, so now, we just have to add another line to convert our most recent date back into the correct ISO format:

const max = dates.reduce(function(a, b) {  return Math.max(a, b); }); const recentReport = new Date(max).toISOString();

Assuming these are the list of reports:

  • 2020-01-31T23_24_41.786Z.json
  • 2020-01-31T23_25_36.827Z.json
  • 2020-01-31T23_37_56.856Z.json
  • 2020-01-31T23_39_20.459Z.json
  • 2020-01-31T23_56_50.959Z.json

The value of recentReport would be 2020-01-31T23:56:50.959Z.

Now that we know the most recent report, we next need to extract its contents. Create a new variable called recentReportContents beneath the recentReport variable and assign it an empty function.

As we know this function will always need to execute, rather than manually calling it, it makes sense to turn it into an IFFE (Immediately invoked function expression), which will run by itself when the JavaScript parser reaches it. This is signified by the extra parenthesis:

const recentReportContents = (() => {  })();

In this function, we can return the contents of the most recent report using the readFileSync() method of the native fs module. Because this will be in JSON format, it’s important to parse it into a regular JavaScript object.

const recentReportContents = (() => {   const output = fs.readFileSync(     dirName + "/" + recentReport.replace(/:/g, "_") + ".json",     "utf8",     (err, results) => {       return results;     }   );   return JSON.parse(output); })();

And then, it’s a matter of calling the compareReports() function and passing both the current report and the most recent report as arguments.

compareReports(recentReportContents, results.js);

At the moment this just print out a few details to the console so we can test the report data is coming through OK:

https://www.lukeharrison.dev/ 2020-02-01T00:25:06.918Z https://www.lukeharrison.dev/ 2020-02-01T00:25:42.169Z

If you’re getting any errors at this point, try deleting any report.json files or reports without valid content from earlier in the tutorial.

Compare any two reports

The remaining key requirement was the ability to compare any two reports from any two websites. The easiest way to implement this would be to allow the user to pass the full report file paths as command line arguments which we’ll then send to the compareReports() function.

In the command line, this would look like:

node lh.js --from lukeharrison.dev/2020-02-01T00:25:06.918Z --to cnn.com/2019-12-16T15:12:07.169Z

Achieving this requires editing the conditional if statement which checks for the presence of a URL command line argument. We’ll add an additional check to see if the user has just passed a from and to path, otherwise check for the URL as before. This way we’ll prevent a new Lighthouse audit.

if (argv.from && argv.to) {  } else if (argv.url) {  // et al }

Let’s extract the contents of these JSON files, parse them into JavaScript objects, and then pass them along to the compareReports() function. 

We’ve already parsed JSON before when retrieving the most recent report. We can just extrapolate this functionality into its own helper function and use it in both locations.

Using the recentReportContents() function as a base, create a new function called getContents() which accepts a file path as an argument. Make sure this is just a regular function, rather than an IFFE, as we don’t want it executing as soon as the JavaScript parser finds it.

const getContents = pathStr => {   const output = fs.readFileSync(pathStr, "utf8", (err, results) => {     return results;   });   return JSON.parse(output); };  const compareReports = (from, to) => {   console.log(from["finalUrl"] + " " + from["fetchTime"]);   console.log(to["finalUrl"] + " " + to["fetchTime"]); };

Then update the recentReportContents() function to use this extrapolated helper function instead:

const recentReportContents = getContents(dirName + '/' + recentReport.replace(/:/g, '_') + '.json');

Back in our new conditional, we need to pass the contents of the comparison reports to the compareReports() function.

if (argv.from && argv.to) {   compareReports(     getContents(argv.from + ".json"),     getContents(argv.to + ".json")   ); }

Like before, this should print out some basic information about the reports in the console to let us know it’s all working fine.

node lh.js --from lukeharrison.dev/2020-01-31T23_24_41.786Z --to lukeharrison.dev/2020-02-01T11_16_25.221Z

Would lead to:

https://www.lukeharrison.dev/ 2020-01-31T23_24_41.786Z https://www.lukeharrison.dev/ 2020-02-01T11_16_25.221Z

Comparison logic

This part of development involves building comparison logic to compare the two reports received by the compareReports() function. 

Within the object which Lighthouse returns, there’s a property called audits that contains another object listing performance metrics, opportunities, and information. There’s a lot of information here, much of which we aren’t interested in for the purposes of this tool.

Here’s the entry for First Contentful Paint, one of the nine performance metrics we wish to compare:

"first-contentful-paint": {   "id": "first-contentful-paint",   "title": "First Contentful Paint",   "description": "First Contentful Paint marks the time at which the first text or image is painted. [Learn more](https://web.dev/first-contentful-paint).",   "score": 1,   "scoreDisplayMode": "numeric",   "numericValue": 1081.661,   "displayValue": "1.1 s" }

Create an array listing the keys of these nine performance metrics. We can use this to filter the audit object:

const compareReports = (from, to) => {   const metricFilter = [     "first-contentful-paint",     "first-meaningful-paint",     "speed-index",     "estimated-input-latency",     "total-blocking-time",     "max-potential-fid",     "time-to-first-byte",     "first-cpu-idle",     "interactive"   ]; };

Then we’ll loop through one of the report’s audits object and then cross-reference its name against our filter list. (It doesn’t matter which audit object, as they both have the same content structure.)

If it’s in there, then brilliant, we want to use it.

const metricFilter = [   "first-contentful-paint",   "first-meaningful-paint",   "speed-index",   "estimated-input-latency",   "total-blocking-time",   "max-potential-fid",   "time-to-first-byte",   "first-cpu-idle",   "interactive" ];  for (let auditObj in from["audits"]) {   if (metricFilter.includes(auditObj)) {     console.log(auditObj);   } }

This console.log() would print the below keys to the console:

first-contentful-paint first-meaningful-paint speed-index estimated-input-latency total-blocking-time max-potential-fid time-to-first-byte first-cpu-idle interactive

Which means we would use from['audits'][auditObj].numericValue and to['audits'][auditObj].numericValue respectively in this loop to access the metrics themselves.

If we were to print these to the console with the key, it would result in output like this:

first-contentful-paint 1081.661 890.774 first-meaningful-paint 1081.661 954.774 speed-index 15576.70313351777 1098.622294504341 estimated-input-latency 12.8 12.8 total-blocking-time 59 31.5 max-potential-fid 153 102 time-to-first-byte 16.859999999999985 16.096000000000004 first-cpu-idle 1704.8490000000002 1918.774 interactive 2266.2835 2374.3615

We have all the data we need now. We just need to calculate the percentage difference between these two values and then log it to the console using the color-coded format outlined earlier.

Do you know how to calculate the percentage change between two values? Me neither. Thankfully, everybody’s favorite monolith search engine came to the rescue.

The formula is:

((From - To) / From) x 100

So, let’s say we have a Speed Index of 5.7s for the first report (from), and then a value of 2.1s for the second (to). The calculation would be:

5.7 - 2.1 = 3.6 3.6 / 5.7 = 0.63157895 0.63157895 * 100 = 63.157895

Rounding to two decimal places would yield a decrease in the speed index of 63.16%.

Let’s put this into a helper function inside the compareReports() function, below the metricFilter array.

const calcPercentageDiff = (from, to) => {   const per = ((to - from) / from) * 100;   return Math.round(per * 100) / 100; };

Back in our auditObj conditional, we can begin to put together the final report comparison output.

First off, use the helper function to generate the percentage difference for each metric.

for (let auditObj in from["audits"]) {   if (metricFilter.includes(auditObj)) {     const percentageDiff = calcPercentageDiff(       from["audits"][auditObj].numericValue,       to["audits"][auditObj].numericValue     );   } }

Next, we need to output values in this format to the console:

First Contentful Paint is 0.49% slower First Meaningful Paint is 0.47% slower Speed Index is 12.92% slower Estimated Input Latency is the same Total Blocking Time is 85.71% faster Max Potential First Input Delay is 10.53% faster Time to first byte is 19.89% slower First CPU Idle is 0.47% slower Time to Interactive is 0.02% slower

This requires adding color to the console output. In Node.js, this can be done by passing a color code as an argument to the console.log() function like so:

console.log('x1b[36m', 'hello') // Would print 'hello' in cyan

You can get a full reference of color codes in this Stackoverflow question.  We need green and red, so that’s x1b[32m and x1b[31m respectively. For metrics where the value remains unchanged, we’ll just use white. This would be x1b[37m.

Depending on if the percentage increase is a positive or negative number, the following things need to happen:

  • Log color needs to change (Green for negative, red for positive, white for unchanged)
  • Log text contents change.
    • ‘[Name] is X% slower for positive numbers
    • ‘[Name] is X% faster’ for negative numbers
    • ‘[Name] is unchanged’ for numbers with no percentage difference.
  • If the number is negative, we want to remove the minus/negative symbol, as otherwise, you’d have a sentence like ‘Speed Index is -92.95% faster’ which doesn’t make sense.

There are many ways this could be done. Here, we’ll use the Math.sign() function, which returns 1 if its argument is positive, 0 if well… 0, and -1 if the number is negative. That’ll do.

for (let auditObj in from["audits"]) {   if (metricFilter.includes(auditObj)) {     const percentageDiff = calcPercentageDiff(       from["audits"][auditObj].numericValue,       to["audits"][auditObj].numericValue     );      let logColor = "x1b[37m";     const log = (() => {       if (Math.sign(percentageDiff) === 1) {         logColor = "x1b[31m";         return `$ {percentageDiff + "%"} slower`;       } else if (Math.sign(percentageDiff) === 0) {         return "unchanged";       } else {         logColor = "x1b[32m";         return `$ {percentageDiff + "%"} faster`;       }     })();     console.log(logColor, `$ {from["audits"][auditObj].title} is $ {log}`);   } }

So, there we have it.

You can create new Lighthouse reports, and if a previous one exists, a comparison is made.

And you can also compare any two reports from any two sites.

Complete source code

Here’s the completed source code for the tool, which you can also view in a Gist via the link below.

const lighthouse = require("lighthouse"); const chromeLauncher = require("chrome-launcher"); const argv = require("yargs").argv; const url = require("url"); const fs = require("fs"); const glob = require("glob"); const path = require("path");  const launchChromeAndRunLighthouse = url => {   return chromeLauncher.launch().then(chrome => {     const opts = {       port: chrome.port     };     return lighthouse(url, opts).then(results => {       return chrome.kill().then(() => {         return {           js: results.lhr,           json: results.report         };       });     });   }); };  const getContents = pathStr => {   const output = fs.readFileSync(pathStr, "utf8", (err, results) => {     return results;   });   return JSON.parse(output); };  const compareReports = (from, to) => {   const metricFilter = [     "first-contentful-paint",     "first-meaningful-paint",     "speed-index",     "estimated-input-latency",     "total-blocking-time",     "max-potential-fid",     "time-to-first-byte",     "first-cpu-idle",     "interactive"   ];    const calcPercentageDiff = (from, to) => {     const per = ((to - from) / from) * 100;     return Math.round(per * 100) / 100;   };    for (let auditObj in from["audits"]) {     if (metricFilter.includes(auditObj)) {       const percentageDiff = calcPercentageDiff(         from["audits"][auditObj].numericValue,         to["audits"][auditObj].numericValue       );        let logColor = "x1b[37m";       const log = (() => {         if (Math.sign(percentageDiff) === 1) {           logColor = "x1b[31m";           return `$ {percentageDiff.toString().replace("-", "") + "%"} slower`;         } else if (Math.sign(percentageDiff) === 0) {           return "unchanged";         } else {           logColor = "x1b[32m";           return `$ {percentageDiff.toString().replace("-", "") + "%"} faster`;         }       })();       console.log(logColor, `$ {from["audits"][auditObj].title} is $ {log}`);     }   } };  if (argv.from && argv.to) {   compareReports(     getContents(argv.from + ".json"),     getContents(argv.to + ".json")   ); } else if (argv.url) {   const urlObj = new URL(argv.url);   let dirName = urlObj.host.replace("www.", "");   if (urlObj.pathname !== "/") {     dirName = dirName + urlObj.pathname.replace(///g, "_");   }    if (!fs.existsSync(dirName)) {     fs.mkdirSync(dirName);   }    launchChromeAndRunLighthouse(argv.url).then(results => {     const prevReports = glob(`$ {dirName}/*.json`, {       sync: true     });      if (prevReports.length) {       dates = [];       for (report in prevReports) {         dates.push(           new Date(path.parse(prevReports[report]).name.replace(/_/g, ":"))         );       }       const max = dates.reduce(function(a, b) {         return Math.max(a, b);       });       const recentReport = new Date(max).toISOString();        const recentReportContents = getContents(         dirName + "/" + recentReport.replace(/:/g, "_") + ".json"       );        compareReports(recentReportContents, results.js);     }      fs.writeFile(       `$ {dirName}/$ {results.js["fetchTime"].replace(/:/g, "_")}.json`,       results.json,       err => {         if (err) throw err;       }     );   }); } else {   throw "You haven't passed a URL to Lighthouse"; }

View Gist

Next steps

With the completion of this basic Google Lighthouse tool, there’s plenty of ways to develop it further. For example:

  • Some kind of simple online dashboard that allows non-technical users to run Lighthouse audits and view metrics develop over time. Getting stakeholders behind web performance can be challenging, so something tangible they can interest with themselves could pique their interest.
  • Build support for performance budgets, so if a report is generated and performance metrics are slower than they should be, then the tool outputs useful advice on how to improve them (or calls you names).

Good luck!

The post Build a Node.js Tool to Record and Compare Google Lighthouse Reports appeared first on CSS-Tricks.

CSS-Tricks

, , , , , , ,
[Top]

A Handy Sass-Powered Tool for Making Balanced Color Palettes

For those who may not come from a design background, selecting a color palette is often based on personal preferences. Choosing colors might be done with an online color tool, sampling from an image, “borrowing” from favorite brands, or just sort of randomly picking from a color wheel until a palette “just feels right.”

Our goal is to better understand what makes a palette “feel right” by exploring key color attributes with Sass color functions. By the end, you will become more familiar with:

  • The value of graphing a palette’s luminance, lightness, and saturation to assist in building balanced palettes
  • The importance of building accessible contrast checking into your tools
  • Advanced Sass functions to extend for your own explorations, including a CodePen you can manipulate and fork

What you’ll ultimately find, however, is that color on the web is a battle of hardware versus human perception.

What makes color graphing useful

You may be familiar with ways of declaring colors in stylesheets, such as RGB and RGBA values, HSL and HSLA values, and HEX codes.

rbg(102,51,153) rbga(102,51,153, 0.6) hsl(270, 50%, 40%) hsla(270, 50%, 40%, 0.6) #663399

Those values give devices instructions on how to render color. Deeper attributes of a color can be exposed programmatically and leveraged to understand how a color relates to a broader palette.

The value of graphing color attributes is that we get a more complete picture of the relationship between colors. This reveals why a collection of colors may or may not feel right together. Graphing multiple color attributes helps hint at what adjustments can be made to create a more harmonious palette. We’ll look into examples of how to determine what to change in a later section.

Two useful measurements we can readily obtain using built-in Sass color functions are lightness and saturation.

  • Lightness refers to the mix of white or black with the color.
  • Saturation refers to the intensity of a color, with 100% saturation resulting in the purest color (no grey present).
$ color: rebeccapurple;  @debug lightness($ color); // 40%  @debug saturation($ color); // 50%;

However, luminance may arguably be the most useful color attribute. Luminance, as represented in our tool, is calculated using the WCAG formula which assumes an sRGB color space. Luminance is used in the contrast calculations, and as a grander concept, also aims to get closer to quantifying the human perception of relative brightness to assess color relationships. This means that a tighter luminance value range among a palette is likely to be perceived as more balanced to the human eye. But machines are fallible, and there are exceptions to this rule that you may encounter as you manipulate palette values. For more extensive information on luminance, and a unique color space called CIELAB that aims to even more accurately represent the human perception of color uniformity, see the links at the end of this article.

Additionally, color contrast is exceptionally important for accessibility, particularly in terms of legibility and distinguishing UI elements, which can be calculated programmatically. That’s important in that it means tooling can test for passing values. It also means algorithms can, for example, return an appropriate text color when passed in the background color. So our tool will incorporate contrast checking as an additional way to gauge how to adjust your palette.

The functions demonstrated in this project can be extracted for helping plan a contrast-safe design system palette, or baked into a Sass framework that allows defining a custom theme.

Sass as a palette building tool

Sass provides several traditional programming features that make it perfect for our needs, such as creating and iterating through arrays and manipulating values with custom functions. When coupled with an online IDE, like CodePen, that has real-time processing, we can essentially create a web app to solve specific problems such as building a color palette.

Here is a preview of the tool we’re going to be using:

See the Pen
Sass Color Palette Grapher
by Stephanie Eckles (@5t3ph)
on CodePen.

Features of the Sass palette builder

  • It outputs an aspect ratio-controlled responsive graph for accurate plot point placement and value comparing.
  • It leverages the result of Sass color functions and math calculations to correctly plot points on a 0–100% scale.
  • It generates a gradient to provide a more traditional “swatch” view.
  • It uses built-in Sass functions to extract saturation and lightness values.
  • It creates luminance and contrast functions (forked from Material Web Components in addition to linking in required precomputed linear color channel values).
  • It returns appropriate text color for a given background, with a settings variable to change the ratio used.
  • It provides functions to uniformly scale saturation and lightness across a given palette.

Using the palette builder

To begin, you may wish to swap from among the provided example palettes to get a feel for how the graph values change for different types of color ranges. Simply copy a palette variable name and swap it for $ default as the value of the $ palette variable which can be found under the comment SWAP THE PALETTE VARIABLE.

Next, try switching the $ contrastThreshold variable value between the predefined ratios, especially if you are less familiar with ensuring contrast passes WCAG guidelines.

Then try to adjust the $ palette-scale-lightness or $ palette-scale-saturation values. Those feed into the palette function and uniformly scale those measurements across the palette (up to the individual color’s limit).

Finally, have a go at adding your own palette, or swap out some colors within the examples. The tool is a great way to explore Sass color functions to adjust particular attributes of a color, some of which are demonstrated in the $ default palette.

Interpreting the graphs and creating balanced, accessible palettes

The graphing tool defaults to displaying luminance due to it being the most reliable indicator of a balanced palette, as we discussed earlier. Depending on your needs, saturation and lightness can be useful metrics on their own, but mostly they are signalers that can help point to what needs adjusting to bring a palette’s luminance more in alignment. An exception may be creating a lightness scale based on each value in your established palette. You can swap to the $ stripeBlue example for that.

The $ default palette is actually in need of adjustment to get closer to balanced luminance:

The $ default palette’s luminance graph

A palette that shows well-balanced luminance is the sample from Stripe ($ stripe):

The $ stripe palette luminance graph

Here’s where the tool invites a mind shift. Instead of manipulating a color wheel, it leverages Sass functions to programmatically adjust color attributes.

Check the saturation graph to see if you have room to play with the intensity of the color. My recommended adjustment is to wrap your color value with the scale-color function and pass an adjusted $ saturation value, e.g. example: scale-color(#41b880, $ saturation: 60%). The advantage of scale-color is that it fluidly adjusts the value based on the given percent.

Lightness can help explain why two colors feel different by assigning a value to their brightness measured against mixing them with white or black. In the $ default palette, the change-color function is used for purple to align it’s relative $ lightness value with the computed lightness() of the value used for the red.

The scale-color function also allows bundling both an adjusted $ saturation and $ lightness value, which is often the most useful. Note that provided percents can be negative.

By making use of Sass functions and checking the saturation and lightness graphs, the $ defaultBalancedLuminance achieves balanced luminance. This palette also uses the map-get function to copy values from the $ default palette and apply further adjustments instead of overwriting them, which is handy for testing multiple variations such as perhaps a hue shift across a palette.

The $ defaultBalancedLuminance luminance graph

Take a minute to explore other available color functions.

http://jackiebalzer.com/color offers an excellent web app to review effects of Sass and Compass color functions.

Contrast comes into play when considering how the palette colors will actually be used in a UI. The tool defaults to the AA contrast most appropriate for all text: 4.5. If you are building for a light UI, then consider that any color used on text should achieve appropriate contrast with white when adjusting against luminance, indicated by the center color of the plot point.

Tip: The graph is set up with a transparent background, so you can add a background rule on body if you are developing for a darker UI.

Further reading

Color is an expansive topic and this article only hits the aspects related to Sass functions. But to truly understand how to create harmonious color systems, I recommend the following resources:

  • Color Spaces – is a super impressive deep-dive with interactive models of various color spaces and how they are computed.
  • Understanding Colors and Luminance – A beginner-friendly overview from MDN on color and luminance and their relationship to accessibility.
  • Perpetually Uniform Color Spaces – More information on perceptually uniform color systems, with an intro the tool HSLuv that converts values from the more familiar HSL color space to the luminance-tuned CIELUV color space.
  • Accessible Color Systems – A case study from Stripe about their experience building an accessible color system by creating custom tooling (which inspired this exploration and article).
  • A Nerd’s Guide to Color on the Web – This is a fantastic exploration of the mechanics of color on the web, available right here on CSS-Tricks.
  • Tanaguru Contrast Finder – An incredible tool to help if you are struggling to adjust colors to achieve accessible contrast.
  • ColorBox – A web app from Lyft that further explores color scales through graphing.
  • Designing Systematic Colors – Describes Mineral UI‘s exceptional effort to create color ramps to support consistent theming via a luminance-honed palette.
  • How we designed the new color palettes in Tableau 10 – Tableau exposed features of their custom tool that helped them create a refreshed palette based on CIELAB, including an approachable overview of that color space.

The post A Handy Sass-Powered Tool for Making Balanced Color Palettes appeared first on CSS-Tricks.

CSS-Tricks

, , , , , ,
[Top]

Why monday.com is the Universal Team Management Tool for Your Team

(This is a sponsored post.)

This platform is perfect for teams sized at 2-to-200 — and gives every employee the same level of transparency.

Every project management tool seeks to do the same instrumental thing: keep teams connected, on task and on deadline to get major initiatives done. But the market is getting pretty crowded, and for good reason — no platform seems to have gotten the right feel for what people need to see, and how that information should be displayed so that it’s both actionable/relevant and contextualized.

That’s why monday.com is worth a shot. The platform is based off a simple, but powerful idea: that as humans, we like to feel like we’re contributing to part of a greater/effort good — an idea that sometimes gets lost in the shuffle as we focus on the details of getting stuff done. So projects are put onto a task board (think of it like a digital whiteboard), where everyone can have the same level of visibility into anyone else who’s contributing a set of tasks. That transparency breaks down the silos between teams that cause communication errors and costly project mistakes — and it’s a beautiful, simple way to connect people to the processes that drive forward big business initiatives.

Whether you’re part of a tech-forward team or not, monday.com is a welcome relief to cumbersome Excel files, messy (physical) whiteboards, or meetings that waste time when actual work could be completed. The scalable, intuitive structure can effectively work for a team of two, or an international team of 2,000+ — and a beautiful, color-coded board lays out tasks you can cleanly see and tag for various stages of completion. That way, employees can see exactly what needs to be done (and who needs to do it), while managers can optimize their time re-allocating resources as necessary to optimize processes. It’s a win-win.

monday.com also allows teams to communicate within the platform, cutting down on the amount of laborious sifting through various email threads to figure out a workflow. Messages can be sent inside of tasks — so all the communication is contextualized before meeting resolution or seeking it. The platform also supports uploads, so documents and videos can be added to facilitate more collaboration, and integration with other productivity apps. So if your team is already using tools like Slack, Google Calendar, Dropbox, Microsoft Excel, Trello, and Jira, there’s specific, clean shortcuts to integrate the information from those platforms into monday.com. And even beyond team communication and management, you can use monday.com for client-facing exchanges, so all your messages are consolidated into a single place.

The platform recently raised $ 50M in funding, and received nods from the likes of Forbes, Entrepreneur, Business Insider, and more for its ability to empower international teams to do better work together. Best of all, unlike other team management software, which can be pricey and time-intensive to scope, test and run, you can try monday.com today — for free.

What can this app do?

  • Creating and managing a project’s milestones
  • Creating and assigning tasks
  • Attaching files to any project’s table projects on the go.
  • Using mobile applications to manage projects
  • Communicating with your team members
  • Updating team using the news feed
  • Keeping clients in the loop
  • Organizing the organization into teams
  • Creating detailed project charts and reports
  • Tracking the time your team members spend on tasks
  • Managing a project’s financials
  • Website as well as a desktop app for Mac and Windows

monday.com to make every user feel empowered and part of something bigger than their own individual tasks, and as a result, to boost collective productivity and transparency.

Direct Link to ArticlePermalink

The post Why monday.com is the Universal Team Management Tool for Your Team appeared first on CSS-Tricks.

CSS-Tricks

, , , ,
[Top]

Why monday.com is the Universal Team Management Tool for Your Team

This platform is perfect for teams sized at 2-to-200 — and gives every employee the same level of transparency.

Every project management tool seeks to do the same instrumental thing: keep teams connected, on task and on deadline to get major initiatives done. But the market is getting pretty crowded, and for good reason — no platform seems to have gotten the right feel for what people need to see, and how that information should be displayed so that it’s both actionable/relevant and contextualized.

That’s why monday.com is worth a shot. The platform is based off a simple, but powerful idea: that as humans, we like to feel like we’re contributing to part of a greater/effort good — an idea that sometimes gets lost in the shuffle as we focus on the details of getting stuff done. So projects are put onto a task board (think of it like a digital whiteboard), where everyone can have the same level of visibility into anyone else who’s contributing a set of tasks. That transparency breaks down the silos between teams that cause communication errors and costly project mistakes — and it’s a beautiful, simple way to connect people to the processes that drive forward big business initiatives.

Whether you’re part of a tech-forward team or not, monday.com is a welcome relief to cumbersome Excel files, messy (physical) whiteboards, or meetings that waste time when actual work could be completed. The scalable, intuitive structure can effectively work for a team of two, or an international team of 2,000+ — and a beautiful, color-coded board lays out tasks you can cleanly see and tag for various stages of completion. That way, employees can see exactly what needs to be done (and who needs to do it), while managers can optimize their time re-allocating resources as necessary to optimize processes. It’s a win-win.

monday.com also allows teams to communicate within the platform, cutting down on the amount of laborious sifting through various email threads to figure out a workflow. Messages can be sent inside of tasks — so all the communication is contextualized before meeting resolution or seeking it. The platform also supports uploads, so documents and videos can be added to facilitate more collaboration, and integration with other productivity apps. So if your team is already using tools like Slack, Google Calendar, Dropbox, Microsoft Excel, Trello, and Jira, there’s specific, clean shortcuts to integrate the information from those platforms into monday.com. And even beyond team communication and management, you can use monday.com for client-facing exchanges, so all your messages are consolidated into a single place.

The platform recently raised $ 50M in funding, and received nods from the likes of Forbes, Entrepreneur, Business Insider, and more for its ability to empower international teams to do better work together. Best of all, unlike other team management software, which can be pricey and time-intensive to scope, test and run, you can try monday.com today — for free.

What can this app do?

  • Creating and managing a project’s milestones
  • Creating and assigning tasks
  • Attaching files to any project’s table projects on the go.
  • Using mobile applications to manage projects
  • Communicating with your team members
  • Updating team using the news feed
  • Keeping clients in the loop
  • Organizing the organization into teams
  • Creating detailed project charts and reports
  • Tracking the time your team members spend on tasks
  • Managing a project’s financials
  • Website as well as a desktop app for Mac and Windows

monday.com to make every user feel empowered and part of something bigger than their own individual tasks, and as a result, to boost collective productivity and transparency.

The post Why monday.com is the Universal Team Management Tool for Your Team appeared first on CSS-Tricks.

CSS-Tricks

, , , ,
[Top]