Pure TailwindMIT licensed

Accessible Tailwind Floating Label Input (ARIA + WCAG)

The floating label audited: real for/id binding, aria-describedby hints, a required marker that isn't colour-only, AA contrast in both label states, and a thick focus-visible:outline-3 outline-offset-3 ring. The float barely shrinks (scale-85), bolds instead, and grows an accent tick via peer-focus:before:w-3 — low-vision users never lose it.

Live Demo
Plain CSSTailwind v4
Try it

Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time

Untitled UI React · Affiliate link

Get Untitled UI Pro

The code

<section class="tfl-07 w-full min-h-svh grid place-items-center bg-tfl07-bg px-5 py-12 font-tfl07-sans" aria-label="Accessibility-first floating label demo">
  <form class="flex w-full max-w-[400px] flex-col gap-1.5 rounded-[18px] bg-white p-8 shadow-[0_20px_50px_-28px_oklch(0.3_0.06_265/0.35)] ring-1 ring-tfl07-ink/5" novalidate>
    <div class="mb-4 grid gap-1">
      <h3 class="text-2xl font-bold tracking-[-0.01em] text-tfl07-ink">Contact details</h3>
      <p class="text-sm text-tfl07-mut">Fields marked <span aria-hidden="true" class="font-bold text-tfl07-req">*</span> are required.</p>
    </div>
    <div class="relative pb-[30px]">
      <input class="peer h-[58px] w-full rounded-[10px] border-2 border-tfl07-line bg-white px-4 pt-6 pb-1.5 text-base text-tfl07-ink outline-hidden transition-[border-color] duration-200 hover:border-tfl07-line-hover focus:border-tfl07-acc focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-solid focus-visible:outline-tfl07-acc motion-reduce:transition-none" type="text" id="tfl-07-name" name="name" placeholder=" " required autocomplete="name" aria-describedby="tfl-07-name-hint">
      <label class="pointer-events-none absolute left-4 top-[29px] origin-left -translate-y-1/2 text-base text-tfl07-mut transition-[translate,scale,color] duration-200 before:inline-block before:h-[3px] before:w-0 before:rounded-sm before:bg-tfl07-acc before:align-middle before:transition-[width,margin] before:duration-200 before:content-[''] peer-focus:translate-y-[calc(-50%_-_15px)] peer-focus:scale-85 peer-focus:font-bold peer-focus:text-tfl07-acc peer-focus:before:mr-1.5 peer-focus:before:w-3 peer-[:not(:placeholder-shown)]:translate-y-[calc(-50%_-_15px)] peer-[:not(:placeholder-shown)]:scale-85 peer-[:not(:placeholder-shown)]:font-bold peer-[:not(:placeholder-shown)]:before:mr-1.5 peer-[:not(:placeholder-shown)]:before:w-3 motion-reduce:transition-none motion-reduce:before:transition-none" for="tfl-07-name">Full name <span class="font-bold text-tfl07-req" aria-hidden="true">*</span></label>
      <p class="absolute bottom-1.5 left-0.5 text-[13px]/snug text-tfl07-mut" id="tfl-07-name-hint">Required — as it appears on your ID</p>
    </div>
    <div class="relative pb-[30px]">
      <input class="peer h-[58px] w-full rounded-[10px] border-2 border-tfl07-line bg-white px-4 pt-6 pb-1.5 text-base text-tfl07-ink outline-hidden transition-[border-color] duration-200 hover:border-tfl07-line-hover focus:border-tfl07-acc focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-solid focus-visible:outline-tfl07-acc motion-reduce:transition-none" type="tel" id="tfl-07-tel" name="tel" placeholder=" " autocomplete="tel" inputmode="tel" aria-describedby="tfl-07-tel-hint">
      <label class="pointer-events-none absolute left-4 top-[29px] origin-left -translate-y-1/2 text-base text-tfl07-mut transition-[translate,scale,color] duration-200 before:inline-block before:h-[3px] before:w-0 before:rounded-sm before:bg-tfl07-acc before:align-middle before:transition-[width,margin] before:duration-200 before:content-[''] peer-focus:translate-y-[calc(-50%_-_15px)] peer-focus:scale-85 peer-focus:font-bold peer-focus:text-tfl07-acc peer-focus:before:mr-1.5 peer-focus:before:w-3 peer-[:not(:placeholder-shown)]:translate-y-[calc(-50%_-_15px)] peer-[:not(:placeholder-shown)]:scale-85 peer-[:not(:placeholder-shown)]:font-bold peer-[:not(:placeholder-shown)]:before:mr-1.5 peer-[:not(:placeholder-shown)]:before:w-3 motion-reduce:transition-none motion-reduce:before:transition-none" for="tfl-07-tel">Phone number</label>
      <p class="absolute bottom-1.5 left-0.5 text-[13px]/snug text-tfl07-mut" id="tfl-07-tel-hint">Optional — include country code</p>
    </div>
    <button class="min-h-[52px] cursor-pointer rounded-[10px] bg-tfl07-acc text-base font-bold text-white transition-[filter] duration-150 hover:brightness-115 focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-tfl07-acc" type="submit">Save contact</button>
  </form>
