Accessibility first: text input

Andrew Bone - Nov 30 '18 - - Dev Community

Before I start I should let you know I'll be using :placeholder-shown.

This is an experimental technology
Check the Browser compatibility table carefully before using this in production.

Though it's been implemented in both Firefox and Chrome, Edge has yet to start developing the technology. That being said there is no other way to achieve this without using Javascript.

What do we want to create?

Today we want to make a material design input box that can handle all the input states and does not use any Javascript.

The markup

If you've read the first entry in this series you'll probably start to recognise my 'style'. I like to have a label element that wraps my input and then have some spans I can animate later.

<label class="md_textbox">
  <input type="text" placeholder="Input field"/>
  <span class="md_textbox__label">Input field</span>
  <span class="md_textbox__line"></span>
</label>
Enter fullscreen mode Exit fullscreen mode

You'll notice there is some repeatition here, technically, we could have placeholder=" " rather than the whole string but it's good to plan for your CSS to fail.

Styles

Just like with the toggle in my previous post there are several states we will need to support with our styles, they are:

  • Empty
  • Focused
  • Containing valid text
  • Containing invalid text
  • Disabled
  • Required
  • Read-only

Empty (much of the legwork is done here)

First I'll import Open Sans for a material look and set my usual display and margin settings.

@import url(http://fonts.googleapis.com/css?family=Open+Sans);
.md_textbox {
  display: inline-flex;
  position: relative;
  font-family: "Open Sans";
  align-items: center;
  margin: 1.2em 0.6em 0.9em 0.6em;
}
Enter fullscreen mode Exit fullscreen mode

Now we only want our .md_textbox__label to show and not the placeholder text so lets make that invisible.

.md_textbox [type=text]::placeholder {
  color: rgba(0, 0, 0, 0)
}
Enter fullscreen mode Exit fullscreen mode

Next, we'll want our label to be in the placeholder's position so we'll give it an absolute position and move it over to left. While we're at it I know I want the line to be on the far left too and the space for the error messages so let's bundle it all together.

I know they'll all need to animate too so I've given them a transition, this is from the material design spec.

.md_textbox .md_textbox__label,
.md_textbox .md_textbox__line,
.md_textbox .md_textbox__line::before {
  position: absolute;
  transition: transform 150ms cubic-bezier(0.4, 0.0, 0.2, 1);
  left: 0;
}
Enter fullscreen mode Exit fullscreen mode

OK, we still have a little housekeeping to do, when we mouse over our replacement placeholder we should still see the text cursor also now's the time to set the colour for that text. We also know we'll need to scale the label when we move it but we'll want to scale it from the left edge so let's add that to our code too.

.md_textbox .md_textbox__label {
  transform-origin: 0 50%;
  cursor: text;
  color: #424242;
}
Enter fullscreen mode Exit fullscreen mode

Let's now do the prep for the line we need to position it at the bottom and give it a colour. For the animation, I think we'll use a transform so let's initialise the element with scale(0, 1) so it's hidden.

.md_textbox .md_textbox__line {
  bottom: 0;
  height: 2px;
  width: 100%;
  transform: scale(0, 1);
  background: #00897B;
}
Enter fullscreen mode Exit fullscreen mode

This is the last big of legwork before we can start styling our actual textbox, we know we're going to have an error message slot so let's position that. We'll not include content so it won't actually render anything.

.md_textbox .md_textbox__line::before {
  content: '';
  margin-top: 2px;
  font-size: 0.7em;
}
Enter fullscreen mode Exit fullscreen mode

Finally we can style our text box. I'm going to make it a little wider, remove the default border, remove the outline, I wouldn't normally like doing this but we'll be showing focus another way, make the background transparent and add a line under our text.

.md_textbox [type=text] {
  width: 200px;
  outline: none;
  border: none;
  border-bottom: 1px solid #BDBDBD;
  background: rgba(0, 0, 0, 0);
}
Enter fullscreen mode Exit fullscreen mode

Phew that was a lot of css almost 50 lines and our input isn't even functional yet that being said we've got the bulk of the styles out of the way now.

MD input box Standard

Focused

When we focus on the text box we want two things to happen, one we want the label to get out the way so we can see where we're typing and two we want a bold line to appear under the element to let us know it's focused.

To move the label we'll use a transform, we should also make it a little smaller but make the text bold and the 'active' colour.

.md_textbox [type=text]:focus~.md_textbox__label {
  transform: translateY(-1em) scale(0.8);
  font-weight: bold;
  color: #00897B;
}
Enter fullscreen mode Exit fullscreen mode

To make the line appear we just need to set the scale, from earlier, back to 1 like so

.md_textbox [type=text]:focus~.md_textbox__line {
  transform: scale(1);
}
Enter fullscreen mode Exit fullscreen mode

And that's all we need to do for focusing, much simpler.

MD input box Focusing

Containing valid text

This bit is even easier but is the first part that uses :placeholder-shown so be aware support is not that great for this feature yet.

We simply need to get the label out of the way, no need to recolour it.

.md_textbox [type=text]:not(:placeholder-shown)~.md_textbox__label {
  transform: translateY(-1em) scale(0.8);
  font-weight: bold;
}
Enter fullscreen mode Exit fullscreen mode

MD input box text

Let me explain what [type=text]:not(:placeholder-shown) actually does.
[type=text] selects the input, :not() inverses the logic of what ever's within its parentheses and :placeholder-shown will only apply styles if a placeholder is there, a placeholder is hidden when text is input so :placeholder-shown becomes false.

[type=text]:not(:placeholder-shown) for the textbox if the placeholder is not shown apply these styles.

Containing invalid text

For the invalid styles, we want to display a message that says "Invalid input" and we want to throw some red at it to imply an error.

First, we'll show the bottom line and make it red.

.md_textbox [type=text]:invalid~.md_textbox__line {
  background: #B00020;
  transform: scale(1);
}
Enter fullscreen mode Exit fullscreen mode

Let's make the label red too

.md_textbox [type=text]:invalid~.md_textbox__label {
  color: #B00020;
}
Enter fullscreen mode Exit fullscreen mode

Now let's add that error message if you remember we've already made space for it, so all we need to do is add content and perhaps colour it. I know we'll make it red.

.md_textbox [type=text]:invalid~.md_textbox__line::before {
  content: 'Invalid input';
  color: #B00020;
}
Enter fullscreen mode Exit fullscreen mode

MD input box validation

Disabled

When we disable the textbox we need to show the user that they can't click on the element. Keyboard users are lucky in that disabled textboxes are just skipped over.

To show it's disabled I've decided to make the whole element slightly translucent and colour the actual textbox a light grey. I'll also change the cursor to not-allowed.

.md_textbox [type=text]:disabled {
  cursor: not-allowed;
  border: none;
  background: #BDBDBD;
  opacity: 0.6;
}

.md_textbox [type=text]:disabled~.md_textbox__label {
  cursor: not-allowed;
  opacity: 0.6;
}
Enter fullscreen mode Exit fullscreen mode

MD input box disabled

Required

When an input is empty but has the required attribute it returns that it is invalid because of this we need :required to be later in our CSS so we can override the styles set by :invalid.

We want the change the text content and colour, we'll change the colour to the standard grey we've been using and the text to "*Required".

.md_textbox [type=text]:invalid:required~.md_textbox__line::before {
  content: '*Required';
  color: #424242;
}
Enter fullscreen mode Exit fullscreen mode

The line and label will also be appearing as red so let's change them back to their usual colours too.

.md_textbox [type=text]:required:placeholder-shown:not(:focus)~.md_textbox__line {
  background: #BDBDBD;
}

.md_textbox [type=text]:required:placeholder-shown:not(:focus)~.md_textbox__label {
  color: #424242;
}
Enter fullscreen mode Exit fullscreen mode

When they're focused or filled in we don't mind the :invalid, :focus or :not(:placeholder-shown) styles taking over so I've used :placeholder-shown:not(:focus) to specify those states are not included.

You'll notice we get the added bonus of some on hover text, that's the joy of relying on the platform.

MD input box required

Read-only

The difference between read-only and disabled is that read-only is still in the tab index. We need to show the user they can't edit the text but they can still select it.

Honestly, I'm not sure I achieved this, so would love some feedback about how to improve it.

What I did was remove the bottom border from the textbox and made sure the line would never appear.

.md_textbox [type=text]:read-only {
  border: none;
}

.md_textbox [type=text]:read-only~.md_textbox__line {
  transform: scale(0, 1);
}
Enter fullscreen mode Exit fullscreen mode

This bit I wasn't so sure on when there is no text present and the user focuses on the text box I decided to hold the animation.

.md_textbox [type=text]:read-only:placeholder-shown~.md_textbox__label {
  transform: translateY(0) scale(1);
}
Enter fullscreen mode Exit fullscreen mode

Finishing up

In the future CSS spec we have :has(), though it's not supported by any browsers yet. With that, we could have done something like

.md_textbox:has(> [type=text]:not(:placeholder-shown))::after {
  bottom: 0;
  height: 2px;
  width: 100%;
  transform: scale(0, 1);
  background: #00897B;
}
Enter fullscreen mode Exit fullscreen mode

meaning we could easily remove our .md_textbox__line span but that's for another post.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .