Skip to content
Suluu

EmailMorph

A quiet email field. Focusing it lets a circular send action pinch off the trailing cap like a drop of water; clicking away draws that drop back into the field until the next tap.

Preview

Focus the field, enter an email, then submit

Installation

Registry — recommended

Copies the component into your configured shadcn UI directory and installs Motion.

npx shadcn@latest add https://suluu.site/r/email-morph.json

npm package

For centralized upgrades, install the optional ESM package and configure Tailwind source detection as shown below.

pnpm add suluu motion

Usage

EmailMorph owns input interaction and native validation. Your application owns the subscription request and passes truthful loading, success, and server-error state back to the component.

Registry
import { EmailMorph } from "@/components/ui/email-morph"

export function Newsletter() {
  return (
    <EmailMorph
      onSubmit={(email) => subscribe(email)}
    />
  )
}
npm
// app/globals.css
@import "suluu/styles.css";
@source "../node_modules/suluu/dist";

// component.tsx
import { EmailMorph } from "suluu/email-morph"

export function Newsletter() {
  return <EmailMorph onSubmit={(email) => subscribe(email)} />
}

Controlled async states

A successful state deliberately retains the submitted address and separated check. Clear or reset those values when your product is ready for another submission.

Controlled
const [email, setEmail] = useState("")
const [loading, setLoading] = useState(false)
const [success, setSuccess] = useState(false)
const [error, setError] = useState<string>()

<EmailMorph
  error={error}
  loading={loading}
  onSubmit={async (nextEmail) => {
    setLoading(true)
    setSuccess(false)
    setError(undefined)

    try {
      await subscribe(nextEmail)
      setSuccess(true)
    } catch {
      setError("We couldn't subscribe that address.")
    } finally {
      setLoading(false)
    }
  }}
  onValueChange={(nextEmail) => {
    setEmail(nextEmail)
    setSuccess(false)
    setError(undefined)
  }}
  success={success}
  value={email}
/>

Custom action glyphs

The defaults are dependency-free inline SVGs. Use the state render hook when the action should follow an existing product icon language.

Icon render hook
<EmailMorph
  renderIcon={(state) => {
    if (state === "success") return <BrandCheck />
    if (state === "loading") return <BrandLoader />
    return <BrandArrow />
  }}
/>

In context

The field stays quiet beneath editorial copy. Focusing it lets the send action pinch away; clicking outside draws it back.

Field notes

One thoughtful update each month.

Product details, motion studies, and the small decisions behind each release.

Props

EmailMorph accepts safe native email-input attributes such as id, name, autoComplete, and ARIA labeling attributes. Its ref points to the native input while className and style customize the complete form.

PropTypeDefaultDescription
valuestringControlled email value.
defaultValuestring""Initial uncontrolled email value.
onValueChange(value) => voidRuns when the input changes.
onSubmit(email, event) => voidRuns only for a valid required email.
loadingbooleanfalseKeeps the split open, makes the field read-only, and shows a spinner.
successbooleanfalseKeeps the split and value visible while showing a check.
disabledbooleanfalseDisables the field and action.
invalidbooleanfalseApplies invalid visual and ARIA state.
errorReactNodeLinked inline error; providing one also marks the field invalid.
placeholderstring"Email address"Email placeholder.
collapseOnBlurbooleantrueRejoins the send action on outside blur, except while loading or success.
motionIntensity"subtle" | "default" | "expressive""default"Spring and liquid-neck character.
labelsPartial<EmailMorphLabels>Built inAccessible input, submit, loading, and success copy.
renderIcon(state) => ReactNodeBuilt inReplaces the action glyph for every state.
classNamestringClass name applied to the form.
styleCSSPropertiesInline style applied to the form.

Theming

Override these variables in your light and dark theme scopes. The registry installs the defaults automatically.

--suluu-email-morph-surfaceInput, liquid bridge, and action surface.
--suluu-email-morph-foregroundPrimary text and arrow color.
--suluu-email-morph-mutedPlaceholder text.
--suluu-email-morph-borderInvalid-state field edge.
--suluu-email-morph-ringKeyboard focus ring.
--suluu-email-morph-offsetFocus ring offset surface.
--suluu-email-morph-shadowInput pill depth.
--suluu-email-morph-action-shadowSeparated action depth.
--suluu-email-morph-errorInvalid edge, arrow, and message.
--suluu-email-morph-error-shadowInvalid field depth.
--suluu-email-morph-successSuccess check color.
--suluu-email-morph-shimmer-intensitySheen strength on a failed-submit error.

Accessibility

A required native type=emailinput owns keyboard entry, autofill, and browser validity. The send action is absent until the field is focused, then Enter and the circular button share normal form submission. Focus can move between field and action without collapsing the widget, and Escape draws the action back unless a request is in flight or succeeded.

Inline errors are linked with aria-describedby and aria-errormessage. Loading marks the form busy, success and error updates are announced politely, and reduced motion removes the gooey split, spatial spring, and spinner rotation while preserving every state change.

↑↓ to navigate↵ to openesc to close