What The SFTP? Transfer Your Files Secure-ly!

You’ve just installed a brand new theme for your new site. You load it up and begin to admire it – it’s perfect! Except…what’s that weird orange tint on all of your photos?! How do you get rid of it? It’s time you unlocked the world of SFTP…

It’s often said that you can build sites on WordPress without ever touching a line of code.

This is absolutely true – however, it’s like owning a Ferrari and never going past the third gear.

I’m not saying you need to start building your own themes and creating new plugins to be able to get the most out of WordPress, just that developing the skills to at least make minor aesthetic changes to your site should be on most people’s to-do list.

Trust me – the sense of accomplishment you get from fixing that annoying layout issue on your site all by yourself is second to none.

But if you want to meddle with the files in order to make these kinds of changes, you need to know how to access them.

There’s a couple of different ways you can do this, but in this article, we’re going to focus on SFTP (Secure File Transfer Protocol).

If you want to skip straight to the good stuff, here’s what we will be covering:

  1. What is Encryption?
  2. SFTP V FTP
  3. How Does SFTP Work?
  4. What is FTPS?
  5. How Do I Access My Files With SFTP?
  6. Which SFTP Client Should I Use?
  7. Accessing Files With FileZilla
  8. Accessing Files Through the Command Line
Cartoon of Devman putting chains and a padlock around a letter, about to post it.
If only Devman knew there was another way to securely send files…

One of the main benefits of using SFTP to transfer your files over other methods is that it encrypts your data.

Therefore, before we delve into how SFTP works, we must first understand the basics of encryption.

What is Encryption?

Encryption is a method of protecting data by converting it to code or ‘cipher’ that can only be solved by the people who have permission to view it.

Simple encryption could be in the form of the A1Z26 cipher, which assigns each letter a numeric value based on their position in the alphabet (A = 1, B = 2), so the word ‘PASSWORD’, for example, would be converted to 16;1;19;19;23;15;18;4.

This is a very simple cipher and wouldn’t take a rocket scientist to crack the code.

With the help of computers, files can be encrypted in virtually uncrackable ways, as machines are able to generate complex, random encryptions that no amount of guesswork would be able to solve.

The recipient possesses the ‘key’, which unscrambles the code and allows them to read it.

A hacker’s goal would be to gain access to plain text data, i.e. data that hasn’t been encrypted.

If a hacker manages to get their hands on ciphertext (encrypted data) the chances of them being able to decode it and view the information it contains are virtually zero, which is why encryption is so widely used.

Take Passwords For Example

Any website which stores user information should encrypt it in case of a breach.

When you log into WordPress, you input your password which could be something as simple as ‘Lilac12’ however WordPress would encrypt it before storing it on the database, so at their end, it could look as complex as ef9ded6169f538c36f9ad613806a7b99.

If a hacker breaches the WordPress database, they wouldn’t be able to access your account unless they were able to transfer the code back into Lilac12 in order to log in.

In theory, the only person who truly knows your password is you!

SFTP v FTP

So, back to SFTP – what is it, and how does it differ from FTP?

One of the main distinctions between SFTP and FTP (File Transfer Protocol), is that SFTP is secure whereas FTP is not (I know, who’d have guessed?!)

Let’s say I call you on the phone and we’re having a private conversation.

If someone manages to tap into the line, they hear us speaking English and they will know everything we discuss. This is FTP, as there is no encryption taking place.

If I call you on an encrypted line, all they hear is gibberish, so even if they managed to intercept the connection, it’s encrypted in a way that means they learn nothing from it and the content is useless to them. This is SFTP – the encryption of files protects your data in the event of an interception.

How Does SFTP Work?

You start the process by logging into an SFTP client to initiate the connection.

The details required include a username, password, host (your site’s URL), and port number, which can all be obtained through your hosting provider.

You can also generate keys that are swapped between the servers, but for the novice user, a username and password is definitely the right way to go.

Once you have authenticated your connection with either of these methods, the files are encrypted and sent to the recipient.

As the files can only be decrypted by the intended recipient, anyone who manages to intercept the connection will only have access to a bunch of jumbled, unreadable files.

So I Guess This Means No One Uses FTP Anymore?

Unfortunately, no, they still do.

FTP has been around a very long time – the specification for it was written before the internet was even invented!

Back then, it was assumed that internet activity wasn’t malicious, and therefore FTP wasn’t created with the need to protect files from various types of hacking methods.

The goal was simply to transfer files from one place to another.

These days, cybersecurity is a huge threat to companies and individuals everywhere, so protecting data should be on the forefront of everyone’s mind when transferring files.

Despite the fact that SFTP is now in existence, millions of people and businesses around the world still use FTP, although some sources suggest that it is dying a slow death.

If you’re not sending confidential or valuable data, it can be easy to think that FTP will be fine in this instance, as no harm would come from anyone having access to this particular set of files.

Whilst this may technically be true, businesses would always be advised to use a secure method when transferring files regardless of the content.

Luckily, there are tons of regulations in place which prevent businesses from taking risks like this, so the bottom line is SFTP over FTP every time!

What About FTPS – Where Does That Come Into It?

With so many acronyms all containing the same few letters, it’s extremely easy to get confused.

So, before we get any deeper, let’s clear up the difference between everything we’ve learnt thus far and the next arrival to the data transfer party – FTPS.

The FTP part of FTPS is indeed the same FTP that we’ve already met – a way to transfer files over the internet, but without the added security that SFTP offers via encryption.

FTPS however, is something slightly different.

Whilst SFTP pairs FTP with an SSH connection in order to securely transfer files, FTPS works with SSL to keep your files safe.

“What Is SSL?!” I Hear You Cry

SSL puts the S in HTTPS – it stands for Secure Sockets Layer and is what differentiates a secure site (HTTPS) from an unsecure one (HTTP).

To do this, your browser binds to the website forming a secure connection which is extremely difficult to penetrate.

This is done with the help of an SSL certificate. The browser connects to the website’s server, checks if it has an SSL certificate and if it does and the browser can authenticate it, it forms a binding connection which allows you to safely transfer information.

How Can I Access My Files Using SFTP?

So now that we’re better versed in the terminology, how do we put this into practice and access our WordPress files?

There are a couple of different ways you can do this.

Some hosting providers supply a platform for you to directly access your files such as C Panel, however, if you don’t have access to anything similar, you can either use a plugin, the command terminal, or an SFTP client.

A plugin File Manager allows you to copy and amend your files, however, we wouldn’t recommend this method.

This is because if you change something in your site’s files, one simple syntax error could mean your whole site crashes and you are unable to even get to the dashboard.

If this is a mistake you have made using a plugin, you won’t even be able to get back into the plugin to fix the issue – it’s so much safer to use an external source such as an SFTP client so that you can get straight back in and correct the issue.

So now we’ve settled on SFTP, which client should we use?

Choosing an SFTP Client

There are a number of SFTP clients that you can use to access your files, and most are free.

Popular choices are WinSCP, Cyberduck and FileZilla

For basic users, all that really differs are the interfaces, so we’ll take a quick look at these three below.

First up, Cyberduck!

Cyberduck

Not all SFTP clients are a great fit for Mac users, but Cyberduck is one of the exceptions.

Screenshot of Cyberduck showing the layout of the folders and files
Cyberduck seems to have its ducks in a row.

It has a Mac-like aesthetic which is very beginner-friendly and supports a wide range of servers, so is a good choice for anyone looking to venture into the world of SFTP.

WinSCP

With 132 million downloads in the bag, WinSCP is also a great choice for your SFTP needs.

Screenshot showing the folder and file layour of WinSCP.
Ahh the good ol’ yellow folders we all know and love.

It’s only available for Windows, so it’s designed in a way that makes it quick and simple for Windows users to navigate, i.e. with lots of well-organized, yellow folders.

Like Cyberduck, it’s also completely free to use and you can easily access your files and make changes to your WordPress files using this client.

FileZilla

One of the most popular by far is FileZilla – it’s completely cross-platform (even Linux) and again, free.

Screenshot showing the folder and file layout of Filezilla.
FileZilla has been a solid choice for SFTP since its creation in 2001.

Below I will take you through how to use FileZilla to download copies of your files from WPMU DEV’s servers.

Not a WPMU DEV Member?

This quick tutorial should give you a good idea of what you would need to do to access your files from any hosting provider.

Orrrrrr, you could just take a look at all the awesome goodness included with our membership, take the plunge and host a site or two on our servers.

Using FileZilla To Access Your Files

First of all, let’s download FileZilla – just head to their site and get it installed.

In order to start the connection, you’ll need to enter your credentials as mentioned above.

To create these, you need to go to our website and into the hosting section of the hub.

You will find your sites in a list – just choose the one you want and click on ‘manage’ which will take you to the back-end of your WordPress site.

The section of the hub where you can click to manage your site preferences.
Click on the black arrow to display the ‘Manage’ button.
The section of the hub where you can make various changes to your site.
From here, click SFTP/SSH.

This will take you to the account creation screen.

The screen where you can click to add either an SSH or SFTP user.
Click ‘Add User’ and then ‘Add SFTP User’.

Here you need to choose a username and password:

The screen from which you can create SFTP credentials by entering a username and password.
You’re provided with a randomly generated password, but feel free to change it.

Your password needs to be a series of letters and numbers – and a pretty long one at that!

If your password isn’t sufficiently long enough, you won’t be able to continue – what’s the point in using a secure method of file transfer if your password is simple enough to guess?!

Now you’ve created your credentials, it’s time to head back to Filezilla to set up the connection.

The host is your website address, so enter that along with your username and password that you’ve just created within your hosting hub, and the port number, which is 22.

Click ‘Quickconnect’ and voila! You now have access to all of your WordPress files.

The screen where you input your credentials to connect to the host server.
You can save your site information for quicker connection by heading to File>Site Manager.

 

The list of files within FileZilla when you first connect to the server.
Just make sure you do your research before you jump in and start editing!

All you need to do now is right-click on the file you want to open and select “View/Edit”.

This will then open the file in your text editor.

Showing the opened style.css file inside the Notepad ++ text editor application.
Your file will open in the text editor (I’m using Notepad ++).

You can make the changes you need and then reupload the file back to the host server.

The pop-up which asks if you want to upload the file back to the server.
Once you click ‘Yes’, the changes will appear live on your site.

And it’s as simple as that!

The hard part is knowing what to do once you have the files within your reach.

There are tons of CSS tutorials online – check out our handy guide packed full of links to awesome resources if you’re hoping to jump into the world of web development languages, or if you just need a refresher. Also, here is another handy dandy tutorial on setting file permissions that you may find useful when working with files on your server.

Ready To Step It Up A Notch?

Another way to access your files is through the command line/terminal.

Be warned – if you have no idea what you’re doing with the command line, then I would strongly advise that you go away, do a bit of research and come back when you’re confident you’re not going to break your site!

I am simply here to show you how to access it via our hosting – what you do with that power is entirely up to you!

Unlike many hosting companies, for an added layer of security, websites hosted on our servers have one set of credentials for SFTP and one for SSH.

If all you want to do is download, make changes to your files and then reupload them, you can do this through SFTP.

