forms – CSS-Tricks https://css-tricks.com Tips, Tricks, and Techniques on using Cascading Style Sheets. Fri, 05 Apr 2024 22:21:03 +0000 en-US hourly 1 https://wordpress.org/?v=6.4.3 https://i0.wp.com/css-tricks.com/wp-content/uploads/2021/07/star.png?fit=32%2C32&ssl=1 forms – CSS-Tricks https://css-tricks.com 32 32 45537868 Managing User Focus with :focus-visible https://css-tricks.com/managing-user-focus-with-focus-visible/ https://css-tricks.com/managing-user-focus-with-focus-visible/#respond Fri, 05 Apr 2024 22:13:47 +0000 https://css-tricks.com/?p=377702 This is going to be the 2nd post in a small series we are doing on form accessibility. If you missed the first post, check out Accessible Forms with Pseudo Classes. In this post we are going to look …


Managing User Focus with :focus-visible originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
This is going to be the 2nd post in a small series we are doing on form accessibility. If you missed the first post, check out Accessible Forms with Pseudo Classes. In this post we are going to look at :focus-visible and how to use it in your web sites!

Focus Touchpoint

Before we move forward with :focus-visible, let’s revisit how :focus works in your CSS. Focus is the visual indicator that an element is being interacted with via keyboard, mouse, trackpad, or assistive technology. Certain elements are naturally interactive, like links, buttons, and form elements. We want to make sure that our users know where they are and the interactions they are making.

Remember don’t do this in your CSS!

:focus {
  outline: 0;
}

/*** OR ***/

:focus {
  outline: none;
}

When you remove focus, you remove it for EVERYONE! We want to make sure that we are preserving the focus.

If for any reason you do need to remove the focus, make sure there is also fallback :focus styles for your users. That fallback can match your branding colors, but make sure those colors are also accessible. If marketing, design, or branding doesn’t like the default focus ring styles, then it is time to start having conversations and collaborate with them on the best way of adding it back in.

What is focus-visible?

The pseudo class, :focus-visible, is just like our default :focus pseudo class. It gives the user an indicator that something is being focused on the page. The way you write :focus-visible is cut and dry:

:focus-visible {
  /* ... */
}

When using :focus-visible with a specific element, the syntax looks something like this:

.your-element:focus-visible {
  /*...*/
}

The great thing about using :focus-visible is you can make your element stand out, bright and bold! No need to worry about it showing if the element is clicked/tapped. If you choose not to implement the class, the default will be the user agent focus ring which to some is undesirable.

Backstory of focus-visible

Before we had the :focus-visible, the user agent styling would apply :focus to most elements on the page; buttons, links, etc. It would apply an outline or “focus ring” to the focusable element. This was deemed to be ugly, most didn’t like the default focus ring the browser provided. As a result of the focus ring being unfavorable to look at, most authors removed it… without a fallback. Remember, when you remove :focus, it decreases usability and makes the experience inaccessible for keyboard users.

In the current state of the web, the browser no longer visibly indicates focus around various elements when they have focus. The browser instead uses varying heuristics to determine when it would help the user, providing a focus ring in return. According to Khan Academy, a heuristic is, “a technique that guides an algorithm to find good choices.”

What this means is that the browser can detect whether or not the user is interacting with the experience from a keyboard, mouse, or trackpad and based on that input type, it adds or removes the focus ring. The example in this post highlights the input interaction.

In the early days of :focus-visible we were using a polyfill to handle the focus ring created by Alice Boxhall and Brian Kardell, Mozilla also came out with their own pseudo class, :moz-focusring, before the official specification. If you want to learn more about the early days of the focus-ring, check out A11y Casts with Rob Dodson.

Focus Importance

There are plenty of reasons why focus is important in your application. For one, like I stated above, we as ambassadors of the web have to make sure we are providing the best, accessible experience we can. We don’t want any of our users guessing where they are while they are navigation through the experience.

One example that always comes to mind is the Two Blind Brothers website. If you go to the website and click/tap (this works on mobile), the closed eye in the bottom left corner, you will see the eye open and a simulation begins. Both the brothers, Bradford and Bryan Manning, were diagnosed at a young age with Stargardt’s Disease. Stargardt’s disease is a form of macular degeneration of the eye. Over time both brothers will be completely blind. Visit the site and click the eye to see how they see.

If you were in their shoes and you had to navigate through a page, you would want to make sure you knew exactly where you were throughout the whole experience. A focus ring gives you that power.

Image of the home page from the Two Blind Brothers website.

Demo

The demo below shows how :focus-visible works when added to your CSS. The first part of the video shows the experience when navigating through with a mouse the second shows navigating through with just my keyboard. I recorded myself as well to show that I did switch from using my mouse, to my keyboard.

Video showing how the heuristics of the browser works based on input and triggering the focus visible pseudo class.
Video showing how the heuristics of the browser works based on input and triggering the focus visible pseudo class.

The browser is predicting what to do with the focus ring based on my input (keyboard/mouse), and then adding a focus ring to those elements. In this case, when I am navigating through this example with the keyboard, everything receives focus. When using the mouse, only the input gets focus and the buttons don’t. If you remove :focus-visible, the browser will apply the default focus ring.

The code below is applying :focus-visible to the focusable elements.

:focus-visible {
  outline-color: black;
  font-size: 1.2em;
  font-family: serif;
  font-weight: bold;
}

If you want to specify the label or the button to receive :focus-visible just prepend the class with input or button respectively.

button:focus-visible {
  outline-color: black;
  font-size: 1.2em;
  font-family: serif;
  font-weight: bold;
}

/*** OR ***/

input:focus-visible {
  outline-color: black;
  font-size: 1.2em;
  font-family: serif;
  font-weight: bold;
}

Support

If the browser does not support :focus-visible you can have a fall back in place to handle the interaction. The code below is from the MDN Playground. You can use the @supports at-rule or “feature query” to check support. One thing to keep in mind, the rule should be placed at the top of the code or nested inside another group at-rule.

<button class="button with-fallback" type="button">Button with fallback</button>
<button class="button without-fallback" type="button">Button without fallback</button>
.button {
  margin: 10px;
  border: 2px solid darkgray;
  border-radius: 4px;
}

.button:focus-visible {
  /* Draw the focus when :focus-visible is supported */
  outline: 3px solid deepskyblue;
  outline-offset: 3px;
}

@supports not selector(:focus-visible) {
  .button.with-fallback:focus {
    /* Fallback for browsers without :focus-visible support */
    outline: 3px solid deepskyblue;
    outline-offset: 3px;
  }
}

Further Accessibility Concerns

Accessibility concerns to keep in mind when building out your experience:

  • Make sure the colors you choose for your focus indicator, if at all, are still accessible according to the information documented in the WCAG 2.2 Non-text Contrast (Level AA)
  • Cognitive overload can cause a user distress. Make sure to keep styles on varying interactive elements consistent

Browser Support

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

Desktop

ChromeFirefoxIEEdgeSafari
864*No8615.4

Mobile / Tablet

Android ChromeAndroid FirefoxAndroidiOS Safari
12312412315.4

Managing User Focus with :focus-visible originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/managing-user-focus-with-focus-visible/feed/ 0 377702
Accessible Forms with Pseudo Classes https://css-tricks.com/accessible-forms-with-pseudo-classes/ https://css-tricks.com/accessible-forms-with-pseudo-classes/#comments Fri, 22 Mar 2024 18:52:31 +0000 https://css-tricks.com/?p=377565 Hey all you wonderful developers out there! In this post, I am going to take you through creating a simple contact form using semantic HTML and an awesome CSS pseudo class known as :focus-within. The :focus-within class allows for …


Accessible Forms with Pseudo Classes originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Hey all you wonderful developers out there! In this post, I am going to take you through creating a simple contact form using semantic HTML and an awesome CSS pseudo class known as :focus-within. The :focus-within class allows for great control over focus and letting your user know this is exactly where they are in the experience. Before we jump in, let’s get to the core of what web accessibility is.


Form Accessibility?


You have most likely heard the term “accessibility” everywhere or the numeronym, a11y. What does it mean? That is a great question with so many answers. When we look at the physical world, accessibility means things like having sharps containers in your bathrooms at your business, making sure there are ramps for wheel assisted people, and having peripherals like large print keyboards on hand for anyone that needs it.

The gamut of accessibility doesn’t stop there, we have digital accessibility that we need to be cognizant of as well, not just for external users, but internal colleagues as well. Color contrast is a low hanging fruit that we should be able to nip in the bud. At our workplaces, making sure that if any employee needs assistive tech like a screen reader, we have that installed and available. There are a lot of things that need to be kept into consideration. This article will focus on web accessibility by keeping the WCAG (web content accessibility guidelines) in mind.

MDN (Mozilla Developer Network)

The :focus-within CSS pseudo-class matches an element if the element or any of its descendants are focused. In other words, it represents an element that is itself matched by the :focus pseudo-class or has a descendant that is matched by :focus. (This includes descendants in shadow trees.)

This pseudo class is really great when you want to emphasize that the user is in fact interacting with the element. You can change the background color of the whole form, for example. Or, if focus is moved into an input, you can make the label bold and larger of an input element when focus is moved into that input. What is happening below in the code snippets and examples is what is making the form accessible. :focus-within is just one way we can use CSS to our advantage.

How To Focus


Focus, in regards to accessibility and the web experience, is the visual indicator that something is being interacted with on the page, in the UI, or within a component. CSS can tell when an interactive element is focused.

“The :focus CSS pseudo-class represents an element (such as a form input) that has received focus. It is generally triggered when the user clicks or taps on an element or selects it with the keyboard’s Tab key.”

MDN (Mozilla Developer Network)

Always make sure that the focus indicator or the ring around focusable elements maintains the proper color contrast through the experience.

Focus is written like this and can be styled to match your branding if you choose to style it.

