{
  "name": "text-field",
  "title": {
    "es": "Campo de texto",
    "en": "Text field"
  },
  "summary": {
    "es": "Campo y área de texto con etiqueta, ayuda y error.",
    "en": "Input and textarea with label, hint and error."
  },
  "category": "forms",
  "status": "stable",
  "nivel": "primitivos",
  "updated": "2026-09-29",
  "license": "MIT",
  "page": "https://componentes-lovat.vercel.app/c/text-field/",
  "requires": [
    "button"
  ],
  "files": [
    {
      "type": "html",
      "url": "https://componentes-lovat.vercel.app/raw/text-field/text-field.html",
      "content": "<form class=\"c-form\" novalidate data-c-validate data-c-demo>\n  <div class=\"c-field\">\n    <label class=\"c-label\" for=\"tf-name\">Nombre</label>\n    <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>\n    <p class=\"c-field__error\" id=\"tf-name-err\"></p>\n  </div>\n  <div class=\"c-field\">\n    <label class=\"c-label\" for=\"tf-email\">Correo</label>\n    <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>\n    <p class=\"c-field__description\" id=\"tf-email-help\">Solo para avisarte cuando se publique.</p>\n    <p class=\"c-field__error\" id=\"tf-email-err\"></p>\n  </div>\n  <div class=\"c-field\">\n    <label class=\"c-label\" for=\"tf-note\">Mensaje <span class=\"c-label__optional\">(opcional)</span></label>\n    <span class=\"c-textarea\"><textarea class=\"c-textarea__field\" id=\"tf-note\" name=\"note\" placeholder=\"Escribe aquí…\"></textarea></span>\n  </div>\n  <div class=\"c-field\">\n    <label class=\"c-label\" for=\"tf-disabled\">Código de invitación</label>\n    <span class=\"c-input\"><input class=\"c-input__field\" id=\"tf-disabled\" type=\"text\" value=\"MIRO-2026\" disabled></span>\n  </div>\n  <div class=\"c-btn-row\">\n    <button class=\"c-btn c-btn--primary\" type=\"submit\">Enviar</button>\n    <span class=\"c-field__description\" role=\"status\" data-c-form-status></span>\n  </div>\n</form>\n"
    },
    {
      "type": "css",
      "url": "https://componentes-lovat.vercel.app/raw/text-field/text-field.css",
      "content": "/* text-field · Componentes (MIRÓ Labs). Diseño de coss ui (MIT, © coss.com, antes Origin UI).\n   Generado por tools/build_css.py desde text-field.tw.json. No editar a mano. Necesita tokens.css. */\n.c-input{position:relative}\n.c-textarea{position:relative}\n.c-field{display:flex}\n.c-label{display:inline-flex}\n.c-input{display:inline-flex}\n.c-textarea{display:inline-flex}\n.c-textarea__field{field-sizing:content}\n.c-input__field{height:calc(var(--spacing)*8.5)}\n.c-textarea__field{min-height:calc(var(--spacing)*17.5)}\n.c-input{width:100%}\n.c-input__field{width:100%}\n.c-textarea{width:100%}\n.c-textarea__field{width:100%}\n.c-input__field{min-width:calc(var(--spacing)*0)}\n.c-field{flex-direction:column}\n.c-label{align-items:center}\n.c-field{align-items:flex-start;gap:calc(var(--spacing)*2)}\n.c-label{gap:calc(var(--spacing)*2)}\n.c-input__field{border-radius:inherit}\n.c-textarea__field{border-radius:inherit}\n.c-input{border-radius:var(--radius)}\n.c-textarea{border-radius:var(--radius)}\n.c-input{border-style:var(--tw-border-style);border-width:1px}\n.c-textarea{border-style:var(--tw-border-style);border-width:1px}\n.c-input{border-color:var(--input)}\n.c-textarea{border-color:var(--input)}\n.c-input{background-color:var(--background)}\n.c-textarea{background-color:var(--background)}\n.c-input__field{padding-inline:calc(calc(var(--spacing)*3) - 1px)}\n.c-textarea__field{padding-inline:calc(calc(var(--spacing)*3) - 1px);padding-block:calc(calc(var(--spacing)*1.5) - 1px)}\n.c-input{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}\n.c-textarea{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}\n.c-label{font-size:var(--text-base);line-height:calc(var(--spacing)*4.5)}\n.c-field__description{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}\n.c-field__error{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}\n.c-input__field{--tw-leading:calc(var(--spacing)*8.5);line-height:calc(var(--spacing)*8.5)}\n.c-label{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}\n.c-field__error{color:var(--destructive-foreground)}\n.c-label{color:var(--foreground)}\n.c-input__field{color:var(--foreground)}\n.c-textarea__field{color:var(--foreground)}\n.c-field__description{color:var(--muted-foreground)}\n.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)}\n.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)}\n@supports (color:lab(0% 0 0)){\n.c-input{--tw-shadow:0 1px 2px 0 var(--tw-shadow-color,lab(0% 0 0/.05))}\n.c-textarea{--tw-shadow:0 1px 2px 0 var(--tw-shadow-color,lab(0% 0 0/.05))}\n}\n.c-input{--tw-ring-color:var(--ring)}\n.c-textarea{--tw-ring-color:var(--ring)}\n@supports (color:color-mix(in lab, red, red)){\n.c-input{--tw-ring-color:color-mix(in oklab,var(--ring)24%,transparent)}\n.c-textarea{--tw-ring-color:color-mix(in oklab,var(--ring)24%,transparent)}\n}\n.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))}\n.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))}\n.c-input__field{--tw-outline-style:none;outline-style:none}\n.c-textarea__field{--tw-outline-style:none;outline-style:none}\n.c-input:not(:is(.dark *)){background-clip:padding-box}\n.c-textarea:not(:is(.dark *)){background-clip:padding-box}\n.c-input__field::placeholder{color:var(--muted-foreground)}\n.c-textarea__field::placeholder{color:var(--muted-foreground)}\n@supports (color:color-mix(in lab, red, red)){\n.c-input__field::placeholder{color:color-mix(in oklab,var(--muted-foreground)72%,transparent)}\n.c-textarea__field::placeholder{color:color-mix(in oklab,var(--muted-foreground)72%,transparent)}\n}\n.c-input:before{content:var(--tw-content);pointer-events:none}\n.c-textarea:before{content:var(--tw-content);pointer-events:none}\n.c-input:before{content:var(--tw-content);position:absolute}\n.c-textarea:before{content:var(--tw-content);position:absolute}\n.c-input:before{content:var(--tw-content);inset:calc(var(--spacing)*0)}\n.c-textarea:before{content:var(--tw-content);inset:calc(var(--spacing)*0)}\n.c-input:before{content:var(--tw-content);border-radius:calc(var(--radius-lg) - 1px)}\n.c-textarea:before{content:var(--tw-content);border-radius:calc(var(--radius-lg) - 1px)}\n.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)}\n@supports (color:color-mix(in lab, red, red)){\n.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))}\n}\n.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)}\n.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)}\n@supports (color:color-mix(in lab, red, red)){\n.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))}\n}\n.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)}\n.c-input:has(:autofill){background-color:var(--foreground)}\n@supports (color:color-mix(in lab, red, red)){\n.c-input:has(:autofill){background-color:color-mix(in oklab,var(--foreground)4%,transparent)}\n}\n.c-input:has(:focus-visible){border-color:var(--ring)}\n.c-textarea:has(:focus-visible){border-color:var(--ring)}\n.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)}\n.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)}\n.c-input:has(:disabled){opacity:.64}\n.c-textarea:has(:disabled){opacity:.64}\n.c-input:has([aria-invalid=true]){border-color:var(--destructive)}\n.c-textarea:has([aria-invalid=true]){border-color:var(--destructive)}\n@supports (color:color-mix(in lab, red, red)){\n.c-input:has([aria-invalid=true]){border-color:color-mix(in oklab,var(--destructive)36%,transparent)}\n.c-textarea:has([aria-invalid=true]){border-color:color-mix(in oklab,var(--destructive)36%,transparent)}\n}\n.c-input:has(:focus-visible):has([aria-invalid=true]){border-color:var(--destructive)}\n.c-textarea:has(:focus-visible):has([aria-invalid=true]){border-color:var(--destructive)}\n@supports (color:color-mix(in lab, red, red)){\n.c-input:has(:focus-visible):has([aria-invalid=true]){border-color:color-mix(in oklab,var(--destructive)64%,transparent)}\n.c-textarea:has(:focus-visible):has([aria-invalid=true]){border-color:color-mix(in oklab,var(--destructive)64%,transparent)}\n}\n.c-input:has(:focus-visible):has([aria-invalid=true]){--tw-ring-color:var(--destructive)}\n.c-textarea:has(:focus-visible):has([aria-invalid=true]){--tw-ring-color:var(--destructive)}\n@supports (color:color-mix(in lab, red, red)){\n.c-input:has(:focus-visible):has([aria-invalid=true]){--tw-ring-color:color-mix(in oklab,var(--destructive)16%,transparent)}\n.c-textarea:has(:focus-visible):has([aria-invalid=true]){--tw-ring-color:color-mix(in oklab,var(--destructive)16%,transparent)}\n}\n@media not all and (min-width:40rem){\n.c-textarea__field{min-height:calc(var(--spacing)*20.5)}\n}\n@media (min-width:40rem){\n.c-input__field{height:calc(var(--spacing)*7.5)}\n.c-input{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}\n.c-textarea{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}\n.c-label{font-size:var(--text-sm);line-height:calc(var(--spacing)*4)}\n.c-input__field{--tw-leading:calc(var(--spacing)*7.5);line-height:calc(var(--spacing)*7.5)}\n}\n.c-input:is(.dark *){background-color:var(--input)}\n.c-textarea:is(.dark *){background-color:var(--input)}\n@supports (color:color-mix(in lab, red, red)){\n.c-input:is(.dark *){background-color:color-mix(in oklab,var(--input)32%,transparent)}\n.c-textarea:is(.dark *){background-color:color-mix(in oklab,var(--input)32%,transparent)}\n}\n.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)}\n@supports (color:color-mix(in lab, red, red)){\n.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))}\n}\n.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)}\n.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)}\n@supports (color:color-mix(in lab, red, red)){\n.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))}\n}\n.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)}\n.c-input:is(.dark *):has(:autofill){background-color:var(--foreground)}\n@supports (color:color-mix(in lab, red, red)){\n.c-input:is(.dark *):has(:autofill){background-color:color-mix(in oklab,var(--foreground)8%,transparent)}\n}\n.c-input:is(.dark *):has([aria-invalid=true]){--tw-ring-color:var(--destructive)}\n.c-textarea:is(.dark *):has([aria-invalid=true]){--tw-ring-color:var(--destructive)}\n@supports (color:color-mix(in lab, red, red)){\n.c-input:is(.dark *):has([aria-invalid=true]){--tw-ring-color:color-mix(in oklab,var(--destructive)24%,transparent)}\n.c-textarea:is(.dark *):has([aria-invalid=true]){--tw-ring-color:color-mix(in oklab,var(--destructive)24%,transparent)}\n}\n/* propio */\n.c-form{display:grid;gap:1.25rem;width:100%;max-width:22rem}\n.c-field{width:100%}\n.c-field__error:empty{display:none}\n.c-input__field,.c-textarea__field{background:transparent;border:0;font:inherit;color:inherit}\n.c-textarea__field{resize:vertical}\n.c-label__optional{font-weight:400;color:var(--muted-foreground)}\n.c-input:has(:disabled,:focus-visible,[aria-invalid=true]),.c-textarea:has(:disabled,:focus-visible,[aria-invalid=true]){box-shadow:none}\n"
    },
    {
      "type": "js",
      "url": "https://componentes-lovat.vercel.app/raw/text-field/text-field.js",
      "content": "/* Validación al enviar: marca cada campo con aria-invalid, muestra su mensaje y enfoca el primero con error.\n   Después del primer error, lo quita en cuanto el valor es válido (al escribir, no al salir del campo:\n   si el mensaje desaparece en el blur, la página se mueve y el clic en «Enviar» se pierde). */\n(function () {\n  const C = (window.Componentes = window.Componentes || {});\n  const msg = (el) => el.dataset.cMsg || el.validationMessage || 'Revisa este campo.';\n  const errEl = (el) => document.getElementById((el.getAttribute('aria-describedby') || '').split(' ').find((id) => id.endsWith('-err')) || '');\n\n  function setError(el, text) {\n    if (text) el.setAttribute('aria-invalid', 'true'); else el.removeAttribute('aria-invalid');\n    const e = errEl(el); if (e) e.textContent = text || '';\n  }\n\n  function validate(form) {\n    const bad = [];\n    form.querySelectorAll('input, textarea, select').forEach((el) => {\n      if (el.disabled || !el.willValidate) return;\n      const ok = el.checkValidity();\n      setError(el, ok ? '' : msg(el));\n      if (!ok) bad.push(el);\n    });\n    return bad;\n  }\n\n  function init(root) {\n    (root || document).querySelectorAll('form[data-c-validate]:not([data-c-ready])').forEach((form) => {\n      form.dataset.cReady = '';\n      form.addEventListener('submit', (e) => {\n        try {\n          const bad = validate(form);\n          const st = form.querySelector('[data-c-form-status]');\n          if (bad.length) { e.preventDefault(); bad[0].focus(); if (st) st.textContent = ''; return; }\n          if (form.hasAttribute('data-c-demo')) { e.preventDefault(); if (st) st.textContent = 'Enviado (demo).'; }\n        } catch (err) { console.warn('[text-field]', err); }\n      });\n      form.addEventListener('input', (e) => {\n        const el = e.target;\n        if (el.getAttribute?.('aria-invalid') === 'true' && el.checkValidity()) setError(el, '');\n      });\n    });\n  }\n\n  C.textField = { init, validate };\n  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', () => init());\n  else init();\n})();\n"
    },
    {
      "type": "standalone",
      "url": "https://componentes-lovat.vercel.app/r/text-field.html"
    }
  ],
  "tokensFile": "https://componentes-lovat.vercel.app/tokens.css",
  "darkTheme": "add class 'dark' to <html>",
  "contexto": "ambos",
  "densidad": "media",
  "tono": "sobria",
  "states": [
    "rest",
    "hover",
    "focus",
    "disabled",
    "error"
  ],
  "use": {
    "es": [
      "Para texto libre corto o largo.",
      "Siempre con etiqueta visible."
    ],
    "en": [
      "For short or long free text.",
      "Always with a visible label."
    ]
  },
  "avoid": {
    "es": [
      "Con el placeholder como única etiqueta."
    ],
    "en": [
      "With the placeholder as the only label."
    ]
  },
  "a11y": {
    "roles": [
      "textbox"
    ],
    "keyboard": [
      {
        "keys": "Tab",
        "es": "Pasa al siguiente campo",
        "en": "Moves to the next field"
      }
    ],
    "notes": {
      "es": [
        "Ayuda y error se enlazan con aria-describedby.",
        "Al enviar con errores, el foco va al primer campo inválido.",
        "El error se quita al escribir un valor válido, no al salir del campo (así no se mueve la página bajo el clic)."
      ],
      "en": [
        "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)."
      ]
    },
    "test": {
      "es": [
        "Envía vacío: el foco salta a «Nombre» y se lee su error."
      ],
      "en": [
        "Submit empty: focus jumps to “Name” and its error is read."
      ]
    }
  },
  "issues": {
    "es": [],
    "en": []
  },
  "credits": [
    {
      "source": "coss ui (MIT) · input.tsx, textarea.tsx, field.tsx",
      "url": "https://coss.com/ui"
    },
    {
      "source": "GOV.UK Design System, Text input",
      "url": "https://design-system.service.gov.uk/components/text-input/"
    }
  ],
  "changes": [
    {
      "date": "2026-09-29",
      "es": "Diseño de coss ui en HTML, CSS y JS sin dependencias",
      "en": "coss ui design in dependency-free HTML, CSS and JS"
    }
  ],
  "partes": [],
  "pieza": "https://componentes-lovat.vercel.app/r/text-field.html"
}