If you want to do the more admin-y things such as add users or change settings, you will need to access the server via SSH instead.

It is a very similar process, however, you would need to head back over to the hosting hub and create an SSH user in the same way you did an SFTP one.

As before with FileZilla, using SFTP through the command line opens up a secure connection that allows files to be transferred over the internet.

If you’re on a Windows machine, you simply open up the command line by hitting Win+R and then typing in “cmd”. If you’re on Mac, head to ‘Terminal’ in your applications.

 

The screen you are greeted with when you open up the command line in Windows.
This is the screen that will appear on a Windows operating system.

You then need to establish the connection to your site by typing in the following:

sftp (your sftp username)@(your website address)

So in this example, mine would be sftp kirstan@kirstan.wpmudev.host

This is telling the server that I want to log in via SFTP with the account name ‘kirstan’ into my kirstan.wpmudev.host website.

It will then ask for your password – once you have entered this, you will have remote access to your files.

Credentials entered into the command line.
If you need to paste your password in, just right-click and hit enter.

Right, so we’re in!!

Now we can download copies of our WordPress files by simply inputting a few commands and hitting enter each time.

First, we type in “ls” so that we’re navigating within the server rather than our local machine.

The command line with the command 'ls' typed in to display the current folder.
This will firstly display the “site” folder, which is the one that contains all of your WordPress files.

Then you can navigate around the folders by using the “ls” command which will show you the list of folders and documents inside the folder you’re currently in, and “cd” which will take you to the folder you specify.

Let’s take a quick look below:

The next screen of the command line where you can see all folders and files inside the public_html folder.
It looks more daunting than it is – promise!

I am now inside the public_html folder and as you can see from above, I navigated here by typing in “cd site”, using “ls” to check the names of the folders and then typing “cd public_html”.

Each time you input a command, remember to hit enter so that it can be processed!

Your WordPress files are always organized in the same series of folders and subfolders no matter which method you choose to access them.

If I wanted to download my theme’s stylesheet, I would head into wp content>themes>twentynineteen.

I navigated to the theme’s files by using the “ls” and “cd” commands, and now I’m ready to download the file I need.

INside the Twenty Ninteen themes folder showing the list of files.
If you want to retrieve a file, you type “get” followed by the name of the file, and to upload, just replace “get” with “put”.

I typed “get stylesheet.css” and was able to easily download a copy of the file directly to my downloads folder:

The screen from where I downloaded the style.css filee.
I can then edit it and use the “put” command to send it back to the server.

Once you’ve learned the list of commands, you’ll be able to navigate your way through your WordPress site this way like it’s second nature.

If this is something you fancy getting to grips with, you can check out this ‘cheat sheet’ of some of the most frequently used SFTP commands.

Congratulations – You’ve Just Unlocked The Next Level of WordPress!

Now that you know how to get your hands on your files, you can start to think about what you’ll do with this new power.

Whether you fancy unleashing your imagination and customizing your perfect theme or you just want to find out a bit more about how the PHP behind WordPress really works, our WordPress Academy has everything you need or there are tons of free tutorials online.

Or, if you’re the type of person that likes to ‘get stuck into it boots and all’ and try things out for yourself, sign up for our no-risk 7-day membership trial and check out our secure file transfer tools and more in the [humblebrag alert!] best managed WordPress hosting on the planet.

Jetpack 8.5 Adds New Podcast Player Block

Jetpack 8.5 was released today with a new podcast player block for sharing audio content. Configuring the block is as simple as entering the podcast RSS feed URL. This will automatically bring in the cover art and recent episodes. Block options allow for further customization of the display, including the number of episodes, colors, and the ability to show/hide cover art and episode descriptions.

Jetpack’s new podcast player has arrived just in time, as podcasting has gotten a little boost in recent months due to the large numbers of people under stay-at-home orders. iHeartRadio, an American audio company with more than 350 podcasts, reports that listenership for its podcasting network is up 6% month-over-month, with California and New York jumping 13% and 8% respectively. iHeartRadio’s insights also showed that certain genres are more popular than others:

During a time of economic uncertainty, Americans are listening to more business and finance podcasts – for which downloads and streams are up 78 percent week-over-week among the iHeartRadio Original podcasts. Listeners are also turning to music, entertainment and comedy during this time, where iHeartPodcasts have shown an increase in listening as well.

Selling ads and marketing a new podcast may be more of a struggle during this economic downturn, but those with extra time on their hands may have an easier time producing and publishing episodes. Jetpack’s new block makes it easy to share your own episodes or podcasts you enjoy from other websites.

The 8.5 release also includes significant improvements to the new Search feature, which is powered by Elasticsearch, to provide better indexing and a simpler onboarding experience.

Automattic developer Brandon Kraft published a post today that details recent changes to the Publicize feature. In the past, Publicize would attach an image from the post when sending out its automatic tweet to Twitter. This has now been changed for Jetpack and WordPress.com sites so that Publicize no longer attaches a picture but defaults to allowing Twitter to display its Twitter card instead. Developers can use a filter to return the plugin to its previous behavior, if necessary.

Jetpack 8.5 also makes more widgets and embed tools AMP-compatible, expands options for the Revue block, and fixes layout issues with several other blocks. Check out the changelog on WordPress.org for a full list of the enhancements and bug fixes.

CircleCI Announces Production-Ready Insights Endpoints

CircleCI, a provider of cloud-native continuous integration services, has announced the release of a production-ready version of the company’s insights endpoints. The new endpoints have been in preview for some time now and are designed to aid developers by “unlocking key data around workflows and consumption.”

Need to Smile Today? Stay WordPress Strong

Lyrics: Zack Katz, Jonathan Mann | Music: Jonathan Mann
Video licensed under Creative Commons – Attribution

For the first time, at least 19 people from the WordPress community can literally call themselves WordPress rock stars without it sounding like an outdated marketing gimmick.

GravityView dropped a community music video and website named WordPress Strong earlier today. It is fun. It is inspirational. It will leave a smile on your face. The video features a wide range of faces, voices, and musical talent from around the planet.

Much of the world is looking for small ways to cope with the ongoing COVID-19 pandemic. Each day is about finding the things we should be thankful for while waiting for life to feel like normal. The WordPress community has been a beacon of hope for many. It has continued providing purpose to people despite their daily lives being upended. This project is one more way to show the strength of our community.

“People were scrambling to adjust to the new reality of living in a pandemic, and there was a rush of uncertainty,” said Zack Katz, the creator of GravityView, on starting the project. “In the middle of all that uncertainty, I felt lucky to be part of the WordPress community: doing what we do, working on an open and thriving platform, with a culture of people who are kind to each other and support each other.”

Many GravityView customers began using the plugin to enable COVID-19 responses, such as sites like Support Redditch, which coordinates relief efforts. “I sensed a movement of coming together to help each other, and I wanted to get the word out: if you need help, ask the community,” said Katz. “We’re here for you. We’ll get through this together.”

A total of 19 volunteers contributed to the music video, including WordPress co-founder Matt Mullenweg. However, the true star of the group was Tracy Apps, the owner of tracy apps design, who laid down the beat on the drums.

“It involved asking a lot of people!” said Katz of finding willing subjects. “I get why people are reluctant. I even waited until the last minute to record my video! Something special happens when people are invited to go beyond their comfort zone, especially when it comes to creative endeavors. It was moving to have the emails come in with their videos. People were willing to share a different part of themselves.”

The #WordPressStrong hashtag is open for anyone to contribute to on Twitter. The project is calling for volunteers to join in on the fun. If you can sing, play an instrument, or dance — or if you can’t — you can be a part of this movement for our community to become stronger. If nothing else, it will give you something to do to pass the time. Tag yourself doing something and share it. I am certain it will brighten at least one person’s day.

The WordPress Strong Project

Katz began the project in March. He shared some initial lyric ideas with Jonathan Mann who then wrote and recorded WordPress Strong. The GravityView team reached out to members of the WordPress community and asked them to lend their voices.

“I deeply respect [Mann] as a musician and how he exposes himself through his music,” said Katz. “His album I Used to Love My Body was my soundtrack for last year.”

Mann is the voice of the GravityView brand and has previously created a song for the product. Katz and Mann also worked on the WordPress Wiggle song in 2017.

“When creating WordPress Strong, I shared a poem with [Mann] and expressed the tone that I wanted to convey,” said Katz. “The email had the subject line ‘WordPress Hope Song.’ He wrote and recorded WordPress Strong, and I think you agree, it’s a great WordPress Hope Song.”

The plan for the WordPress Strong website goes beyond releasing a song. Katz wants to expand the site to be a place where people from the community can ask and receive help during the pandemic. The team is currently working on a part of the site where community members can request assistance or offer help anonymously.

“I was hoping artists of all stripes would be interested in sharing their work on the WordPress Strong website,” said Katz. “Sharing creativity together empowers us to be vulnerable in our despair as well as our hope. I would like to help foster that.”

`lh` and `rlh` units

There’s some new units I was totally unaware of from the Level 4 spec for CSS values! The lh unit is “equal to the computed value of line-height” and rlh is the same only of the root element (probably the <html> element) rather than the current element.

Why would that be useful? Šime Vidas’ has a strong point:

“Vertical Inline Centering” of an icon
.inline-icon {
  display: inline-block;
  width: 1lh;
  height: 1lh;
}

The post `lh` and `rlh` units appeared first on CSS-Tricks.

How to Use Formulas with Google Form Responses in Sheets

When people submit your Google Form, a new row is inserted in the Google Sheet that is storing the form responses. This spreadsheet row contains a Timestamp column, the actual date when the form was submitted, and the other columns in the sheet contain all the user’s answers, one per column.

You can extend the Google Forms sheet to also include formula fields and the cell values are automatically calculated whenever a new row is added to the sheet by the Google Form. For instance:

  • You can have an auto-number formula that assigns an auto-incrementing but sequential ID to every form response. It can be useful when you are using Google Forms for invoicing.
  • For customer order forms, a formula can be written in Google Sheets to calculate the total amount based on the item selection, the country (tax rates are different) and the quantity selected in the form.
  • For hotel reservations forms, a formula can automatically calculate the room rent based on the check-in and check-out date filled by the customer in the Google Form.
  • For quizzes, a teacher can automatically calculate the final score of the student by matching the values entered in the form with the actual answers and assigning scores.
  • If a users has made multiple form submissions, a formula can help you determine the total number of entries made by a user as soon as they submit a form.

Autofill Google Sheets Formulas

Google Sheets Formulas for Google Forms

In this step by step guide, you’ll learn how to add formulas to Google Sheets that are associated with Google Forms. The corresponding cell values in the response rows will be automatically calculated when a new response is submitted.

To get a better understanding of what we are trying to achieve, open this Google Form and submit a response. Next, open this Google Sheet and you’ll find your response in a new row. The columns F-K are autofilled using formulas.

All examples below will use the ArrayFormula function of Google Sheets though some of these example can also be written using the FILTER function.

Auto-Number Form Responses with a Unique ID

Open the Google Sheet that is storing form responses, go to first empty column and copy-paste the following formula in the row #1 of the empty column.