:focus {
  * / INSERT STYLES HERE /*
}

Whatever you do, never set your outline to 0 or none. Doing so will remove a visible focus indicator for everyone across the whole experience. If you need to remove focus, you can, but make sure to add that back in later. When you remove focus from your CSS or set the outline to 0 or none, it removes the focus ring for all your users. This is seen a lot when using a CSS reset. A CSS reset will reset the styles to a blank canvas. This way you are in charge of the empty canvas to style as you wish. If you wish to use a CSS reset, check out Josh Comeau’s reset.

*DO NOT DO what is below!

:focus {
  outline: 0;
}

:focus {
  outline: none;
}


Look Within!


One of the coolest ways to style focus using CSS is what this article is all about. If you haven’t checked out the :focus-within pseudo class, definitely give that a look! There are a lot of hidden gems when it comes to using semantic markup and CSS, and this is one of them. A lot of things that are overlooked are accessible by default, for instance, semantic markup is by default accessible and should be used over div’s at all times.

<header>
  <h1>Semantic Markup</h1>
  <nav>
    <ul>
      <li><a href="/">Home</a></li>
      <li><a href="/about">About</a></li>
    </ul>
  </nav>
</header>

<section><!-- Code goes here --></section>

<section><!-- Code goes here --></section>

<aside><!-- Code goes here --></aside>

<footer><!-- Code goes here --></footer>

The header, nav, main, section, aside, and footer are all semantic elements. The h1 and ul are also semantic and accessible.

Unless there is a custom component that needs to be created, then a div is fine to use, paired with ARIA (Accessible Rich Internet Applications). We can do a deep dive into ARIA in a later post. For now let’s focus…see what I did there…on this CSS pseudo class.

The :focus-within pseudo class allows you to select an element when any descendent element it contains has focus.


:focus-within in Action!

HTML

<form>
  <div>
    <label for="firstName">First Name</label><input id="firstName" type="text">
  </div>
  <div>
    <label for="lastName">Last Name</label><input id="lastName" type="text">
  </div>
  <div>
    <label for="phone">Phone Number</label><input id="phone" type="text">
  </div>
  <div>
    <label for="message">Message</label><textarea id="message"></textarea>
  </div>
</form>

CSS

form:focus-within {
  background: #ff7300;
  color: black;
  padding: 10px;
}

The example code above will add a background color of orange, add some padding, and change the color of the labels to black.

The final product looks something like below. Of course the possibilities are endless to change up the styling, but this should get you on a good track to make the web more accessible for everyone!

First example of focus-within css class highlighting the form background and changing the label text color.

Another use case for using :focus-within would be turning the labels bold, a different color, or enlarging them for users with low vision. The example code for that would look something like below.

HTML

<form>
  <h1>:focus-within part 2!</h1>
  <label for="firstName">First Name: <input name="firstName" type="text" /></label>
  <label for="lastName">Last Name: <input name="lastName" type="text" /></label>
  <label for="phone">Phone number: <input type="tel" id="phone" /></label>
  <label for="message">Message: <textarea name="message" id="message"/></textarea></label>
</form>

CSS

label {
  display: block;
  margin-right: 10px;
  padding-bottom: 15px;
}

label:focus-within {
  font-weight: bold;
  color: red;
  font-size: 1.6em;
}
Showing how to bold, change color and font size of labels in a form using :focus-within.

:focus-within also has great browser support across the board according to Can I use.

Focus within css pseudo class browser support according to the can i use website.

Conclusion

Creating amazing, accessible user experience should always be a top priority when shipping software, not just externally but internally as well. We as developers, all the way up to senior leadership need to be cognizant of the challenges others face and how we can be ambassadors for the web platform to make it a better place.

Using technology like semantic markup and CSS to create inclusive spaces is a crucial part in making the web a better place, let’s continue moving forward and changing lives.

Check out another great resource here on CSS-Tricks on using :focus-within.


Accessible Forms with Pseudo Classes originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/accessible-forms-with-pseudo-classes/feed/ 12 377565
What’s New With Forms in 2022? https://css-tricks.com/whats-new-with-forms-in-2022/ https://css-tricks.com/whats-new-with-forms-in-2022/#comments Thu, 08 Sep 2022 13:12:51 +0000 https://css-tricks.com/?p=372979 Browsers are constantly adding new HTML, JavaScript and CSS features. Here are some useful additions to working with forms that you might have missed…

requestSubmit()

Safari 16 will be the final browser to add support for requestSubmit.

Before we …


What’s New With Forms in 2022? originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Browsers are constantly adding new HTML, JavaScript and CSS features. Here are some useful additions to working with forms that you might have missed…

requestSubmit()

Safari 16 will be the final browser to add support for requestSubmit.

Before we look at how .requestSubmit() works, let’s remind ourselves how programmatically submitting a form with JavaScript works when using the .submit() method. Submitting a form with submit() does not trigger a submit event. So in the following code, the form is submitted, preventDefault() has no effect, and nothing is logged to the console:

const form = document.forms[0];
form.addEventListener('submit', function(event) {
  // code to submit the form goes here
  event.preventDefault();
  console.log('form submitted!');
});

document.querySelector('.btn').addEventListener('click', function() {
  form.submit();
})

.submit() will also ignore any HTML form validation. Given the following markup, the form will be submitted when the input is empty even though the input has a required attribute:

<form>   
  <label for="name">Name</label>
  <input required name="name" id="name" type="text">
</form>

.requestSubmit() is an alternative way to submit a form using JavaScript, but in contrast to .submit(), HTML form validation will prevent the form from being submitted. If all the data entered in the form passes validation, the submit event will be fired, meaning “form submitted!” would be logged to the console in the following example:

form.addEventListener('submit', function(event) {
  event.preventDefault();
  console.log('form submitted!');
});

document.querySelector('.btn').addEventListener('click', function() {
  form.requestSubmit();
})

You could already achieve this by programmatically clicking the form’s submit button, but requestSubmit is perhaps a more elegant solution.

submitter property of submit event

The SubmitEvent.submitter property gained full cross-browser support with the release of Safari 15.4. This read-only property specifies the <button> or <input type="submit"> element that caused a form to be submitted.

<form>
  <button name="foo" value="bar" type="submit">Bar</button>
  <button name="foo" value="baz" type="submit">Baz</button>
</form>

When you have multiple submit buttons or inputs, each with a different value, only the value of the button or input that was clicked on to submit the form will be sent to the server, rather than both values. That’s nothing new. What is new is that the event listener for the submit event now has access to the event.submitter property. You can use this to add a class to the button or input that triggered the form submission, for example, or to obtain its value or any other of its HTML attributes.

document.forms[0].addEventListener('submit', function(event) {
  event.preventDefault();
  console.log(event.submitter.value);
  console.log(event.submitter.formaction);
  event.submitter.classList.add('spinner-animation');
})

formdata event

This isn’t particularly new, but only achieved cross-browser support with the release of Safari 15. The main use case for the formdata event is enabling custom elements to take part in form submissions. Outside of web components, though, it can still be useful.

You add a formdata event listener to the form you want to interact with:

document.querySelector('form').addEventListener('formdata', handleFormdata);

The event is fired both by a regular HTML form submission and also by an occurrence of new FormData(). event.formData holds all of the data being submitted.

function handleFormdata(event) {
  for (const entry of event.formData.values()) {
    console.log(entry);
  }
}

The callback function for the formdata event listener runs before the data is sent to the server, giving you a chance to add to or modify the data being sent.

function handleFormdata(event) {
  event.formData.append('name', 'John');
}

You could have modified or appended the FormData inside the submit event handler but formdata allows you to separate out the logic. It’s also an alternative to using hidden inputs in the markup of your form in cases where you are submitting the form “the old fashioned way” — i.e. relying on the built-in functionality of HTML to submit the form rather than doing it with fetch.

showPicker() for input elements

showPicker() has been supported since Chrome 99, Firefox 101, and in the upcoming Safari 16. For an input element whose type attribute is either Date, Month, Week, Time, datetime-local, color, or file, showPicker() provides a programmatic way to display the selection UI. For color and file inputs, it’s always been possible to programmatically show the picker by calling .click on the input:

document.querySelector('input[type="color"]').click();

That approach doesn’t work on date inputs, which is why this new API was added. .showPicker() will also work with color and file inputs but there’s no real advantage to using it over .click().

Datepicker open to August 2022.

Inert attribute

It’s always been possible to disable multiple inputs at once by wrapping them in a HTML fieldset and disabling the fieldset:

Inert is a new HTML attribute. It isn’t only for forms, but forms are certainly a key use-case. Unlike the disabled attribute, inert can be applied to a form element itself. Everything within the form will be non-focusable and non-clickable. When it comes to assistive technologies, inert is similar to setting aria-hidden="true". Unlike the disabled attribute, inert does not apply any styling by default, but it’s easy to add your own:

form[inert] {
  opacity: .2;
}

There’s more to come…

The big one is styling <select> elements, something developers have wanted for decades. It looks set to finally become a reality sometime soon with the introduction of selectmenu.

But that’s it for now! The recent updates bring full browser support to form features we’ve been waiting for, making them prime for production use.


What’s New With Forms in 2022? originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/whats-new-with-forms-in-2022/feed/ 9 372979
Zero Trickery Custom Radios and Checkboxes https://css-tricks.com/zero-trickery-custom-radios-and-checkboxes/ https://css-tricks.com/zero-trickery-custom-radios-and-checkboxes/#comments Thu, 18 Nov 2021 00:16:17 +0000 https://css-tricks.com/?p=356895 I feel like half of all “custom-designed radio buttons and checkboxes” do two things:

  1. Make them bigger
  2. Colorize them

I always think of SurveyMonkey for having big chunky radios and checkboxes. And indeed, just poking at their interface quickly, even …


Zero Trickery Custom Radios and Checkboxes originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
I feel like half of all “custom-designed radio buttons and checkboxes” do two things:

  1. Make them bigger
  2. Colorize them

I always think of SurveyMonkey for having big chunky radios and checkboxes. And indeed, just poking at their interface quickly, even internally, the app uses has those all over the place:

SurveyMonkey’s implementation appears to be pseudo-elements on a <label> element with icon fonts and such.

I think it’s worth noting that if that’s all you are doing, you might be able to do that with zero CSS trickery. Just… make them bigger and colorize them.

Like…

input[type="radio"],
input[type="checkbox"] {
  width: 3em;
  height: 3rem;
  accent-color: green;
}

That’ll chunk those suckers up and colorize them pretty good!

Firefox
Chrome

Except (obligatory sad trombone) in Safari:

Safari

We’re so close to having some very simple CSS to accomplish the main use-case for custom checkboxes and radios. But no cigar, that is, unless you can bring yourself to just not care about the Safari UI (it is still perfectly functional, after all).

If you do need to give up and go for a completely custom design, Stephanie Eckles has got you covered:

In related news, I always think of a “toggle” UI control as a set of 2 radio buttons progressively enhanced, but it turns out <button> is the way, as Michelle Barker covers here. No native UI for slider toggle thingies, alas.


Zero Trickery Custom Radios and Checkboxes originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/zero-trickery-custom-radios-and-checkboxes/feed/ 14 356895
Detecting Specific Text Input with HTML and CSS https://css-tricks.com/detecting-specific-text-input-with-html-and-css/ https://css-tricks.com/detecting-specific-text-input-with-html-and-css/#comments Tue, 09 Nov 2021 22:53:45 +0000 https://css-tricks.com/?p=356142 Louis Lazaris breaks down some bonafide CSS trickery from Jane. The Pen shows off interactivity where:

  1. You have to press a special combination of keys on a keyboard.
  2. Then type a secret password.

From there, a special message pops …


Detecting Specific Text Input with HTML and CSS originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Louis Lazaris breaks down some bonafide CSS trickery from Jane. The Pen shows off interactivity where:

  1. You have to press a special combination of keys on a keyboard.
  2. Then type a secret password.

From there, a special message pops up on the screen. Easily JavaScript territory, but no, this is done here entirely in HTML and CSS, which is wild.

A lot of little known features and tricks is combined here to pull this off, like HTML’s accesskey and pattern attributes, as well as :not(), :placeholder-shown, and :valid in CSS—not to mention the custom property toggle trick.

That’s… wow. And yet, look how very little code it is.

To Shared LinkPermalink on CSS-Tricks


Detecting Specific Text Input with HTML and CSS originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/detecting-specific-text-input-with-html-and-css/feed/ 1 356142
enterkeyhint https://css-tricks.com/enterkeyhint/ https://css-tricks.com/enterkeyhint/#comments Fri, 05 Nov 2021 14:43:25 +0000 https://css-tricks.com/?p=355902 I only just recently learned the enterkeyhint attribute on form inputs was a thing! It seems like kind of a big deal to me, as crafting HTML form markup is a decent slice of a front-end developer’s life, and this …


enterkeyhint originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
I only just recently learned the enterkeyhint attribute on form inputs was a thing! It seems like kind of a big deal to me, as crafting HTML form markup is a decent slice of a front-end developer’s life, and this attribute could (should?) be used on nearly every input.

The enterkeyhint attribute changes the action key on a mobile keyboard to change the text/affordance. Stefan Judis spells it out nicely in this tweet from 2020:

https://twitter.com/stefanjudis/status/1249958064041734144

So, say I have a form like this:

<form>
  <label>
    Name:
    <input type="text" name="name">
  </label>
  <label>
    Favorite songs:
    <textarea name="songs"></textarea>
  </label>
  <button>Save</button>
</form>

The user experience there suggests form that “Saves” but by default on the iPhone in my hand, the blue button in the keyboard says… “go.”

iPhone screenshot of a simple form on a plain webpage and the phone's touch keyboard displayed. The primary button where the Enter key normally is located is replaced by go.
The default action button on the mobile keyboard of iOS is “go”

That’s not a disaster or anything, but now I’ve got some options for that button. I’ll steal this from the spec, which refers to them as “input modalities”:

KeywordDescription
enterThe user agent should present a cue for the operation ‘enter’, typically inserting a new line.
doneThe user agent should present a cue for the operation ‘done’, typically meaning there is nothing more to input and the input method editor (IME) will be closed.
goThe user agent should present a cue for the operation ‘go’, typically meaning to take the user to the target of the text they typed.
nextThe user agent should present a cue for the operation ‘next’, typically taking the user to the next field that will accept text.
previousThe user agent should present a cue for the operation ‘previous’, typically taking the user to the previous field that will accept text.
searchThe user agent should present a cue for the operation ‘search’, typically taking the user to the results of searching for the text they have typed.
sendThe user agent should present a cue for the operation ‘send’, typically delivering the text to its target.

As a serious web professional, I also tried enterkeykint="poop" but, alas, it was not respected by the browser. Note that on Android the action button doesn’t just always turn into text, but uses icons. So for the value send, you get a little paper airplane icon rather than the “send” label. So, yeah, obviously poop should have turned into 💩.

For the little form example above… the value enter or done somehow feels better to me than go. If I want to change that, I’d add the attribute for all interactive form elements:

<form>
  <label>
    Name:
    <input type="text" name="name" enterkeyhint="done">
  </label>
  <label>
    Favorite songs:
    <textarea name="songs" enterkeyhint="done"></textarea>
  </label>
  <button enterkeyhint="done">Save</button>
</form>

This attribute goes directly on the form inputs, so it feels a bit repetitive writing it for each and every input, especially in longer forms. But it does give you an opportunity to change it depending on what input is focused.

I notice that save isn’t a valid option. That seems weird as it feels like what a lot of forms would offer. Perhaps the web platform doesn’t want to offer something that tells users something they can’t possibly enforce? I’m not sure that’s the case, though, because if you put in next or previous that doesn’t change the behavior at all—you’d have to code that yourself if what you want to do is move focus to the next or previous inputs. By default, the action button just submits the form as it normally would.

I came across all this as Stefan more recently tweeted that Firefox is supporting it now, offering a largely complete set of support for this feature. This feature is only relevant to mobile browsers (or, I suppose, browsers that support virtual keyboards?) so it had me thinking about that Firefox Mobile exists. I’m so used to the fact that all browsers are Safari on iOS (🤬). But on Android, you can use “real” Firefox, which is a good reminder that not only do different mobile browsers exist, but different mobile browsers have different behavior as well.

In this video, which I’m sure predates support for enterkeyhint, note how the virtual keyboard offers UI for next automatically when focused on the first input (and no way to submit directly), but on the second (and last) the action, the button turns into a submit button (and there is no “previous” button). This is markedly different from iOS where all that’s offered is navigation between inputs with little up and down arrows that sit above the keyboard itself.

All in all, a pretty cool feature that we should all at least be aware of if not use on most HTML forms we create to match the behaviors.


enterkeyhint originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/enterkeyhint/feed/ 7 355902
How to Create a Contact Form With Next.js and Netlify https://css-tricks.com/how-to-create-a-contact-form-with-next-js-and-netlify/ https://css-tricks.com/how-to-create-a-contact-form-with-next-js-and-netlify/#comments Thu, 21 Oct 2021 14:26:03 +0000 https://css-tricks.com/?p=353128 We’re going to create a contact form with Next.js and Netlify that displays a confirmation screen and features enhanced spam detection.

Next.js is a powerful React framework for developing performant React applications that scale. By integrating a Next.js site with …


How to Create a Contact Form With Next.js and Netlify originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
We’re going to create a contact form with Next.js and Netlify that displays a confirmation screen and features enhanced spam detection.

Next.js is a powerful React framework for developing performant React applications that scale. By integrating a Next.js site with Netlify’s technology, we can quickly get a working contact form up and running without having to write any server-side code.

Not only is it a relatively fast process to set up forms to be processed by Netlify, but it’s also free to get started (with up to 100 free submissions/per site hosted on Netlify). Form submissions automatically go through Netlify’s built-in spam filter which uses Akismet and there are also options that can be configured to increase the level of spam detection.

Creating the contact form

Within the Next.js application we should create a ContactForm component to render the contact form inside of the contact page. If you’d like for this form to render at /contact, then the ContactForm component below with labels and input fields should be used within the pages/contact.js file.

const ContactForm = (
  <form
    name="contact-form"
    method="POST"
    action="contact/?success=true"
  >
    <label htmlFor="name">Name *</label>
    <input
      id="name"
      name="name"
      required
      type="text"
    />
    <label htmlFor="company">Company *</label>
    <input id="company" name="company" required type="text" />
    <label htmlFor="email">E-mail Address *</label>
    <input id="email" type="email" name="email" required />
    <label htmlFor="message">Message *</label>
    <textarea id="message" name="message" required></textarea>
    <button type="submit">Submit</button>
  </form>
);

The above markup is required to render a form with a field for Name, Company, Email address and message with a submit button. When submitting the form, based on the value of the form’s action, it should redirect to contact/?success=true from /contact. Right now there is not yet a difference between the page’s appearance with and without the success query parameter, but we will update that later.

Our Contact.js file looks like this so far:

import React from "react";
const ContactPage = () => {
 const ContactForm = (/* code in above code sample*/)
 
 return (
   <div>
     <h1>Contact Us</h1>
     {ContactForm}
   </div>
 );
};
 
