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.jsonnpm package
For centralized upgrades, install the optional ESM package and configure Tailwind source detection as shown below.
pnpm add suluu motionUsage
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.
import { EmailMorph } from "@/components/ui/email-morph"
export function Newsletter() {
return (
<EmailMorph
onSubmit={(email) => subscribe(email)}
/>
)
}// 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.
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.
<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.
| Prop | Type | Default | Description |
|---|---|---|---|
| value | string | — | Controlled email value. |
| defaultValue | string | "" | Initial uncontrolled email value. |
| onValueChange | (value) => void | — | Runs when the input changes. |
| onSubmit | (email, event) => void | — | Runs only for a valid required email. |
| loading | boolean | false | Keeps the split open, makes the field read-only, and shows a spinner. |
| success | boolean | false | Keeps the split and value visible while showing a check. |
| disabled | boolean | false | Disables the field and action. |
| invalid | boolean | false | Applies invalid visual and ARIA state. |
| error | ReactNode | — | Linked inline error; providing one also marks the field invalid. |
| placeholder | string | "Email address" | Email placeholder. |
| collapseOnBlur | boolean | true | Rejoins the send action on outside blur, except while loading or success. |
| motionIntensity | "subtle" | "default" | "expressive" | "default" | Spring and liquid-neck character. |
| labels | Partial<EmailMorphLabels> | Built in | Accessible input, submit, loading, and success copy. |
| renderIcon | (state) => ReactNode | Built in | Replaces the action glyph for every state. |
| className | string | — | Class name applied to the form. |
| style | CSSProperties | — | Inline 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.