=ArrayFormula(
  IFS(
    ROW(A:A)=1, "Invoice ID",
    LEN(A:A)=0, IFERROR(1/0),
    LEN(A:A)>0, LEFT(CONCAT(REPT("0",5), ROW(A:A) -1),6)
  )
)

The ROW() function returns the row number of the current response row. It returns 1 for the first row in the Invoice Column and thus we set the column title in the first row. For subsequent rows, if the first column of the row (usually Timestamp) is not empty, the invoice ID is auto generated.

The IDs will be like 00001, 00002 and so on. You only need to place the formula is first row of the column and it auto-populates all the other rows in the column.

The IFERROR function returns the first argument if it is not an error value, otherwise returns the second argument if present, or a blank if the second argument is absent. So in this case 1/0 is an error and thus it always returns a blank value.

Date Calculation Formula for Google Forms

Your Google Form has two date fields - the check-in date and the check-out date. The hotel rates may vary every season so you have a separate table in the Google Sheet that maintains the room rent per month.

Google Sheets Date Formula

The Column C in the Google Sheet holds the responses for the check-in date while the D column is storing the check-out dates.

=ArrayFormula(
    IF(ROW(A:A) = 1,
      "Room Rent",
      IF(NOT(ISBLANK(A:A)),
       (D:D - C:C) *
       VLOOKUP(MONTH(D:D), 'Room Rates'!$B$2:$C$13,2, TRUE),
       ""
      )
   )
)

The formulas uses VLOOKUP to get the room rates for the travel date specified in the form response and then calculates the room rent by multiplying the room rent with duration of stay.

The same formula can also be written with IFS instead of VLOOKUP

=ArrayFormula(
    IF(ROW(A:A) = 1,
        "Room Rent",
        IFS(ISBLANK(C:C), "",
           MONTH(C:C) < 2, 299,
           MONTH(C:C) < 5, 499,
           MONTH(C:C) < 9, 699,
           TRUE, 199
        )
    )
)

Calculate Tax Amount Based on Invoice Value

In this approach, we’ll use the FILTER function and that could lead to a less complicated formula than using using IF function. The downside is that you have to write the column title in row #1 and paste the formulas in row #2 (so one form response should exist for the formula to work).

=ArrayFormula(FILTER(E2:E, E2:E<>"")*1.35)

Here we apply 35% tax to the invoice value and this formula should be added in the row #2 of the column titled “Tax Amount” as shown in the screenshot.

Assign Quiz Scores in Google Forms

Which city is known as the big apple? This is a short-answer question in Google Forms so students can give responses like New York, New York City, NYC and they’ll still be correct. The teacher has to assign 10 points to the correct answer.

=ArrayFormula(
    IF(ROW(A:A) = 1,
      "Quiz Score",
      IFS(
        ISBLANK(A:A), "",
        REGEXMATCH(LOWER({B:B}), "new\s?york"), 10,
        {B:B} = "NYC", 10,
        TRUE, 0
      )
    )
)

In this formula, we are making use of the IFS function that like an IF THEN statement in programming. We are using REGEXMATCH to match values like New York, New York, newyork in one go using regular expressions.

The IFS function returns an NA if none of the conditions are true so we add a TRUE check at the end that will always be evaluated to true if none of the previous conditions matched and returns 0.

Extract the First Name of the Form Respondent

If you have form field that asks the user to entire their full name, you can use Google Sheets function to extract the first name from the full name and use that field to send personalised emails.

=ArrayFormula(
  IFS(
    ROW(A:A)=1, "First Name",
    LEN(A:A)=0, IFERROR(1/0),
    LEN(A:A)>0, PROPER(REGEXEXTRACT(B:B, "^[^\s+]+"))
  )
)

We’ve used RegexExtract method here to fetch the string before the first space in the name field. The PROPER function will capitalise the first letter of the name incase the user entered their name in lower case.

Find Duplicate Google Form Submissions

If your Google Form is collection email addresses, you can use that field to quickly detect responses that have been submitted by the same user multiple times.

=ArrayFormula(
  IFS(
    ROW(A:A)=1, "Is Duplicate Entry?",
    LEN(A:A)=0, IFERROR(1/0),
    LEN(A:A)>0, IF(COUNTIF(B:B, B:B) > 1, "YES", "")
  )
)

Assuming that the Column B is storing the email addresses of the form respondents, we can use the COUNTIF function to quickly mark duplicate entries in our responses spreadsheet. You can also use conditional formatting in Sheets to highlight rows that are possible duplicate entries.

Email Form Responses with AutoFill Values

You can use Document Studio to automatically send an email to the form respondents. The email is sent after the formular values are auto-filled by the Google Sheet. The original form response and the calculated values can also be included in the generated PDF document.

The Anatomy of a Tablist Component in Vanilla JavaScript Versus React

If you follow the undercurrent of the JavaScript community, there seems to be a divide as of late. It goes back over a decade. Really, this sort of strife has always been. Perhaps it is human nature.

Whenever a popular framework gains traction, you inevitably see people comparing it to rivals. I suppose that is to be expected. Everyone has a particular favorite.

Lately, the framework everyone loves (to hate?) is React. You often see it pitted against others in head-to-head blog posts and feature comparison matrices of enterprise whitepapers. Yet a few years ago, it seemed like jQuery would forever be king of the hill.

Frameworks come and go. To me, what is more interesting is when React — or any JS framework for that matter — gets pitted against the programming language itself. Because of course, under the hood, it is all built atop JS.

The two are not inherently at odds. I would even go so far as to say that if you do not have a good handle on JS fundamentals, you probably are not going to reap the full benefits of using React. It can still be helpful, similar to using a jQuery plugin without understanding its internals. But I feel like React presupposes more JS familiarity.

HTML is equally important. There exists a fair bit of FUD around how React affects accessibility. I think this narrative is inaccurate. In fact, the ESLint JSX a11y plugin will warn of possible accessibility violations in the console.

Console warnings from eslint-jsx-a11y-plugin
ESLint warnings about empty <a> tags

Recently, an annual study of the top 1 million sites was released. It shows that for sites using JS frameworks, there is an increased likelihood of accessibility problems. This is correlation, not causation.

This does not necessarily mean that the frameworks caused these errors, but it does indicate that home pages with these frameworks had more errors than on average.

In a manner of speaking, React’s magic incantations work regardless of whether you recognize the words. Ultimately, you are still responsible for the outcome.

Philosophical musings aside, I am a firm believer in choosing the best tool for the job. Sometimes, that means building a single page app with a Jamstack approach. Or maybe a particular project is better suited to offloading HTML rendering to the server, where it has historically been handled.

Either way, there inevitably comes the need for JS to augment the user experience. At Reaktiv Studios, to that end I have been attempting to keep most of our React components in sync with our “flat HTML” approach. I have been writing commonly used functionality in vanilla JS as well. This keeps our options open, so that our clients are free to choose. It also allows us to reuse the same CSS.

If I may, I would like to share how I built our <Tabs> and <Accordion> React components. I will also demonstrate how I wrote the same functionality without using a framework.

Hopefully, this lesson will feel like we are making a layered cake. Let us first start with the base markup, then cover the vanilla JS, and finish with how it works in React.

For reference, you can tinker with our live examples:

Reaktiv Studios UI components
Reaktiv Studios UI components

Flat HTML examples

Since we need JavaScript to make interactive widgets either way, I figured the easiest approach — from a server side implementation standpoint — would be to require only the bare minimum HTML. The rest can be augmented with JS.

The following are examples of markup for tabs and accordion components, showing a before/after comparison of how JS affects the DOM.

I have added id="TABS_ID" and id="ACCORDION_ID" for demonstrative purposes. This is to make it more obvious what is happening. But the JS that I will be explaining automatically generates unique IDs if nothing is supplied in the HTML. It would work fine either way, with or without an id specified.

<div class="tabs" id="TABS_ID">
  <ul class="tabs__list">
    <li class="tabs__item">
      Tab 1
    </li>
    <!-- .tabs__item -->

    <li class="tabs__item">
      Tab 2
    </li>
    <!-- .tabs__item -->

    <li class="tabs__item" disabled>
      Tab 3 (disabled)
    </li>
    <!-- .tabs__item -->
  </ul>
  <!-- .tabs__list -->

  <div class="tabs__panel">
    <p>
      Tab 1 content
    </p>
  </div>
  <!-- .tabs__panel -->

  <div class="tabs__panel">
    <p>
      Tab 2 content
    </p>
  </div>
  <!-- .tabs__panel -->

  <div class="tabs__panel">
    <p>
      NOTE: This tab is disabled.
    </p>
  </div>
  <!-- .tabs__panel -->
</div>
<!-- .tabs -->

Tabs (with ARIA)

<div class="tabs" id="TABS_ID">
  <ul class="tabs__list" role="tablist">
    <li
      aria-controls="tabpanel_TABS_ID_0"
      aria-selected="false"
      class="tabs__item"
      id="tab_TABS_ID_0"
      role="tab"
      tabindex="0"
    >
      Tab 1
    </li>
    <!-- .tabs__item -->

    <li
      aria-controls="tabpanel_TABS_ID_1"
      aria-selected="true"
      class="tabs__item"
      id="tab_TABS_ID_1"
      role="tab"
      tabindex="0"
    >
      Tab 2
    </li>
    <!-- .tabs__item -->

    <li
      aria-controls="tabpanel_TABS_ID_2"
      aria-disabled="true"
      aria-selected="false"
      class="tabs__item"
      disabled
      id="tab_TABS_ID_2"
      role="tab"
    >
      Tab 3 (disabled)
    </li>
    <!-- .tabs__item -->
  </ul>
  <!-- .tabs__list -->

  <div
    aria-hidden="true"
    aria-labelledby="tab_TABS_ID_0"
    class="tabs__panel"
    id="tabpanel_TABS_ID_0"
    role="tabpanel"
  >
    <p>
      Tab 1 content
    </p>
  </div>
  <!-- .tabs__panel -->

  <div
    aria-hidden="false"
    aria-labelledby="tab_TABS_ID_1"
    class="tabs__panel"
    id="tabpanel_TABS_ID_1"
    role="tabpanel"
  >
    <p>
      Tab 2 content
    </p>
  </div>
  <!-- .tabs__panel -->

  <div
    aria-hidden="true"
    aria-labelledby="tab_TABS_ID_2"
    class="tabs__panel"
    id="tabpanel_TABS_ID_2"
    role="tabpanel"
  >
    <p>
      NOTE: This tab is disabled.
    </p>
  </div>
  <!-- .tabs__panel -->
</div>
<!-- .tabs -->

Accordion (without ARIA)

<div class="accordion" id="ACCORDION_ID">
  <div class="accordion__item">
    Tab 1
  </div>
  <!-- .accordion__item -->

  <div class="accordion__panel">
    <p>
      Tab 1 content
    </p>
  </div>
  <!-- .accordion__panel -->

  <div class="accordion__item">
    Tab 2
  </div>
  <!-- .accordion__item -->

  <div class="accordion__panel">
    <p>
      Tab 2 content
    </p>
  </div>
  <!-- .accordion__panel -->

  <div class="accordion__item" disabled>
    Tab 3 (disabled)
  </div>
  <!-- .accordion__item -->

  <div class="accordion__panel">
    <p>
      NOTE: This tab is disabled.
    </p>
  </div>
  <!-- .accordion__panel -->
