Signature
An animated SVG signature effect that draws out text as if hand-written.
Live preview recorded from Bakai Lab; experiment and demo by Bakai Tolondu uulu. Preview: platform recorded · rights authorized.
A compact admissions CRM interface showcasing navigation, pipeline columns, and applicant records.
LSource LandLab - Bakai Tolondu uuluWhy it stands out
A compact admissions CRM interface showcasing navigation, pipeline columns, and applicant records.
Build an "Add member" dialog using Framer Motion (imported from the "motion/react" package).
Shell. A blue trigger button (linear-gradient 180deg #1E3A8A to #2563EB, 44px tall, 18px side padding, 16px radius, white text, glow box-shadow 0 2px 6px rgba(37,99,235,0.4) plus insets rgba(59,130,246,1) and rgba(127,176,255,1), scales to 0.97 on active) opens a modal. The scrim is fixed inset 0, z-index 60, background rgba(0,0,0,0.16), flex-centred, padding 28px 20px. Inside sits a 376px card (full width under 420px), background #FFFFFF, text #1A1C1E, 18px radius, six-layer shadow: 0 0 0 1px rgba(0,0,0,0.03), 0 65px 26px rgba(0,0,0,0.01), 0 36px 22px rgba(0,0,0,0.04), 0 16px 16px rgba(0,0,0,0.06), 0 4px 9px rgba(0,0,0,0.07), inset 0 0 0 1px rgba(26,28,30,0.04).
Grow from the trigger. Capture the trigger's DOMRect before mount, compute the percentage position of its centre relative to the card's own box, clamp to -25% and 125% per axis, and set that as the card's transform-origin, so it grows out of the button.
Entrance and exit. Card and scrim share one entrance spring: type spring, visualDuration 0.34, bounce 0.16. The card animates opacity 0 to 1, scale 0.955 to 1, y 6 to 0, filter blur(4px) to blur(0px). The scrim just fades in over 0.22s, ease cubic-bezier(0.19,1,0.22,1). Exit is faster but watchable, not a blink: the card goes to opacity 0, scale 0.94, blur(7px) over 0.24s ease cubic-bezier(0.32,0.72,0,1) for scale and blur, while opacity uses its own slower curve, 0.24s ease cubic-bezier(0.5,0,1,1), so it lags the shrink; the scrim reuses that opacity curve. Under prefers-reduced-motion, drop all of this to a flat 0.12s opacity fade, no scale, no blur, no spring.
Header. 48px tall, padding 0 10px 0 16px, space-between. Title is "Add" plus a swapping role word, 16px weight 600, line-height 18px, -0.0056em, colour rgba(26,28,30,0.85). Close button 30x30, 10px radius, an inline X of two crossed strokes at strokeWidth 2.2, colour rgba(26,28,30,0.42) going to rgba(26,28,30,0.85) on hover with background rgba(26,28,30,0.05), scaling 0.9 on active. Below the header a 1px rule at rgba(26,28,30,0.04).
Swapping role word. One component, used after "Add" in both the header title and the CTA. Render every candidate word ("member", "trainer") off-layout and invisible in a hidden ruler, measure each with a ResizeObserver so it stays right once the webfont loads, and lock the visible slot to the widest, so nothing around it ever moves. The swap is a pure opacity crossfade, no x/y/scale/blur: incoming fades in over 0.24s, outgoing fades out over 0.16s, both ease cubic-bezier(0.19,1,0.22,1).
Body. Padding 12px 16px 16px, column, 24px gap to the CTA, 12px gap between fields. Wrap the body in a container that measures its own content height with a ResizeObserver and animates its parent's height to that value whenever the content swaps, spring visualDuration 0.3, bounce 0.06. Never scale the box for this, only animate height (scaling would distort the shadow).
Segmented switch. First element in the body. A track with 3px padding, background rgba(26,28,30,0.04), 16px radius, holding two buttons, Member and Trainer, each 30px tall, flex 1, 13px radius (16 minus the 3px padding, concentric), 13px weight 500 Inter, colour rgba(26,28,30,0.55) idle / rgba(26,28,30,0.85) active. One absolutely positioned thumb, inset 3px, width calc(50% - 3px), background #FAFAFA with its own soft shadow stack, sliding by translateX of role index times 100% on a spring, visualDuration 0.26, bounce 0.16, landing exactly on the next segment.
Fields. Two, Name then Number, each a label (16px weight 600, -0.0056em, rgba(26,28,30,0.85)) above a 4px gap to the input. Each input wrap is 44px tall, 16px radius, background rgba(18,19,20,0.06), and on focus-within switches to background rgba(18,19,20,0.045) plus a ring 0 0 0 1px rgba(37,99,235,0.5), 0 0 0 4px rgba(37,99,235,0.12). Input text is DM Sans (falling back to Inter), weight 500, 16px, -0.0251em, colour #1A1C1E, 16px side padding, placeholder rgba(26,28,30,0.24). Name is autofocused 200ms after mount (0ms if reduced motion). Number placeholder "(555) 123-4567".
Phone formatting. Only digits are typed; separators are generated, never typed, and strictly append-only: emit "(" the instant the first digit exists, ") " the instant the third (area code) completes, "-" the instant the sixth (exchange) completes, then the rest, capped at 10 digits. Because a separator is only appended once the digits in front of it are done, nothing on screen ever slides sideways when one appears. If a keystroke shortens the value but the digit content is unchanged, backspace landed on a generated character, so delete the trailing digit instead, never let backspace stick on a separator. Use tabular-nums so digits never re-space. Implement the field as two stacked layers with identical font metrics: a visible span underneath rendering each character, and the real input on top with transparent text and a visible caret owning focus, selection, and every keystroke. Only the generated separators fade in, via a plain CSS keyframe opacity 0 to 1, 0.18s cubic-bezier(0.19,1,0.22,1), not a Motion animation, so the digits themselves stay instantaneous.
CTA. Full-width flex button, 44px tall, 18px side padding, 7px gap, 16px radius, white 14px weight 600 Inter at -0.004em, the same gradient and glow as the trigger, scaling 0.97 on active. Disabled (0.4 opacity, default cursor) until the name is non-empty AND the phone has exactly 10 digits. A plus icon (two crossed strokes, strokeWidth 2.6) precedes "Add" plus the same swapping role word.
Submit and confirmation. On submit the form view exits (opacity 0, up 6px, blur 4px, 0.14s ease cubic-bezier(0.19,1,0.22,1)) and a confirmation view mounts in its place (opacity 0 to 1, down 8px to 0, blur 5px clearing, 0.26s same ease) while the wrapper springs its height. The confirmation is centred: a 52px circular badge (the CTA gradient and glow) holding a loader-to-check icon, a status line, then the formatted phone in rgba(26,28,30,0.4) at 13.5px. The badge pops in on mount, spring visualDuration 0.32, bounce 0.24, scale 0.86 to 1.
Loader to check, exactly three beats that never overlap. A ring from an 8.6-radius circle, strokeWidth 2.1, spins continuously forever at one turn per second, linear, drawn as a ~28% arc via strokeDasharray/strokeDashoffset on a normalized pathLength of 1; this spin runs the whole 1500ms saving phase. Once submit resolves, the arc closes to a full circle by animating dashoffset to 0 over 460ms, ease cubic-bezier(0.77,0,0.175,1); the spin never stops, it just becomes invisible once the arc is whole. ONLY once that 460ms close finishes does a checkmark path (d "M7.4 12.3 L10.6 15.6 L16.9 8.6", strokeWidth 2.3) trace from dashoffset 1 to 0 over 500ms, same ease, like a hand-drawn stroke, starting with zero overlap as the ring finishes. The status line reads "Adding {name}" while saving and crossfades (enter from y -8 over 0.26s, exit to y 8 over 0.16s, both ease cubic-bezier(0.77,0,0.175,1)) to "{name} added" the instant the check finishes (460 + 500ms after saving began). At that same instant only the white loader-to-check artwork kicks once (the blue disc holds still), scaling 1, 1.15, 1 over 0.46s with two stitched eases, cubic-bezier(0.19,1,0.22,1) up and an overshooting cubic-bezier(0.34,1.32,0.64,1) down, so it lands like a spring even though it is a plain tween, not a physics spring. The whole dialog auto-closes 1800ms after the confirmation lands.
Accessibility. role dialog, aria-modal true, aria-label reflecting the current role. Escape closes. Clicking the scrim outside the card closes. Tab is trapped inside the card between its first and last focusable node. Body scroll is locked while open, and the scrollbar's width is added back as right padding so the page does not jump.
Corners. Exactly two radii, everything else derived as outer minus padding: card 18px; fields, segmented track, and CTA 16px; the segmented thumb 13px.
Easing rules. Anything already on screen and morphing (the ring closing, the check tracing, the status crossfade) uses ease-in-out-quart cubic-bezier(0.77,0,0.175,1). Anything entering or leaving as a crossfade (the scrim, the swapping role word, the confirmation mounting, the form leaving) uses ease-out-quart cubic-bezier(0.19,1,0.22,1). The continuous ring spin is the one exception, always linear. The dialog's own mount and dismiss, the thumb slide, the height morph, and the badge's mount-in pop all use their own springs. Honour prefers-reduced-motion throughout: fall back to a flat 0.12s opacity fade for mount and dismiss, skip the badge kick and the separator fade entirely, and remove hover and active transitions from every button.Discovery vocabulary
Related by governed terms
An animated SVG signature effect that draws out text as if hand-written.
More from Lab - Bakai Tolondu uulu