Inputs

A field role is a surface plus caret, placeholder and states, defined in the theme; an input names the role.

Shared engine playground

Actual viewport: 0px. Resize the browser to test responsive values. Hover or focus a field with the keyboard to inspect configured states. Try typing; readonly and disabled use native attributes. JSON edits are scoped to this demo and do not save source files.

Visible label and help text remain separate from styling.

.field { @ds-input(contained); }
CSS from the shared engine
.field { box-sizing: border-box; font: inherit; width: 100%; padding: var(--uxdsl__surface__contained-padding); border-radius: var(--uxdsl__surface__contained-radius); background: var(--uxdsl__surface__contained-bg); color: var(--uxdsl__surface__contained-color); border: var(--uxdsl__surface__contained-border); box-shadow: var(--uxdsl__surface__contained-shadow); caret-color: var(--uxdsl__input__contained-base-caret); }
.field::placeholder { color: var(--uxdsl__input__contained-base-placeholder); }
.field:focus { border: var(--uxdsl__input__contained-focus-border); box-shadow: var(--uxdsl__input__contained-focus-shadow); }
.field:invalid, .field[aria-invalid="true"] { border: var(--uxdsl__input__contained-invalid-border); }
.field:disabled, .field[aria-disabled="true"] { color: var(--uxdsl__input__contained-disabled-color); opacity: var(--uxdsl__input__contained-disabled-opacity); cursor: var(--uxdsl__input__contained-disabled-cursor); }
{
  "--uxdsl__input__contained-base-caret": "var(--uxdsl__input__tone-main, var(--uxdsl__palette__primary-main))",
  "--uxdsl__input__contained-base-placeholder": "var(--uxdsl__palette__neutral-dark)",
  "--uxdsl__input__contained-tone-primary-base-caret": "var(--uxdsl__palette__primary-main)",
  "--uxdsl__input__contained-tone-secondary-base-caret": "var(--uxdsl__palette__secondary-main)",
  "--uxdsl__input__contained-tone-surface-base-caret": "var(--uxdsl__palette__surface-main)",
  "--uxdsl__input__contained-tone-tertiary-base-caret": "var(--uxdsl__palette__tertiary-main)",
  "--uxdsl__input__contained-tone-success-base-caret": "var(--uxdsl__palette__success-main)",
  "--uxdsl__input__contained-tone-info-base-caret": "var(--uxdsl__palette__info-main)",
  "--uxdsl__input__contained-tone-warning-base-caret": "var(--uxdsl__palette__warning-main)",
  "--uxdsl__input__contained-tone-error-base-caret": "var(--uxdsl__palette__error-main)",
  "--uxdsl__input__contained-tone-dark-base-caret": "var(--uxdsl__palette__dark-main)",
  "--uxdsl__input__contained-tone-neutral-base-caret": "var(--uxdsl__palette__neutral-main)",
  "--uxdsl__input__contained-tone-light-base-caret": "var(--uxdsl__palette__light-main)",
  "--uxdsl__input__contained-focus-border": "1px solid var(--uxdsl__input__tone-main, var(--uxdsl__palette__primary-main))",
  "--uxdsl__input__contained-focus-shadow": "var(--uxdsl__shadow__2)",
  "--uxdsl__input__contained-tone-primary-focus-border": "1px solid var(--uxdsl__palette__primary-main)",
  "--uxdsl__input__contained-tone-secondary-focus-border": "1px solid var(--uxdsl__palette__secondary-main)",
  "--uxdsl__input__contained-tone-surface-focus-border": "1px solid var(--uxdsl__palette__surface-main)",
  "--uxdsl__input__contained-tone-tertiary-focus-border": "1px solid var(--uxdsl__palette__tertiary-main)",
  "--uxdsl__input__contained-tone-success-focus-border": "1px solid var(--uxdsl__palette__success-main)",
  "--uxdsl__input__contained-tone-info-focus-border": "1px solid var(--uxdsl__palette__info-main)",
  "--uxdsl__input__contained-tone-warning-focus-border": "1px solid var(--uxdsl__palette__warning-main)",
  "--uxdsl__input__contained-tone-error-focus-border": "1px solid var(--uxdsl__palette__error-main)",
  "--uxdsl__input__contained-tone-dark-focus-border": "1px solid var(--uxdsl__palette__dark-main)",
  "--uxdsl__input__contained-tone-neutral-focus-border": "1px solid var(--uxdsl__palette__neutral-main)",
  "--uxdsl__input__contained-tone-light-focus-border": "1px solid var(--uxdsl__palette__light-main)",
  "--uxdsl__input__contained-invalid-border": "1px solid var(--uxdsl__palette__error-main)",
  "--uxdsl__input__contained-disabled-color": "var(--uxdsl__palette__neutral-dark)",
  "--uxdsl__input__contained-disabled-opacity": "0.6",
  "--uxdsl__input__contained-disabled-cursor": "not-allowed"
}

