<text-field></text-field>
const styles = new CSSStyleSheet();
styles.replaceSync(`
:host::part(number) {
  width: 24px;
}

:host(:state(error))::part(text) {
  border-color: red;
}`);

class TextField extends HTMLElement {
  static formAssociated = true;
  #internals;
  #shadowRoot;
  #text;
  #minlength;

  constructor() {
    super();
    // instantiate Element Internals
    this.#internals = this.attachInternals();
    
    // attach ShadowDOM
    this.#shadowRoot = this.attachShadow({
      mode: 'open',
      delegatesFocus: true
    });
    this.#shadowRoot.adoptedStyleSheets.push(styles);
    this.#shadowRoot.innerHTML = `
      <label>minlength=<input type="number" part="number" min=0></label>
      <input type="text" part="text">`;
  }
  
  connectedCallback() {
    this.#text = this.#shadowRoot.querySelector('input[type="text"]');
    this.#min = this.#shadowRoot.querySelector('input[type="number"]');
 
    this.#text.addEventListener('change', () => {
      this.#internals.setFormValue(this.#text.value);
      this.validate(this.#text.value);
    });

    this.#min.addEventListener('input', () => {
      this.validate(this.#text.value);
    });
    
    this.#min.value = 3;
  }

  validate(newValue) {
    if (newValue.length >= this.#min.valueAsNumber) {
      this.#internals.setValidity({});
      this.#internals.states.delete('error');
      return;
    }

    this.#internals.setValidity({
      tooShort: true
    }, 'value is too short', this.#text);
    this.#internals.reportValidity();
    this.#internals.states.add('error');
  }
}

customElements.define('text-field', TextField);

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.