</div>
<!-- .accordion -->

Accordion (with ARIA)

<div
  aria-multiselectable="true"
  class="accordion"
  id="ACCORDION_ID"
  role="tablist"
>
  <div
    aria-controls="tabpanel_ACCORDION_ID_0"
    aria-selected="true"
    class="accordion__item"
    id="tab_ACCORDION_ID_0"
    role="tab"
    tabindex="0"
  >
    <i aria-hidden="true" class="accordion__item__icon"></i>
    Tab 1
  </div>
  <!-- .accordion__item -->

  <div
    aria-hidden="false"
    aria-labelledby="tab_ACCORDION_ID_0"
    class="accordion__panel"
    id="tabpanel_ACCORDION_ID_0"
    role="tabpanel"
  >
    <p>
      Tab 1 content
    </p>
  </div>
  <!-- .accordion__panel -->

  <div
    aria-controls="tabpanel_ACCORDION_ID_1"
    aria-selected="false"
    class="accordion__item"
    id="tab_ACCORDION_ID_1"
    role="tab"
    tabindex="0"
  >
    <i aria-hidden="true" class="accordion__item__icon"></i>
    Tab 2
  </div>
  <!-- .accordion__item -->

  <div
    aria-hidden="true"
    aria-labelledby="tab_ACCORDION_ID_1"
    class="accordion__panel"
    id="tabpanel_ACCORDION_ID_1"
    role="tabpanel"
  >
    <p>
      Tab 2 content
    </p>
  </div>
  <!-- .accordion__panel -->

  <div
    aria-controls="tabpanel_ACCORDION_ID_2"
    aria-disabled="true"
    aria-selected="false"
    class="accordion__item"
    disabled
    id="tab_ACCORDION_ID_2"
    role="tab"
  >
    <i aria-hidden="true" class="accordion__item__icon"></i>
    Tab 3 (disabled)
  </div>
  <!-- .accordion__item -->

  <div
    aria-hidden="true"
    aria-labelledby="tab_ACCORDION_ID_2"
    class="accordion__panel"
    id="tabpanel_ACCORDION_ID_2"
    role="tabpanel"
  >
    <p>
      NOTE: This tab is disabled.
    </p>
  </div>
  <!-- .accordion__panel -->
</div>
<!-- .accordion -->

Vanilla JavaScript examples

Okay. Now that we have seen the aforementioned HTML examples, let us walk through how we get from before to after.

First, I want to cover a few helper functions. These will make more sense in a bit. I figure it is best to get them documented first, so we can stay focused on the rest of the code once we dive in further.

File: getDomFallback.js

This function provides common DOM properties and methods as no-op, rather than having to make lots of typeof foo.getAttribute checks and whatnot. We could forego those types of confirmations altogether.

Since live HTML changes can be a potentially volatile environment, I always feel a bit safer making sure my JS is not bombing out and taking the rest of the page with it. Here is what that function looks like. It simply returns an object with the DOM equivalents of falsy results.

/*
  Helper to mock DOM methods, for
  when an element might not exist.
*/
const getDomFallback = () => {
  return {
    // Props.
    children: [],
    className: '',
    classList: {
      contains: () => false,
    },
    id: '',
    innerHTML: '',
    name: '',
    nextSibling: null,
    previousSibling: null,
    outerHTML: '',
    tagName: '',
    textContent: '',

    // Methods.
    appendChild: () => Object.create(null),
    cloneNode: () => Object.create(null),
    closest: () => null,
    createElement: () => Object.create(null),
    getAttribute: () => null,
    hasAttribute: () => false,
    insertAdjacentElement: () => Object.create(null),
    insertBefore: () => Object.create(null),
    querySelector: () => null,
    querySelectorAll: () => [],
    removeAttribute: () => undefined,
    removeChild: () => Object.create(null),
    replaceChild: () => Object.create(null),
    setAttribute: () => undefined,
  };
};

// Export.
export { getDomFallback };

File: unique.js

This function is a poor man’s UUID equivalent.

It generates a unique string that can be used to associate DOM elements with one another. It is handy, because then the author of an HTML page does not have to ensure that every tabs and accordion component have unique IDs. In the previous HTML examples, this is where TABS_ID and ACCORDION_ID would typically contain the randomly generated numeric strings instead.

// ==========
// Constants.
// ==========

const BEFORE = '0.';
const AFTER = '';

// ==================
// Get unique string.
// ==================

const unique = () => {
  // Get prefix.
  let prefix = Math.random();
  prefix = String(prefix);
  prefix = prefix.replace(BEFORE, AFTER);

  // Get suffix.
  let suffix = Math.random();
  suffix = String(suffix);
  suffix = suffix.replace(BEFORE, AFTER);

  // Expose string.
  return `${prefix}_${suffix}`;
};

// Export.
export { unique };

On larger JavaScript projects, I would typically use npm install uuid. But since we are keeping this simple and do not require cryptographic parity, concatenating two lightly edited Math.random() numbers will suffice for our string uniqueness needs.

File: tablist.js

This file does the bulk of the work. What is cool about it, if I do say so myself, is that there are enough similarities between a tabs component and an accordion that we can handle both with the same *.js file. Go ahead and scroll through the entirety, and then we will break down what each function does individually.

// Helpers.
import { getDomFallback } from './getDomFallback';
import { unique } from './unique';

// ==========
// Constants.
// ==========

// Boolean strings.
const TRUE = 'true';
const FALSE = 'false';

// ARIA strings.
const ARIA_CONTROLS = 'aria-controls';
const ARIA_DISABLED = 'aria-disabled';
const ARIA_LABELLEDBY = 'aria-labelledby';
const ARIA_HIDDEN = 'aria-hidden';
const ARIA_MULTISELECTABLE = 'aria-multiselectable';
const ARIA_SELECTED = 'aria-selected';

// Attribute strings.
const DISABLED = 'disabled';
const ID = 'id';
const ROLE = 'role';
const TABLIST = 'tablist';
const TABINDEX = 'tabindex';

// Event strings.
const CLICK = 'click';
const KEYDOWN = 'keydown';

// Key strings.
const ENTER = 'enter';
const FUNCTION = 'function';

// Tag strings.
const LI = 'li';

// Selector strings.
const ACCORDION_ITEM_ICON = 'accordion__item__icon';
const ACCORDION_ITEM_ICON_SELECTOR = `.${ACCORDION_ITEM_ICON}`;

const TAB = 'tab';
const TAB_SELECTOR = `[${ROLE}=${TAB}]`;

const TABPANEL = 'tabpanel';
const TABPANEL_SELECTOR = `[${ROLE}=${TABPANEL}]`;

const ACCORDION = 'accordion';
const TABLIST_CLASS_SELECTOR = '.accordion, .tabs';
const TAB_CLASS_SELECTOR = '.accordion__item, .tabs__item';
const TABPANEL_CLASS_SELECTOR = '.accordion__panel, .tabs__panel';

// ===========
// Get tab ID.
// ===========

const getTabId = (id = '', index = 0) => {
  return `tab_${id}_${index}`;
};

// =============
// Get panel ID.
// =============

const getPanelId = (id = '', index = 0) => {
  return `tabpanel_${id}_${index}`;
};

// ==============
// Click handler.
// ==============

const globalClick = (event = {}) => {
  // Get target.
  const { key = '', target = getDomFallback() } = event;

  // Get parent.
  const { parentNode = getDomFallback(), tagName = '' } = target;

  // Set later.
  let wrapper = getDomFallback();

  /*
    =====
    NOTE:
    =====

    We test for this, because the method does
    not exist on `document.documentElement`.
  */
  if (typeof target.closest === FUNCTION) {
    // Get wrapper.
    wrapper = target.closest(TABLIST_CLASS_SELECTOR) || getDomFallback();
  }

  // Is `<li>`?
  const isListItem = tagName.toLowerCase() === LI;

  // Is multi?
  const isMulti = wrapper.getAttribute(ARIA_MULTISELECTABLE) === TRUE;

  // Valid key?
  const isValidKey = !key || key.toLowerCase() === ENTER;

  // Valid target?
  const isValidTarget =
    !target.hasAttribute(DISABLED) &&
    target.getAttribute(ROLE) === TAB &&
    parentNode.getAttribute(ROLE) === TABLIST;

  // Valid event?
  const isValidEvent = isValidKey && isValidTarget;

  // Continue?
  if (isValidEvent) {
    // Get panel.
    const panelId = target.getAttribute(ARIA_CONTROLS);
    const panel = wrapper.querySelector(`#${panelId}`) || getDomFallback();

    // Get booleans.
    let boolPanel = panel.getAttribute(ARIA_HIDDEN) !== TRUE;
    let boolTab = target.getAttribute(ARIA_SELECTED) !== TRUE;

    // List item?
    if (isListItem) {
      boolPanel = FALSE;
      boolTab = TRUE;
    }

    // [aria-multiselectable="false"]
    if (!isMulti) {
      // Get tabs & panels.
      const childTabs = wrapper.querySelectorAll(TAB_SELECTOR);
      const childPanels = wrapper.querySelectorAll(TABPANEL_SELECTOR);

      // Loop through tabs.
      childTabs.forEach((tab = getDomFallback()) => {
        tab.setAttribute(ARIA_SELECTED, FALSE);
      });

      // Loop through panels.
      childPanels.forEach((panel = getDomFallback()) => {
        panel.setAttribute(ARIA_HIDDEN, TRUE);
      });
    }

    // Set individual tab.
    target.setAttribute(ARIA_SELECTED, boolTab);

    // Set individual panel.
    panel.setAttribute(ARIA_HIDDEN, boolPanel);
  }
};

// ====================
// Add ARIA attributes.
// ====================