export default ContactPage;

Now that we have the basic form set up, the real magic will happen after we add additional information for Netlify to auto-recognize the form during future site deployments. To accomplish this we should update the form to have the attribute data-netlify="true" and a hidden input field that contains the name of our contact form. In Netlify, once we navigate to our site in the dashboard and then click on the “forms” tab  we will be able to view our form responses based on the name that we’ve put in our hidden field. It’s important that if you have multiple forms within a site that they have unique names so that they are recorded properly in Netlify.

<form
  method="POST"
  name="contact-form"
  action="contact/?success=true"
  data-netlify="true"
>
<input type="hidden" name="form-name" value="contact-form" />

After successfully deploying the site to Netlify with the data-netlify attribute and the form-name field  then we can go to the deployed version of the site and fill out the form. Upon submitting the form and navigating to https://app.netlify.com/sites/site-name/forms (where site-name is the name of your site) then our most recent form submission should appear if we have successfully set up the form. 

Redirect to confirmation screen

In order to improve the user experience, we should add some logic to redirect to a confirmation screen on form submission when the URL changes to /contact/?success=true. There is also the option to redirect to an entirely different page as the action when the form is submitted but using query params we can achieve something similar with the Next Router. We can accomplish this by creating a new variable to determine if the confirmation screen or the form should be visible based on the query parameter. The next/router which is imported with import { useRouter } from "next/router"; can be used to retrieve the current query params. 

const router = useRouter();  
const confirmationScreenVisible = router.query?.success && router.query.success === "true";

In our case, the confirmation screen and form can never be visible at the same time; therefore, the following statement can be used to determine if the form is visible or not.

const formVisible = !confirmationScreenVisible; 

To give users the option to resubmit the form, we can add a button to the confirmation screen to reset the form by clearing the query params. Using router.replace (instead of router.push) not only updates the page but replaces the current page in the history to the version without query params. 

<button onClick={() => router.replace("/contact", undefined, { shallow: true })}> Submit Another Response </button>

We can then conditionally render the form based on whether or not the form is visible with:

{formVisible ? ContactForm : ConfirmationMessage}

Putting it all together, we can use the following code to conditionally render the form based on the query params (which are updated when the form is submitted):

import React, { useState } from "react";
import { useRouter } from "next/router";
 
const ContactPage = () => {
 const [submitterName, setSubmitterName] = useState("");
 const router = useRouter();
 const confirmationScreenVisible =
   router.query?.success && router.query.success === "true";
 const formVisible = !confirmationScreenVisible;
 
 const ConfirmationMessage = (
   <React.Fragment>
     <p>
       Thank you for submitting this form. Someone should get back to you within 24-48 hours.
     </p>
 
     <button onClick={() => router.replace("/contact", undefined, { shallow: true })}> Submit Another Response </button>
   </React.Fragment>
 );
 
 const ContactForm = (/* code in first code example */);
 
 return (
   <div>
     <h1>Contact Us</h1>
{formVisible ? ContactForm : ConfirmationMessage}
   </div>
 );
};
 
export default ContactPage;

Adding a hidden bot field

Now that the core functionality of our form is working, we can add additional spam detection to our form in addition to the base spam detection because Akismet is included with all Netlify Forms by default. We can enable this by adding data-netlify-honeypot="bot-field" to our form.

<form
  className="container"
  method="POST"
  name="contact-form"
  action="contact/?success=true"
  data-netlify="true"
  data-netlify-honeypot="bot-field"
>

We also need to create a new hidden paragraph that contains a label named bot-field that contains the input. This field is “visible” to bots, but not humans. When this honeypot form field is filled, Netlify detects a bot and then the submission is flagged as spam.

<p hidden>
  <label>
    Don’t fill this out: <input name="bot-field" />
  </label>
</p>

Further customizations

  • We could explore another spam prevention option that Netlify supports by adding reCAPTCHA 2 to a Netlify form.
  • We could update the form to allow uploaded files with input <input type="file">.
  • We could set up notifications for form submissions. That happens over at https://app.netlify.com/sites/[your-site-name]/settings/forms where we can include a custom subject field (which can be hidden) for email notifications.

Full code

The code for the full site code is available over at GitHub.

Bonus

The following code includes everything we covered as well as the logic for setting a custom subject line with what was submitted in the name field.

import React, { useState } from "react";
import { useRouter } from "next/router";
 
const ContactPage = () => {
 const [submitterName, setSubmitterName] = useState("");
 const router = useRouter();
 const confirmationScreenVisible =
   router.query?.success && router.query.success === "true";
 const formVisible = !confirmationScreenVisible;
 
 const ConfirmationMessage = (
   <React.Fragment>
     <p>
       Thank you for submitting this form. Someone should get back to you
       within 24-48 hours.
     </p>
 
     <button onClick={() => router.replace("/contact", undefined, { shallow: true })}> Submit Another Response </button>
   </React.Fragment>
 );
 
 const ContactForm = (
   <form
     className="container"
     method="POST"
     name="contact-form"
     action="contact/?success=true"
     data-netlify="true"
     data-netlify-honeypot="bot-field"
   >
     <input
       type="hidden"
       name="subject"
       value={`You've got mail from ${submitterName}`}
     />
     <input type="hidden" name="form-name" value="contact-form" />
     <p hidden>
       <label>
         Don’t fill this out: <input name="bot-field" />
       </label>
     </p>
 
     <label htmlFor="name">Name *</label>
     <input
       id="name"
       name="name"
       required
       onChange={(e) => setSubmitterName(e.target.value)}
       type="text"
     />
     <label htmlFor="company">Company *</label>
     <input id="company" name="company" required type="text" />
     <label htmlFor="email">E-mail Address *</label>
     <input id="email" type="email" name="email" required />
     <label htmlFor="message">Message *</label>
     <textarea id="message" name="message" required/>
     <button type="submit">Submit</button>
   </form>
 );
 
 return (
   <div>
     <h1>Contact Us</h1>
{formVisible ? ContactForm : ConfirmationMessage}
   </div>
 );
};
 
export default ContactPage;

How to Create a Contact Form With Next.js and Netlify originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/how-to-create-a-contact-form-with-next-js-and-netlify/feed/ 1 353128
Building a Form in PHP Using DOMDocument https://css-tricks.com/building-a-form-in-php-using-domdocument/ https://css-tricks.com/building-a-form-in-php-using-domdocument/#comments Tue, 14 Sep 2021 14:36:24 +0000 https://css-tricks.com/?p=351454 Learn how to build an HTML form in PHP using DOMDocument — a structured and expressive way to build logical markup.


Building a Form in PHP Using DOMDocument originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Templating makes the web go round. The synthesis of data and structure into content. It’s our coolest superpower as developers — grab some data, then make it work for us, in whatever presentation we need. An array of objects can become a table, a list of cards, a chart, or whatever we think is most useful to the user. Whether the data is our own blog posts in Markdown files, or on-the-minute global exchange rates, the markup and resulting UX are up to us as front-end developers.

PHP is an amazing language for templating, providing many ways to merge data with markup. Let’s get into an example of using data to build out an HTML form in this post.

Want to get your hands dirty right away? Jump to the implementation.

In PHP, we can inline variables into string literals that use double quotes, so if we have a variable $name = 'world', we can write echo "Hello, {$name}", and it prints the expected Hello, world. For more complex templating, we can always concatenate strings, like: echo "Hello, " . $name . ".".

For the old-schoolers, there’s printf("Hello, %s", $name). For multiline strings, you can use Heredoc (the one that starts like <<<MYTEXT). And, last but certainly not least, we can sprinkle PHP variables inside HTML, like <p>Hello, <?= $name ?></p>.

All of these options are great, but things can get messy when a lot of inline logic is required. If we need to build compound HTML strings, say a form or navigation, the complexity is potentially infinite, since HTML elements can nest inside each other.

What we’re trying to avoid

Before we go ahead and do the thing we want to do, it’s worth taking a minute to consider what we don’t want to do. Consider the following abridged passage from the scripture of WordPress Core, class-walker-nav-menu.php, verses 170-270:

<?php // class-walker-nav-menu.php
// ...
$output .= $indent . '<li' . $id . $class_names . '>';
// ...
$item_output  = $args->before;
$item_output .= '<a' . $attributes . '>';
$item_output .= $args->link_before . $title . $args->link_after;
$item_output .= '</a>';
$item_output .= $args->after;
// ...
$output .= apply_filters( 'walker_nav_menu_start_el', $item_output, $item, $depth, $args );
// ...
$output .= "</li>{$n}";

In order to build out a navigation <ul> in this function, we use a variable, $output, which is a very long string to which we keep adding stuff. This type of code has a very specific and limited order of operations. If we wanted to add an attribute to the <a>, we must have access to $attributes before this runs. And if we wanted to optionally nest a <span> or an <img> inside the <a>, we’d need to author a whole new block of code that would replace the middle of line 7 with about 4-10 new lines, depending on what exactly we want to add. Now imagine you need to optionally add the <span> , and then optionally add the <img>, either inside the <span> or after it. That alone is three if statements, making the code even less legible.

It’s very easy to end up with string spaghetti when concatenating like this, which is as fun to say as it is painful to maintain.

The essence of the problem is that when we try to reason about HTML elements, we’re not thinking about strings. It just so happens that strings are what the browser consumes and PHP outputs. But our mental model is more like the DOM — elements are arranged into a tree, and each node has many potential attributes, properties, and children.

Wouldn’t it be great if there were a structured, expressive way to build our tree?

Enter…

The DOMDocument class

PHP 5 added the DOM module to it’s roster of Not So Strictly Typed™ types. Its main entry point is the DOMDocument class, which is intentionally similar to the Web API’s JavaScript DOM. If you’ve ever used document.createElement or, for those of us of a certain age, jQuery’s $('<p>Hi there!</p>') syntax, this will probably feel quite familiar.

We start out by initializing a new DOMDocument:

$dom = new DOMDocument();

Now we can add a DOMElement to it:

$p = $dom->createElement('p');

The string 'p' represents the type of element we want, so other valid strings would be 'div', 'img' , etc.

Once we have an element, we can set its attributes:

$p->setAttribute('class', 'headline');

We can add children to it:

$span = $dom->createElement('span', 'This is a headline'); // The 2nd argument populates the element's textContent
$p->appendChild($span);

And finally, get the complete HTML string in one go:

$dom->appendChild($p);
$htmlString = $dom->saveHTML();
echo $htmlString;

Notice how this style of coding keeps our code organized according to our mental model — a document has elements; elements can have any number of attributes; and elements nest inside one another without needing to know anything about each other. The whole “HTML is just a string” part comes in at the end, once our structure is in place.

The “document” here is a bit different from the actual DOM, in that it doesn’t need to represent an entire document, just a block of HTML. In fact, if you need to create two similar elements, you could save a HTML string using saveHTML(), modify the DOM “document” some more, and then save a new HTML string by calling saveHTML() again.

Getting data and setting the structure

Say we need to build a form on the server using data from a CRM provider and our own markup. The API response from the CRM looks like this:

{
  "submit_button_label": "Submit now!",
  "fields": [
    {
      "id": "first-name",
      "type": "text",
      "label": "First name",
      "required": true,
      "validation_message": "First name is required.",
      "max_length": 30
    },
    {
      "id": "category",
      "type": "multiple_choice",
      "label": "Choose all categories that apply",
      "required": false,
      "field_metadata": {
        "multi_select": true,
        "values": [
          { "value": "travel", "label": "Travel" },
          { "value": "marketing", "label": "Marketing" }
        ]
      }
    }
  ]
}

This example doesn’t use the exact data structure of any specific CRM, but it’s rather representative.

And let’s suppose we want our markup to look like this:

<form>
  <label class="field">
    <input type="text" name="first-name" id="first-name" placeholder=" " required>
    <span class="label">First name</span>
    <em class="validation" hidden>First name is required.</em>
  </label>
  <label class="field checkbox-group">
    <fieldset>
      <div class="choice">
        <input type="checkbox" value="travel" id="category-travel" name="category">
        <label for="category-travel">Travel</label>
      </div>
      <div class="choice">
        <input type="checkbox" value="marketing" id="category-marketing" name="category">
        <label for="category-marketing">Marketing</label>
      </div>
    </fieldset>
    <span class="label">Choose all categories that apply</span>
  </label>
