summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBen Burwell <ben@benburwell.com>2019-09-19 12:35:12 -0400
committerBen Burwell <ben@benburwell.com>2019-09-19 12:35:12 -0400
commitb39810ef013bba082a3b38e4c22ea577cebafd13 (patch)
tree910613f518103ba4922739f4c6102a389c6be3dd
parent4d56462262e29e3748d13ba784a41d115cfe831b (diff)
Add material text fields post
-rw-r--r--_posts/2019-09-19-almost-pure-css-material-text-fields.md207
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!