const addAriaAttributes = () => {
  // Get elements.
  const allWrappers = document.querySelectorAll(TABLIST_CLASS_SELECTOR);

  // Loop through.
  allWrappers.forEach((wrapper = getDomFallback()) => {
    // Get attributes.
    const { id = '', classList } = wrapper;
    const parentId = id || unique();

    // Is accordion?
    const isAccordion = classList.contains(ACCORDION);

    // Get tabs & panels.
    const childTabs = wrapper.querySelectorAll(TAB_CLASS_SELECTOR);
    const childPanels = wrapper.querySelectorAll(TABPANEL_CLASS_SELECTOR);

    // Add ID?
    if (!wrapper.getAttribute(ID)) {
      wrapper.setAttribute(ID, parentId);
    }

    // Add multi?
    if (isAccordion && wrapper.getAttribute(ARIA_MULTISELECTABLE) !== FALSE) {
      wrapper.setAttribute(ARIA_MULTISELECTABLE, TRUE);
    }

    // ===========================
    // Loop through tabs & panels.
    // ===========================

    for (let index = 0; index < childTabs.length; index++) {
      // Get elements.
      const tab = childTabs[index] || getDomFallback();
      const panel = childPanels[index] || getDomFallback();

      // Get IDs.
      const tabId = getTabId(parentId, index);
      const panelId = getPanelId(parentId, index);

      // ===================
      // Add tab attributes.
      // ===================

      // Tab: add icon?
      if (isAccordion) {
        // Get icon.
        let icon = tab.querySelector(ACCORDION_ITEM_ICON_SELECTOR);

        // Create icon?
        if (!icon) {
          icon = document.createElement(I);
          icon.className = ACCORDION_ITEM_ICON;
          tab.insertAdjacentElement(AFTER_BEGIN, icon);
        }

        // [aria-hidden="true"]
        icon.setAttribute(ARIA_HIDDEN, TRUE);
      }

      // Tab: add id?
      if (!tab.getAttribute(ID)) {
        tab.setAttribute(ID, tabId);
      }

      // Tab: add controls?
      if (!tab.getAttribute(ARIA_CONTROLS)) {
        tab.setAttribute(ARIA_CONTROLS, panelId);
      }

      // Tab: add selected?
      if (!tab.getAttribute(ARIA_SELECTED)) {
        const bool = !isAccordion && index === 0;

        tab.setAttribute(ARIA_SELECTED, bool);
      }

      // Tab: add role?
      if (tab.getAttribute(ROLE) !== TAB) {
        tab.setAttribute(ROLE, TAB);
      }

      // Tab: add tabindex?
      if (tab.hasAttribute(DISABLED)) {
        tab.removeAttribute(TABINDEX);
        tab.setAttribute(ARIA_DISABLED, TRUE);
      } else {
        tab.setAttribute(TABINDEX, 0);
      }

      // Tab: first item?
      if (index === 0) {
        // Get parent.
        const { parentNode = getDomFallback() } = tab;

        /*
          We do this here, instead of outside the loop.

          The top level item isn't always the `tablist`.

          The accordion UI only has `<dl>`, whereas
          the tabs UI has both `<div>` and `<ul>`.
        */
        if (parentNode.getAttribute(ROLE) !== TABLIST) {
          parentNode.setAttribute(ROLE, TABLIST);
        }
      }

      // =====================
      // Add panel attributes.
      // =====================

      // Panel: add ID?
      if (!panel.getAttribute(ID)) {
        panel.setAttribute(ID, panelId);
      }

      // Panel: add hidden?
      if (!panel.getAttribute(ARIA_HIDDEN)) {
        const bool = isAccordion || index !== 0;

        panel.setAttribute(ARIA_HIDDEN, bool);
      }

      // Panel: add labelled?
      if (!panel.getAttribute(ARIA_LABELLEDBY)) {
        panel.setAttribute(ARIA_LABELLEDBY, tabId);
      }

      // Panel: add role?
      if (panel.getAttribute(ROLE) !== TABPANEL) {
        panel.setAttribute(ROLE, TABPANEL);
      }
    }
  });
};

// =====================
// Remove global events.
// =====================

const unbind = () => {
  document.removeEventListener(CLICK, globalClick);
  document.removeEventListener(KEYDOWN, globalClick);
};

// ==================
// Add global events.
// ==================

const init = () => {
  // Add attributes.
  addAriaAttributes();

  // Prevent doubles.
  unbind();

  document.addEventListener(CLICK, globalClick);
  document.addEventListener(KEYDOWN, globalClick);
};

// ==============
// Bundle object.
// ==============

const tablist = {
  init,
  unbind,
};

// =======
// Export.
// =======

export { tablist };

Function: getTabId and getPanelId

These two functions are used to create individually unique IDs for elements in a loop, based on an existing (or generated) parent ID. This is helpful to ensure matching values for attributes like aria-controls="…" and aria-labelledby="…". Think of those as the accessibility equivalents of <label for="…">, telling the browser which elements are related to one another.

const getTabId = (id = '', index = 0) => {
  return `tab_${id}_${index}`;
};
const getPanelId = (id = '', index = 0) => {
  return `tabpanel_${id}_${index}`;
};

Function: globalClick

This is a click handler that is applied at the document level. That means we are not having to manually add click handlers to a number of elements. Instead, we use event bubbling to listen for clicks further down in the document, and allow them to propagate up to the top. Conveniently, this is also how we can handle keyboard events such as the Enter key being pressed. Both are necessary to have an accessible UI.

In the first part of the function, we destructure key and target from the incoming event. Next, we destructure the parentNode and tagName from the target.

Then, we attempt to get the wrapper element. This would be the one with either class="tabs" or class="accordion". Because we might actually be clicking on the ancestor element highest in the DOM tree — which exists but possibly does not have the *.closest(…) method — we do a typeof check. If that function exists, we attempt to get the element. Even still, we might come up without a match. So we have one more getDomFallback to be safe.

// Get target.
const { key = '', target = getDomFallback() } = event;

// Get parent.
const { parentNode = getDomFallback(), tagName = '' } = target;

// Set later.
let wrapper = getDomFallback();

/*
  =====
  NOTE:
  =====

  We test for this, because the method does
  not exist on `document.documentElement`.
*/
if (typeof target.closest === FUNCTION) {
  // Get wrapper.
  wrapper = target.closest(TABLIST_CLASS_SELECTOR) || getDomFallback();
}

Then, we store whether or not the tag that was clicked is a <li>. Likewise, we store a boolean about whether the wrapper element has aria-multiselectable="true". I will get back to that. We need this info later on.

We also interrogate the event a bit, to determine if it was triggered by the user pressing a key. If so, then we are only interested if that key was Enter. We also determine if the click happened on a relevant target. Remember, we are using event bubbling so really the user could have clicked anything.

We want to make sure it:

  • Is not disabled
  • Has role="tab"
  • Has a parent element with role="tablist"

Then we bundle up our event and target booleans into one, as isValidEvent.

// Is `<li>`?
const isListItem = tagName.toLowerCase() === LI;

// Is multi?
const isMulti = wrapper.getAttribute(ARIA_MULTISELECTABLE) === TRUE;

// Valid key?
const isValidKey = !key || key.toLowerCase() === ENTER;

// Valid target?
const isValidTarget =
  !target.hasAttribute(DISABLED) &&
  target.getAttribute(ROLE) === TAB &&
  parentNode.getAttribute(ROLE) === TABLIST;

// Valid event?
const isValidEvent = isValidKey && isValidTarget;

Assuming the event is indeed valid, we make it past our next if check. Now, we are concerned with getting the role="tabpanel" element with an id that matches our tab’s aria-controls="…".

Once we have got it, we check whether the panel is hidden, and if the tab is selected. Basically, we first presuppose that we are dealing with an accordion and flip the booleans to their opposites.

This is also where our earlier isListItem boolean comes into play. If the user is clicking an <li> then we know we are dealing with tabs, not an accordion. In which case, we want to flag our panel as being visible (via aria-hiddden="false") and our tab as being selected (via aria-selected="true").

Also, we want to ensure that either the wrapper has aria-multiselectable="false" or is completely missing aria-multiselectable. If that is the case, then we loop through all neighboring role="tab" and all role="tabpanel" elements and set them to their inactive states. Finally, we arrive at setting the previously determined booleans for the individual tab and panel pairing.

// Continue?
if (isValidEvent) {
  // Get panel.
  const panelId = target.getAttribute(ARIA_CONTROLS);
  const panel = wrapper.querySelector(`#${panelId}`) || getDomFallback();

  // Get booleans.
  let boolPanel = panel.getAttribute(ARIA_HIDDEN) !== TRUE;
  let boolTab = target.getAttribute(ARIA_SELECTED) !== TRUE;

  // List item?
  if (isListItem) {
    boolPanel = FALSE;
    boolTab = TRUE;
  }

  // [aria-multiselectable="false"]
  if (!isMulti) {
    // Get tabs & panels.
    const childTabs = wrapper.querySelectorAll(TAB_SELECTOR);
    const childPanels = wrapper.querySelectorAll(TABPANEL_SELECTOR);

    // Loop through tabs.
    childTabs.forEach((tab = getDomFallback()) => {
      tab.setAttribute(ARIA_SELECTED, FALSE);
    });

    // Loop through panels.
    childPanels.forEach((panel = getDomFallback()) => {
      panel.setAttribute(ARIA_HIDDEN, TRUE);
    });
  }

  // Set individual tab.
  target.setAttribute(ARIA_SELECTED, boolTab);

  // Set individual panel.
  panel.setAttribute(ARIA_HIDDEN, boolPanel);
}

Function: addAriaAttributes

The astute reader might be thinking:

You said earlier that we start with the most bare possible markup, yet the globalClick function was looking for attributes that would not be there. Why would you lie!?

Or perhaps not, for the astute reader would have also noticed the function named addAriaAttributes. Indeed, this function does exactly what it says on the tin. It breathes life into the base DOM structure, by adding all the requisite aria-* and role attributes.

This not only makes the UI inherently more accessible to assistive technologies, but it also ensures the functionality actually works. I prefer to build vanilla JS things this way, rather than pivoting on class="…" for interactivity, because it forces me to think about the entirety of the user experience, beyond what I can see visually.

First off, we get all elements on the page that have class="tabs" and/or class="accordion". Then we check if we have something to work with. If not, then we would exit our function here. Assuming we do have a list, we loop through each of the wrapping elements and pass them into the scope of our function as wrapper.

// Get elements.
const allWrappers = document.querySelectorAll(TABLIST_CLASS_SELECTOR);

// Loop through.
allWrappers.forEach((wrapper = getDomFallback()) => {
  /*
    NOTE: Cut, for brevity.
  */
});

Inside the scope of our looping function, we destructure id and classList from wrapper. If there is no ID, then we generate one via unique(). We set a boolean flag, to identify if we are working with an accordion. This is used later.

We also get decendants of wrapper that are tabs and panels, via their class name selectors.

Tabs:

  • class="tabs__item" or
  • class="accordion__item"

Panels:

  • class="tabs__panel" or
  • class="accordion__panel"

We then set the wrapper’s id if it does not already have one.

If we are dealing with an accordion that lacks aria-multiselectable="false", we set its flag to true. Reason being, if developers are reaching for an accordion UI paradigm — and also have tabs available to them, which are inherently mutually exclusive — then the safer assumption is that the accordion should support expanding and collapsing of several panels.

// Get attributes.
const { id = '', classList } = wrapper;
const parentId = id || unique();

// Is accordion?
const isAccordion = classList.contains(ACCORDION);

// Get tabs & panels.
const childTabs = wrapper.querySelectorAll(TAB_CLASS_SELECTOR);
const childPanels = wrapper.querySelectorAll(TABPANEL_CLASS_SELECTOR);

// Add ID?
if (!wrapper.getAttribute(ID)) {
  wrapper.setAttribute(ID, parentId);
}

// Add multi?
if (isAccordion && wrapper.getAttribute(ARIA_MULTISELECTABLE) !== FALSE) {
  wrapper.setAttribute(ARIA_MULTISELECTABLE, TRUE);
}

Next, we loop through tabs. Wherein, we also handle our panels.

You may be wondering why this is an old school for loop, instead of a more modern *.forEach. The reason is that we want to loop through two NodeList instances: tabs and panels. Assuming they each map 1-to-1 we know they both have the same *.length. This allows us to have one loop instead of two.