</form>

What’s that placeholder=" "? It’s a small trick that allows us to track in CSS whether the field is empty, without needing JavaScript. As long as the input is empty, it matches input:placeholder-shown, but the user doesn’t see any visible placeholder text. Just the kind of thing you can do when we control the markup!

Now that we know what our desired result is, here’s the game plan:

  1. Get the field definitions and other content from the API
  2. Initialize a DOMDocument
  3. Iterate over the fields and build each one as required
  4. Get the HTML output

So let’s stub out our process and get some technicalities out of the way:

<?php
function renderForm ($endpoint) {
  // Get the data from the API and convert it to a PHP object
  $formResult = file_get_contents($endpoint);
  $formContent = json_decode($formResult);
  $formFields = $formContent->fields;

  // Start building the DOM
  $dom = new DOMDocument();
  $form = $dom->createElement('form');

  // Iterate over the fields and build each one
  foreach ($formFields as $field) {
    // TODO: Do something with the field data
  }

  // Get the HTML output
  $dom->appendChild($form);
  $htmlString = $dom->saveHTML();
  echo $htmlString;
}

So far, we’ve gotten the data and parsed it, initialized our DOMDocument and echoed its output. What do we want to do for each field? First off, let’s build the container element which, in our example, should be a <label>, and the labelling <span> which is common to all field types:

<?php
// ...
// Iterate over the fields and build each one
foreach ($formFields as $field) {
  // Build the container `<label>`
  $element = $dom->createElement('label');
  $element->setAttribute('class', 'field');

  // Reset input values
  $label = null;

  // Add a `<span>` for the label if it is set
  if ($field->label) {
    $label = $dom->createElement('span', $field->label);
    $label->setAttribute('class', 'label');
  }

  // Add the label to the `<label>`
  if ($label) $element->appendChild($label);
}

Since we’re in a loop, and PHP doesn’t scope variables in loops, we reset the $label element on each iteration. Then, if the field has a label, we build the element. At the end, we append it to the container element.

Notice that we set classes using the setAttribute method. Unlike the Web API, there unfortunately is no special handing of class lists. They’re just another attribute. If we had some really complex class logic, since It’s Just PHP™, we could create an array and then implode it:
$label->setAttribute('class', implode($labelClassList)).

Single inputs

Since we know that the API will only return specific field types, we can switch over the type and write specific code for each one:

<?php
// ...
// Iterate over the fields and build each one
foreach ($formFields as $field) {
  // Build the container `<label>`
  $element = $dom->createElement('label');
  $element->setAttribute('class', 'field');

  // Reset input values
  $input = null;
  $label = null;

  // Add a `<span>` for the label if it is set
  // ...

  // Build the input element
  switch ($field->type) {
    case 'text':
    case 'email':
    case 'telephone':
      $input = $dom->createElement('input');
      $input->setAttribute('placeholder', ' ');
      if ($field->type === 'email') $input->setAttribute('type', 'email');
      if ($field->type === 'telephone') $input->setAttribute('type', 'tel');
      break;
  }
}

Now let’s handle text areas, single checkboxes and hidden fields:

<?php
// ...
// Iterate over the fields and build each one
foreach ($formFields as $field) {
  // Build the container `<label>`
  $element = $dom->createElement('label');
  $element->setAttribute('class', 'field');

  // Reset input values
  $input = null;
  $label = null;

  // Add a `<span>` for the label if it is set
  // ...

  // Build the input element
  switch ($field->type) {
    //...  
    case 'text_area':
      $input = $dom->createElement('textarea');
      $input->setAttribute('placeholder', ' ');
      if ($rows = $field->field_metadata->rows) $input->setAttribute('rows', $rows);
      break;

    case 'checkbox':
      $element->setAttribute('class', 'field single-checkbox');
      $input = $dom->createElement('input');
      $input->setAttribute('type', 'checkbox');
      if ($field->field_metadata->initially_checked === true) $input->setAttribute('checked', 'checked');
      break;

    case 'hidden':
      $input = $dom->createElement('input');
      $input->setAttribute('type', 'hidden');
      $input->setAttribute('value', $field->field_metadata->value);
      $element->setAttribute('hidden', 'hidden');
      $element->setAttribute('style', 'display: none;');
      $label->textContent = '';
      break;
  }
}

Notice something new we’re doing for the checkbox and hidden cases? We’re not just creating the <input> element; we’re making changes to the container <label> element! For a single checkbox field we want to modify the class of the container, so we can align the checkbox and label horizontally; a hidden <input>‘s container should also be completely hidden.

Now if we were merely concatenating strings, it would be impossible to change at this point. We would have to add a bunch of if statements regarding the type of element and its metadata in the top of the block. Or, maybe worse, we start the switch way earlier, then copy-paste a lot of common code between each branch.

And here is the real beauty of using a builder like DOMDocument — until we hit that saveHTML(), everything is still editable, and everything is still structured.

Nested looping elements

Let’s add the logic for <select> elements:

<?php
// ...
// Iterate over the fields and build each one
foreach ($formFields as $field) {
  // Build the container `<label>`
  $element = $dom->createElement('label');
  $element->setAttribute('class', 'field');

  // Reset input values
  $input = null;
  $label = null;

  // Add a `<span>` for the label if it is set
  // ...

  // Build the input element
  switch ($field->type) {
    //...  
    case 'select':
      $element->setAttribute('class', 'field select');
      $input = $dom->createElement('select');
      $input->setAttribute('required', 'required');
      if ($field->field_metadata->multi_select === true)
        $input->setAttribute('multiple', 'multiple');
    
      $options = [];
    
      // Track whether there's a pre-selected option
      $optionSelected = false;
    
      foreach ($field->field_metadata->values as $value) {
        $option = $dom->createElement('option', htmlspecialchars($value->label));
    
        // Bail if there's no value
        if (!$value->value) continue;
    
        // Set pre-selected option
        if ($value->selected === true) {
          $option->setAttribute('selected', 'selected');
          $optionSelected = true;
        }
        $option->setAttribute('value', $value->value);
        $options[] = $option;
      }
    
      // If there is no pre-selected option, build an empty placeholder option
      if ($optionSelected === false) {
        $emptyOption = $dom->createElement('option');
    
        // Set option to hidden, disabled, and selected
        foreach (['hidden', 'disabled', 'selected'] as $attribute)
          $emptyOption->setAttribute($attribute, $attribute);
        $input->appendChild($emptyOption);
      }
    
      // Add options from array to `<select>`
      foreach ($options as $option) {
        $input->appendChild($option);
      }
  break;
  }
}

OK, so there’s a lot going on here, but the underlying logic is the same. After setting up the outer <select>, we make an array of <option>s to append inside it.

We’re also doing some <select>-specific trickery here: If there is no pre-selected option, we add an empty placeholder option that is already selected, but can’t be selected by the user. The goal is to place our <label class="label"> as a “placeholder” using CSS, but this technique can be useful for all kinds of designs. By appending it to the $input before appending the other options, we make sure it is the first option in the markup.

Now let’s handle <fieldset>s of radio buttons and checkboxes:

<?php
// ...
// Iterate over the fields and build each one
foreach ($formFields as $field) {
  // Build the container `<label>`
  $element = $dom->createElement('label');
  $element->setAttribute('class', 'field');

  // Reset input values
  $input = null;
  $label = null;

  // Add a `<span>` for the label if it is set
  // ...

  // Build the input element
  switch ($field->type) {
    // ...  
    case 'multiple_choice':
      $choiceType = $field->field_metadata->multi_select === true ? 'checkbox' : 'radio';
      $element->setAttribute('class', "field {$choiceType}-group");
      $input = $dom->createElement('fieldset');
    
      // Build a choice `<input>` for each option in the fieldset
      foreach ($field->field_metadata->values as $choiceValue) {
        $choiceField = $dom->createElement('div');
        $choiceField->setAttribute('class', 'choice');
    
        // Set a unique ID using the field ID + the choice ID
        $choiceID = "{$field->id}-{$choiceValue->value}";
    
        // Build the `<input>` element
        $choice = $dom->createElement('input');
        $choice->setAttribute('type', $choiceType);
        $choice->setAttribute('value', $choiceValue->value);
        $choice->setAttribute('id', $choiceID);
        $choice->setAttribute('name', $field->id);
        $choiceField->appendChild($choice);
    
        // Build the `<label>` element
        $choiceLabel = $dom->createElement('label', $choiceValue->label);
        $choiceLabel->setAttribute('for', $choiceID);
        $choiceField->appendChild($choiceLabel);
    
        $input->appendChild($choiceField);
      }
  break;
  }
}

So, first we determine if the field set should be for checkboxes or radio button. Then we set the container class accordingly, and build the <fieldset>. After that, we iterate over the available choices and build a <div> for each one with an <input> and a <label>.

Notice we use regular PHP string interpolation to set the container class on line 21 and to create a unique ID for each choice on line 30.

Fragments

One last type we have to add is slightly more complex than it looks. Many forms include instruction fields, which aren’t inputs but just some HTML we need to print between other fields.

We’ll need to reach for another DOMDocument method, createDocumentFragment(). This allows us to add arbitrary HTML without using the DOM structuring:

<?php
// ...
// Iterate over the fields and build each one
foreach ($formFields as $field) {
  // Build the container `<label>`
  $element = $dom->createElement('label');
  $element->setAttribute('class', 'field');

  // Reset input values
  $input = null;
  $label = null;

  // Add a `<span>` for the label if it is set
  // ...

  // Build the input element
  switch ($field->type) {
    //...  
    case 'instruction':
      $element->setAttribute('class', 'field text');
      $fragment = $dom->createDocumentFragment();
      $fragment->appendXML($field->text);
      $input = $dom->createElement('p');
      $input->appendChild($fragment);
      break;
  }
}

At this point you might be wondering how we found ourselves with an object called $input, which actually represents a static <p> element. The goal is to use a common variable name for each iteration of the fields loop, so at the end we can always add it using $element->appendChild($input) regardless of the actual field type. So, yeah, naming things is hard.

Validation

The API we are consuming kindly provides an individual validation message for each required field. If there’s a submission error, we can show the errors inline together with the fields, rather than a generic “oops, your bad” message at the bottom.

Let’s add the validation text to each element:

<?php
// ...
// Iterate over the fields and build each one
foreach ($formFields as $field) {
  // build the container `<label>`
  $element = $dom->createElement('label');
  $element->setAttribute('class', 'field');

  // Reset input values
  $input = null;
  $label = null;
  $validation = null;

  // Add a `<span>` for the label if it is set
  // ...

  // Add a `<em>` for the validation message if it is set
  if (isset($field->validation_message)) {
    $validation = $dom->createElement('em');
    $fragment = $dom->createDocumentFragment();
    $fragment->appendXML($field->validation_message);
    $validation->appendChild($fragment);
    $validation->setAttribute('class', 'validation-message');
    $validation->setAttribute('hidden', 'hidden'); // Initially hidden, and will be unhidden with Javascript if there's an error on the field
  }

  // Build the input element
  switch ($field->type) {
    // ...
  }
}

That’s all it takes! No need to fiddle with the field type logic — just conditionally build an element for each field.

Bringing it all together

So what happens after we build all the field elements? We need to add the $input, $label, and $validation objects to the DOM tree we’re building. We can also use the opportunity to add common attributes, like required. Then we’ll add the submit button, which is separate from the fields in this API.

<?php
function renderForm ($endpoint) {
  // Get the data from the API and convert it to a PHP object
  // ...

  // Start building the DOM
  $dom = new DOMDocument();
  $form = $dom->createElement('form');

  // Iterate over the fields and build each one
  foreach ($formFields as $field) {
    // Build the container `<label>`
    $element = $dom->createElement('label');
    $element->setAttribute('class', 'field');
  
    // Reset input values
    $input = null;
    $label = null;
    $validation = null;
  
    // Add a `<span>` for the label if it is set
    // ...
  
    // Add a `<em>` for the validation message if it is set
    // ...
  
    // Build the input element
    switch ($field->type) {
      // ...
    }
  
    // Add the input element
    if ($input) {
      $input->setAttribute('id', $field->id);
      if ($field->required)
        $input->setAttribute('required', 'required');
      if (isset($field->max_length))
        $input->setAttribute('maxlength', $field->max_length);
      $element->appendChild($input);
  
      if ($label)
        $element->appendChild($label);
  
      if ($validation)
        $element->appendChild($validation);
  
      $form->appendChild($element);
    }
  }
  
  // Build the submit button
  $submitButtonLabel = $formContent->submit_button_label;
  $submitButtonField = $dom->createElement('div');
  $submitButtonField->setAttribute('class', 'field submit');
  $submitButton = $dom->createElement('button', $submitButtonLabel);
  $submitButtonField->appendChild($submitButton);
  $form->appendChild($submitButtonField);

  // Get the HTML output
  $dom->appendChild($form);
  $htmlString = $dom->saveHTML();
  echo $htmlString;
}

Why are we checking if $input is truthy? Since we reset it to null at the top of the loop, and only build it if the type conforms to our expected switch cases, this ensures we don’t accidentally include unexpected elements our code can’t handle properly.

Hey presto, a custom HTML form!

Bonus points: rows and columns

As you may know, many form builders allow authors to set rows and columns for fields. For example, a row might contain both the first name and last name fields, each in a single 50% width column. So how would we go about implementing this, you ask? By exemplifying (once again) how loop-friendly DOMDocument is, of course!

Our API response includes the grid data like this:

