This is the solution to Challenge #001, so let’s first identify what we’re trying to achieve:
- We’re presenting a form that checks to see if the email is valid before it submits
- If the email isn’t valid, we present an error message
- If the email is valid, we submit the form and replace it with a success message
It’s a pretty straightforward context, but there’s a few things we need to make sure that we get right to make it as helpful as possible and the first of those is working out what our minimum viable experience is:
A HTML rendered form that when submission is attempted, the email address is validated. If it is valid it will submit. If not, an error will show on the field if the browser supports native HTML validation.
You could go even further and say our minimum viable experience is just a HTML form that submits, which is true, but it’s also implied already.
Now we know what we want to build, let’s dig into it!
This example has the following code files:
index.html
, referred to as “your markup” for the rest of this tutorialcss/global.css
, referred to as “your CSS” for the rest of this tutorialjs/main.js
, referred to as “your JS” for the rest of this tutorial
You can see a live demo of the final solution here.
HTMLpermalink
Let’s first add the base HTML structure to your empty index.html
file:
- Code language
- html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta http-equiv="X-UA-Compatible" content="ie=edge" /> <title>Front-End Challenges Club - 001</title> <link rel="stylesheet" href="https://unpkg.com/modern-css-reset/dist/reset.min.css" /> <link rel="stylesheet" href="/css/global.css" /> </head> <body> <main class="[ signup ] [ flow ]"> <h1 class="signup__heading">Sign up for the latest updates</h1> </main> <script type="module" src="/js/main.js" async defer></script> </body> </html>
This gives us our basics and importantly, we pull in our CSS and JS. There’s a couple of bits that I want to explain before we move on:
- We’re pulling in a reset before our CSS file. This is a reference to a tiny reset I like to use that you can grab here and read about here.
- Our
<main>
element is used as oursignup
component which contains a single<h1>
for our main heading.
There’s not much going on, so let’s add the form and the elements within it. Add this to your markup, after the <h1>
element.
- Code language
- html
<form id="signupForm" action="/" class="[ signup__form ] [ flow ]" method="POST"> <label for="email">Email address</label> <div class="inline-field-control"> <input type="email" name="email" id="email" autocapitalize="none" autocorrect="off" required pattern="[^@]+@[^\.]+\..+" /> <button type="submit" class="button"> <span class="visually-hidden">Submit email</span> <svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" width="1em" height="1em" viewBox="0 0 24 24" > <path fill="currentColor" d="M11.293 5.707L16.586 11H5a1 1 0 000 2h11.586l-5.293 5.293a.999.999 0 101.414 1.414l7-7a1.006 1.006 0 000-1.414l-7-7a.999.999 0 10-1.414 1.414z" /> </svg> </button> </div> </form>
You’ll notice I’ve added the action
and method
attributes to the form, but in the example they don’t really make a difference.
Label
I have a <label>
which has a for
attribute. This links the label to the element that has the same id
as the label’s for
attribute. In this instance, the label is linked to the element that has the id
of “email”.
Input
The <input />
has a type
of email
. This attribute is important because it instructs your browser and operating system to provide the relevant assistance to fill it in. This could be a change in keyboard on your phone or the way it is announced to screen readers. It also has the usual name
and id
attributes. Because the id
is “email”, it’s now linked up to the <label>
.
You’ll also notice there’s some other attributes on that <input />
, so let’s go through them one-by-one:
autocapitalize="none"
prevents the browser from capitalising the first letter of your email address. This is often a problem with iOS devices.autocorrect="off"
prevents the browser from autocorrecting your spelling while you type. This can be incredibly frustrating for a user when they are inputting an email address or a search query.required
tells the browser that the user must complete this field before submitting the formpattern="…"
is the regular expression that the browser can test the user’s input with. By default, anemail
input type only looks for an@
in the string, which isn’t good enough. The regular expression that we are using is simple, but more robust and will make sure that the email is better formatted.
Button
We’re using a proper <button>
here, so we get all of that lovely accessibility and usability goodness for free. We don’t need to add a type
of submit
because that’s a button’s default behaviour. Because we’ve added it inside a form element, it’ll submit that form by default.
We have added a class of .button
to it which we will hook onto with our CSS, when it’s available. We could just style the button directly, but often, some links will be made to look like buttons too, so this approach helps with that.
Inside the button, we have two key elements. The visual design calls for this button to just have an arrow icon, but we have to provide better context for those who can’t see the elements. We use the sibling <span class="visually-hidden">
element to do that for us and we’ll discuss the CSS that powers that later. Importantly, this approach means that if there’s no CSS, the context is available wether you can see or not.
The <svg>
itself has a couple of important features. First of all, we hide it from assistive tech, using aria-hidden="true"
and we prevent it from being focused (IE, pre-Chromium Edge) by adding focusable="false"
. By setting both the width and height to 1em
gives us two advantages:
- When CSS isn’t available, the icons won’t take up the entire width of the viewport.
- The icons can size themselves based on the
font-size
of their parent context. I wrote about that in some detail.
The last trick on the <svg>
is setting the <path>
fill to be currentColor
. This means that it will inherit whatever the color
value is of its parent. You could apply this to the <svg>
, if that’s what you prefer, too.
Right, that was a lot to digest, so less reading: more coding. Add this HTML after the </form>
(~ line 26):
- Code language
- html
<div aria-atomic="true" role="alert" class="signup__alert"></div>
This is the container for our feedback messages: both success and error states. We add role="alert"
so a screen reader interrupts the user. This is useful when there is important information to convey, such as an invalid field error. If you have lots of errors on a form, don’t be tempted to use loads of different role="alert"
elements, but instead, group errors into a single one. Lastly, we add aria-atomic="true"
so multiple invalid submissions or changes are announced. This is particularly a problem with VoiceOver for iOS.
You’ll notice that the element is being left empty. This is very deliberate because although it’s tempting to present an error message on input or blur, it can actually be very confusing an overloading for someone with a cognitive impairment, so a safer, more inclusive approach is to validate when a user attempts to submit the form.
Thats it for HTML and guess what: we have a minimum viable experience 🎉
Just so we’re all on the same page: your HTML should look like this:
- Code language
- html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta http-equiv="X-UA-Compatible" content="ie=edge" /> <title>Front-End Challenges Club - 001</title> <link rel="preload" as="font" type="font/woff2" href="/fonts/Inter-Black.woff2" /> <link rel="stylesheet" href="https://unpkg.com/modern-css-reset/dist/reset.min.css" /> <link rel="stylesheet" href="/css/global.css" /> </head> <body> <main class="[ signup ] [ flow ]"> <h1 class="signup__heading">Sign up for the latest updates</h1> <form id="signupForm" action="/" class="[ signup__form ] [ flow ]" method="POST"> <label for="email">Email address</label> <div class="inline-field-control"> <input type="email" name="email" id="email" autocomplete="off" autocapitalize="none" autocorrect="off" required pattern="[^@]+@[^\.]+\..+" /> <button class="button"> <span class="visually-hidden">Submit email</span> <svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" width="1em" height="1em" viewBox="0 0 24 24" > <path fill="currentColor" d="M11.293 5.707L16.586 11H5a1 1 0 000 2h11.586l-5.293 5.293a.999.999 0 101.414 1.414l7-7a1.006 1.006 0 000-1.414l-7-7a.999.999 0 10-1.414 1.414z" /> </svg> </button> </div> </form> <div aria-atomic="true" role="alert" class="signup__alert"></div> </main> <script type="module" src="/js/main.js" async defer></script> </body> </html>
Let’s move on to some CSS!
CSSpermalink
Let’s start with the basics and add the global style stuff. You’ll need to add the woff2
font file from the provided font assets. Open up your CSS file (/css/global.css
) and add the following:
- Code language
- css
/** * FONT FACE */ @font-face { font-family: 'Inter'; font-style: normal; font-weight: 900; font-display: swap; src: url('/fonts/Inter-Black.woff2') format('woff2'); } /** * VARIABLES */ :root { --color-primary: #4c2982; --color-secondary: #f9d170; --color-bg: #f9f7f3; --color-text: #252525; --color-light: #f3f3f3; --color-success: #067973; --color-success-bg: #f5fffe; --color-error: #b71540; --color-error-bg: #fdeff3; --color-shadow: rgba(23, 11, 41, 0.12); --font-base-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif; --font-heading-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif; --metric-rhythm: 2rem; --metric-gutter: 2rem; --metric-box-padding: 1rem 1rem; --metric-interaction-padding: 0.6rem 0.6rem; }
All we’ve done here is set our @font-face
and our Custom Properties. The Custom Properties are mostly colour and font settings, but we’ve also got some metrics like gutter and rhythm set to keep things consistent.
Let’s add some global styles now and tackle our base body
and raw HTML elements:
- Code language
- css
/** * GLOBAL STYLES */ body { font-family: var(--font-base-family, sans-serif); display: grid; place-items: center; background: var(--color-light); color: var(--color-text); padding: var(--metric-gutter, 2rem); } h1 { font-family: var(--font-heading-family, sans-serif); font-size: 2rem; font-weight: 900; color: var(--color-primary); line-height: 1.1; } label { color: var(--color-primary); text-transform: uppercase; font-weight: 700; }
This is all pretty straightforward stuff, really, so I won’t dwell too much on it. One bit I will pick out is this on the <body>
, though:
- Code language
- css
display: grid; place-items: center;
Because all of our content is a <main>
element and the <body>
has min-height: 100vh
in the reset: using the CSS above vertically and horizontally centers the content. Neat, huh?
Let’s jog along and add some more global styles that handle both our <button>
and <input />
elements. Add this to your CSS:
- Code language
- css
input[type], button { border: none; margin: 0; font: inherit; line-height: 1; padding: 0.8rem; padding: var(--metric-interaction-padding); outline-offset: -1px; } @media screen and (-ms-high-contrast: active) { input[type], button { border: 1px solid; } }
Because our main <button>
and <input />
elements sit inline, we need to make sure that a lot of the styles are shared. A little trick to pick out is font: inherit
, which will grab outer context typography which generally means writing a lot less typography CSS.
The -ms-high-contrast: active
media query lets us target high contrast mode on windows where I’m adding the border back to our elements to make them easier to see.
Let’s tackle global focus. Add this to your CSS:
- Code language
- css
/** * GLOBAL FOCUS */ :focus { outline: 1px solid var(--color-primary); }
I’ve just targeted all elements by using :focus
as the selector. You could add a *
if you want, too. This is a great way of setting solid, consistent focus styles across the board.
Utilities
Add this flow utility to your CSS:
- Code language
- css
/** * FLOW UTILITY */ .flow { --flow-space: var(--metric-rhythm); } .flow > * + * { margin-top: 1em; margin-top: var(--flow-space); }
This is a great way of adding space between sibling elements. You may have spotted the use of this already in the HTML and if you refresh your project, you’ll notice that elements magically have space between them! You can read more about this utility in a post I wrote last year.
Let’s add another utility to your CSS:
- Code language
- css
/** * VISUALLY HIDDEN UTILITY */ .visually-hidden { border: 0; clip: rect(0 0 0 0); height: auto; margin: 0; overflow: hidden; padding: 0; position: absolute; width: 1px; white-space: nowrap; }
This does exactly what it says on the tin. It hides the element visually, but the content is still accessible to assistive technology like screen readers. If you refresh your project now, you’ll notice the button text has disappeared and only the icon can be seen.
Signup component
Let’s add the CSS for our .signup
component, which is the <main>
element of this demo. Add this CSS:
- Code language
- css
/** * SIGNUP COMPONENT */ .signup { max-width: 20rem; } .signup__form + .signup__alert { --flow-space: 1rem; }
It’s pretty straightforward is this one. I’m setting a sensible max width which, if the user of the site hasn’t changed their base font size, will be about 320px
wide. This also means that the .signup
component will be 100% until it hits that max, which is great for mobile-first design.
The second bit sets the --flow-space
for the .flow
utility. One of my favourite features CSS Custom Properties is that they can be contextually overridden. This gives .flow
so much power to be useful in this sort of context.
Inline Field Control component
Now let’s tackle the inline field control which smooshes the submit button and the field together. I think it goes without saying that we’re getting our flex
on here, so let’s dig in and add this to your CSS:
- Code language
- css
/** * INLINE FIELD CONTROL COMPONENT */ .inline-field-control { --flow-space: 0.5rem; display: flex; box-shadow: 0 2px 10px var(--color-shadow); } .inline-field-control input { flex: auto; }
Again, you can see --flow-space
being overridden. This time, we’re saying that this element—.inline-field-control
—should only have 0.5rem
of margin if it’s a sibling element. In our context, it means that there’s only 0.5rem
of space between the <label>
and our field.
We set the .inline-field-control
component to be display: flex
which forces our <input>
and <button>
to sit inline with each other. By setting .inline-field-control input
to be flex: auto
we’re saying: “Hey <input>
, if there’s any space left on the horizontal axis, do us a solid and fill it up for us”. Handy, right?
Button component
This one is very straightforward. Add this to your CSS:
- Code language
- css
/** * BUTTON COMPONENT */ .button { background: var(--color-secondary); color: var(--color-primary); font-size: 1.6rem; min-width: 3.5rem; cursor: pointer; } .button:hover { filter: brightness(1.05); } .button svg { transform: translateY(1px); /* Optical adjustment */ }
We tackled most of the style globally, so all we’re doing here is setting some specific styles and then adding a hover state. There’s also an optical adjustment where I’ve nudged the <svg>
by 1px
. Optical adjustments are fascinating and there’s a great post here.
Alert component
The last component is the .alert
component. This is what get’s rendered by JavaScript in our role="alert"
container. Add this CSS:
- Code language
- css
/** * ALERT COMPONENT */ .alert { --alert-text: var(--color-text); --alert-bg: var(--color-bg); display: flex; align-items: flex-start; background: var(--alert-bg); color: var(--alert-text); border: 1px solid; padding: var(--metric-box-padding); margin-top: 1rem; animation: slide-up 250ms ease; } .alert[data-state='error'] { --alert-text: var(--color-error); --alert-bg: var(--color-error-bg); } .alert[data-state='success'] { --alert-text: var(--color-success); --alert-bg: var(--color-success-bg); } .alert__icon { font-size: 1.6em; flex-shrink: 0; } .alert__content { padding-left: 0.8rem; } .alert__content b { display: block; }
There’s quite a bit of CSS here, so I’ll just pick out the headlines:
- We’re using and then overriding Custom Properties using
[data-state]
hooks. This means that when the JavaScript sets a state, we can set the correct colours, without touching the actual CSS properties. - The
.alert__icon
element hasflex-shrink: 0
that means if the container has run out of space, it can’t attempt to shrink that element. This is really useful for inline icons. - Lastly, there’s this animation rule:
slide-up 250ms ease
. We haven’t set that yet so it’ll have no effect. CSS is awesome because regardless of what that animation does, this component still works without it. Proper progressive enhancement!
Animation
As mentioned in the last section, we have a slide-up
animation to define, so let’s add it to your CSS:
- Code language
- css
/** * ANIMATIONS */ @keyframes slide-up { 0% { opacity: 0; transform: translateY(0.4rem); } 100% { opacity: 1; transform: translateY(0); } }
This is as simple as it get’s really. I set the default state so that the element is hidden and nudged down a bit, then at 100%
this is reversed again. That’ll mean in the context of our demo, the little .alert
will slide up and fade in when it’s rendered.
Notice how I use an animation and not a transition for this. Yeh, we could use a transition, but it would mean tweaking states and using timeouts in JavaScript to make sure it isn’t janky when it get’s rendered in the DOM. Setting it as an animation means we can set it and wait until it’s rendered. Nice and simple!
Ok, that’s all the CSS done. It should look like this:
- Code language
- css
/** * FONT FACE */ @font-face { font-family: 'Inter'; font-style: normal; font-weight: 900; font-display: swap; src: url('/fonts/Inter-Black.woff2') format('woff2'); } /** * VARIABLES */ :root { --color-primary: #4c2982; --color-secondary: #f9d170; --color-bg: #f9f7f3; --color-text: #252525; --color-light: #f3f3f3; --color-success: #067973; --color-success-bg: #f5fffe; --color-error: #b71540; --color-error-bg: #fdeff3; --color-shadow: rgba(23, 11, 41, 0.12); --font-base-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif; --font-heading-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif; --metric-rhythm: 2rem; --metric-gutter: 2rem; --metric-box-padding: 1rem 1rem; --metric-interaction-padding: 0.6rem 0.6rem; } /** * GLOBAL STYLES */ body { font-family: var(--font-base-family, sans-serif); display: grid; place-items: center; background: var(--color-light); color: var(--color-text); padding: var(--metric-gutter, 2rem); } h1 { font-family: var(--font-heading-family, sans-serif); font-size: 2rem; font-weight: 900; color: var(--color-primary); line-height: 1.1; } label { color: var(--color-primary); text-transform: uppercase; font-weight: 700; } input[type], button { border: none; margin: 0; font: inherit; line-height: 1; padding: 0.8rem; padding: var(--metric-interaction-padding); outline-offset: -1px; } @media screen and (-ms-high-contrast: active) { input[type], button { border: 1px solid; } } /** * GLOBAL FOCUS */ :focus { outline: 1px solid var(--color-primary); } /** * FLOW UTILITY */ .flow { --flow-space: var(--metric-rhythm); } .flow > * + * { margin-top: 1em; margin-top: var(--flow-space); } /** * VISUALLY HIDDEN UTILITY */ .visually-hidden { border: 0; clip: rect(0 0 0 0); height: auto; margin: 0; overflow: hidden; padding: 0; position: absolute; width: 1px; white-space: nowrap; } /** * SIGNUP COMPONENT */ .signup { max-width: 20rem; } .signup__form + .signup__alert { --flow-space: 1rem; } /** * INLINE FIELD CONTROL COMPONENT */ .inline-field-control { --flow-space: 0.5rem; display: flex; box-shadow: 0 2px 10px var(--color-shadow); } .inline-field-control input { flex: auto; } /** * BUTTON COMPONENT */ .button { background: var(--color-secondary); color: var(--color-primary); font-size: 1.6rem; min-width: 3.5rem; cursor: pointer; } .button:hover { filter: brightness(1.05); } .button svg { transform: translateY(1px); /* Optical adjustment */ } /** * ALERT COMPONENT */ .alert { --alert-text: var(--color-text); --alert-bg: var(--color-bg); display: flex; align-items: flex-start; background: var(--alert-bg); color: var(--alert-text); border: 1px solid; padding: var(--metric-box-padding); margin-top: 1rem; animation: slide-up 250ms ease; } .alert[data-state='error'] { --alert-text: var(--color-error); --alert-bg: var(--color-error-bg); } .alert[data-state='success'] { --alert-text: var(--color-success); --alert-bg: var(--color-success-bg); } .alert__icon { font-size: 1.6em; flex-shrink: 0; } .alert__content { padding-left: 0.8rem; } .alert__content b { display: block; } /** * ANIMATIONS */ @keyframes slide-up { 0% { opacity: 0; transform: translateY(0.4rem); } 100% { opacity: 1; transform: translateY(0); } }
JavaScriptpermalink
We’re on the final hurdle now and we’re using JavaScript as it should be used: as an enhancement.
The role of the JavaScript in this context is to replace the native HTML validation and present a more accessible, helpful alternative. This is a good way to work because we know when our JavaScript fails, we will still be helping users, but when it’s available, we’re providing an enhanced experience.
Open up your JS (js/main.js
) and add the following function:
- Code language
- js
/** * Generate an alert component based on the passed state key * @param {String} state must be 'error' or 'success' * @return {String} A HTML string of the component output */ const renderAlert = (state = 'error') => { const iconPaths = { error: 'M11.148 4.374a.973.973 0 01.334-.332c.236-.143.506-.178.756-.116s.474.216.614.448l8.466 14.133a.994.994 0 01-.155 1.192.99.99 0 01-.693.301H3.533a.997.997 0 01-.855-1.486zM9.432 3.346l-8.47 14.14c-.422.731-.506 1.55-.308 2.29s.68 1.408 1.398 1.822c.464.268.976.4 1.475.402H20.47a3 3 0 002.572-4.507L14.568 3.346a2.995 2.995 0 00-4.123-1.014c-.429.26-.775.615-1.012 1.014zM11 9v4a1 1 0 002 0V9a1 1 0 00-2 0zm2 8a1 1 0 10-2 0 1 1 0 002 0z', success: 'M19.293 5.293L9 15.586l-4.293-4.293a.999.999 0 10-1.414 1.414l5 5a.999.999 0 001.414 0l11-11a.999.999 0 10-1.414-1.414z' }; const messages = { error: '<b>Please use a valid email.</b> Like: [email protected].', success: '<b>Yay! Thank you!</b> We’ve sent a confirmation link to your inbox.' }; return ` <figure class="alert" data-state="${state}"> <svg class="alert__icon" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" width="1em" height="1em" viewBox="0 0 24 24"> <path fill="currentColor" d="${iconPaths[state]}"/> </svg> <p class="alert__content">${messages[state]}</p> </figure> `; };
What we have here is a simple function that takes a state
parameter and returns back an .alert
component with the relevant content in it. If it’s an error, we get the error content and data-state
attribute (which controls style) or if it’s success, we get the opposite content and icon.
Notice how both the iconPath
and messages
objects have consistent error
and success
keys. This makes rendering the output markup much easier because we know (from the rest of our upcoming JS) that our state is either error
or success
, so we can confidently grab the icon with iconPaths[state]
.
Let’s move on the the next bit. Add the following function to your JS:
- Code language
- js
/** * Main app function. Grabs signup elements and validates email * with regex and blocks submission and renders alert if it fails. * If successful, it’ll allow the form to progress. */ const init = () => { const emailElement = document.querySelector('#email'); const formElement = document.querySelector('#signupForm'); const alertElement = document.querySelector('[role="alert"]'); const validationRegex = new RegExp( emailElement.getAttribute('pattern') || '[^@]+@[^.]+..+', 'i' ); emailElement.removeAttribute('required'); emailElement.removeAttribute('pattern'); formElement.setAttribute('novalidate', ''); formElement.addEventListener('submit', evt => { evt.preventDefault(); if (!validationRegex.test(emailElement.value.trim())) { alertElement.innerHTML = renderAlert('error'); emailElement.setAttribute('aria-invalid', 'true'); return; } // POST YOUR FORM WITH AJAX OR WHATNOT THEN RUN THIS formElement.parentElement.removeChild(formElement); alertElement.innerHTML = renderAlert('success'); }); };
This is our main function. Pretty tiny, right? Let’s break it down:
- We start off by grabbing three elements: the email
<input />
, the outer<form>
element and the<div role="alert">
element that is our alert container. - We then define our validation Regular Exception by trying to grab it from the email field. We set a static version as a fallback, by using the
||
operator, just in case we can’t grab it from the email input. - Next up, we remove the validation attributes. You might be thinking “what the heck is this guy doing?!”, but there’s a reason for this. Now that we’re validating with JavaScript, we should be providing the most consistent user experience as possible and because we’re handling feedback with a
role="alert"
element, the user, regardless of their tech, will see or hear a feedback message. The form also won’t submit. Solid, right? - We also add a
novalidate
attribute to the form, to make sure any other native messages don’t sneak in. - The last bit is a good ol’ event. We listen for the
<form>
’s submit event and run our validation check by testing the email<input>
’s value against our Regular Exception. This returnsfalse
if it fails, so we can use a!
in ourif
block to test for the opposite return value. This means that code in ourif
block is the error code and we can set the success state as our default state, without using anelse
. Remember, to keep things clean like this, you mustreturn
out of yourif
block to prevent the default code running! - When the email validation fails, we set
aria-invalid="true"
, which instructs assistive technology that there’s an error with the inputted content. We also run ourrenderMessage
function and immediately set it as the HTML of therole="alert"
element, which makes it immediately announce itself to a screen reader. We can use theinnerHTML
property safely because we have complete control of the HTML that’s generated. - The default success state is now up to you. You could post the data with
[fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API)
to the server or if you want the native<form>
functionality, you could move theevt.preventDefault()
line to inside the validation error block instead. This means it’ll only prevent postback if there’s an issue! For our demo, we remove the form and let the success.alert
completely take over. This would ideally happen after the form data has posted.
The last bit of this tutorial is to add a single line of JS:
- Code language
- js
init();
And now, if you refresh your browser, you’ll have a complete, working demo 🎉
Your JavaScript should look like this:
- Code language
- js
/** * Generate an alert component based on the passed state key * @param {String} state must be 'error' or 'success' * @return {String} A HTML string of the component output */ const renderAlert = (state = 'error') => { const iconPaths = { error: 'M11.148 4.374a.973.973 0 01.334-.332c.236-.143.506-.178.756-.116s.474.216.614.448l8.466 14.133a.994.994 0 01-.155 1.192.99.99 0 01-.693.301H3.533a.997.997 0 01-.855-1.486zM9.432 3.346l-8.47 14.14c-.422.731-.506 1.55-.308 2.29s.68 1.408 1.398 1.822c.464.268.976.4 1.475.402H20.47a3 3 0 002.572-4.507L14.568 3.346a2.995 2.995 0 00-4.123-1.014c-.429.26-.775.615-1.012 1.014zM11 9v4a1 1 0 002 0V9a1 1 0 00-2 0zm2 8a1 1 0 10-2 0 1 1 0 002 0z', success: 'M19.293 5.293L9 15.586l-4.293-4.293a.999.999 0 10-1.414 1.414l5 5a.999.999 0 001.414 0l11-11a.999.999 0 10-1.414-1.414z' }; const messages = { error: '<b>Please use a valid email.</b> Like: [email protected].', success: '<b>Yay! Thank you!</b> We’ve sent a confirmation link to your inbox.' }; return ` <figure class="alert" data-state="${state}"> <svg class="alert__icon" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" width="1em" height="1em" viewBox="0 0 24 24"> <path fill="currentColor" d="${iconPaths[state]}"/> </svg> <p class="alert__content">${messages[state]}</p> </figure> `; }; /** * Main app function. Grabs signup elements and validates email * with regex and blocks submission and renders alert if it fails. * If successful, it’ll allow the form to progress. */ const init = () => { const emailElement = document.querySelector('#email'); const formElement = document.querySelector('#signupForm'); const alertElement = document.querySelector('[role="alert"]'); const validationRegex = new RegExp( emailElement.getAttribute('pattern') || '[^@]+@[^.]+..+', 'i' ); emailElement.removeAttribute('required'); emailElement.removeAttribute('pattern'); formElement.setAttribute('novalidate', ''); formElement.addEventListener('submit', evt => { evt.preventDefault(); if (!validationRegex.test(emailElement.value.trim())) { alertElement.innerHTML = renderAlert('error'); emailElement.setAttribute('aria-invalid', 'true'); return; } // POST YOUR FORM WITH AJAX OR WHATNOT THEN RUN THIS formElement.parentElement.removeChild(formElement); alertElement.innerHTML = renderAlert('success'); }); }; init();
How you could improve thispermalink
There’s a few things you could do to make this better:
- You could make the animations slicker
- You could implement a loading state for when the form is posting back to the server with
fetch
- You could modify the
renderAlert
function to accept an optional message parameter and use that for if the form fails to post back
Wrapping uppermalink
I really hope you’ve enjoyed this tutorial. You can grab a zip of the final code that I wrote and also see a live version too!
My favourite attempt at this was by Dana Byerly. Check out their Twitter thread. Their attention to detail and documentation was fantastic. Nice work, Dana!