Let us peer inside of the loop. First, we get unique IDs for each tab and panel. These would look like one of the two following scenarios. These are used later on, to associate tabs with panels and vice versa.

  • tab_WRAPPER_ID_0 or
    tab_GENERATED_STRING_0
  • tabpanel_WRAPPER_ID_0 or
    tabpanel_GENERATED_STRING_0
for (let index = 0; index < childTabs.length; index++) {
  // Get elements.
  const tab = childTabs[index] || getDomFallback();
  const panel = childPanels[index] || getDomFallback();

  // Get IDs.
  const tabId = getTabId(parentId, index);
  const panelId = getPanelId(parentId, index);

  /*
    NOTE: Cut, for brevity.
  */
}

As we loop through, we first ensure that an expand/collapse icon exists. We create it if necessary, and set it to aria-hidden="true" since it is purely decorative.

Next, we check on attributes for the current tab. If an id="…" does not exist on the tab, we add it. Likewise, if aria-controls="…" does not exist we add that as well, pointing to our newly created panelId.

You will notice there is a little pivot here, checking if we do not have aria-selected and then further determining if we are not in the context of an accordion and if the index is 0. In that case, we want to make our first tab look selected. The reason is that though an accordion can be fully collapsed, tabbed content cannot. There is always at least one panel visible.

Then we ensure that role="tab" exists.

It is worth noting we do some extra work, based on whether the tab is disabled. If so, we remove tabindex so that the tab cannot receive :focus. If the tab is not disabled, we add tabindex="0" so that it can receive :focus.

We also set aria-disabled="true", if need be. You might be wondering if that is redundant. But it is necessary to inform assistive technologies that the tab is not interactive. Since our tab is either a <div> or <li>, it technically cannot be disabled like an <input>. Our styles pivot on [disabled], so we get that for free. Plus, it is less cognitive overhead (as a developer creating HTML) to only worry about one attribute.

ℹ️ Fun Fact: It is also worth noting the use of hasAttribute(…) to detect disabled, instead of getAttribute(…). This is because the mere presence of disabled will cause form elements to be disabled.

If the HTML is compiled, via tools such as Parcel

  • Markup like this: <tag disabled>
  • Is changed to this: <tag disabled="">

In which case, getting the attribute is still a falsy string.

In the days of XHTML, that would have been disabled="disabled". But really, it was only ever the existence of the attribute that mattered. Not its value. That is why we simply test if the element has the disabled attribute.

Lastly, we check if we are on the first iteration of our loop where index is 0. If so, we go up one level to the parentNode. If that element does not have role="tablist", then we add it.

We do this via parentNode instead of wrapper because in the context of tabs (not accordion) there is a <ul>element around the tab <li> that needs role="tablist". In the case of an accordion, it would be the outermost <div> ancestor. This code accounts for both.

// Tab: add icon?
if (isAccordion) {
  // Get icon.
  let icon = tab.querySelector(ACCORDION_ITEM_ICON_SELECTOR);

  // Create icon?
  if (!icon) {
    icon = document.createElement(I);
    icon.className = ACCORDION_ITEM_ICON;
    tab.insertAdjacentElement(AFTER_BEGIN, icon);
  }

  // [aria-hidden="true"]
  icon.setAttribute(ARIA_HIDDEN, TRUE);
}

// Tab: add id?
if (!tab.getAttribute(ID)) {
  tab.setAttribute(ID, tabId);
}

// Tab: add controls?
if (!tab.getAttribute(ARIA_CONTROLS)) {
  tab.setAttribute(ARIA_CONTROLS, panelId);
}

// Tab: add selected?
if (!tab.getAttribute(ARIA_SELECTED)) {
  const bool = !isAccordion && index === 0;

  tab.setAttribute(ARIA_SELECTED, bool);
}

// Tab: add role?
if (tab.getAttribute(ROLE) !== TAB) {
  tab.setAttribute(ROLE, TAB);
}

// Tab: add tabindex?
if (tab.hasAttribute(DISABLED)) {
  tab.removeAttribute(TABINDEX);
  tab.setAttribute(ARIA_DISABLED, TRUE);
} else {
  tab.setAttribute(TABINDEX, 0);
}

// Tab: first item?
if (index === 0) {
  // Get parent.
  const { parentNode = getDomFallback() } = tab;

  /*
    We do this here, instead of outside the loop.

    The top level item isn't always the `tablist`.

    The accordion UI only has `<dl>`, whereas
    the tabs UI has both `<div>` and `<ul>`.
  */
  if (parentNode.getAttribute(ROLE) !== TABLIST) {
    parentNode.setAttribute(ROLE, TABLIST);
  }
}

Continuing within the earlier for loop, we add attributes for each panel. We add an id if needed. We also set aria-hidden to either true or false depending on the context of being an accordion (or not).

Likewise, we ensure that our panel points back to its tab trigger via aria-labelledby="…", and that role="tabpanel" has been set.

// Panel: add ID?
if (!panel.getAttribute(ID)) {
  panel.setAttribute(ID, panelId);
}

// Panel: add hidden?
if (!panel.getAttribute(ARIA_HIDDEN)) {
  const bool = isAccordion || index !== 0;

  panel.setAttribute(ARIA_HIDDEN, bool);
}

// Panel: add labelled?
if (!panel.getAttribute(ARIA_LABELLEDBY)) {
  panel.setAttribute(ARIA_LABELLEDBY, tabId);
}

// Panel: add role?
if (panel.getAttribute(ROLE) !== TABPANEL) {
  panel.setAttribute(ROLE, TABPANEL);
}

At the very end of the file, we have a few setup and teardown functions. As a way to play nicely with other JS that might be in the page, we provide an unbind function that removes our global event listeners. It can be called by itself, via tablist.unbind() but is mostly there so that we can unbind() before (re-)binding. That way we prevent doubling up.

Inside our init function, we call addAriaAttributes() which modifies the DOM to be accessible. We then call unbind() and then add our event listeners to the document.

Finally, we bundle both methods into a parent object and export it under the name tablist. That way, when dropping it into a flat HTML page, we can call tablist.init() when we are ready to apply our functionality.

// =====================
// Remove global events.
// =====================

const unbind = () => {
  document.removeEventListener(CLICK, globalClick);
  document.removeEventListener(KEYDOWN, globalClick);
};

// ==================
// Add global events.
// ==================

const init = () => {
  // Add attributes.
  addAriaAttributes();

  // Prevent doubles.
  unbind();

  document.addEventListener(CLICK, globalClick);
  document.addEventListener(KEYDOWN, globalClick);
};

// ==============
// Bundle object.
// ==============

const tablist = {
  init,
  unbind,
};

// =======
// Export.
// =======

export { tablist };

React examples

There is a scene in Batman Begins where Lucius Fox (played by Morgan Freeman) explains to a recovering Bruce Wayne (Christian Bale) the scientific steps he took to save his life after being poisoned.

Lucius Fox: “I analyzed your blood, isolating the receptor compounds and the protein-based catalyst.”

Bruce Wayne: “Am I meant to understand any of that?”

Lucius Fox: “Not at all, I just wanted you to know how hard it was. Bottom line, I synthesized an antidote.”

Morgan Freeman and Christian Bale, sitting inside the Batmobile
“How do I configure Webpack?”

↑ When working with a framework, I think in those terms.

Now that we know “hard” it is — not really, but humor me — to do raw DOM manipulation and event binding, we can better appreciate the existence of an antidote. React abstracts a lot of that complexity away, and handles it for us automatically.

File: Tabs.js

Now that we are diving into React examples, we will start with the <Tabs> component.

// =============
// Used like so…
// =============

<Tabs>
  <div label="Tab 1">
    <p>
      Tab 1 content
    </p>
  </div>
  <div label="Tab 2">
    <p>
      Tab 2 content
    </p>
  </div>
</Tabs>

Here is the content from our Tabs.js file. Note that in React parlance, it is standard practice to name the file with the same capitalization as its export default component.

We start out with the same getTabId and getPanelId functions as in our vanilla JS approach, because we still need to make sure to accessibly map tabs to components. Take a look at the entirey of the code, and then we will continue to break it down.

import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { v4 as uuid } from 'uuid';
import cx from 'classnames';

// UI.
import Render from './Render';

// ===========
// Get tab ID.
// ===========

const getTabId = (id = '', index = 0) => {
  return `tab_${id}_${index}`;
};

// =============
// Get panel ID.
// =============

const getPanelId = (id = '', index = 0) => {
  return `tabpanel_${id}_${index}`;
};

// ==========
// Is active?
// ==========

const getIsActive = ({ activeIndex = null, index = null, list = [] }) => {
  // Index matches?
  const isMatch = index === parseFloat(activeIndex);

  // Is first item?
  const isFirst = index === 0;

  // Only first item exists?
  const onlyFirstItem = list.length === 1;

  // Item doesn't exist?
  const badActiveItem = !list[activeIndex];

  // Flag as active?
  const isActive = isMatch || onlyFirstItem || (isFirst && badActiveItem);

  // Expose boolean.
  return !!isActive;
};

getIsActive.propTypes = {
  activeIndex: PropTypes.number,
  index: PropTypes.number,
  list: PropTypes.array,
};

// ================
// Get `<ul>` list.
// ================

const getTabsList = ({ activeIndex = null, id = '', list = [], setActiveIndex = () => {} }) => {
  // Build new list.
  const newList = list.map((item = {}, index) => {
    // =========
    // Get data.
    // =========

    const { props: itemProps = {} } = item;
    const { disabled = null, label = '' } = itemProps;
    const idPanel = getPanelId(id, index);
    const idTab = getTabId(id, index);
    const isActive = getIsActive({ activeIndex, index, list });

    // =======
    // Events.
    // =======

    const handleClick = (event = {}) => {
      const { key = '' } = event;

      if (!disabled) {
        // Early exit.
        if (key && key.toLowerCase() !== 'enter') {
          return;
        }

        setActiveIndex(index);
      }
    };

    // ============
    // Add to list.
    // ============

    return (
      <li
        aria-controls={idPanel}
        aria-disabled={disabled}
        aria-selected={isActive}
        className="tabs__item"
        disabled={disabled}
        id={idTab}
        key={idTab}
        role="tab"
        tabIndex={disabled ? null : 0}
        // Events.
        onClick={handleClick}
        onKeyDown={handleClick}
      >
        {label || `${index + 1}`}
      </li>
    );
  });

  // ==========
  // Expose UI.
  // ==========

  return (
    <Render if={newList.length}>
      <ul className="tabs__list" role="tablist">
        {newList}
      </ul>
    </Render>
  );
};

getTabsList.propTypes = {
  activeIndex: PropTypes.number,
  id: PropTypes.string,
  list: PropTypes.array,
  setActiveIndex: PropTypes.func,
};

// =================
// Get `<div>` list.
// =================

