Docs
Inputs

Form Field

A liquid glass text field with a floating label, a clear bubble, a character counter and validation that waits until you leave.

$npx swiftpieces add FormField
Try it in the Playground Use it on a real screen, take it apart, remix it, and copy the SwiftUI. No account needed.
Free · MIT + Commons ClauseiOS 17.0+Liquid Glasstextfieldformfloating-labelvalidationcharacter-limitcounter
Type
FormField
Files
FormField.swift
Depends on
Nothing (Apple frameworks only)
Version
1.2.0

Text field with a floating label, help and error text, a character limit and a counter. The field is a liquid glass shape. Whatever appears because of it buds out of it and melts back in: the clear button out of its end while you type, the counter out of its bottom edge while it is focused or filled, and the warning bubble that carries an error message. The bubbles rest joined to the field, so a liquid neck holds them. Chain fields by applying .focused($focus, equals: .email) to each FormField (focus reaches the text field inside) and moving focus in onSubmit. Apply SwiftUI/View/formFieldRevealsErrors(_:) to a form to show every field's validation at once, for example after a submit button is tapped.

Notes

  • The field is a liquid glass shape (Liquid Glass on iOS 26, a frosted fallback before it, a solid fill under Reduce Transparency). Everything that appears because of it buds out of it and melts back in, and rests joined to it, 6pt away, so a liquid neck holds the two: the clear button out of its end, the counter out of its bottom edge, and the warning bubble that carries an error. Each bubble is born inside the field, springs out pulling the neck thin, and goes home on a spring with no bounce, its glyph or label gone the moment it heads back. Style.field tints the glass; .clear, the default, leaves it neutral.
  • The label is one Text in .body that moves between two alignments and scales down into a reserved caption slot. The scale is the caption-to-body line-height ratio at the current Dynamic Type size, so the label lands in the same place at every size, including the accessibility sizes, with no font swap and no jump.
  • prompt shows only while the field is focused and empty, under the floated label. The system placeholder is suppressed so the label is never drawn twice.
  • The clear button is a round glass bubble as tall as the field. It buds out of the field's end while the field is focused and holds text: the field's trailing edge pulls back on the bud's own spring and the bubble parts from it through a neck, so the whole control keeps its width. It sinks slightly under the finger, clears at once and keeps the keyboard up, then melts back into the field.
  • limit counts extended grapheme clusters (String.count), so a flag, a family emoji or a letter with a combining accent is one character. Typing or pasting past the limit trims with prefix(limit), which never cuts a character in half. The counter is count/limit on a glass capsule that buds down out of the field while it is focused or holds text, with localized digits that roll without overshoot. At the limit the capsule floods Style.error (butter) with ink digits, and a refused keystroke fires a light impact as the counter swells and rebounds, a value pushed against its bound. The counter's place is kept while it is home, so reaching the limit or the counter coming and going never shifts the help text; while the clear bubble holds the column beside the field, the counter moves in with the field's end so it stays under the field. The trim runs a frame after an over-limit edit, and the counter never shows that frame's count. With an IME that composes text (Japanese, Chinese), trimming can end a composition early when it runs past the limit.
  • Validation timing: .onBlur (default) waits until the field has been edited and left, .onSubmit waits for Return, .live starts with the first edit. After that the field validates on every keystroke, so the error clears the moment the text is fixed and a sage check blurs in quietly inside the field. Once validation has started the check keeps its slot while the field has text, so typing never reflows as the text turns valid or invalid. An empty field gives the slot back, so its resting label keeps the full width. An error passed from outside shows immediately and takes priority.
  • formFieldRevealsErrors(true) on a container arms every field inside, for a Save button that should flag untouched required fields. Each field holding an error as it turns on shakes once. It is a Bool, so a second tap while it is already on does not shake again.
  • When an error first appears the ring turns Style.error, the help text blurs away, and a butter bubble with an exclamation mark buds down out of the field's leading corner with the message arriving beside it a beat behind the ring; an .error haptic plays once. When the message changes while an error is showing, the new text blurs in place. When the error is fixed the message goes, the bubble melts back into the field, and the help text returns once it is home.
  • A refused submit shakes the field once, with the .error haptic on the same beat: Return pressed while the field has an error, or formFieldRevealsErrors turning on. The bubbles under it hold still, so their necks stretch with the shake. An error found while typing or when you leave the field never shakes; the ring and the warning bubble carry it. One haptic per event: a new error plays it as it appears, and a refusal of an error already on screen plays its own. A Return whose onSubmit also turns formFieldRevealsErrors on is one submit, with one shake and one haptic.
  • Focus: apply .focused($focus, equals: .email) to the FormField itself and move focus in onSubmit, with submitLabel: .next. Tapping anywhere on the field, including the icon and padding, focuses it. The ring, icon and label answer focus together on one short spring that lands with only a hint of give.
  • axis: .vertical grows up to eight lines. Return inserts a newline there, so onSubmit does not fire; put a multiline field last in a chain. The clear bubble sits level with the field's top.
  • .emailAddress and .URL keyboards (and email, URL and username content types) turn off autocapitalization and autocorrection.
  • Type is one weight, semibold, for the label, the text, the help, the counter and every glyph; size alone sets the hierarchy.
  • VoiceOver reads the label, the text with the counter ("12 of 160 characters") as the value, and the error or help as the hint. A new or changed error is announced. The clear bubble is a button labelled "Clear" with the field's label. The floating label, icon, check and footer are hidden to avoid double reading.
  • Style sets the field tint, label, secondary, focus ring, error, success and ink colors plus minimum height (44pt floor, also the clear bubble's size) and corner radius. .disabled(true) dims the field and calls the clear bubble home.
  • Typing never waits on motion: nothing animates the typed text, and the check turning on or off never moves the input or caret.
  • Reduce Motion: bubbles fade in and out in place instead of budding, the label still moves (it is the state change) but on a short spring with no bounce, the check and messages fade instead of blurring, and the counter fades between counts instead of rolling. A refused keystroke dims the counter for a beat instead of swelling it. A refused submit plays its haptic and dims the error ring for a beat instead of shaking, so a Return on an error already showing still shows on screen. The clear button dims instead of sinking. Reduce Transparency draws the field and bubbles as solid shapes.

Usage

FormFieldExample()

Parameters

ParameterDescription
labelThe label. Rests inside the empty field as its placeholder, floats up into a caption on focus or once filled, and is the accessibility label.
textBound text.
promptPlaceholder shown under the floated label only while the field is focused and empty, e.g. "you@example.com".
helpSupporting text below the field, shown whenever there is no error. Also the VoiceOver hint.
leadingIcon at the leading edge, e.g. Image(systemName: "envelope").
limitMaximum length in characters (extended grapheme clusters, so an emoji or accented letter counts as one). Typing or pasting past it is trimmed without splitting a character, and a "12/50" counter buds out of the field while it is focused or filled.
axis.horizontal for one line; .vertical for a multiline field that grows up to eight lines, where Return inserts a newline.
validationWhen validate starts running. .onBlur (default) waits until the field has been edited and left once, then validates live. .onSubmit waits for Return. .live starts with the first edit.
validateReturns an error message for invalid text, or nil when valid. A sage check appears once the text is valid.
errorAn error from outside the field (e.g. "Email already in use" from a server). Shown immediately and ahead of validate.
textContentTypeAutofill hint, e.g. .emailAddress, .name, .oneTimeCode.
keyboardTypeKeyboard. .emailAddress and .URL also turn off autocapitalization and autocorrection.
submitLabelThe Return key label, e.g. .next or .done.
onSubmitCalled when Return is pressed in a one-line field. Move focus to the next field here.
styleColors and field metrics. Defaults to the SwiftPieces house palette, adapting to light and dark.

Install

Pick one. Run it from the folder that contains your .xcodeproj and the files land inside your app.

Terminal

$npx swiftpieces add FormField

Or ask your coding agent · Claude Code, Cursor or Xcode, once the MCP server is connected

>Add the SwiftPieces "Form Field" piece to my app

Or copy the source above into your app.

New to SwiftUI?If you know React, CSS or Tailwind, SwiftUI is easier to pick up than it looks.Try real app patterns first

Building a whole app? See SwiftPieces Pro →

All inputs and forms →

Build it yourself: SwiftUI haptics guide

On this page

SwiftPiecesPro

Build the whole app with one library.

Production-ready screens, whole flows, complete app templates, a Build Kit your agent follows and Pro remixing.

54 screens · 46 patterns · 48 flows · 12 templates · 31 Build Kit skills

Explore Pro