Visible label and help text remain separate from styling.

.field { @ds-input(outlined); }
CSS from the shared engine
.field { box-sizing: border-box; font: inherit; width: 100%; padding: var(--uxdsl__surface__outlined-padding); border-radius: var(--uxdsl__surface__outlined-radius); background: var(--uxdsl__surface__outlined-bg); color: var(--uxdsl__surface__outlined-color); border: var(--uxdsl__surface__outlined-border); box-shadow: var(--uxdsl__surface__outlined-shadow); caret-color: var(--uxdsl__input__outlined-base-caret); }
.field::placeholder { color: var(--uxdsl__input__outlined-base-placeholder); }
.field:focus { border: var(--uxdsl__input__outlined-focus-border); }
.field:invalid, .field[aria-invalid="true"] { border: var(--uxdsl__input__outlined-invalid-border); }
.field:disabled, .field[aria-disabled="true"] { opacity: var(--uxdsl__input__outlined-disabled-opacity); cursor: var(--uxdsl__input__outlined-disabled-cursor); }
{
  "--uxdsl__input__outlined-base-caret": "var(--uxdsl__input__tone-main, var(--uxdsl__palette__primary-main))",
  "--uxdsl__input__outlined-base-placeholder": "var(--uxdsl__palette__neutral-dark)",
  "--uxdsl__input__outlined-tone-primary-base-caret": "var(--uxdsl__palette__primary-main)",
  "--uxdsl__input__outlined-tone-secondary-base-caret": "var(--uxdsl__palette__secondary-main)",
  "--uxdsl__input__outlined-tone-surface-base-caret": "var(--uxdsl__palette__surface-main)",
  "--uxdsl__input__outlined-tone-tertiary-base-caret": "var(--uxdsl__palette__tertiary-main)",
  "--uxdsl__input__outlined-tone-success-base-caret": "var(--uxdsl__palette__success-main)",
  "--uxdsl__input__outlined-tone-info-base-caret": "var(--uxdsl__palette__info-main)",
  "--uxdsl__input__outlined-tone-warning-base-caret": "var(--uxdsl__palette__warning-main)",
  "--uxdsl__input__outlined-tone-error-base-caret": "var(--uxdsl__palette__error-main)",
  "--uxdsl__input__outlined-tone-dark-base-caret": "var(--uxdsl__palette__dark-main)",
  "--uxdsl__input__outlined-tone-neutral-base-caret": "var(--uxdsl__palette__neutral-main)",
  "--uxdsl__input__outlined-tone-light-base-caret": "var(--uxdsl__palette__light-main)",
  "--uxdsl__input__outlined-focus-border": "1px solid var(--uxdsl__input__tone-main, var(--uxdsl__palette__primary-main))",
  "--uxdsl__input__outlined-tone-primary-focus-border": "1px solid var(--uxdsl__palette__primary-main)",
  "--uxdsl__input__outlined-tone-secondary-focus-border": "1px solid var(--uxdsl__palette__secondary-main)",
  "--uxdsl__input__outlined-tone-surface-focus-border": "1px solid var(--uxdsl__palette__surface-main)",
  "--uxdsl__input__outlined-tone-tertiary-focus-border": "1px solid var(--uxdsl__palette__tertiary-main)",
  "--uxdsl__input__outlined-tone-success-focus-border": "1px solid var(--uxdsl__palette__success-main)",
  "--uxdsl__input__outlined-tone-info-focus-border": "1px solid var(--uxdsl__palette__info-main)",
  "--uxdsl__input__outlined-tone-warning-focus-border": "1px solid var(--uxdsl__palette__warning-main)",
  "--uxdsl__input__outlined-tone-error-focus-border": "1px solid var(--uxdsl__palette__error-main)",
  "--uxdsl__input__outlined-tone-dark-focus-border": "1px solid var(--uxdsl__palette__dark-main)",
  "--uxdsl__input__outlined-tone-neutral-focus-border": "1px solid var(--uxdsl__palette__neutral-main)",
  "--uxdsl__input__outlined-tone-light-focus-border": "1px solid var(--uxdsl__palette__light-main)",
  "--uxdsl__input__outlined-invalid-border": "1px solid var(--uxdsl__palette__error-main)",
  "--uxdsl__input__outlined-disabled-opacity": "0.6",
  "--uxdsl__input__outlined-disabled-cursor": "not-allowed"
}

