# Text field / Campo de texto

> Input and textarea with label, hint and error.

Page: https://componentes-lovat.vercel.app/c/text-field/  
License: MIT · Design: coss ui (MIT) · input.tsx, textarea.tsx, field.tsx  
Requires: tokens.css, button  
Context: ambos · Density: media · Tone: sobria

## Files

- Tokens (once): https://componentes-lovat.vercel.app/tokens.css
- Dependency `button`: https://componentes-lovat.vercel.app/r/button.json
- HTML: https://componentes-lovat.vercel.app/raw/text-field/text-field.html
- CSS: https://componentes-lovat.vercel.app/raw/text-field/text-field.css
- JS: https://componentes-lovat.vercel.app/raw/text-field/text-field.js
- Everything in one file: https://componentes-lovat.vercel.app/r/text-field.html

Dark theme: add class `dark` to `<html>`.

## When to use

- For short or long free text.
- Always with a visible label.

## When not to use

- With the placeholder as the only label.

## Accessibility

Roles: textbox

| Keys | Action |
|---|---|
| Tab | Moves to the next field |

- Hint and error are linked with aria-describedby.
- On a failed submit, focus moves to the first invalid field.
- Errors clear while typing a valid value, not on blur (so the page doesn't shift under the click).

## Code

```html
<form class="c-form" novalidate data-c-validate data-c-demo>
  <div class="c-field">
    <label class="c-label" for="tf-name">Nombre</label>
    <span class="c-input"><input class="c-input__field" id="tf-name" name="name" type="text" autocomplete="name" placeholder="Ada Lovelace" required aria-describedby="tf-name-err" data-c-msg="Escribe tu nombre."></span>
    <p class="c-field__error" id="tf-name-err"></p>
  </div>
  <div class="c-field">
    <label class="c-label" for="tf-email">Correo</label>
    <span class="c-input"><input class="c-input__field" id="tf-email" name="email" type="email" autocomplete="email" placeholder="tu@correo.com" required spellcheck="false" aria-describedby="tf-email-help tf-email-err" data-c-msg="Escribe un correo con el formato nombre@dominio.com."></span>
    <p class="c-field__description" id="tf-email-help">Solo para avisarte cuando se publique.</p>
    <p class="c-field__error" id="tf-email-err"></p>
  </div>
  <div class="c-field">
    <label class="c-label" for="tf-note">Mensaje <span class="c-label__optional">(opcional)</span></label>
    <span class="c-textarea"><textarea class="c-textarea__field" id="tf-note" name="note" placeholder="Escribe aquí…"></textarea></span>
  </div>
  <div class="c-field">
    <label class="c-label" for="tf-disabled">Código de invitación</label>
    <span class="c-input"><input class="c-input__field" id="tf-disabled" type="text" value="MIRO-2026" disabled></span>
  </div>
  <div class="c-btn-row">
    <button class="c-btn c-btn--primary" type="submit">Enviar</button>
    <span class="c-field__description" role="status" data-c-form-status></span>
  </div>
</form>
```

```css
/* text-field · Componentes (MIRÓ Labs). Diseño de coss ui (MIT, © coss.com, antes Origin UI).
   Generado por tools/build_css.py desde text-field.tw.json. No editar a mano. Necesita tokens.css. */
.c-input{position:relative}
.c-textarea{position:relative}
.c-field{display:flex}
.c-label{display:inline-flex}
.c-input{display:inline-flex}
.c-textarea{display:inline-flex}
.c-textarea__field{field-sizing:content}
.c-input__field{height:calc(var(--spacing)*8.5)}
.c-textarea__field{min-height:calc(var(--spacing)*17.5)}
.c-input{width:100%}
.c-input__field{width:100%}
.c-textarea{width:100%}
.c-textarea__field{width:100%}
.c-input__field{min-width:calc(var(--spacing)*0)}
.c-field{flex-direction:column}
.c-label{align-items:center}
.c-field{align-items:flex-start;gap:calc(var(--spacing)*2)}
.c-label{gap:calc(var(--spacing)*2)}
.c-input__field{border-radius:inherit}
.c-textarea__field{border-radius:inherit}
.c-input{border-radius:var(--radius)}
.c-textarea{border-radius:var(--radius)}
.c-input{border-style:var(--tw-border-style);border-width:1px}
.c-textarea{border-style:var(--tw-border-style);border-width:1px}
.c-input{border-color:var(--input)}
.c-textarea{border-color:var(--input)}
.c-input{background-color:var(--background)}
.c-textarea{background-color:var(--background)}
.c-input__field{padding-inline:calc(calc(var(--spacing)*3) - 1px)}
.c-textarea__field{padding-inline:calc(calc(var(--spacing)*3) - 1px);padding-block:calc(calc(var(--spacing)*1.5) - 1px)}
.c-input{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}
.c-textarea{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}
.c-label{font-size:var(--text-base);line-height:calc(var(--spacing)*4.5)}
.c-field__description{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}
.c-field__error{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}
.c-input__field{--tw-leading:calc(var(--spacing)*8.5);line-height:calc(var(--spacing)*8.5)}
.c-label{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}
.c-field__error{color:var(--destructive-foreground)}
.c-label{color:var(--foreground)}
.c-input__field{color:var(--foreground)}
.c-textarea__field{color:var(--foreground)}
.c-field__description{color:var(--muted-foreground)}
.c-input{--tw-shadow-alpha:5%;--tw-shadow:0 1px 2px 0 var(--tw-shadow-color,#0000000d);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}
.c-textarea{--tw-shadow-alpha:5%;--tw-shadow:0 1px 2px 0 var(--tw-shadow-color,#0000000d);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}
@supports (color:lab(0% 0 0)){
.c-input{--tw-shadow:0 1px 2px 0 var(--tw-shadow-color,lab(0% 0 0/.05))}
.c-textarea{--tw-shadow:0 1px 2px 0 var(--tw-shadow-color,lab(0% 0 0/.05))}
}
.c-input{--tw-ring-color:var(--ring)}
.c-textarea{--tw-ring-color:var(--ring)}
@supports (color:color-mix(in lab, red, red)){
.c-input{--tw-ring-color:color-mix(in oklab,var(--ring)24%,transparent)}
.c-textarea{--tw-ring-color:color-mix(in oklab,var(--ring)24%,transparent)}
}
.c-input{transition-property:box-shadow;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}
.c-textarea{transition-property:box-shadow;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}
.c-input__field{--tw-outline-style:none;outline-style:none}
.c-textarea__field{--tw-outline-style:none;outline-style:none}
.c-input:not(:is(.dark *)){background-clip:padding-box}
.c-textarea:not(:is(.dark *)){background-clip:padding-box}
.c-input__field::placeholder{color:var(--muted-foreground)}
.c-textarea__field::placeholder{color:var(--muted-foreground)}
@supports (color:color-mix(in lab, red, red)){
.c-input__field::placeholder{color:color-mix(in oklab,var(--muted-foreground)72%,transparent)}
.c-textarea__field::placeholder{color:color-mix(in oklab,var(--muted-foreground)72%,transparent)}
}
.c-input:before{content:var(--tw-content);pointer-events:none}
.c-textarea:before{content:var(--tw-content);pointer-events:none}
.c-input:before{content:var(--tw-content);position:absolute}
.c-textarea:before{content:var(--tw-content);position:absolute}
.c-input:before{content:var(--tw-content);inset:calc(var(--spacing)*0)}
.c-textarea:before{content:var(--tw-content);inset:calc(var(--spacing)*0)}
.c-input:before{content:var(--tw-content);border-radius:calc(var(--radius-lg) - 1px)}
.c-textarea:before{content:var(--tw-content);border-radius:calc(var(--radius-lg) - 1px)}
.c-input:not(:has(:disabled)):not(:has(:focus-visible)):not(:has([aria-invalid=true])):before{content:var(--tw-content);--tw-shadow:0 1px var(--tw-shadow-color,#0000000a)}
@supports (color:color-mix(in lab, red, red)){
.c-input:not(:has(:disabled)):not(:has(:focus-visible)):not(:has([aria-invalid=true])):before{--tw-shadow:0 1px var(--tw-shadow-color,color-mix(in oklab,var(--color-black)4%,transparent))}
}
.c-input:not(:has(:disabled)):not(:has(:focus-visible)):not(:has([aria-invalid=true])):before{box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}
.c-textarea:not(:has(:disabled)):has(:not(:focus-visible)):not(:has([aria-invalid=true])):before{content:var(--tw-content);--tw-shadow:0 1px var(--tw-shadow-color,#0000000a)}
@supports (color:color-mix(in lab, red, red)){
.c-textarea:not(:has(:disabled)):has(:not(:focus-visible)):not(:has([aria-invalid=true])):before{--tw-shadow:0 1px var(--tw-shadow-color,color-mix(in oklab,var(--color-black)4%,transparent))}
}
.c-textarea:not(:has(:disabled)):has(:not(:focus-visible)):not(:has([aria-invalid=true])):before{box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}
.c-input:has(:autofill){background-color:var(--foreground)}
@supports (color:color-mix(in lab, red, red)){
.c-input:has(:autofill){background-color:color-mix(in oklab,var(--foreground)4%,transparent)}
}
.c-input:has(:focus-visible){border-color:var(--ring)}
.c-textarea:has(:focus-visible){border-color:var(--ring)}
.c-input:has(:focus-visible){--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(3px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}
.c-textarea:has(:focus-visible){--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(3px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}
.c-input:has(:disabled){opacity:.64}
.c-textarea:has(:disabled){opacity:.64}
.c-input:has([aria-invalid=true]){border-color:var(--destructive)}
.c-textarea:has([aria-invalid=true]){border-color:var(--destructive)}
@supports (color:color-mix(in lab, red, red)){
.c-input:has([aria-invalid=true]){border-color:color-mix(in oklab,var(--destructive)36%,transparent)}
.c-textarea:has([aria-invalid=true]){border-color:color-mix(in oklab,var(--destructive)36%,transparent)}
}
.c-input:has(:focus-visible):has([aria-invalid=true]){border-color:var(--destructive)}
.c-textarea:has(:focus-visible):has([aria-invalid=true]){border-color:var(--destructive)}
@supports (color:color-mix(in lab, red, red)){
.c-input:has(:focus-visible):has([aria-invalid=true]){border-color:color-mix(in oklab,var(--destructive)64%,transparent)}
.c-textarea:has(:focus-visible):has([aria-invalid=true]){border-color:color-mix(in oklab,var(--destructive)64%,transparent)}
}
.c-input:has(:focus-visible):has([aria-invalid=true]){--tw-ring-color:var(--destructive)}
.c-textarea:has(:focus-visible):has([aria-invalid=true]){--tw-ring-color:var(--destructive)}
@supports (color:color-mix(in lab, red, red)){
.c-input:has(:focus-visible):has([aria-invalid=true]){--tw-ring-color:color-mix(in oklab,var(--destructive)16%,transparent)}
.c-textarea:has(:focus-visible):has([aria-invalid=true]){--tw-ring-color:color-mix(in oklab,var(--destructive)16%,transparent)}
}
@media not all and (min-width:40rem){
.c-textarea__field{min-height:calc(var(--spacing)*20.5)}
}
@media (min-width:40rem){
.c-input__field{height:calc(var(--spacing)*7.5)}
.c-input{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}
.c-textarea{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}
.c-label{font-size:var(--text-sm);line-height:calc(var(--spacing)*4)}
.c-input__field{--tw-leading:calc(var(--spacing)*7.5);line-height:calc(var(--spacing)*7.5)}
}
.c-input:is(.dark *){background-color:var(--input)}
.c-textarea:is(.dark *){background-color:var(--input)}
@supports (color:color-mix(in lab, red, red)){
.c-input:is(.dark *){background-color:color-mix(in oklab,var(--input)32%,transparent)}
.c-textarea:is(.dark *){background-color:color-mix(in oklab,var(--input)32%,transparent)}
}
.c-input:is(.dark *):not(:has(:disabled)):not(:has(:focus-visible)):not(:has([aria-invalid=true])):before{content:var(--tw-content);--tw-shadow:0 -1px var(--tw-shadow-color,#ffffff0f)}
@supports (color:color-mix(in lab, red, red)){
.c-input:is(.dark *):not(:has(:disabled)):not(:has(:focus-visible)):not(:has([aria-invalid=true])):before{--tw-shadow:0 -1px var(--tw-shadow-color,color-mix(in oklab,var(--color-white)6%,transparent))}
}
.c-input:is(.dark *):not(:has(:disabled)):not(:has(:focus-visible)):not(:has([aria-invalid=true])):before{box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}
.c-textarea:is(.dark *):not(:has(:disabled)):has(:not(:focus-visible)):not(:has([aria-invalid=true])):before{content:var(--tw-content);--tw-shadow:0 -1px var(--tw-shadow-color,#ffffff0f)}
@supports (color:color-mix(in lab, red, red)){
.c-textarea:is(.dark *):not(:has(:disabled)):has(:not(:focus-visible)):not(:has([aria-invalid=true])):before{--tw-shadow:0 -1px var(--tw-shadow-color,color-mix(in oklab,var(--color-white)6%,transparent))}
}
.c-textarea:is(.dark *):not(:has(:disabled)):has(:not(:focus-visible)):not(:has([aria-invalid=true])):before{box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}
.c-input:is(.dark *):has(:autofill){background-color:var(--foreground)}
@supports (color:color-mix(in lab, red, red)){
.c-input:is(.dark *):has(:autofill){background-color:color-mix(in oklab,var(--foreground)8%,transparent)}
}
.c-input:is(.dark *):has([aria-invalid=true]){--tw-ring-color:var(--destructive)}
.c-textarea:is(.dark *):has([aria-invalid=true]){--tw-ring-color:var(--destructive)}
@supports (color:color-mix(in lab, red, red)){
.c-input:is(.dark *):has([aria-invalid=true]){--tw-ring-color:color-mix(in oklab,var(--destructive)24%,transparent)}
.c-textarea:is(.dark *):has([aria-invalid=true]){--tw-ring-color:color-mix(in oklab,var(--destructive)24%,transparent)}
}
/* propio */
.c-form{display:grid;gap:1.25rem;width:100%;max-width:22rem}
.c-field{width:100%}
.c-field__error:empty{display:none}
.c-input__field,.c-textarea__field{background:transparent;border:0;font:inherit;color:inherit}
.c-textarea__field{resize:vertical}
.c-label__optional{font-weight:400;color:var(--muted-foreground)}
.c-input:has(:disabled,:focus-visible,[aria-invalid=true]),.c-textarea:has(:disabled,:focus-visible,[aria-invalid=true]){box-shadow:none}
```

```js
/* Validación al enviar: marca cada campo con aria-invalid, muestra su mensaje y enfoca el primero con error.
   Después del primer error, lo quita en cuanto el valor es válido (al escribir, no al salir del campo:
   si el mensaje desaparece en el blur, la página se mueve y el clic en «Enviar» se pierde). */
(function () {
  const C = (window.Componentes = window.Componentes || {});
  const msg = (el) => el.dataset.cMsg || el.validationMessage || 'Revisa este campo.';
  const errEl = (el) => document.getElementById((el.getAttribute('aria-describedby') || '').split(' ').find((id) => id.endsWith('-err')) || '');

  function setError(el, text) {
    if (text) el.setAttribute('aria-invalid', 'true'); else el.removeAttribute('aria-invalid');
    const e = errEl(el); if (e) e.textContent = text || '';
  }

  function validate(form) {
    const bad = [];
    form.querySelectorAll('input, textarea, select').forEach((el) => {
      if (el.disabled || !el.willValidate) return;
      const ok = el.checkValidity();
      setError(el, ok ? '' : msg(el));
      if (!ok) bad.push(el);
    });
    return bad;
  }

  function init(root) {
    (root || document).querySelectorAll('form[data-c-validate]:not([data-c-ready])').forEach((form) => {
      form.dataset.cReady = '';
      form.addEventListener('submit', (e) => {
        try {
          const bad = validate(form);
          const st = form.querySelector('[data-c-form-status]');
          if (bad.length) { e.preventDefault(); bad[0].focus(); if (st) st.textContent = ''; return; }
          if (form.hasAttribute('data-c-demo')) { e.preventDefault(); if (st) st.textContent = 'Enviado (demo).'; }
        } catch (err) { console.warn('[text-field]', err); }
      });
      form.addEventListener('input', (e) => {
        const el = e.target;
        if (el.getAttribute?.('aria-invalid') === 'true' && el.checkValidity()) setError(el, '');
      });
    });
  }

  C.textField = { init, validate };
  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', () => init());
  else init();
})();
```

## Credits

- coss ui (MIT) · input.tsx, textarea.tsx, field.tsx: https://coss.com/ui
- GOV.UK Design System, Text input: https://design-system.service.gov.uk/components/text-input/