</section>
@import "tailwindcss";

@theme {
  --color-tfl07-bg: #eef0f4;
  --color-tfl07-bg: oklch(0.95 0.007 265);
  --color-tfl07-ink: #171a26;
  --color-tfl07-ink: oklch(0.21 0.025 270);
  --color-tfl07-mut: #555a69;
  --color-tfl07-mut: oklch(0.45 0.025 270);
  --color-tfl07-line: #8f95a6;
  --color-tfl07-line: oklch(0.66 0.025 270);
  --color-tfl07-line-hover: #6d7386;
  --color-tfl07-line-hover: oklch(0.56 0.03 270);
  --color-tfl07-acc: #3b3fce;
  --color-tfl07-acc: oklch(0.46 0.21 272);
  --color-tfl07-req: #b8302a;
  --color-tfl07-req: oklch(0.5 0.19 27);
  --font-tfl07-sans: 'Atkinson Hyperlegible', system-ui, sans-serif;
}

.tfl-07 {
  width: 100%;
  min-height: 100svh;
}

Setup — do this once per project

Skip this if your project already runs Tailwind v4.

1Load Tailwind
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>

Compiles in the browser — perfect for a CodePen or a quick test, not for production.

2Add the stylesheetRequiredthe @theme block is what makes the custom utilities exist
@import "tailwindcss";

@theme {
  --color-tfl07-bg: #eef0f4;
  --color-tfl07-bg: oklch(0.95 0.007 265);
  --color-tfl07-ink: #171a26;
  --color-tfl07-ink: oklch(0.21 0.025 270);
  --color-tfl07-mut: #555a69;
  --color-tfl07-mut: oklch(0.45 0.025 270);
  --color-tfl07-line: #8f95a6;
  --color-tfl07-line: oklch(0.66 0.025 270);
  --color-tfl07-line-hover: #6d7386;
  --color-tfl07-line-hover: oklch(0.56 0.03 270);
  --color-tfl07-acc: #3b3fce;
  --color-tfl07-acc: oklch(0.46 0.21 272);
  --color-tfl07-req: #b8302a;
  --color-tfl07-req: oklch(0.5 0.19 27);
  --font-tfl07-sans: 'Atkinson Hyperlegible', system-ui, sans-serif;
}

.tfl-07 {
  width: 100%;
  min-height: 100svh;
}
3Fonts used by this demooptional — skip if you have your own type stack
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Atkinson+Hyperlegible:wght@400;700&display=swap">

What changed in the port

