āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā ā š shadcn/directory/nolly-studio/cult-ui/components/morph-surface ā āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā
<ComponentPreview name="morph-surface-demo" className="[&_.preview>[data-orientation=vertical]]:sm:max-w-[70%]" description="All variations" />
npx shadcn@latest add https://cult-ui.com/r/morph-surface.json
</TabsContent>
<TabsContent value="manual">
<Steps>
<Step>Copy and paste the following code into your project.</Step>
<ComponentSource name="morph-surface" /><Step>Update the import paths to match your project setup.</Step>
</Steps> </TabsContent> </CodeTabs>import { MorphSurface } from "@/components/ui/morph-surface"
The component works out of the box with no configuration needed:
<MorphSurface />
Customize the trigger label, placeholder, and submit behavior:
<MorphSurface
triggerLabel="Send Feedback"
placeholder="Share your thoughts..."
onSubmit={async (formData) => {
const message = formData.get("message") as string
console.log("Submitted:", message)
// Handle submission
}}
onSuccess={() => {
console.log("Feedback submitted successfully!")
}}
/>
Adjust the size when collapsed and expanded:
<MorphSurface
collapsedWidth="auto"
collapsedHeight={48}
expandedWidth={400}
expandedHeight={250}
triggerLabel="Custom Size"
placeholder="This surface is larger..."
/>
Add an icon to the trigger button:
import { HelpCircle } from "lucide-react"
;<MorphSurface
triggerLabel="Help"
triggerIcon={<HelpCircle className="w-4 h-4" />}
placeholder="How can we help you?"
/>
Control the open/close state externally:
import { useState } from "react"
function ControlledExample() {
const [isOpen, setIsOpen] = useState(false)
return (
<div>
<button onClick={() => setIsOpen(!isOpen)}>
{isOpen ? "Close" : "Open"} Morph Surface
</button>
<MorphSurface
isOpen={isOpen}
onOpenChange={setIsOpen}
triggerLabel="Controlled"
placeholder="This surface is controlled externally..."
/>
</div>
)
}
Speed up or slow down animations (higher values = slower animations):
// Slow animations (2x slower)
<MorphSurface animationSpeed={2} triggerLabel="Slow Animation" />
// Fast animations (2x faster)
<MorphSurface animationSpeed={0.5} triggerLabel="Fast Animation" />
Apply custom className props for styling:
<MorphSurface
className="shadow-2xl"
triggerClassName="hover:bg-primary/10"
contentClassName="border-2 border-primary/20"
triggerLabel="Styled"
placeholder="Custom styling applied..."
/>
| Prop | Type | Default | Description |
| ------------------ | -------------------------------------------- | ------------------------ | --------------------------------------------------------- |
| collapsedWidth | number \| "auto" | 360 | Width when collapsed |
| collapsedHeight | number | 44 | Height when collapsed |
| expandedWidth | number | 360 | Width when expanded |
| expandedHeight | number | 200 | Height when expanded |
| animationSpeed | number | 1 | Animation speed divisor (higher = slower, lower = faster) |
| springConfig | SpringConfig | - | Custom spring animation configuration |
| triggerLabel | string | "Feedback" | Label text for the trigger button |
| triggerIcon | React.ReactNode | - | Icon to display in the trigger button |
| placeholder | string | "What's on your mind?" | Placeholder text for the textarea input |
| submitLabel | string | "ā Enter" | Label for the submit button |
| onSubmit | (data: FormData) => void \| Promise<void> | - | Callback when form is submitted |
| onOpen | () => void | - | Callback when surface opens |
| onClose | () => void | - | Callback when surface closes |
| onSuccess | () => void | - | Callback after successful submission |
| isOpen | boolean | - | Controlled state for open/close |
| onOpenChange | (open: boolean) => void | - | Callback for controlled state changes |
| className | string | - | Additional CSS classes for the root container |
| triggerClassName | string | - | Additional CSS classes for the trigger button |
| contentClassName | string | - | Additional CSS classes for the content area |
| renderTrigger | (props: TriggerProps) => React.ReactNode | - | Custom render function for the trigger |
| renderContent | (props: ContentProps) => React.ReactNode | - | Custom render function for the content |
| renderIndicator | (props: IndicatorProps) => React.ReactNode | - | Custom render function for the indicator dot |
type SpringConfig = {
type: "spring"
stiffness: number
damping: number
mass?: number
delay?: number
}
interface TriggerProps {
isOpen: boolean
onClick: () => void
className?: string
}
interface ContentProps {
isOpen: boolean
onClose: () => void
onSubmit: (data: FormData) => void | Promise<void>
className?: string
}
interface IndicatorProps {
success: boolean
isOpen: boolean
className?: string
}
Use as a feedback collection component:
<MorphSurface
triggerLabel="Send Feedback"
placeholder="What can we improve?"
onSubmit={async (formData) => {
const message = formData.get("message") as string
await fetch("/api/feedback", {
method: "POST",
body: JSON.stringify({ message }),
})
}}
onSuccess={() => {
toast.success("Thank you for your feedback!")
}}
/>
Use as a help widget:
import { HelpCircle } from "lucide-react"
;<MorphSurface
triggerLabel="Need Help?"
triggerIcon={<HelpCircle className="w-4 h-4" />}
placeholder="How can we assist you?"
onSubmit={async (formData) => {
// Handle help request
}}
/>
Create a larger feedback surface:
<MorphSurface
collapsedWidth="auto"
collapsedHeight={56}
expandedWidth={500}
expandedHeight={300}
triggerLabel="Extended Feedback"
placeholder="Share your detailed thoughts..."
/>
Customize animation spring physics:
<MorphSurface
springConfig={{
type: "spring",
stiffness: 400,
damping: 30,
mass: 0.5,
}}
triggerLabel="Custom Animation"
/>
Control the component state externally:
import { useState } from "react"
function ControlledMorphSurface() {
const [isOpen, setIsOpen] = useState(false)
return (
<div className="space-y-4">
<button
type="button"
onClick={() => setIsOpen(!isOpen)}
className="px-4 py-2 bg-primary text-primary-foreground rounded-md"
>
{isOpen ? "Close" : "Open"} Feedback
</button>
<MorphSurface
isOpen={isOpen}
onOpenChange={setIsOpen}
triggerLabel="Feedback"
placeholder="Controlled state example..."
/>
</div>
)
}
Use render props for maximum customization:
<MorphSurface
renderTrigger={({ isOpen, onClick, className }) => (
<button onClick={onClick} className={cn("custom-trigger", className)}>
{isOpen ? "Close" : "Open"}
</button>
)}
renderContent={({ isOpen, onClose, onSubmit, className }) => (
<div className={cn("custom-content", className)}>
<textarea placeholder="Custom content..." />
<button onClick={() => onSubmit(new FormData())}>Submit</button>
</div>
)}
/>
Handle async form submissions:
<MorphSurface
triggerLabel="Submit Issue"
placeholder="Describe the issue..."
onSubmit={async (formData) => {
const message = formData.get("message") as string
try {
const response = await fetch("/api/issues", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message }),
})
if (!response.ok) {
throw new Error("Failed to submit")
}
// Success handling is automatic
} catch (error) {
console.error("Submission error:", error)
throw error // Re-throw to prevent success state
}
}}
onSuccess={() => {
toast.success("Issue submitted successfully!")
}}
/>
Use multiple instances with different configurations:
<div className="flex gap-4">
<MorphSurface triggerLabel="Feedback" placeholder="General feedback..." />
<MorphSurface
triggerLabel="Bug Report"
placeholder="Report a bug..."
expandedWidth={400}
/>
<MorphSurface
triggerLabel="Feature Request"
placeholder="Suggest a feature..."
animationSpeed={0.8}
/>
</div>
This component is inspired by the Morph Surface prototype from Devouring Details, an interactive reference manual for interaction design by Rauno Freiberg. Devouring Details explores interaction design principles through 23 downloadable React components, showcasing the craft of interaction design with attention to detail.
ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā