Use more HTML Elements - Floating Form Field Label
Tags: html, css, webdev
Published: 2020-06-03 14:36:07

I read this [article](https://dev.to/notiz_dev/floating-form-field-with-tailwind-css-47n3), which motivated to build the floating form field using two seemingly forgotten HTML elements: 

- [`fieldset`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/fieldset)
- [`legend`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/legend)

## Expectation

![Floating Label Example](https://dev-to-uploads.s3.amazonaws.com/i/6znlo82yh0q4yitrtsmx.gif)

## Implementation

{% codepen https://codepen.io/icyJoseph/pen/LYGPeLp default-tab=css,result %}

The [MDN page](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/legend) for the `legend` already shows it doing the floating label trick, at least at the final frame, in combination with a `fieldset` element.

```html
<fieldset>
  <input type="text" id="my-input" placeholder="my input"/>
  <legend>
    <label for="my-input">my input</label>
  </legend>
</fieldset>
```

What we want to do is:

- Translate the `legend` to align with the `input::placeholder`.
- When the `input::placeholder` is not shown, translate the `legend` up.

Consider:

- You can translate the label all the way up, above the input, for this example I align it with the top-border of the input field.
- You can also remove the padding added to the left on the legend, or translate it on the X axis when the placeholder is gone.

If you are going to implement this in an application it is better to use proper class names to avoid extremely specific selectors, here I just style the HTML elements. 

First style the `fieldset` element:

- Declare a `CSS` var named `--typography-size`, set to `14px`.
- Declare a `CSS` var named `--base-ratio`, set to `4px`.
- Set `font-family` to `monospace`.
- Set `padding` to `0`
- Set `border` to `none`.

The margin in this case is optional, and set for the demo only.

```css
fieldset {
  --typography-size: 14px;
  --base-ratio: 4px;
  font-family: monospace;
  padding: 0;
  border: none;
  margin: 16px;
}
```

Second, the `input` element.

- Set the `font-size` to the `--typography-size` variable.
- The `input` field does not inherit to the root element `font-family` by default, so do so.
- Declare a `border` of  `1px` with `solid` style line, and color `#c3c3c3`.
- Set `margin` to `0`.
- Set `padding` to `0`.
- To create spacing between the top border and actual input text, defined `padding-top`, in this case, `3` times the `--base-ratio`.
- Similarly create space at the bottom, in this case, `2` times the `--base-ratio`.
- Last but not least, space the text content from the left setting `padding-left` to `3` times the `--base-ratio`.

```css
input {
  font-size: var(--typography-size);
  font-family: inherit;
  border: 1px solid #c3c3c3;
  margin: 0;
  padding: 0;
  padding-top: calc(var(--base-ratio) * 3);
  padding-bottom: calc(var(--base-ratio) * 2);
  padding-left: calc(var(--base-ratio) * 3);
}
```

It is **very important** to realize that for the `legend`, the input text is actually `13px` from the left. That is because the border has `1px` width. And in general:

```
3 * var(--base-ratio) + 1px
```

You could set the `input::placeholder` to `opacity:0` (in the [Codepen](https://codepen.io/icyJoseph/pen/LYGPeLp) above it is done like that) but if you want to gracefully hide it when the input gets focused/unfocused do this:

```css
input::placeholder {
  opacity: 1;
  transition: opacity 0.75s ease;
}

input:focus::placeholder {
  opacity: 0;
  transition: opacity 0.75s ease;
}
```

Third the `legend` element:

- Match the `font-size`, using the `--typography-size` variable.
- To align the `legend` with the `input::placeholder` text we need to  account for `13px` or rather `3` times the `--base-ratio` and `1px` for the border of the input field.
  - We break down this into `padding: 0 var(--base-ratio)` which implicitly sets the `padding-left` to `4px`
  - The other `9px` are set using the `margin-left` property to `2 * var(--base-ratio) + 1px`
  - The combination of padding and margin is done to create `4px` of white space around the x-axis of the legend.
- `transform` the `legend` element in the y-axis by `100% + 3 * var(--base-ratio) + 1px`. Because the `input` element has `padding-top` set to `3 * var(--base-ratio)`, and the `1px` because of the top border in the `input` element.
- Set the `background` to transparent color
- Make sure all changes have `transition`

```css
legend {
  font-size: var(--typography-size);
  padding: 0 var(--base-ratio);
  margin-left: calc(2 * var(--base-ratio) + 1px);
  transform: translateY(calc(100% + 3 * var(--base-ratio) + 1px));
  background: transparent;
  transition: all 0.75s ease;
}
```

When the `input::placeholder` is not being shown, the user is typing, select the `legend` element and change its `color` to **hotpink**, `background` to **white**, and `transform` to 50% in the y-axis, `translateY(50%)`. Adjust these values to your desire. 

> This is the floating label effect.

Optionally, we can set the `padding:0`, this sets the `padding-left:0` implicitly, and the `legend` will float to the top left corner of the `input` field.

```css
input:not(:placeholder-shown) ~ legend  {
  color: hotpink;
  background: white;
  transform: translateY(50%);
  /* optional */
  /* padding: 0; */ 
}
```

And finally for the label.

- Set the cursor to what one would normally get on a `placeholder`, that is `text` cursor.
- Set the `font-size` to the `--typography--size` variable.
- As a bonus,
  - Allow `font-size` transition
  - When the `input::placeholder` is not shown, select the `label` and decrease the `font-size`

```css
label {
  cursor: text;
  font-size: var(--typography-size);
  transition: font-size 0.75s ease;
}

input:not(:placeholder-shown) ~ legend > label  {
 font-size: calc(var(--typography) * 0.9);
}
```

Hopefully I have shown you that HTML elements can already do a lot, the main part of this implementation lies on translating the `legend` element from its natural position to align with the `input::placeholder`, the rest are minor tweaks!

> There's some more refactoring that could be done at the `fieldset` level, for example, calculate `3 * var(--base-ratio)` as yet another `CSS` variable.


Happy hacking!