const getPanelsList = ({ activeIndex = null, id = '', list = [] }) => {
  // Build new list.
  const newList = list.map((item = {}, index) => {
    // =========
    // Get data.
    // =========

    const { props: itemProps = {} } = item;
    const { children = '', className = null, style = null } = itemProps;
    const idPanel = getPanelId(id, index);
    const idTab = getTabId(id, index);
    const isActive = getIsActive({ activeIndex, index, list });

    // =============
    // Get children.
    // =============

    let content = children || item;

    if (typeof content === 'string') {
      content = <p>{content}</p>;
    }

    // =================
    // Build class list.
    // =================

    const classList = cx({
      tabs__panel: true,
      [String(className)]: className,
    });

    // ==========
    // Expose UI.
    // ==========

    return (
      <div
        aria-hidden={!isActive}
        aria-labelledby={idTab}
        className={classList}
        id={idPanel}
        key={idPanel}
        role="tabpanel"
        style={style}
      >
        {content}
      </div>
    );
  });

  // ==========
  // Expose UI.
  // ==========

  return newList;
};

getPanelsList.propTypes = {
  activeIndex: PropTypes.number,
  id: PropTypes.string,
  list: PropTypes.array,
};

// ==========
// Component.
// ==========

const Tabs = ({
  children = '',
  className = null,
  selected = 0,
  style = null,
  id: propsId = uuid(),
}) => {
  // ===============
  // Internal state.
  // ===============

  const [id] = useState(propsId);
  const [activeIndex, setActiveIndex] = useState(selected);

  // =================
  // Build class list.
  // =================

  const classList = cx({
    tabs: true,
    [String(className)]: className,
  });

  // ===============
  // Build UI lists.
  // ===============

  const list = Array.isArray(children) ? children : [children];

  const tabsList = getTabsList({
    activeIndex,
    id,
    list,
    setActiveIndex,
  });

  const panelsList = getPanelsList({
    activeIndex,
    id,
    list,
  });

  // ==========
  // Expose UI.
  // ==========

  return (
    <Render if={list[0]}>
      <div className={classList} id={id} style={style}>
        {tabsList}
        {panelsList}
      </div>
    </Render>
  );
};

Tabs.propTypes = {
  children: PropTypes.node,
  className: PropTypes.string,
  id: PropTypes.string,
  selected: PropTypes.number,
  style: PropTypes.object,
};

export default Tabs;

Function: getIsActive

Due to a <Tabs> component always having something active and visible, this function contains some logic to determine whether an index of a given tab should be the lucky winner. Essentially, in sentence form the logic goes like this.

This current tab is active if:

  • Its index matches the activeIndex, or
  • The tabs UI has only one tab, or
  • It is the first tab, and the activeIndex tab does not exist.
const getIsActive = ({ activeIndex = null, index = null, list = [] }) => {
  // Index matches?
  const isMatch = index === parseFloat(activeIndex);

  // Is first item?
  const isFirst = index === 0;

  // Only first item exists?
  const onlyFirstItem = list.length === 1;

  // Item doesn't exist?
  const badActiveItem = !list[activeIndex];

  // Flag as active?
  const isActive = isMatch || onlyFirstItem || (isFirst && badActiveItem);

  // Expose boolean.
  return !!isActive;
};

Function: getTabsList

This function generates the clickable <li role="tabs"> UI, and returns it wrapped in a parent <ul role="tablist">. It assigns all the relevant aria-* and role attributes, and handles binding the onClickand onKeyDown events. When an event is triggered, setActiveIndex is called. This updates the component’s internal state.

It is noteworthy how the content of the <li> is derived. That is passed in as <div label="…"> children of the parent <Tabs> component. Though this is not a real concept in flat HTML, it is a handy way to think about the relationship of the content. The children of that <div> become the the innards of our role="tabpanel" later.

const getTabsList = ({ activeIndex = null, id = '', list = [], setActiveIndex = () => {} }) => {
  // Build new list.
  const newList = list.map((item = {}, index) => {
    // =========
    // Get data.
    // =========

    const { props: itemProps = {} } = item;
    const { disabled = null, label = '' } = itemProps;
    const idPanel = getPanelId(id, index);
    const idTab = getTabId(id, index);
    const isActive = getIsActive({ activeIndex, index, list });

    // =======
    // Events.
    // =======

    const handleClick = (event = {}) => {
      const { key = '' } = event;

      if (!disabled) {
        // Early exit.
        if (key && key.toLowerCase() !== 'enter') {
          return;
        }

        setActiveIndex(index);
      }
    };

    // ============
    // Add to list.
    // ============

    return (
      <li
        aria-controls={idPanel}
        aria-disabled={disabled}
        aria-selected={isActive}
        className="tabs__item"
        disabled={disabled}
        id={idTab}
        key={idTab}
        role="tab"
        tabIndex={disabled ? null : 0}
        // Events.
        onClick={handleClick}
        onKeyDown={handleClick}
      >
        {label || `${index + 1}`}
      </li>
    );
  });

  // ==========
  // Expose UI.
  // ==========

  return (
    <Render if={newList.length}>
      <ul className="tabs__list" role="tablist">
        {newList}
      </ul>
    </Render>
  );
};

Function: getPanelsList

This function parses the incoming children of the top level component and extracts the content. It also makes use of getIsActive to determine whether (or not) to apply aria-hidden="true". As one might expect by now, it adds all the other relevant aria-* and role attributes too. It also applies any extra className or style that was passed in.

It also is “smart” enough to wrap any string content — anything lacking a wrapping tag already — in <p> tags for consistency.

const getPanelsList = ({ activeIndex = null, id = '', list = [] }) => {
  // Build new list.
  const newList = list.map((item = {}, index) => {
    // =========
    // Get data.
    // =========

    const { props: itemProps = {} } = item;
    const { children = '', className = null, style = null } = itemProps;
    const idPanel = getPanelId(id, index);
    const idTab = getTabId(id, index);
    const isActive = getIsActive({ activeIndex, index, list });

    // =============
    // Get children.
    // =============

    let content = children || item;

    if (typeof content === 'string') {
      content = <p>{content}</p>;
    }

    // =================
    // Build class list.
    // =================

    const classList = cx({
      tabs__panel: true,
      [String(className)]: className,
    });

    // ==========
    // Expose UI.
    // ==========

    return (
      <div
        aria-hidden={!isActive}
        aria-labelledby={idTab}
        className={classList}
        id={idPanel}
        key={idPanel}
        role="tabpanel"
        style={style}
      >
        {content}
      </div>
    );
  });

  // ==========
  // Expose UI.
  // ==========

  return newList;
};

Function: Tabs

This is the main component. It sets an internal state for an id, to essentially cache any generated uuid() so that it does not change during the lifecycle of the component. React is finicky about its key attributes (in the previous loops) changing dynamically, so this ensures they remain static once set.

We also employ useState to track the currently selected tab, and pass down a setActiveIndex function to each <li> to monitor when they are clicked. After that, it is pretty straightfowrard. We call getTabsList and getPanelsList to build our UI, and then wrap it all up in <div role="tablist">.

It accepts any wrapper level className or style, in case anyone wants further tweaks during implementation. Providing other developers (as consumers) this flexibility means that the likelihood of needing to make further edits to the core component is lower. Lately, I have been doing this as a “best practice” for all components I create.

const Tabs = ({
  children = '',
  className = null,
  selected = 0,
  style = null,
  id: propsId = uuid(),
}) => {
  // ===============
  // Internal state.
  // ===============

  const [id] = useState(propsId);
  const [activeIndex, setActiveIndex] = useState(selected);

  // =================
  // Build class list.
  // =================

  const classList = cx({
    tabs: true,
    [String(className)]: className,
  });

  // ===============
  // Build UI lists.
  // ===============

  const list = Array.isArray(children) ? children : [children];

  const tabsList = getTabsList({
    activeIndex,
    id,
    list,
    setActiveIndex,
  });

  const panelsList = getPanelsList({
    activeIndex,
    id,
    list,
  });

  // ==========
  // Expose UI.
  // ==========

  return (
    <Render if={list[0]}>
      <div className={classList} id={id} style={style}>
        {tabsList}
        {panelsList}
      </div>
    </Render>
  );
};

If you are curious about the <Render> function, you can read more about that in this example.

File: Accordion.js

// =============
// Used like so…
// =============

<Accordion>
  <div label="Tab 1">
    <p>
      Tab 1 content
    </p>
  </div>
  <div label="Tab 2">
    <p>
      Tab 2 content
    </p>
  </div>
</Accordion>

As you may have deduced — due to the vanilla JS example handling both tabs and accordion — this file has quite a few similarities to how Tabs.js works.

Rather than belabor the point, I will simply provide the file’s contents for completeness and then speak about the specific areas in which the logic differs. So, take a gander at the contents and I will explain what makes <Accordion> quirky.

import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { v4 as uuid } from 'uuid';
import cx from 'classnames';

// UI.
import Render from './Render';

// ===========
// Get tab ID.
// ===========

const getTabId = (id = '', index = 0) => {
  return `tab_${id}_${index}`;
};

// =============
// Get panel ID.
// =============

const getPanelId = (id = '', index = 0) => {
  return `tabpanel_${id}_${index}`;
};

// ==============================
// Get `tab` and `tabpanel` list.
// ==============================

const getTabsAndPanelsList = ({
  activeItems = {},
  id = '',
  isMulti = true,
  list = [],
  setActiveItems = () => {},
}) => {
  // Build new list.
  const newList = [];

  // Loop through.
  list.forEach((item = {}, index) => {
    // =========
    // Get data.
    // =========

    const { props: itemProps = {} } = item;

    const {
      children = '',
      className = null,
      disabled = null,
      label = '',
      style = null,
    } = itemProps;

    const idPanel = getPanelId(id, index);
    const idTab = getTabId(id, index);
    const isActive = !!activeItems[index];

    // =======
    // Events.
    // =======

    const handleClick = (event = {}) => {
      const { key = '' } = event;

      if (!disabled) {
        // Early exit.
        if (key && key.toLowerCase() !== 'enter') {
          return;
        }

        // Keep active items?
        const state = isMulti ? activeItems : null;

        // Update active item.
        const newState = {
          ...state,
          [index]: !activeItems[index],
        };

        // Set active item.
        setActiveItems(newState);
      }
    };

    // =============
    // Get children.
    // =============

    let content = children || item;

    if (typeof content === 'string') {
      content = <p>{content}</p>;
    }

    // =================
    // Build class list.
    // =================

    const classList = cx({
      accordion__panel: true,
      [String(className)]: className,
    });

    // ========
    // Add tab.
    // ========

    newList.push(
      <div
        aria-controls={idPanel}
        aria-disabled={disabled}
        aria-selected={isActive}
        className="accordion__item"
        disabled={disabled}
        id={idTab}
        key={idTab}
        role="tab"
        tabIndex={disabled ? null : 0}
        // Events.
        onClick={handleClick}
        onKeyDown={handleClick}
      >
        <i aria-hidden="true" className="accordion__item__icon" />
        {label || `${index + 1}`}
      </div>
    );

    // ==========
    // Add panel.
    // ==========

    newList.push(
      <div
        aria-hidden={!isActive}
        aria-labelledby={idTab}
        className={classList}
        id={idPanel}
        key={idPanel}
        role="tabpanel"
        style={style}
      >
        {content}
      </div>
    );
  });

  // ==========
  // Expose UI.
  // ==========

  return newList;
};

getTabsAndPanelsList.propTypes = {
  activeItems: PropTypes.object,
  id: PropTypes.string,
  isMulti: PropTypes.bool,
  list: PropTypes.array,
  setActiveItems: PropTypes.func,
};