Visible label and help text remain separate from styling.

.field { @ds-input(underline); }
CSS from the shared engine
.field { box-sizing: border-box; font: inherit; width: 100%; padding: var(--uxdsl__surface__flat-padding); border-radius: var(--uxdsl__surface__flat-radius); background: var(--uxdsl__surface__flat-bg); color: var(--uxdsl__surface__flat-color); border: var(--uxdsl__input__underline-base-border); box-shadow: var(--uxdsl__input__underline-base-shadow); border-bottom: var(--uxdsl__input__underline-base-underline); caret-color: var(--uxdsl__input__underline-base-caret); }
.field::placeholder { color: var(--uxdsl__input__underline-base-placeholder); }
.field:focus { border-bottom: var(--uxdsl__input__underline-focus-underline); }
.field:invalid, .field[aria-invalid="true"] { border-bottom: var(--uxdsl__input__underline-invalid-underline); }
.field:disabled, .field[aria-disabled="true"] { opacity: var(--uxdsl__input__underline-disabled-opacity); cursor: var(--uxdsl__input__underline-disabled-cursor); }
{
  "--uxdsl__input__underline-base-border": "none",
  "--uxdsl__input__underline-base-shadow": "none",
  "--uxdsl__input__underline-base-underline": "1px solid var(--uxdsl__palette__neutral-dark)",
  "--uxdsl__input__underline-base-caret": "var(--uxdsl__input__tone-main, var(--uxdsl__palette__primary-main))",
  "--uxdsl__input__underline-base-placeholder": "var(--uxdsl__palette__neutral-dark)",
  "--uxdsl__input__underline-tone-primary-base-caret": "var(--uxdsl__palette__primary-main)",
  "--uxdsl__input__underline-tone-secondary-base-caret": "var(--uxdsl__palette__secondary-main)",
  "--uxdsl__input__underline-tone-surface-base-caret": "var(--uxdsl__palette__surface-main)",
  "--uxdsl__input__underline-tone-tertiary-base-caret": "var(--uxdsl__palette__tertiary-main)",
  "--uxdsl__input__underline-tone-success-base-caret": "var(--uxdsl__palette__success-main)",
  "--uxdsl__input__underline-tone-info-base-caret": "var(--uxdsl__palette__info-main)",
  "--uxdsl__input__underline-tone-warning-base-caret": "var(--uxdsl__palette__warning-main)",
  "--uxdsl__input__underline-tone-error-base-caret": "var(--uxdsl__palette__error-main)",
  "--uxdsl__input__underline-tone-dark-base-caret": "var(--uxdsl__palette__dark-main)",
  "--uxdsl__input__underline-tone-neutral-base-caret": "var(--uxdsl__palette__neutral-main)",
  "--uxdsl__input__underline-tone-light-base-caret": "var(--uxdsl__palette__light-main)",
  "--uxdsl__input__underline-focus-underline": "1px solid var(--uxdsl__input__tone-main, var(--uxdsl__palette__primary-main))",
  "--uxdsl__input__underline-tone-primary-focus-underline": "1px solid var(--uxdsl__palette__primary-main)",
  "--uxdsl__input__underline-tone-secondary-focus-underline": "1px solid var(--uxdsl__palette__secondary-main)",
  "--uxdsl__input__underline-tone-surface-focus-underline": "1px solid var(--uxdsl__palette__surface-main)",
  "--uxdsl__input__underline-tone-tertiary-focus-underline": "1px solid var(--uxdsl__palette__tertiary-main)",
  "--uxdsl__input__underline-tone-success-focus-underline": "1px solid var(--uxdsl__palette__success-main)",
  "--uxdsl__input__underline-tone-info-focus-underline": "1px solid var(--uxdsl__palette__info-main)",
  "--uxdsl__input__underline-tone-warning-focus-underline": "1px solid var(--uxdsl__palette__warning-main)",
  "--uxdsl__input__underline-tone-error-focus-underline": "1px solid var(--uxdsl__palette__error-main)",
  "--uxdsl__input__underline-tone-dark-focus-underline": "1px solid var(--uxdsl__palette__dark-main)",
  "--uxdsl__input__underline-tone-neutral-focus-underline": "1px solid var(--uxdsl__palette__neutral-main)",
  "--uxdsl__input__underline-tone-light-focus-underline": "1px solid var(--uxdsl__palette__light-main)",
  "--uxdsl__input__underline-invalid-underline": "1px solid var(--uxdsl__palette__error-main)",
  "--uxdsl__input__underline-disabled-opacity": "0.6",
  "--uxdsl__input__underline-disabled-cursor": "not-allowed"
}