Which parts became utilities, which stayed CSS, and why.

  • The label::before tick and its two float rules became before: and peer-focus:before: utilities — the stacked variant order mirrors the selector exactly.
  • The :focus-visible outline stays an outline (outline-3 outline-offset-3), not a ring, so it paints in forced-colors mode.
  • UI upgrade from the CSS original: the resting border went from #c2c7d4 (1.7:1) to oklch(0.66) (3.3:1), meeting WCAG 1.4.11 non-text contrast — the original field edge was nearly invisible to low-vision users.
  • UI upgrade: Atkinson Hyperlegible replaces Segoe UI, the legend explains the asterisk up front, and the phone field gained inputmode="tel".
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS demo from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Accessible Tailwind Floating Label Input (ARIA + WCAG)
Source: https://codefronts.com/components/tailwind-css-floating-label-inputs/accessible-tailwind-floating-label-input-aria-wcag/

The floating label audited: real for/id binding, aria-describedby hints, a required marker that isn't colour-only, AA contrast in both label states, and a thick focus-visible:outline-3 outline-offset-3 ring. The float barely shrinks (scale-85), bolds instead, and grows an accent tick via peer-focus:before:w-3 — low-vision users never lose it.
## HTML
```html
<section class="tfl-07 w-full min-h-svh grid place-items-center bg-tfl07-bg px-5 py-12 font-tfl07-sans" aria-label="Accessibility-first floating label demo">
  <form class="flex w-full max-w-[400px] flex-col gap-1.5 rounded-[18px] bg-white p-8 shadow-[0_20px_50px_-28px_oklch(0.3_0.06_265/0.35)] ring-1 ring-tfl07-ink/5" novalidate>
    <div class="mb-4 grid gap-1">
      <h3 class="text-2xl font-bold tracking-[-0.01em] text-tfl07-ink">Contact details</h3>
      <p class="text-sm text-tfl07-mut">Fields marked <span aria-hidden="true" class="font-bold text-tfl07-req">*</span> are required.</p>
    </div>
    <div class="relative pb-[30px]">
      <input class="peer h-[58px] w-full rounded-[10px] border-2 border-tfl07-line bg-white px-4 pt-6 pb-1.5 text-base text-tfl07-ink outline-hidden transition-[border-color] duration-200 hover:border-tfl07-line-hover focus:border-tfl07-acc focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-solid focus-visible:outline-tfl07-acc motion-reduce:transition-none" type="text" id="tfl-07-name" name="name" placeholder=" " required autocomplete="name" aria-describedby="tfl-07-name-hint">
      <label class="pointer-events-none absolute left-4 top-[29px] origin-left -translate-y-1/2 text-base text-tfl07-mut transition-[translate,scale,color] duration-200 before:inline-block before:h-[3px] before:w-0 before:rounded-sm before:bg-tfl07-acc before:align-middle before:transition-[width,margin] before:duration-200 before:content-[''] peer-focus:translate-y-[calc(-50%_-_15px)] peer-focus:scale-85 peer-focus:font-bold peer-focus:text-tfl07-acc peer-focus:before:mr-1.5 peer-focus:before:w-3 peer-[:not(:placeholder-shown)]:translate-y-[calc(-50%_-_15px)] peer-[:not(:placeholder-shown)]:scale-85 peer-[:not(:placeholder-shown)]:font-bold peer-[:not(:placeholder-shown)]:before:mr-1.5 peer-[:not(:placeholder-shown)]:before:w-3 motion-reduce:transition-none motion-reduce:before:transition-none" for="tfl-07-name">Full name <span class="font-bold text-tfl07-req" aria-hidden="true">*</span></label>
      <p class="absolute bottom-1.5 left-0.5 text-[13px]/snug text-tfl07-mut" id="tfl-07-name-hint">Required — as it appears on your ID</p>
    </div>
    <div class="relative pb-[30px]">
      <input class="peer h-[58px] w-full rounded-[10px] border-2 border-tfl07-line bg-white px-4 pt-6 pb-1.5 text-base text-tfl07-ink outline-hidden transition-[border-color] duration-200 hover:border-tfl07-line-hover focus:border-tfl07-acc focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-solid focus-visible:outline-tfl07-acc motion-reduce:transition-none" type="tel" id="tfl-07-tel" name="tel" placeholder=" " autocomplete="tel" inputmode="tel" aria-describedby="tfl-07-tel-hint">
      <label class="pointer-events-none absolute left-4 top-[29px] origin-left -translate-y-1/2 text-base text-tfl07-mut transition-[translate,scale,color] duration-200 before:inline-block before:h-[3px] before:w-0 before:rounded-sm before:bg-tfl07-acc before:align-middle before:transition-[width,margin] before:duration-200 before:content-[''] peer-focus:translate-y-[calc(-50%_-_15px)] peer-focus:scale-85 peer-focus:font-bold peer-focus:text-tfl07-acc peer-focus:before:mr-1.5 peer-focus:before:w-3 peer-[:not(:placeholder-shown)]:translate-y-[calc(-50%_-_15px)] peer-[:not(:placeholder-shown)]:scale-85 peer-[:not(:placeholder-shown)]:font-bold peer-[:not(:placeholder-shown)]:before:mr-1.5 peer-[:not(:placeholder-shown)]:before:w-3 motion-reduce:transition-none motion-reduce:before:transition-none" for="tfl-07-tel">Phone number</label>
      <p class="absolute bottom-1.5 left-0.5 text-[13px]/snug text-tfl07-mut" id="tfl-07-tel-hint">Optional — include country code</p>
    </div>
    <button class="min-h-[52px] cursor-pointer rounded-[10px] bg-tfl07-acc text-base font-bold text-white transition-[filter] duration-150 hover:brightness-115 focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-tfl07-acc" type="submit">Save contact</button>
  </form>
</section>
```
## CSS
```css
@import "tailwindcss";

@theme {
  --color-tfl07-bg: #eef0f4;
  --color-tfl07-bg: oklch(0.95 0.007 265);
  --color-tfl07-ink: #171a26;
  --color-tfl07-ink: oklch(0.21 0.025 270);
  --color-tfl07-mut: #555a69;
  --color-tfl07-mut: oklch(0.45 0.025 270);
  --color-tfl07-line: #8f95a6;
  --color-tfl07-line: oklch(0.66 0.025 270);
  --color-tfl07-line-hover: #6d7386;
  --color-tfl07-line-hover: oklch(0.56 0.03 270);
  --color-tfl07-acc: #3b3fce;
  --color-tfl07-acc: oklch(0.46 0.21 272);
  --color-tfl07-req: #b8302a;
  --color-tfl07-req: oklch(0.5 0.19 27);
  --font-tfl07-sans: 'Atkinson Hyperlegible', system-ui, sans-serif;
}

.tfl-07 {
  width: 100%;
  min-height: 100svh;
}
```