// ==========
// Component.
// ==========

const Accordion = ({
  children = '',
  className = null,
  isMulti = true,
  selected = {},
  style = null,
  id: propsId = uuid(),
}) => {
  // ===============
  // Internal state.
  // ===============

  const [id] = useState(propsId);
  const [activeItems, setActiveItems] = useState(selected);

  // =================
  // Build class list.
  // =================

  const classList = cx({
    accordion: true,
    [String(className)]: className,
  });

  // ===============
  // Build UI lists.
  // ===============

  const list = Array.isArray(children) ? children : [children];

  const tabsAndPanelsList = getTabsAndPanelsList({
    activeItems,
    id,
    isMulti,
    list,
    setActiveItems,
  });

  // ==========
  // Expose UI.
  // ==========

  return (
    <Render if={list[0]}>
      <div
        aria-multiselectable={isMulti}
        className={classList}
        id={id}
        role="tablist"
        style={style}
      >
        {tabsAndPanelsList}
      </div>
    </Render>
  );
};

Accordion.propTypes = {
  children: PropTypes.node,
  className: PropTypes.string,
  id: PropTypes.string,
  isMulti: PropTypes.bool,
  selected: PropTypes.object,
  style: PropTypes.object,
};

export default Accordion;

Function: handleClick

While most of our <Accordion> logic is similar to <Tabs>, it differs in how it stores the currently active tab.

Since <Tabs> are always mutually exclusive, we only really need a single numeric index. Easy peasy.

However, because an <Accordion> can have concurrently visible panels — or be used in a mutually exclusive manner — we need to represent that to useState in a way that could handle both.

If you were beginning to think…

“I would store that in an object.”

…then congrats. You are right!

This function does a quick check to see if isMulti has been set to true. If so, we use the spread syntax to apply the existing activeItems to our newState object. We then set the current index to its boolean opposite.

const handleClick = (event = {}) => {
  const { key = '' } = event;

  if (!disabled) {
    // Early exit.
    if (key && key.toLowerCase() !== 'enter') {
      return;
    }

    // Keep active items?
    const state = isMulti ? activeItems : null;

    // Update active item.
    const newState = {
      ...state,
      [index]: !activeItems[index],
    };

    // Set active item.
    setActiveItems(newState);
  }
};

For reference, here is how our activeItems object looks if only the first accordion panel is active and a user clicks the second. Both indexes would be set to true. This allows for viewing two expanded role="tabpanel" simultaneously.

/*
  Internal representation
  of `activeItems` state.
*/

{
  0: true,
  1: true,
}

Whereas if we were not operating in isMulti mode — when the wrapper has aria-multiselectable="false" — then activeItems would only ever contain one key/value pair.

Because rather than spreading the current activeItems, we would be spreading null. That effectively wipes the slate clean, before recording the currently active tab.

/*
  Internal representation
  of `activeItems` state.
*/

{
  1: true,
}

Conclusion

Still here? Awesome.

Hopefully you found this article informative, and maybe even learned a bit more about accessibility and JS(X) along the way. For review, let us look one more time at our flat HTML example and and the React usage of our <Tabs>component. Here is a comparison of the markup we would write in a vanilla JS approach, versus the JSX it takes to generate the same thing.

I am not saying that one is better than the other, but you can see how React makes it possible to distill things down into a mental model. Working directly in HTML, you always have to be aware of every tag.

HTML

<div class="tabs">
  <ul class="tabs__list">
    <li class="tabs__item">
      Tab 1
    </li>
    <li class="tabs__item">
      Tab 2
    </li>
  </ul>
  <div class="tabs__panel">
    <p>
      Tab 1 content
    </p>
  </div>
  <div class="tabs__panel">
    <p>
      Tab 2 content
    </p>
  </div>
</div>

JSX

<Tabs>
  <div label="Tab 1">
    Tab 1 content
  </div>
  <div label="Tab 2">
    Tab 2 content
  </div>
</Tabs>

↑ One of these probably looks preferrable, depending on your point of view.

Writing code closer to the metal means more direct control, but also more tedium. Using a framework like React means you get more functionality “for free,” but also it can be a black box.

That is, unless you understand the underlying nuances already. Then you can fluidly operate in either realm. Because you can see The Matrix for what it really is: Just JavaScript™. Not a bad place to be, no matter where you find yourself.

The post The Anatomy of a Tablist Component in Vanilla JavaScript Versus React appeared first on CSS-Tricks.

How To Conditionally Enqueue Scripts In WordPress

There are situations that may arise where you want to load a JavaScript file only on a certain page or pages instead of loading it on every page of the website, primarily because you only want it to run on that page and don’t need it to load on pages where it is not running. Most WordPress themes enqueue (load) their necessary scripts on all pages already, but what if you want to conditionally enqueue scripts or a script instead? This quick tutorial will show you how to do that – to check if you are on a certain page or pages and, if so, load the corresponding script file.

UNLIMITED DOWNLOADS: 500,000+ WordPress & Design Assets

Sign up for Envato Elements and get unlimited downloads starting at only $16.50 per month!




 

WordPress has an abundance of conditional tags built in (see details here). Utilizing these to match the conditions you’re looking for, you can wrap the enqueuing of your script so that it only loads when the condition is met. For instance, if you only wanted the script to run on a page with the slug of ‘mypage’, you would use the conditional tag if(is_page('mypage')) { load that script! } to conditionally enqueue your script only on that page.

Of course, there are a multitude of other conditional tags you can use, found at the link above. Some more common uses may be:

  • is_single()  – When a single post of any post type (except attachment and page post types) is being displayed, narrowed down by using the post ID, title, slug, or an array of a combination of any of the three.
  • is_front_page() – When the main blog page is being displayed and the ‘Settings > Reading ->Front page displays’ is set to “Your latest posts”, or when ‘Settings > Reading ->Front page displays’ is set to “A static page” and the “Front Page” value is the current Page being displayed. (Confusing? Click here for more explanation.)
  • is_home() – This one gets a bit more tricky, but when used correctly it will display your blog posts page. (See here.)
  • is_category() – When the archive page of a specific category or group of categories is being displayed, narrowed down by using the category ID, name, slug, or an array of a combination of any of the three, plus a few more conditions can be designated.

So let’s move on to how to use these tags to conditionally enqueue scripts in WordPress. Simply open your theme’s functions.php file and add the following code at the bottom.

add_action('wp_enqueue_scripts', 'firstwd_enqueue');
function firstwd_enqueue() {
    if (is_page('mypage')) {
        wp_enqueue_script('script-name', get_template_directory_uri().'/path-to-script-name.js', array( 'jquery' ), '', true);
    }
}

You will need to make sure to change ‘mypage’ to the slug of your page, ‘script-name’ to whatever you want to use as a unique name of the script, and ‘/path-to-name-of-script’ to match the url to the script within your theme’s directory (often something like ‘/assets/js/name-of-script.js’).

Again, refer to the WordPress Codex for all of the variations of tags that you can use to conditionally enqueue scripts in WordPress. be sure to check out our other WordPress tutorials for more quick snippets like this along with more in-depth articles as well.

How to Create a Free Gift Coupon in WooCommerce (Easy Way)

Do you want to offer your WooCommerce customers a free gift along with their purchase?

Offering free gifts can be a great way to drive more sales, increase average order value, and reward returning customers on your online store.

In this article, we will show you how to add a free gift coupon in WooCommerce.

How to create a free gift coupon in WooCommerce (easy way)

Why Offer Your Customers a Free Gift?

There are several reasons why you might want to offer a free gift to your customers on your WooCommerce store.

One great reason is to drive more sales and increase average order value. Many online stores, offer a free gift on spending a set amount or when buying a specific product.

For example: get a free bag on all orders above $200.

This kind of promotional campaign adds extra value to users order and encourages them to complete a purchase.

Another great benefit is that you grow your email list.

You see, more than 70% of visitors coming to your website will never come back. Getting them to sign up for an email newsletter helps you bring them back to your website.

A free gift coupon will help you capture more email addresses and ultimately grow your business.

If you want to get creative with this and add a bit of fun for your customers, you could even use a “spin to win” coupon wheel to give them the free gift coupon.

What to give away as a free gift?

Obviously, you don’t want to spend too much money by giving away free items. However, you need to offer something that is useful or valuable while still cost-effective for your business.

Many popular online stores use branded merchandise as a free gift. That way, you’ll be getting your brand in front of customers and reminding them to come back for more.

Others offer complimentary accessories as a free gift to reward customers and boost brand loyalty.

That being said, let’s take a look at how to add the free gift coupon in WooCommerce.

Creating a Free Gift Coupon in WooCommerce

For this tutorial, we’ll be using the Advanced Coupons plugin, which is the best WordPress coupon code plugin on the market.

It allows you to easily manage coupons in WooCommerce with a ton of options. This helps you boost your WooCommerce sales and grow your business.

First, you need to install and activate the Advanced Coupons plugin. If you’re not sure how to do that, just check out our step by step guide on how to install a WordPress plugin.

Upon activation, go to WooCommerce » Coupons page in your WordPress admin dashboard.

The WooCommerce Coupons tab in the dashboard

First, you need to click the ‘Add Coupon’ button to create a new coupon. On the next screen, enter the coupon code that you want to use for the offer at the top of the screen.

Entering a code for your free gift coupon - we've used 'freeshirt' for ours

After that, scroll down to the ‘Coupon Data’ section and switch to the ‘Add Products’ tab from the left column.

Click on the 'Add Products' tab

From here you need to click the ‘+ Add Product’ button to select product that you want to offer as free gift.

Using the search dropdown, find the product that you want to use. After that, select ‘$: Override price’ option under the ‘In the Price/Discount’ column.

Making your product free using the 'Price/Discount' dropdown

Next, click the ‘Add’ button to save your product, and you should now see it listed like this:

The free product shown in the table of products

If you want to give away more than one free gift to the customer, then you can enter other products here as well.

When the customer enters the coupon code, the product(s) will automatically be added to their shopping cart for free. Here’s the free red shirt, after applying the coupon:

The freeshirt coupon in action on our site

Offering a Free Gift if the Customer Spends Enough

What if you want to use a free gift to encourage your customers to spend more? You can do this too with Advanced Coupons.

Simply set up your coupon code as mentioned above, and then click the Usage Restriction tab.

On this tab, you need to set the Minimum Spend to whatever amount you want. We’ve used $20.

Setting a minimum spend of $20

Now, the free gift coupon can only be used if the customer has $20 or more of products in their cart. If they try to use it before reaching this amount, they’ll see an error message:

The error 'The minimum spend for this coupon is $20.00' appears if the customer hasn't met the $20 minimum

That’s it! You’ve created a coupon code that gives your customers a free gift.

We hope this article helped you learn how to create a free gift coupon in WordPress. You may also want to see our list of the best WooCommerce plugins for your store, and our comparison of the best email marketing services.

If you liked this article, then please subscribe to our YouTube Channel for WordPress video tutorials. You can also find us on Twitter and Facebook.

The post How to Create a Free Gift Coupon in WooCommerce (Easy Way) appeared first on WPBeginner.