Theme uxdsl.theme.json, merged over the base theme

{
  "inputs": {
    "search": {
      "surface": "outlined",
      "base": {
        "placeholder": "palette(neutral.dark)"
      },
      "states": {
        "invalid": {
          "border": "2px solid palette(error.main)"
        }
      }
    }
  }
}

UXDSL you write example.uxdsl

.search { @ds-input(search); }

CSS you get compiled by uxdsl; :root shows only these tokens

:root {
  --uxdsl__input__search-base-placeholder: var(--uxdsl__palette__neutral-dark);
  --uxdsl__input__search-invalid-border: 2px solid var(--uxdsl__palette__error-main);
}
.search {
  box-sizing: border-box;
  font: inherit;
  width: 100%;
  padding: var(--uxdsl__surface__outlined-padding);
  border-radius: var(--uxdsl__surface__outlined-radius);
  background: var(--uxdsl__surface__outlined-bg);
  color: var(--uxdsl__surface__outlined-color);
  border: var(--uxdsl__surface__outlined-border);
  box-shadow: var(--uxdsl__surface__outlined-shadow);
  caret-color: var(--uxdsl__input__search-base-caret);
}
.search::placeholder { color: var(--uxdsl__input__search-base-placeholder); }
.search:focus {
  border: var(--uxdsl__input__search-focus-border);
  box-shadow: var(--uxdsl__input__search-focus-shadow);
}
.search:invalid, .search[aria-invalid="true"] { border: var(--uxdsl__input__search-invalid-border); }
.search:disabled, .search[aria-disabled="true"] {
  color: var(--uxdsl__input__search-disabled-color);
  opacity: var(--uxdsl__input__search-disabled-opacity);
  cursor: var(--uxdsl__input__search-disabled-cursor);
}

Rules

  • An input role is a surface plus base overrides and per-state fields. Base roles: contained, outlined, underline. Fields: padding, radius, bg, color, border, shadow, opacity, outline, outline-offset, transform, cursor, font-weight, caret, placeholder, underline. States: hover, focus, focusvisible, readonly, invalid, disabled.
  • placeholder emits its own ::placeholder rule, in states too; underline is the bottom border, and the underline role clears the others.
  • invalid matches :invalid and aria-invalid="true" — styling, not validation; native :invalid can match before the user types. A placeholder is not a label: keep a <label>.
  • For text-like inputs and textareas: the role sets width: 100%, box-sizing: border-box and inherits the font. Do not apply it to checkboxes, radios, ranges or file inputs.
  • A tone recolors the surface, the caret and the focus border when the theme has that Palette family; an explicit value stays explicit, and invalid keeps the error role.
.email { @ds-input(outlined); }
.search { @ds-input(contained info 2); }
.code { @ds-input(underline); font-family: var(--uxdsl__font__code), monospace; }

The guide for coding agents is on For AI agents.