Disabled controls, ARIA states, and forms

State variants can reflect native HTML states, for example disabled:opacity-50, disabled:cursor-not-allowed, invalid:border-red-500, and checked:bg-blue-600. Prefer native controls and native states where possible. An aria-* attribute communicates meaning to assistive technology; Tailwind can style matching attributes, but the attribute itself must be set correctly by the application.

A disabled button should actually have the disabled attribute when the action cannot be used. Styling a button to look disabled without disabling it creates a misleading interface.

Visual validation is not enough

A red border alone may not explain the problem. Add clear error text, connect it with aria-describedby, and validate on the server as well as the client.

Form field with validation styling
<label class="block">
  <span class="text-sm font-medium text-slate-800">Work email</span>
  <input type="email" required aria-describedby="email-help"
    class="mt-2 w-full rounded-lg border border-slate-300 px-3 py-2.5 outline-none focus-visible:ring-2 focus-visible:ring-blue-600 invalid:border-red-500"
    placeholder="you@example.com">
  <span id="email-help" class="mt-1 block text-sm text-slate-600">Use your company email address.</span>
</label>

<button disabled class="mt-4 rounded-lg bg-blue-600 px-4 py-2.5 font-semibold text-white disabled:cursor-not-allowed disabled:opacity-50">
  Submit request
</button>
đź’ˇ The input has a native email type and required constraint. The invalid border reflects browser validation state, and the focus ring makes keyboard focus visible. In a production form, explain validation errors in text and associate them with the relevant input.