{
  "submit_button_label": "Submit now!",
  "fields": [
    {
      "id": "first-name",
      "type": "text",
      "label": "First name",
      "required": true,
      "validation_message": "First name is required.",
      "max_length": 30,
      "row": 1,
      "column": 1
    },
    {
      "id": "category",
      "type": "multiple_choice",
      "label": "Choose all categories that apply",
      "required": false,
      "field_metadata": {
        "multi_select": true,
        "values": [
          { "value": "travel", "label": "Travel" },
          { "value": "marketing", "label": "Marketing" }
        ]
      },
      "row": 2,
      "column": 1
    }
  ]
}

We’re assuming that adding a data-column attribute is enough for styling the width, but that each row needs to be it’s own element (i.e. no CSS grid).

Before we dive in, let’s think through what we need in order to add rows. The basic logic goes something like this:

  1. Track the latest row encountered.
  2. If the current row is larger, i.e. we’ve jumped to the next row, create a new row element and start adding to it instead of the previous one.

Now, how would we do this if we were concatenating strings? Probably by adding a string like '</div><div class="row">' whenever we reach a new row. This kind of “reversed HTML string” is always very confusing to me, so I can only imagine how my IDE feels. And the cherry on top is that thanks to the browser auto-closing open tags, a single typo will result in a gazillion nested <div>s. Just like fun, but the opposite.

So what’s the structured way to handle this? Thanks for asking. First let’s add row tracking before our loop and build an additional row container element. Then we’ll make sure to append each container $element to its $rowElement rather than directly to $form.

<?php
function renderForm ($endpoint) {
  // Get the data from the API and convert it to a PHP object
  // ...

  // Start building the DOM
  $dom = new DOMDocument();
  $form = $dom->createElement('form');

  // init tracking of rows
  $row = 0;
  $rowElement = $dom->createElement('div');
  $rowElement->setAttribute('class', 'field-row');

  // Iterate over the fields and build each one
  foreach ($formFields as $field) {
    // Build the container `<label>`
    $element = $dom->createElement('label');
    $element->setAttribute('class', 'field');
    $element->setAttribute('data-row', $field->row);
    $element->setAttribute('data-column', $field->column);
    
    // Add the input element to the row
    if ($input) {
      // ...
      $rowElement->appendChild($element);
      $form->appendChild($rowElement);
    }
  }
  // ...
}

So far we’ve just added another <div> around the fields. Let’s build a new row element for each row inside the loop:

<?php
// ...
// Init tracking of rows
$row = 0;
$rowElement = $dom->createElement('div');
$rowElement->setAttribute('class', 'field-row');

// Iterate over the fields and build each one
foreach ($formFields as $field) {
  // ...
  // If we've reached a new row, create a new $rowElement
  if ($field->row > $row) {
    $row = $field->row;
    $rowElement = $dom->createElement('div');
    $rowElement->setAttribute('class', 'field-row');
  }

  // Build the input element
  switch ($field->type) {
    // ...  
    // Add the input element to the row
      if ($input) {
        // ...
        $rowElement->appendChild($element);

        // Automatically de-duped
        $form->appendChild($rowElement);
      }
  }
}

All we need to do is overwrite the $rowElement object as a new DOM element, and PHP treats it as a new unique object. So, at the end of every loop, we just append whatever the current $rowElement is — if it’s still the same one as in the previous iteration, then the form is updated; if it’s a new element, it is appended at the end.

Where do we go from here?

Forms are a great use case for object-oriented templating. And thinking about that snippet from WordPress Core, an argument might be made that nested menus are a good use case as well. Any task where the markup follows complex logic makes for a good candidate for this approach. DOMDocument can output any XML, so you could also use it to build an RSS feed from posts data.

Here’s the entire code snippet for our form. Feel free it adapt it to any form API you find yourself dealing with. Here’s the official documentation, which is good for getting a sense of the available API.

We didn’t even mention DOMDocument can parse existing HTML and XML. You can then look up elements using the XPath API, which is kinda similar to document.querySelector, or cheerio on Node.js. There’s a bit of a learning curve, but it’s a super powerful API for handling external content.

Fun(?) fact: Microsoft Office files that end with x (e.g. .xlsx) are XML files. Don’t tell the marketing department, but it’s possible to parse Word docs and output HTML on the server.

The most important thing is to remember that templating is a superpower. Being able to build the right markup for the right situation can be the key to a great UX.


Building a Form in PHP Using DOMDocument originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/building-a-form-in-php-using-domdocument/feed/ 5 351454
Making Disabled Buttons More Inclusive https://css-tricks.com/making-disabled-buttons-more-inclusive/ https://css-tricks.com/making-disabled-buttons-more-inclusive/#comments Wed, 12 May 2021 14:31:07 +0000 https://css-tricks.com/?p=339960 Let’s talk about disabled buttons. Specifically, let’s get into why we use them and how we can do better than the traditional disabled attribute in HTML (e.g. <button disabled> ) to mark a button as disabled.

There are lots of …


Making Disabled Buttons More Inclusive originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Let’s talk about disabled buttons. Specifically, let’s get into why we use them and how we can do better than the traditional disabled attribute in HTML (e.g. <button disabled> ) to mark a button as disabled.

There are lots of use cases where a disabled button makes a lot of sense, and we’ll get to those reasons in just a moment. But throughout this article, we’ll be looking at a form that allows us to add a number of tickets to a shopping cart.

This is a good baseline example because there’s a clear situation for disabling the “Add to cart” button: when there are no tickets to add to the cart.

But first, why disabled buttons?

Preventing people from doing an invalid or unavailable action is the most common reason we might reach for a disabled button. In the demo below, we can only add tickets if the number of tickets being added to the cart is greater than zero. Give it a try:

Allow me to skip the code explanation in this demo and focus our attention on what’s important: the “Add to cart” button.

<button type="submit" disabled="disabled">
  Add to cart
</button>

This button is disabled by the disabled attribute. (Note that this is a boolean attribute, which means that it can be written as disabled or disabled="disabled".)

Everything seems fine… so what’s wrong with it?

Well, to be honest, I could end the article right here asking you to not use disabled buttons because they suck, and instead use better patterns. But let’s be realistic: sometimes disabling a button is the solution that makes the most sense.

With that being said, for this demo purpose, we’ll pretend that disabling the “Add to cart” button is the best solution (spoiler alert: it’s not). We can still use it to learn how it works and improve its usability along the way.

Types of interactions

I’d like to clarify what I mean by disabled buttons being usable. You may think, If the button is disabled, it shouldn’t be usable, so… what’s the catch? Bear with me.

On the web, there are multiple ways to interact with a page. Using a mouse is one of the most common, but there are others, like sighted people who use the keyboard to navigate because of a motor impairment.

Try to navigate the demo above using only the Tab key to go forward and Tab + Shift to go backward. You’ll notice how the disabled button is skipped. The focus goes directly from the ticket input to the “dummy terms” link.

Using the Tab key, it changes the focus from the input to the link, skipping the “Add to cart” button.

Let’s pause for a second and recap the reason that lead us to disable the button in the first place versus what we had actually accomplished.

It’s common to associate “interacting” with “clicking” but they are two different concepts. Yes, click is a type of interaction, but it’s only one among others, like hover and focus.

In other words…

All clicks are interactions, but not all interactions are clicks.

Our goal is to prevent the click, but by using disabled, we are preventing not only the click, but also the focus, which means we might be doing as much harm as good. Sure, this behavior might seem harmless, but it causes confusion. People with a cognitive disability may struggle to understand why they are unable to focus the button.

In the following demo, we tweaked the layout a little. If you use a mouse, and hover over the submit button, a tooltip is shown explaining why the button is disabled. That’s great!

But if you use only the keyboard, there’s no way of seeing that tooltip because the button cannot be focused with disabled. The same thing happens in touch devices too. Ouch!

Using the mouse, the tooltip on the “Add to cart” button is visible on hover. But the tooltip is missing when using the Tab key.

Allow me to once again skip past the code explanation. I highly recommend reading “Inclusive Tooltips” by Haydon Pickering and “Tooltips in the time of WCAG 2.1” by Sarah Higley to fully understand the tooltip pattern.

ARIA to the rescue

The disabled attribute in a <button> is doing more than necessary. This is one of the few cases I know of where a native HTML attribute can do more harm than good. Using an ARIA attribute can do a better job, allowing us to instruct screen readers how to interpret the button, but do so consistently to create an inclusive experience for more people and use cases.

The disabled attribute correlates to aria-disabled="true". Give the following demo a try, again, using only the keyboard. Notice how the button, although marked disabled, is still accessible by focus and triggers the tooltip!

Using the Tab key, the “Add to cart” button is focused and it shows the tooltip.

Cool, huh? Such tiny tweak for a big improvement!

But we’re not done quite yet. The caveat here is that we still need to prevent the click programmatically, using JavaScript.

elForm.addEventListener('submit', function (event) {
  event.preventDefault(); /* prevent native form submit */

  const isDisabled = elButtonSubmit.getAttribute('aria-disabled') === 'true';

  if (isDisabled || isSubmitting) {
    // return early to prevent the ticket from being added
    return;
  }

  isSubmitting = true;
  // ... code to add to cart...
  isSubmitting = false;
})

You might be familiar with this pattern as a way to prevent double clicks from submitting a form twice. If you were using the disabled attribute for that reason, I’d prefer not to do it because that causes the temporary loss of the keyboard focus while the form is submitting.

The difference between disabled and aria-disabled

You might ask: if aria-disabled doesn’t prevent the click by default, what’s the point of using it? To answer that, we need to understand the difference between both attributes:

Feature / Attributedisabledaria-disabled="true"
Prevent click
Prevent hover
Prevent focus
Default CSS styles
Semantics

The only overlap between the two is semantics. Both attributes will announce that the button is indeed disabled, and that’s a good thing.

Contrary to the disabled attribute, aria-disabled is all about semantics. ARIA attributes never change the application behavior or styles by default. Their only purpose is to help assistive technologies (e.g. screen readers) to announce the page content in a more meaningful and robust way.

So far, we’ve talked about two types of people, those who click and those who Tab. Now let’s talk about another type: those with visual impairments (e.g. blindness, low vision) who use screen readers to navigate the web.

People who use screen readers, often prefer to navigate form fields using the Tab key. Now look an how VoiceOver on macOS completely skips the disabled button.

The VoiceOver screen reader skips the “Add to cart” button when using the Tab key.

Once again, this is a very minimal form. In a longer one, looking for a submit button that isn’t there right away can be annoying. Imagine a form where the submit button is hidden and only visible when you completely fill out the form. That’s what some people feel like when the disabled attribute is used.

Fortunately, buttons with disabled are not totally unreachable by screen readers. You can still navigate each element individually, one by one, and eventually you’ll find the button.

The VoiceOver screen reader is able to find and announce the “Add to cart” button.

Although possible, this is an annoying experience. On the other hand, with aria-disabled, the screen reader will focus the button normally and properly announce its status. Note that the announcement is slightly different between screen readers. For example, NVDA and JWAS say “button, unavailable” but VoiceOver says “button, dimmed.”

The VoiceOver screen reader can find the “Add to cart” button using Tab key because of aria-disabled.

I’ve mapped out how both attributes create different user experiences based on the tools we just used:

Tool / Attributedisabledaria-disabled="true"
Mouse or tapPrevents a button click.Requires JS to prevent the click.
TabUnable to focus the button.Able to focus the button.
Screen readerButton is difficult to locate.Button is easily located.

So, the main differences between both attributes are:

  • disabled might skip the button when using the Tab key, leading to confusion.
  • aria-disabled will still focus the button and announce that it exists, but that it isn’t enabled yet; the same way you might perceive it visually.

This is the case where it’s important to acknowledge the subtle difference between accessibility and usability. Accessibility is a measure of someone being able to use something. Usability is a measure of how easy something is to use.

Given that, is disabled accessible? Yes. Does it have a good usability? I don’t think so.

Can we do better?

I wouldn’t feel good with myself if I finished this article without showing you the real inclusive solution for our ticket form example. Whenever possible, don’t use disabled buttons. Let people click it at any time and, if necessary, show an error message as feedback. This approach also solves other problems:

  • Less cognitive friction: Allow people to submit the form at any time. This removes the uncertainty of whether the button is even disabled in the first place.
  • Color contrast: Although a disabled button doesn’t need to meet the WCAG 1.4.3 color contrast, I believe we should guarantee that an element is always properly visible regardless of its state. But that’s something we don’t have to worry about now because the button isn’t disabled anymore.

Final thoughts

The disabled attribute in <button> is a peculiar case where, although highly known by the community, it might not be the best approach to solve a particular problem. Don’t get me wrong because I’m not saying disabled is always bad. There are still some cases where it still makes sense to use it (e.g. pagination).

To be honest, I don’t see the disabled attribute exactly as an accessibility issue. What concerns me is more of a usability issue. By swapping the disabled attribute with aria-disabled, we can make someone’s experience much more enjoyable.

This is yet one more step into my journey on web accessibility. Over the years, I’ve discovered that accessibility is much more than complying with web standards. Dealing with user experiences is tricky and most situations require making trade-offs and compromises in how we approach a solution. There’s no silver bullet for perfect accessibility.

Our duty as web creators is to look for and understand the different solutions that are available. Only then we can make the best possible choice. There’s no sense in pretending the problems don’t exist.

At the end of the day, remember that there’s nothing preventing you from making the web a more inclusive place.

Bonus

Still there? Let me mention two last things about this demo that I think are worthy:

1. Live Regions will announce dynamic content

In the demo, two parts of the content changed dynamically: the form submit button and the success confirmation (“Added [X] tickets!”).

These changes are visually perceived, however, for people with vision impairments using screen readers, that just ain’t the reality. To solve it, we need to turn those messages into Live Regions. Those allow assistive technologies to listen for changes and announce the updated messages when they happen.

