We could go with labels on the left and inputs on the right—with a grid to keep everything aligned. But the labels will have various lengths. Do we right-align them against the inputs, leaving a ragged axis on the left? Or do we left-align them, leaving internal space between the shortest labels and the inputs?
Or we could go with labels and inputs all in one vertical stack. The danger with this arrangement is ambigous visual parsing. Does a label associate with the input above it or below it?
I think neither approach is good. Both suffer from separateness. The label and input are distinct elements. Why not just put the label and the input in the same box? That’s easy enough to do in HTML.
<label class="infield">
<span>Name</span>
<input type="text">
</label>
<label class="infield">
<span>Regrets</span>
<input type="text">
</label>
<label class="infield">
<span>Birth Order</span>
<input type="text" class="number-input">
</label>
But these label-input pairs are very much separated when this HTML is rendered. We can hide this separateness with some CSS that visually blends the label, span, and input together. We strip out all of the input decorations and make the label look like one big box that we can type in. Really the label is just a vertical stack of its two children.
.infield {
margin-top: 10px;
display: flex;
flex-direction: column;
padding: 5px;
border: 1px solid darkgray;
border-radius: 3px;
outline-width: 3px;
outline-offset: -2px;
background-color: #F6F6F6;
box-shadow: inset 0 0 2px #999999;
}
.infield > span {
color: darkgray;
font-family: sans-serif;
font-size: 12px;
margin-bottom: 5px;
}
.infield > input {
border: none;
outline: none;
background: none;
font-size: 16px;
}
We end up with the form shown in Figure 1.
What about our outline? Usually the browser shows us what form element we are focused on. Well, if we have :focus-within, we can apply the outline to the surrounding label.
.infield:focus-within {
outline-color: blue;
outline-style: auto;
}
.infield:focus-within > span {
font-weight: bold;
color: blue;
}
The form shown in Figure 2 is more like what we are used to.
Perhaps we could highlight the fields when something has been entered, or when something has been entered incorrectly. Let’s add some styles for these two states.
.infield:focus-within > span, .infield.non-empty > span {
color: blue;
font-weight: bold;
}
.infield.error {
outline-color: red;
outline-style: auto;
background-color: #FFEEEE;
}
.infield.error > span {
color: red;
font-weight: bold;
}
Some JavaScript is needed to dynamically add or remove these classes when the user types in the boxes.
const infields = document.querySelectorAll('.infield');
for (let infield of infields) {
const input = infield.children[1];
input.addEventListener('input', () => {
infield.classList.toggle('non-empty', input.value.length > 0);
});
if (input.classList.contains('number-input')) {
input.addEventListener('input', () => {
infield.classList.toggle('error', input.value.length > 0 && !input.value.match(new RegExp('^\\d+$')));
});
}
}
There we have it! A form that is clearly labeled and easy to align. All that’s left is to put a really big Submit button below it, as we see in Figure 4.
display propertyThe display property dictates how the content appears in relation to other content, and it silently cancels out other properties that don’t make sense for one its settings. I find understanding its behavior daunting, so I’m compiling a catalog of the differences between three common display settings: block, inline-block, and inline.
Here’s what I’ve come to understand so far:
| property | block | inline-block | inline |
|---|---|---|---|
width
|
defaults to fill parent | defaults to fit content | fits content |
height
|
defaults to fit content | defaults to fit content | fits content |
margin
|
all directions can be set | all directions can be set |
only -left and -right can be set
|
padding
|
all directions can be set | all directions can be set |
-left and -right affect flow, -top and -bottom bleed
|
transform
|
supported | supported | not supported |
| flow | surrounded by linebreaks | surrounded by linebreaks only if it overflows parent’s width | not surrounded by linebreaks |
| overflow | if height is fixed, content bleeds | if height is fixed, content bleeds | content wraps into next line of flow |
| wrapping between elements | mandatory, as blocks do not appear on the same line | elements in sequence do not bind to each other; if sequence overflows parent, breaks appear between elements | elements in sequence and not separated by whitespace bind to each other; if sequence overflows parent, breaks only appear at whitespace |
I’m including a little sample for experimenting with these settings. The blue square below is by default set to display as a block. Inspect the element and change its properties to see how they are influenced by display.
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris a dapibus purus. Donec ut purus at massa mattis porta. Nam varius eget mi sit amet maximus. Right-click on me, click Inspect, and change my properties. Fusce porta neque eu lacus dapibus dignissim. Aliquam sed sem nisl. Quisque bibendum posuere eleifend. Nulla ultricies tristique massa id vestibulum. Sed feugiat eros sit amet leo malesuada condimentum. Sed eleifend tortor augue.
]]>First, here’s a div with some text that we will be putting through the gauntlet:
The problem I’m trying to solve is a bit ill-defined. That won’t stop us from exploring. Let’s first just try rotating when the viewport drops below a certain width. Here we force the viewport at 490 pixels, which triggers the media query, and apply a CSS rotation:
@media (max-width: 500px) {
div {
transform: rotate(-90deg);
}
}
Note that the CSS standard bucks convention: a positive angle rotates clockwise. I want the “head” to be down—choosing to preserve the left-to-right reading—so I spin counterclockwise with a negative angle.
Unfortunately, the default pivot point for rotate is the center of the element. We’ll have an easier time thinking about this if we pivot around one of the corners instead. We can, by setting transform-origin:
@media (max-width: 500px) {
div {
transform: rotate(-90deg);
transform-origin: top left;
}
}
Thank goodness for that margin. Without it, I wouldn’t know where my text had gone. We succeeded in altering our pivot, but now we need to shift the whole thing down. This lands us in the mire of combining transforms, which are generally not commutative. The way I think about it is that the rightmost transform gets applied first, and the leftmost gets applied last. This is backward from how the transformation matrix is multiplied together.
Anyway, let’s first shift the whole element completely left of itself. And then rotate around that corner, which is now effectively the top right:
@media (max-width: 500px) {
div {
transform: rotate(-90deg) translate(-100%, 0);
transform-origin: top left;
}
}
I tried writing this so that shifting down was done on the Y axis, which seems more natural. To do this, I simply put the translate after the rotate (by putting it before—ugh):
@media (max-width: 500px) {
div {
transform: translate(0, 100%) rotate(-90deg);
transform-origin: top left;
}
}
But at this point, 100% refers to the height instead of the width and the shift amount is not correct. It’s the width of the element that we want to shift by.
The standalone page for the working solution is available here.
]]>