How this works

The semantics carry the meaning; the utilities carry the look. Nothing about accessibility here depends on Tailwind: the <label for>, the required attribute, the aria-describedby hint and the word 'Required' in the hint are all plain HTML.

Stacked variants read left to right. The tick is peer-focus:before:w-3 — when the peer is focused, on this element's ::before, width 3. Tailwind emits .peer:focus ~ label::before.

Outline, not ring, for the keyboard indicator: outlines survive Windows High Contrast, box-shadow rings do not.

Make it yours

  • Ring thickness/offset: outline-3 / outline-offset-3.
  • Floated size: scale-85 — keep 16 × scale ≥ 12.
  • Tick size: before:w-3 before:h-[3px].
  • Accent: --color-tfl07-acc is AA on white in both label states.

Gotchas — read before shipping

  • Placeholder text is never the label — it vanishes and fails WCAG 3.3.2.
  • Don't mark 'required' with colour or asterisk alone; the attribute plus visible text covers both.
  • Tab through with a screen reader: label, then hint, should be announced for each field.

Browser support

ChromeSafariFirefoxEdge
111+16.4+128+111+

Every technique is baseline; tested pattern with NVDA and VoiceOver (label + hint announced).

Techniques used in this demo

Utilitiesfirst-class utility or variant

Search CodeFronts

Loading…