There is a .sr-only class in the demo that hides a <span> containing a loading message, but allows it to be announced by screen readers. In this case, aria-live="assertive" is applied to the <span> and it holds a meaningful message after the form is submitting and is in the process of loading. This way, we can announce to the user that the form is working and to be patient as it loads. Additionally, we do the same to the form feedback element.

<button type="submit" aria-disabled="true">
  Add to cart
  <span aria-live="assertive" class="sr-only js-loadingMsg">
     <!-- Use JavaScript to inject the the loading message -->
  </span>
</button>

<p aria-live="assertive" class="formStatus">
  <!-- Use JavaScript to inject the success message -->
</p>

Note that the aria-live attribute must be present in the DOM right from the beginning, even if the element doesn’t hold any message yet, otherwise, Assistive Technologies may not work properly.

Form submit feedback message being announced by the screen reader.

There’s much more to tell you about this little aria-live attribute and the big things it does. There are gotchas as well. For example, if it is applied incorrectly, the attribute can do more harm than good. It’s worth reading “Using aria-live” by Ire Aderinokun and Adrian Roselli’s “Loading Skeletons” to better understand how it works and how to use it.

2. Do not use pointer-events to prevent the click

This is an alternative (and incorrect) implementation that I’ve seen around the web. This uses pointer-events: none; in CSS to prevent the click (without any HTML attribute). Please, do not do this. Here’s an ugly Pen that will hopefully demonstrate why. I repeat, do not do this.

Although that CSS does indeed prevent a mouse click, remember that it won’t prevent focus and keyboard navigation, which can lead to unexpected outcomes or, even worse, bugs.

In other words, using this CSS rule as a strategy to prevent a click, is pointless (get it?). ;)


Making Disabled Buttons More Inclusive originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/making-disabled-buttons-more-inclusive/feed/ 25 https://css-tricks.com/wp-content/uploads/2021/05/40df467a4f094300a7275b9222304185-1618767462271.mp4 forms Archives - CSS-Tricks nonadult 339960
16px or Larger Text Prevents iOS Form Zoom https://css-tricks.com/16px-or-larger-text-prevents-ios-form-zoom/ https://css-tricks.com/16px-or-larger-text-prevents-ios-form-zoom/#comments Tue, 04 May 2021 21:16:04 +0000 https://css-tricks.com/?p=339455 This was a great “Today I Learned” for me from Josh W. Comeau. If the font-size of an <input> is 16px or larger, Safari on iOS will focus into the input normally. But as soon as the font-size is …


16px or Larger Text Prevents iOS Form Zoom originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
This was a great “Today I Learned” for me from Josh W. Comeau. If the font-size of an <input> is 16px or larger, Safari on iOS will focus into the input normally. But as soon as the font-size is 15px or less, the viewport will zoom into that input. Presumably, because it considers that type too small and wants you to see what you are doing. So it zooms in to help you. Accessibility. If you don’t want that, make the font big enough.

https://twitter.com/joshwcomeau/status/1379782931116351490?s=12

Here’s Josh’s exact Pen if you want to have a play yourself.

In general, I’d say I like this feature. It helps people see what they are doing and discourages super-tiny font sizes. What is a slight bummer — and I really don’t blame anyone here — is that not all typefaces are created equal in terms of readability at different sizes. For example, here’s San Francisco versus Caveat at 16px.

San Francisco on the left, Cavet on the right. Caveat looks visually much smaller even though the font-size is the same.

You can view that example in Debug Mode to see for yourself and change the font size to see what does and doesn’t zoom.


16px or Larger Text Prevents iOS Form Zoom originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/16px-or-larger-text-prevents-ios-form-zoom/feed/ 9 339455
Creating an Editable Textarea That Supports Syntax-Highlighted Code https://css-tricks.com/creating-an-editable-textarea-that-supports-syntax-highlighted-code/ https://css-tricks.com/creating-an-editable-textarea-that-supports-syntax-highlighted-code/#comments Fri, 16 Apr 2021 18:58:59 +0000 https://css-tricks.com/?p=338296 When I was working on a project that needed an editor component for source code, I really wanted a way to have that editor highlight the syntax that is typed. There are projects like this, like CodeMirror, Ace, …


Creating an Editable Textarea That Supports Syntax-Highlighted Code originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
When I was working on a project that needed an editor component for source code, I really wanted a way to have that editor highlight the syntax that is typed. There are projects like this, like CodeMirror, Ace, and Monaco, but they are all heavy-weight, full-featured editors, not just editable textareas with syntax highlighting like I wanted.

It took a little finagling, but I wound up making something that does the job and wanted to share how I did it, because it involves integrating a popular syntax highlighting library with HTML’s editing capabilities, as well as a few interesting edge cases to take into consideration.

Live syntax highlighting while you type

Go ahead and give it a spin as we dig in!

After a suggestion, I have also released this as a custom element on GitHub, so you can quickly use the component in a webpage as a single <code-input> element.

The problem

First, I tried using the contenteditable attribute on a div. I typed some source code into the div and ran it through Prism.js, a popular syntax highlighter, on oninput via JavaScript. Seems like a decent idea, right? We have an element that can be edited on the front end, and Prism.js applies its syntax styling to what’s typed in the element.

Chris covers how to use Prism.js in this video.

But that was a no-go. Each time the content in the element changes, the DOM is manipulated and the user’s cursor is pushed back to the start of the code, meaning the source code appears backwards, with the last characters at the start, and the first characters at the end.

White monospaced text on a black background that does not spell a coherent word.
Backwards code: not very useful

Next, I tried about using a <textarea> but that also didn’t work, as textareas can only contain plain text. In other words, we’re unable to style the content that’s entered. A textarea seems to be the only way to edit the text without unwanted bugs — it just doesn’t let Prism.js do its thing.

Prism.js works a lot better when the source code is wrapped in a typical <pre><code> tag combo — it’s only missing the editable part of the equation.

So, neither seems to work by themselves. But, I thought, why not both?

The solution

I added both a syntax-highlighted <pre><code> and a textarea to the page, and made the innerText content of <pre><code> change oninput, using a JavaScript function. I also added an aria-hidden attribute to the <pre><code> result so that screen readers would only read what is entered into the <textarea> instead of being read aloud twice.

<textarea id="editing" oninput="update(this.value);"></textarea>

<pre id="highlighting" aria-hidden="true">
  <code class="language-html" id="highlighting-content"></code>
</pre>
function update(text) {
  let result_element = document.querySelector("#highlighting-content");
  // Update code
  result_element.innerText = text;
  // Syntax Highlight
  Prism.highlightElement(result_element);
}
The HTML for a link element pointed to CSS-Tricks is in black monospace on a white background above the same HTML link markup, but syntax-highlighted on a black background.
Now it’s editable and highlighted!

Now, when the textarea is edited — as in, a pressed key on the keyboard comes back up — the syntax-highlighted code changes. There are a few bugs we’ll get to, but I want to focus first on making it look like you are directly editing the syntax-highlighted element, rather than a separate textarea.

Making it “feel” like a code editor

The idea is to visibly merge the elements together so it looks like we’re interacting with one element when there are actually two elements at work. We can add some CSS that basically allows the <textarea> and the <pre><code> elements to be sized and spaced consistently.

#editing, #highlighting {
  /* Both elements need the same text and space styling so they are directly on top of each other */
  margin: 10px;
  padding: 10px;
  border: 0;
  width: calc(100% - 32px);
  height: 150px;
}

#editing, #highlighting, #highlighting * {
  /* Also add text styles to highlighting tokens */
  font-size: 15pt;
  font-family: monospace;
  line-height: 20pt;
}

Then we want to position them right on top of each other:

#editing, #highlighting {
  position: absolute;
  top: 0;
  left: 0;
}

From there, z-index allows the textarea to stack in front the highlighted result:

/* Move the textarea in front of the result */
#editing {
  z-index: 1;
}

#highlighting {
  z-index: 0;
}

If we stop here, we’ll see our first bug. It doesn’t look like Prism.js is highlighting the syntax, but that is only because the textarea is covering up the result.

Where has the highlighting gone? (Clue: It’s hiding in the background.)

We can fix this with CSS! We’ll make the <textarea> completely transparent except the caret (cursor):

/* Make textarea almost completely transparent */
#editing {
  color: transparent;
  background: transparent;
  caret-color: white; /* Or choose your favorite color */
}

Ah, much better!

HTML markup for a link element pointed to CSS tricks in a syntax-highlighted monospace font on a black background.
Where will this link take you? You decide.

More fixing!

Once I got this far, I played around with the editor a bit and was able to find a few more things that needed fixing. The good thing is that all of the issues are quite easy to fix using JavaScript, CSS, or even HTML.

Remove native spell checking

We’re making a code editor, and code has lots of words and attributes that a browser’s native spell checker will think are misspellings.

Showing the basic HTML markup for a link with syntax highlighting on a black background, where the href attribute is underlined in red.

Spell checking isn’t a bad thing; it’s just unhelpful in this situation. Is something marked incorrect because it is incorrectly spelled or because the code is invalid? It’s tough to tell. To fix this, all we need is to set the spellcheck attribute on the <textarea> to false:

<textarea id="editing" spellcheck="false" ...>

Handling new lines

Turns out that innerText doesn’t support newlines (\n).

White monospace text on a black background. The first line says "Hello, World" and the second line says "World" and is highlight in blue.
Even though “World!” should be on a new line, the syntax highlighted section displays it on the same line.

The update function needs to be edited. Instead of using innerText, we can use innerHTML, replacing the open bracket character (<) with &lt; and replace the ampersand character (&) with &amp; to stop HTML escapes from being evaluated. This prevents new HTML tags from being created, allowing the actual source code displays instead of the browser attempting to render the code.

result_element.innerHTML = text.replace(new RegExp("&", "g"), "&").replace(new RegExp("<", "g"), "<"); /* Global RegExp */
Showing the basic HTML document boilerplate with syntax highlighting on a black background. The body element contains the markup yo a paragraph that contains a link that says "CSS-Tricks is brilliant!"

Scrolling and resizing

Here’s another thing: the highlighted code cannot scroll while the editing is taking place. And when the textarea is scrolled, the highlighted code does not scroll with it.

First, let’s make sure that both the textarea and result support scrolling:

/* Can be scrolled */
#editing, #highlighting {
  overflow: auto;
  white-space: nowrap; /* Allows textarea to scroll horizontally */
}

Then, to make sure that the result scrolls with the textarea, we’ll update the HTML and JavaScript like this:

<textarea id="editing" oninput="update(this.value); sync_scroll(this);" onscroll="sync_scroll(this);"></textarea>
function sync_scroll(element) {
  /* Scroll result to scroll coords of event - sync with textarea */
  let result_element = document.querySelector("#highlighting");
  // Get and set x and y
  result_element.scrollTop = element.scrollTop;
  result_element.scrollLeft = element.scrollLeft;
}

Some browsers also allow a textarea to be resized, but this means that the textarea and result could become different sizes. Can CSS fix this? Of course it can. We’ll simply disable resizing:

/* No resize on textarea */
#editing {
  resize: none;
}

Final newlines

Thanks to this comment for pointing out this bug.

Now the scrolling is almost always synchronized, but there is still one case where it still doesn’t work. When the user creates a new line, the cursor and the textarea’s text are temporarily in the wrong position before any text is entered on the new line. This is because the <pre><code> block ignores an empty final line for aesthetic reasons. Because this is for a functional code input, rather than a piece of displayed code, the empty final line needs to be shown. This is done by giving the final line content so it is no longer empty, with a few lines of JavaScript added to the update function. I have used a space character because it is invisible to the user.

function update(text) {
  let result_element = document.querySelector("#highlighting-content");
  // Handle final newlines (see article)
  if(text[text.length-1] == "\n") { // If the last character is a newline character
    text += " "; // Add a placeholder space character to the final line 
  }
  // Update code
  result_element.innerHTML = text.replace(new RegExp("&", "g"), "&").replace(new RegExp("<", "g"), "<"); /* Global RegExp */
  // Syntax Highlight
  Prism.highlightElement(result_element);
}

Indenting lines

One of the trickier things to adjust is how to handle line indentations in the result. The way the editor is currently set up, indenting lines with spaces works fine. But, if you’re more into tabs than spaces, you may have noticed that those aren’t working as expected.

JavaScript can be used to make the Tab key properly work. I have added comments to make it clear what is happening in the function.

<textarea ... onkeydown="check_tab(this, event);"></textarea>
function check_tab(element, event) {
  let code = element.value;
  if(event.key == "Tab") {
    /* Tab key pressed */
    event.preventDefault(); // stop normal
    let before_tab = code.slice(0, element.selectionStart); // text before tab
    let after_tab = code.slice(element.selectionEnd, element.value.length); // text after tab
    let cursor_pos = element.selectionEnd + 1; // where cursor moves after tab - moving forward by 1 char to after tab
    element.value = before_tab + "\t" + after_tab; // add tab char
    // move cursor
    element.selectionStart = cursor_pos;
    element.selectionEnd = cursor_pos;
    update(element.value); // Update text to include indent
  }
}

To make sure the Tab characters are the same size in both the <textarea> and the syntax-highlighted code block, edit the CSS to include the tab-size property:

#editing, #highlighting, #highlighting * {
  /* Also add text styles to highlighing tokens */
  [...]
  tab-size: 2;
}

The final result

Not too crazy, right? All we have are <textarea>, <pre> and <code> elements in the HTML, a new lines of CSS that stack them together, and a syntax highlighting library to format what’s entered. And what I like best about this is that we’re working with normal, semantic HTML elements, leveraging native attributes to get the behavior we want, leaning on CSS to create the illusion that we’re only interacting with one element, then reaching for JavaScript to solve some edge cases.

