diff options
Diffstat (limited to '_posts')
-rw-r--r-- | _posts/2019-09-19-almost-pure-css-material-text-fields.md | 207 |
1 files changed, 207 insertions, 0 deletions
diff --git a/_posts/2019-09-19-almost-pure-css-material-text-fields.md b/_posts/2019-09-19-almost-pure-css-material-text-fields.md new file mode 100644 index 0000000..1d78cc4 --- /dev/null +++ b/_posts/2019-09-19-almost-pure-css-material-text-fields.md @@ -0,0 +1,207 @@ +--- +title: (Almost) Pure CSS Material-like Text Fields +--- + +Despite what you may believe from simply looking at this site, I've actually +done quite a bit of front-end development. A couple of years ago, I worked on a +project with a friend of mine. For part of the project, he'd designed the +behavior of a form control inspired by Material Design which I then built from +scratch. Recently, he asked me to remind him how I'd implemented it, and I +thought I'd take the opportunity to turn it into a blog post. + +<!--more--> + +Here's what it looks like: + +<style type="text/css"> +/* Input container */ +.form-group { + position: relative; + height: 52px; + margin: 8px 0; +} + +/* Bottom border widget, at rest */ +.form-group::after { + content: ''; + height: 2px; + position: absolute; + bottom: 0; + left: 50%; + background-color: #900; + width: 0; + transition-property: all; + transition-duration: 0.15s; + transition-timing-function: ease-out; +} + +/* Bottom widget when focused */ +.form-group.focused::after { + width: 100%; + left: 0; +} + +/* The input itself */ +.form-control { + outline: none; + border: none; + display: block; + background-color: transparent; + position: absolute; + top: 20px; + height: 24px; + font-size: 16px; + width: 100%; +} + +/* Label/Placeholder at rest */ +.control-label { + display: block; + font-weight: 300; + position: absolute; + transition-duration: 0.15s; + transition-property: all; + transition-timing-function: ease-out; + top: 26px; + color: #666; +} + +/* Placeholder disappears when the field is populated and blurred */ +.form-group.populated:not(.focused) .control-label { + top: 0; + font-size: 12px; + font-weight: 600; + color: transparent; +} + +/* Placeholder moves up above the input when the user is editing */ +.form-group.focused .control-label { + top: 0; + font-size: 12px; + font-weight: 600; + color: #900; +} + +.demo { + background-color: white; + color: black; + position: relative; + border: 2px solid black; + padding: 2em; + font-family: sans-serif; +} +</style> + +<div class="demo"> + <div class="form-group" id="demo-group"> + <label class="control-label" for="demo-control">First Name</label> + <input type="text" class="form-control" id="demo-control"> + </div> +</div> + +<script type="text/javascript"> +(function() { + function setPopulated() { + const group = document.getElementById('demo-group'); + if (this.value) { + group.classList.add('populated'); + } else { + group.classList.remove('populated'); + } + } + + function setFocused(focused) { + return () => { + const group = document.getElementById('demo-group'); + if (focused) { + group.classList.add('focused'); + } else { + group.classList.remove('focused'); + } + } + } + + const input = document.getElementById('demo-control'); + input.addEventListener('input', setPopulated); + input.addEventListener('paste', setPopulated); + input.addEventListener('focus', setFocused(true)); + input.addEventListener('blur', setFocused(false)); +})() +</script> + +It's not _quite_ pure CSS, but it's pretty close. Let's think about how this is +put together. + +At a high level, the appearance of the text field at any given moment is the +result of two CSS classes, `focused` and `populated`, being added and removed +via JavaScript. On this page, I've simply written a few lines of code to add and +remove them at the proper times, but in practice this is probably best done +through your frontend JavaScript framework (Angular/React/Vue/...), if you're +using one. + +First, let's talk about the moving placeholder. While CSS does have a +`::placeholder` pseudo-element that we can use for styling how the `placeholder` +attribute of the `<input>` is displayed, unfortunately we can't use it here +because we want the placeholder to remain visible while the user edits the +field, and the browser-supplied placeholder vanishes when the field isn't empty. + +Another semantically-useful way to display this is the `<label>` element, so +that's what I've used. The label is absolutely positioned to appear over the +`<input>` where you'd expect the placeholder. So our basic markup looks like +this: + + <div class="form-group"> + <label class="control-label"> + First Name + </label> + <input type="text" class="form-control"> + </div> + +When the `populated` class is applied to the `form-group` div, an extra CSS rule +gets applied to the `control-label`, changing its position, size, and color. CSS +transitions are used to gently animate the movement. + +The next interesting element is the heavy bottom border. It would be nice if we +could simply use `border-bottom` on the `<input>`, but we want to animate it +collapsing and expanding, and that wouldn't be possible using `border-bottom` +without also collapsing and expanding the content of the text input, which we +definitely don't want. + +The solution I came up with was to use the `::after` pseudo-element to just +display a block of color. At rest, it has `width: 0`, but when the `focused` +class is applied to the containing `form-group`, then it gets `width: 100%` and +is again animated using CSS transitions. + +This is annoyingly close to pure CSS. There are some hacks that can get even +closer to being pure CSS, like using the CSS sibling combinator `~` to write +rules like + + .form-control:focus ~ .control-label { + /* the control is focused, move the label to the top */ + } + +but the ultimate stumbling block is that there's no way to use the current value +of the text input in a CSS rule, so we can't make the label disappear when the +input is blurred and non-empty. You can of course use an attribute selector in +your CSS like `input:not([value=''])`, but this only considers the actual +original attribute value, not whatever it might get changed to by the user later +on. You could of course write some JavaScript to make that happen, but if you've +resorted to JavaScript then you may as well just use the easier and cleaner +approach that toggles the classes. + +There is _one_ way I thought of that could work to do a pure CSS implementation. +There's a `:valid` pseudo-class that considers the HTML form validation +state. If we make the `<input>` only valid when it is non-empty, either with the +`pattern` or `required` attributes, then we could write a rule like + + .form-control:not(:focus):valid ~ .control-label { + /* the control is blurred and has a value, hide the label */ + } + +However, `:valid` isn't supported in all browsers, and this presumes you aren't +using the HTML form validation for anything else, so it's a little too hacky to +rely on. In our case, we were already using React, so adding and removing the +classes with JavaScript ended up being quite easy. + +Check out the source code for this page to get the code, I promise it's easy to +understand! |