While I used Prism.js for syntax highlighting, this technique will work with others. It would even work with a syntax highlighter you create yourself, if you want it to. I hope this becomes useful, and can be used in many places, whether it’s a WYSIWYG editor for a CMS, or even a forms where the ability to enter source code is a requirement like a front-end job application or perhaps a quiz. It is a<textarea>, after all, so it’s capable of being used in any form — you can even add a placeholder if you need to!


Creating an Editable Textarea That Supports Syntax-Highlighted Code originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/creating-an-editable-textarea-that-supports-syntax-highlighted-code/feed/ 44 338296
HTML Inputs and Labels: A Love Story https://css-tricks.com/html-inputs-and-labels-a-love-story/ https://css-tricks.com/html-inputs-and-labels-a-love-story/#comments Tue, 30 Mar 2021 20:29:49 +0000 https://css-tricks.com/?p=337074 Most inputs have something in common — they are happiest with a companion label! And the happiness doesn’t stop there. Forms with proper inputs and labels are much easier for people to use and that makes people happy too.

A…


HTML Inputs and Labels: A Love Story originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Most inputs have something in common — they are happiest with a companion label! And the happiness doesn’t stop there. Forms with proper inputs and labels are much easier for people to use and that makes people happy too.

A happy label and input pair

In this post, I want to focus on situations where the lack of a semantic label and input combination makes it much harder for all sorts of people to complete forms. Since millions of people’s livelihoods rely on forms, let’s get into the best tips I know for creating a fulfilling and harmonious relationship between an input and a label.

Content warning: In this post are themes of love and relationships.

The love story starts here! Let’s cover the basics for creating happy labels and inputs.

How to pair a label and an input

There are two ways to pair a label and an input. One is by wrapping the input in a label (implicit), and the other is by adding a for attribute to the label and an id to the input (explicit).

Think of an implicit label as hugging an input, and an explicit label as standing next to an input and holding its hand.

<label> 
  Name:
  <input type="text" name="name" />
</label>

An explicit label’s for attribute value must match its input’s id value. For example, if for has a value of name, then id should also have a value of name.

<label for="name">Name: </label>
<input type="text" id="name" name="name" />

Unfortunately, an implicit label is not handled correctly by all assistive technologies, even if for and id attributes are used. Therefore, it is always the best idea to use an explicit label instead of an implicit label.

<!-- IMPLICIT LABEL (not recommended for use) - the label wraps the input. -->
<label> 
  Name:
  <input type="text" name="name" />
</label>

<!-- EXPLICIT LABEL (recommended for use) - the label is connected to an input via "for" and "id" -->
<label for="explicit-label-name">Name: </label>
<input type="text" id="explicit-label-name" name="name" />

Each separate input element should only be paired with one label. And a label should only be paired with one input. Yes, inputs and labels are monogamous. ♥️

Note: There’s one sort of exception to this rule: when we’re working with a group of inputs, say several radio buttons or checkboxes. In these cases, a <legend> element is used to group certain input elements, such as radio buttons, and serves as the accessible label for the entire group.

Not all inputs need labels

An input with a type="submit" or type="button" does not need a label — the value attribute acts as the accessible label text instead. An input with type="hidden" is also fine without a label. But all other inputs, including <textarea> and <select> elements, are happiest with a label companion.

What goes in a label

The content that goes inside of a label should:

  • Describe its companion input. A label wants to show everyone that it belongs with its input partner.
  • Be visible. A label can be clicked or tapped to focus its input. The extra space it provides to interact with the input is beneficial because it increases the tap or click target. We’ll go into that more in just a bit.
  • Only contain plain text. Don’t add elements like headings or links. Again, we’ll go into the “why” behind this further on down.

One useful thing you can do with the content in a label is add formatting hints. For example, the label for <input type="date" id="date"> will be <label for="date"> as we’d expect. But then we can provide a hint for the user that the date needs to be entered in a specific format, say DD-MM-YYYY, in the form of a <span> between the label and input that specifies that requirement. The hint not only specifies the date format, but has an id value that corresponds to an aria-describedby attribute on the input.

<!-- Describes what the input is for - should be plain text only -->
<label for="date">Start date</label>
<!-- Describes additional information, usually about formatting -->
<span id="date-format">(DD-MM-YYYY):</span>
<input type="date" id="date" aria-describedby="date-format" min="2021-03-01" max="2031-01-01" />

This way, we get the benefit of a clear label that describes what the input is for, and a bonus hint to the user that the input needs to be entered in a specific format.

Best practices for a healthy relationship

The following tips go beyond the basics to explain how to make sure a label and input are as happy as can be.

Do: Add a label in the right place

There is a WCAG success criterion that states the visual order of a page should follow the order in which elements appear in the DOM. That’s because:

A user with low vision who uses a screen magnifier in combination with a screen reader may be confused when the reading order appears to skip around on the screen. A keyboard user may have trouble predicting where focus will go next when the source order does not match the visual order.

Think about it. If we have some standard HTML where the label comes before the input:

<label for="orange">Orange</label>
<input type="checkbox" id="orange" name="orange">

That places the label before the input in the DOM. But now let’s say our label and form are inside a flexible container and we use CSS order to reverse things where the input visually comes before the label:

label { order: 2; }
input { order: 1; }

A screen reader user, who is navigating between elements, might expect the input to gain focus before the label because the input comes first visually. But what really happens is the label comes into focus instead. See here for the difference between navigating with a screen reader and a keyboard.

So, we should be mindful of that. It is conventional to place the label on the right-hand side of the input for checkboxes and radio buttons. This can be done by placing the label after the input in the HTML, ensuring the DOM and visual order match.

<form>
  <!-- Checkbox with label on the right -->
  <span>
    <input type="checkbox" id="orange" name="orange">
    <label for="orange">Orange</label>
  </span>
  <!-- Radio button with label on the right -->
  <span>
    <input type="radio" id="banana" name="banana">
    <label for="banana">Banana</label>
  </span>
  <!-- Text input with label on the left -->
  <span>
    <label for="apple">How do you like them apples?</label>
    <input type="text" id="apple" name="apple">
  </span>
</form>

Do: Check inputs with a screen reader

Whether an input is written from scratch or generated with a library, it is a good idea to check your work using a screen reader. This is to make sure that:

  • All relevant attributes exist — especially the matching values of the for and id attributes.
  • The DOM matches the visual order.
  • The label text sounds clear. For example, “dd-mm-yyyy” is read out differently to its uppercase equivalent (DD-MM-YYYY).
Screen reader reading out a lower case and upper case date input hint.
Just because the letters are the same doesn’t mean that they get read the same way by a screen reader.

Over the last few years, I have used JavaScript libraries, like downshift, to build complex form elements such as autocomplete or comboboxes on top of native HTML ones, like inputs or selects. Most libraries make these complex elements accessible by adding ARIA attributes with JavaScript.

However, the benefits of native HTML elements enhanced using JavaScript are totally lost if JavaScript is broken or disabled, making them inaccessible. So check for this and provide a server-rendered, no-JavaScript alternative as a safe fallback.

Check out these basic form tests to determine how an input and its companion label or legend should be written and announced by different screen readers.

Do: Make the label visible

Connecting a label and an input is important, but just as important is keeping the label visible. Clicking or tapping a visible label focuses its input partner. This is a native HTML behavior that benefits a huge number of people.

Imagine a label wanting to proudly show its association with an input:

A visible happy label proudly showing off its input partner.
A label really wants to show off its input arm candy. 💪🍬

That said, there are going to be times when a design calls for a hidden label. So, if a label must be hidden, it is crucial to do it in an accessible way. A common mistake is to use display: none or visibility: hidden to hide a label. These CSS display properties completely hide an element — not only visually but also from screen readers.

Consider using the following code to visually hide labels:

/* For non-natively-focusable elements. For natively focusable elements */
/* Use .visually-hidden:not(:focus):not(:active) */
.visually-hidden {
  border-width: 0 !important;
  clip: rect(1px, 1px, 1px, 1px) !important;
  height: 1px !important;
  overflow: hidden !important;
  padding: 0 !important;
  position: absolute !important;
  white-space: nowrap !important;
  width: 1px !important;
}

Kitty Giraudel explains in depth how to hide content responsibly.

What to Avoid

To preserve and maintain a healthy relationship between inputs and labels, there are some things not to do when pairing them. Let’s get into what those are and how to prevent them.

Don’t: Expect your input to be the same in every browser

There are certain types of inputs that are unsupported In some older desktop browsers. For example, an input that is type="date" isn’t supported in Internet Explorer (IE) 11, or even in Safari 141 (released September 2020). An input like this falls back to type="text". If a date input does not have a clear label, and it automatically falls back to a text input in older browsers, people may get confused.

Don’t: Substitute a label with a placeholder

Here is why a placeholder attribute on an input should not be used in place of a label:

  • Not all screen readers announce placeholders.
  • The value of a placeholder is in danger of being cut-off on smaller devices, or when a page is translated in the browser. In contrast, the text content of a label can easily wrap onto a new line.
  • Just because a developer can see the pale grey placeholder text on their large retina screen, in a well-lit room, in a distraction-free environment, doesn’t mean everyone else can. A placeholder can make even those with good vision scrunch their eyes up and eventually give up on a form.
An input on a mobile device where the placeholder text is cut off.
Make sure this is somewhere I can what? It’s hard to tell when the placeholder gets cut off like that.
An input where the translated placeholder is cut off.
Work on a multi-lingual site? A translated placeholder can get cut off even though the English translation is just fine. In this case, the translated placeholder should read “Durchsuchen Sie die Dokumentation.”

  • Once a character is entered into an input, its placeholder becomes invisible — both visually and to screen readers. If someone has to back-track to review information they’ve entered in a form, they’d have to delete what was entered just to see the placeholder again.
  • The placeholder attribute is not supported in IE 9 and below, and disappears when an input is focused in IE 11. Another thing to note: the placeholder color is unable to be customized with CSS in IE 11.

Placeholders are like the friend that shows up when everything is perfect, but disappears when you need them most. Pair up an input with a nice, high-contrast label instead. Labels are not flaky and are loyal to inputs 100% of the time.

The Nielsen Norman Group has an in-depth article that explains why placeholders in form fields are harmful.

Don’t: Substitute a label with another attribute or element

When no label is present, some screen readers will look for adjacent text and announce that instead. This is a hit-and-miss approach because it’s possible that a screen reader won’t find any text to announce.

The below code sample comes from a real website. The label has been substituted with a <span> element that is not semantically connected to the input.

<div>
  <span>Card number</span>
  <div>
    <input type="text" value="" id="cardNumber" name="cardNumber" maxlength="40">
  </div>
</div>

The above code should be re-written to ensure accessibility by replacing the span with a label with for="cardNumber" on it. This is by far the most simple and robust solution that benefits the most people.

While a label could be substituted with a span that has an id with a value matching the input’s aria-labelledby attribute, people won’t be able to click the span to focus the input in the same way a label allows. It is always best to harness the power of native HTML elements instead of re-inventing them. The love story between native input and label elements doesn’t need to be re-written! It’s great as-is.

Don’t: Put interactive elements inside labels

Only plain text should be included inside a label. Avoid inserting things such as headings, or interactive elements such as links. Not all screen readers will announce a label correctly if it contains something other than plain text. Also, if someone wants to focus an input by clicking its label, but that label contains a link, they may click the link by mistake.

<form>
  <div>
    <!-- Don't do this -->
    <input type="checkbox" id="t-and-c" name="name" />
    <label for="t-and-c">I accept the <a href="https://link.com">terms and conditions</a></label>
  </div>
  <div>
    <!-- Try this -->
    <input type="checkbox" id="t-and-c2" name="name" />
    <label for="t-and-c2">I accept the terms and conditions.</label>
    <span>Read <a href="https://link.com">terms and conditions</a></span>
  </div>
</form>

Real-life examples

I always find that real-life examples help me to properly understand something. I searched the web and found examples of labels and inputs from a popular component library and website. Below, I explain where the elements fall short and how they can be improved to ensure a better pairing.

Component library: Material

MaterialUI is a React component library based on Google’s design system. It includes a text input component with a floating label pattern that has become a popular go-to for many designers and developers:

The floating label pattern starts with the label in the placeholder position that moves the label to its proper place above the field. The idea is that this achieves a minimal design while solving the issue of placeholders disappearing when typing.

Clicking on the input feels smooth and looks great. But that’s the problem. Its qualities are mostly skin-deep.

At the time of writing this post, some issues I found with this component include:

  • There is no option to have the label outside of the input in order to offer an increased interactive area for focusing the input.
  • There is an option to add a hint like we saw earlier. Unfortunately, the hint is only associated with the input via proximity and not through a matching id and aria-describedby. This means that not all screen readers will be able to associate the helper message with the input.
  • The label is behind the input in the DOM, making the visual order is incorrect.
  • The empty input looks look like it is already filled out, at least as long as it is not active.
  • The label slides up when the input is clicked, making it unsuitable for those who prefer reduced motion.

Adam Silver also explains why float labels are problematic and gets into a detailed critique of Material’s text input design.

Website: Huffpost

The Huffpost website has articles containing a newsletter subscription form:

Huffpost newsletter signup form
This form looks quite nice but could be improved

At the time of writing this blog post, the email input that Huffpost uses could benefit from a number of improvements:

  • The lack of a label means a smaller click or tap target. Instead of a label there is an aria-label attribute on the input.
  • The font size of the placeholder and input values are a tiny 11px, which can be hard to read.
  • The entire input disappears without JavaScript, meaning people have no way to sign up to the newsletter if JavaScript is disabled or broken.

Closing remarks

A surprising number of people struggle to enter information into poorly-constructed inputs. That list includes people with cognitive, motor and physical disabilities, autism spectrum disorders, brain injuries, and poor vision. Other people who struggle include those in a hurry, on a poor connection, on a small device, on an old device, and unfamiliar with digital forms, among many others.

Additionally, there are numerous reasons why JavaScript might break or be switched off in a browser, meaning inputs become dysfunctional or completely inaccessible. There are also people who are fully capable of viewing a web page but who may choose to use a keyboard along with a screen reader.

The message I want to get across is that happy label and input pairs are crucial. It doesn’t matter if your form is beautiful if it is unusable. I can bet that almost everyone would rather fill out an ugly but easy-to-use form rather than a pretty one that causes problems.

Thanks

I want to warmly thank the following people for helping me with this post: Eric Eggert, Adam Silver, Dion Dajka, and Kitty Giraudel. Their combined accessibility knowledge is a force to be reckoned with!

Footnotes

  • 1 The datepicker is actually well-supported in iOS 14 and very nice. One would imagine that a macOS version has got to be on the horizon.


HTML Inputs and Labels: A Love Story originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/html-inputs-and-labels-a-love-story/feed/ 20 337074
Creating Custom Form Controls with ElementInternals https://css-tricks.com/creating-custom-form-controls-with-elementinternals/ https://css-tricks.com/creating-custom-form-controls-with-elementinternals/#comments Wed, 24 Mar 2021 14:22:49 +0000 https://css-tricks.com/?p=336775 Ever since the dawn of time, humanity has dreamed of having more control over form elements. OK, I might be overselling it a tiny bit, but creating or customizing form components has been a holy grail of front-end web development …


Creating Custom Form Controls with ElementInternals originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Ever since the dawn of time, humanity has dreamed of having more control over form elements. OK, I might be overselling it a tiny bit, but creating or customizing form components has been a holy grail of front-end web development for years.

One of the lesser-heralded, but most powerful features of custom elements (e.g. <my-custom-element>) has quietly made its way into Google Chrome as of version 77 and is working its way into other browsers. The ElementInternals standard is a very exciting set of features with a very unassuming name. Among the features internals adds are the ability to participate in forms and an API around accessibility controls.

In this article, we’re going to look at how to create a custom form control, integrate constraint validation, introduce the basics of internal accessibility and see a way to combine these features to create a highly-portable macro form control.

Let’s start by creating a very simple custom element that matches our design system. Our element will hold all of its styles within the shadow DOM and ensure some basic accessibility. We’ll use the wonderful LitElement library from the Polymer team at Google for our code examples and, although you definitely don’t need it, it does provide a great abstraction for writing custom elements.

In this Pen, we’ve created a <rad-input> that has some basic design to it. We have also added a second input to our form that is a vanilla HTML input, and added a default value (so you can simply press submit and see it work).

When we click our submit button a few things happen. First, the submit event’s preventDefault method is called, in this case, to ensure our page doesn’t reload. After this, we create a FormData object which gives us access to information about our form which we use to construct a JSON string and append it to an <output> element. Notice, however, that the only value-added to our output is from the element with name="basic".

That’s because our element doesn’t know how to interact with the form just yet, so let’s set up our <rad-input> with an ElementInternals instance to help it live up to its name. To start, we’ll need to call our method’s attachInternals method in the element’s constructor, we’ll also be importing an ElementInternals polyfill into our page to work with browsers that don’t support the spec yet.

The attachInternals method returns a new element internals instance which contains some new APIs we can use in our method. In order to let our element take advantage of these APIs, we need to add a static formAssociated getter that returns true.

class RadInput extends LitElement {
  static get formAssociated() {
    return true;
  }

  constructor() {
    super();
    this.internals = this.attachInternals();
  }
}

Let’s take a look at some of the APIs in our element’s internals property:

  • setFormValue(value: string|FormData|File, state?: any): void — This method will set the element’s value on its parent form if one is present. If the value is null, the element will not participate in the form submission process.
  • form — A reference to our element’s parent form, if one exists.
  • setValidity(flags: Partial<ValidityState>, message?: string, anchor?: HTMLElement): void — The setValidity method will help control our element’s validity state within the form. If the form is invalid, a validation message must be present.
  • willValidate — Will be true if the element will be evaluated when the form is submitted.
  • validity — A validity object that matches the APIs and semantics attached to HTMLInputElement.prototype.validity.
  • validationMessage — If the control has been set as invalid with setValidity, this is the message that was passed in describing the error.
  • checkValidity — Will return true if the element is valid, otherwise this will return false and fire an invalid event on the element.
  • reportValidity — Does the same as checkValidity, and will report problems to the user if the event isn’t cancelled.
  • labels — A list of elements that label this element using the label[for] attribute.
  • A number of other controls used to set aria information on the element.

Setting a custom element’s value

Let’s modify our <rad-input> to take advantage of some of these APIs:

Here we’ve modified the element’s _onInput method to include a call to this.internals.setFormValue. This tells the form our element wants to register a value with the form under its given name (which is set as an attribute in our HTML). We’ve also added a firstUpdated method (loosely analogous with connectedCallback when not using LitElement) which sets the element’s value to an empty string whenever the element is done rendering. This is to make sure our element always has a value with the form (and though it is not necessary, you may want to exclude your element from the form by passing in a null value).

Now when we add a value to our input and submit the form, we will see that we have a radInput value in our <output> element. We can also see our element has been added to the HTMLFormElement’s radInput property. One thing you might have noticed, however, is that despite the fact that despite the fact that our element doesn’t have a value, it will still allow the form submission to take place. Let’s add some validation to our element next.

Adding constraint validation

In order to set our field’s validation, we need to modify our element a little bit to make use of the setValidity method on our element internals object. This method will take in three arguments (the second one is only required if the element is invalid, the third is always optional). The first argument is a partial ValidityState object. If any flag is set to true the control will be marked as invalid. If one of the built-in validity keys doesn’t meet your needs, there is a catch-all customError key that should work. Lastly, if the control is valid, we pass in an object literal ({}) to reset the control’s validity.

The second argument here is the control’s validity message. This argument is required if the control is invalid, and not allowed if the control is valid. The third argument is an optional validation target that will control the user’s focus if and when the form is submitted as invalid or reportValidity is called.

We’re going to introduce a new method to our <rad-input> that will take care of this logic for us:

_manageRequired() {
  const { value } = this;
  const input = this.shadowRoot.querySelector('input');
  if (value === '' && this.required) {
    this.internals.setValidity({
      valueMissing: true
    }, 'This field is required', input);
  } else {
    this.internals.setValidity({});
  }
}

This function gets the control’s value and input. If the value is equal to an empty string and the element is marked as required, we’ll call the internals.setValidity and toggle the control’s validity. Now we all we need to do is call this method in our firstUpdated and _onInput methods and we’ll have added some basic validation to our element.

Clicking the submit button before a value is entered into our <rad-input> will now display an error message in browsers that support the ElementInternals spec. Unfortunately, displaying validation errors is still not supported by the polyfill as there isn’t any reliable way to trigger the built-in validation popup in non-supporting browsers.

We’ve also added some basic accessibility information to our example by using our internals object. We’ve added an additional property to our element, _required, which will serve as a proxy for this.required and as a getter/setter for required.

get required() {
  return this._required;
}

set required(isRequired) {
  this._required = isRequired;
  this.internals.ariaRequired = isRequired;
}

By passing the required property to internals.ariaRequired, we are alerting screen readers that our element is currently expecting a value. In the polyfill, this is done by adding an aria-required attribute; however, in supporting browsers, the attribute won’t be added to the element because that property is inherent to the element.

Creating a micro-form

Now that we have a working input that meets our design system, we might want to begin composing our elements into patterns that we can reuse throughout several applications. One of the most compelling features for ElementInternals is that the setFormValue method can take not only string and file data, but also FormData objects. So let’s say we want to create a common address form that might be used in multiple organizations, we can do that easily with our newly-created elements.

In this example, we have a form created inside our element’s shadow root where we have composed four <rad-input> elements to make an address form. Instead of calling setFormValue with a string, this time we’ve chosen to pass along the entire value of our form. As a result, our element passes along the values of each individual element inside its child form to the outer form.


Adding constraint validation to this form would be a fairly straightforward process, as would providing additional styles, behaviors and slotting in content. Using these newer APIs finally allows developers to unlock a ton of potential inside custom elements and finally gives us free-range on controlling our user experiences.


Creating Custom Form Controls with ElementInternals originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/creating-custom-form-controls-with-elementinternals/feed/ 6 336775
Make Your Own Tools https://css-tricks.com/make-your-own-tools/ https://css-tricks.com/make-your-own-tools/#comments Mon, 04 Jan 2021 14:59:07 +0000 https://css-tricks.com/?p=331965 Spencer Miskoviak on the Wealthfront blog:

By creating custom DevTools specific to an app, they can operate at an even higher abstraction to handle things like user interactions, or debugging tracking events. While this requires building and maintaining the


Make Your Own Tools originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Spencer Miskoviak on the Wealthfront blog:

By creating custom DevTools specific to an app, they can operate at an even higher abstraction to handle things like user interactions, or debugging tracking events. While this requires building and maintaining the custom DevTools, it also means it can be tailored to the needs of the app and engineers to streamline development.

I think it’s super cool and smart to build custom tools for your team of developers. Even if custom tools are just for yourself they can be a productivity boon. But by building custom tools for your whole team, and opening the door to their ideas, that’s extra smart and compounds the value.

Spencer showcased a variety of different tools they have, all under the umbrellas of a UI popup widget thingy:

  • Shows current branch and CI status
  • Fills out forms, performs user actions, switches between users
  • Highlights components

Clever stuff.

We don’t have a fancy UI widget like that at CodePen, but do have some productivity-helping fuctionality sprinkled into the app. For example, many forms have a prefill button that only shows up for devs:

And we have a custom tool for our support inbox that gives context to the users and content that the support ticket references:

Not to mention a whole protected admin area on the site itself to perform a whole slew of admin and developer focused tasks:


I think the “component highlighter” that Spencer talked about is particularly neat:

React DevTools can be helpful in seeing what parts of the current page are which components, but that’s not on-page like this. I think it would be rad to have a little 🔗 next to each title that would open that file in VS Code.


Speaking of building your own tools, Shawn Wang wrote “You’re Allowed To Make Your Own Tools” recently:

Even the greatest software has parts that aren’t so great for you. But the difference between you and everyone else is that you can code.

Shawn talks about things like…

  • Building your own custom stylesheets
  • Building a UI query generator
  • Building your own CLIs (I’m reminded of Mina Markham’s dotfiles)
  • Building your own proxies

Shawn wrote his own dang proxy for Google Search Results to optimize them and present them how he likes:

Once in a while, I’m in the mood to focus on tooling, which leads me to do stuff like when I decided to “Run Gulp as You Open a VS Code Project using VS Code Tasks” which I had to learn all about and struggle through weird problems. I’d think a great DevOps person at a company would be all over stuff like this—constantly thinking of developer experience for their own people.

I even scripted the opening of a text-based multi-player video game I play not long ago to save myself some time.


And speaking of building your own tools generally, I think of Dick Proenneke’s in Alone in the Wilderness documentary. In this intro clip, you can hear Dick talk about quite literally building tools, which was useful for him as he didn’t need to hand-haul them deep into the Alaskan wilderness.

🛠


Make Your Own Tools originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/make-your-own-tools/feed/ 2 331965
“Yes or No?” https://css-tricks.com/yes-or-no/ https://css-tricks.com/yes-or-no/#comments Tue, 22 Dec 2020 23:50:28 +0000 https://css-tricks.com/?p=331232 Sara Soueidan digs into this HTML/UX situation. “Yes” or “no” is a boolean situation. A checkbox represents this: it’s either on or off (uh, mostly). But is a checkbox always the best UX? It depends, of course:

Use radio


“Yes or No?” originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Sara Soueidan digs into this HTML/UX situation. “Yes” or “no” is a boolean situation. A checkbox represents this: it’s either on or off (uh, mostly). But is a checkbox always the best UX? It depends, of course:

Use radio buttons if you expect the answer to be equally distributed. If I expect the answer to be heavily biased to one answer I prefer the checkbox. That way the user either makes an explicit statement or just acknowledges the expected answer.

If you want a concrete, deliberate, explicit answer and don’t want a default selection, use radio buttons. A checkbox has an implicit default state. And the user might be biased to the default option. So having the requirement for an explicit “No” is the determinig factor.

So you’ve got the checkbox approach:

<label>
  <input type="checkbox">
  Yes?
</label>

Which is nice and compact but you can’t make it “required” (easily) because it’s always in a valid state.

So if you need to force a choice, radio buttons (with no default) are easier:

<label>
  <input type="radio" name="choice-radio">
  Yes
</label>
<label>
  <input type="radio" name="choice-radio">
  No
</label>

I mean, we might as well consider another a range input too, which can function as a toggle if you max it out at 1:

<label class="screen-reader-only" for="choice">Yes or No?</label>
<span aria-hidden="true">No</span>
<input type="range" max="1" id="choice" name="choice">
<span aria-hidden="true">Yes</span>

Lolz.

And I suppose a <select> could force a user choice too, right?

<label>
  Yes or no?
  <select>
    <option value="">---</option>
    <option value="">Yes</option>
    <option value="">No</option>
  </select>
</label>

I weirdly don’t hate that only because selects are so compact and styleable.

If you really wanna stop and make someone think, though, make them type it, right?

<label>
  Type "yes" or "no"
  <input type="text" pattern="[Yy]es|[Nn]o">
</label>

Ha.


“Yes or No?” originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/yes-or-no/feed/ 1 331232