Skip to content

Objectuve — Interaction Design Reference

A living reference for all user interaction patterns in the Enkidu app. Consult this document when building or updating any interactive component. It is a companion to desi-design-context.md (tokens, spacing, typography) and docs/brand/brand.md (voice and philosophy).

Source of truth for motion tokens: DESIGN.md (the motion: section). Source of truth for component tokens: design_system/tailwind-tokens.cjs.


Quick-reference table

PatternDurationEasingTailwind utility
Hover color / background150msease-in (cubic-bezier(0.4,0,1,1))transition-colors duration-150
Hover shadow / elevation300msstandard (cubic-bezier(0.4,0,0.2,1))transition-shadow duration-300
Card lift on hover300msstandardtransition-transform duration-300
Button press150msease-intransition-transform duration-150
Focus ringinstantfocus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2
Tooltip appear/disappear200msease-out / ease-inCSS transition: opacity 200ms
Modal / sheet enter400–500msstandardscale-in + fade-in
Modal / sheet exit200msease-inopacity fade
Page enter400msstandardslide-up or fade-in
Toast enter300msstandardslide-up + fade
Toast exit200msease-inopacity fade
Gamification pop (XP, badge)400msbounce (cubic-bezier(0.34,1.56,0.64,1))animate-point-pop
Skeleton shimmer2slinearanimate-shimmer
Streak fire pulse2sease-in-outanimate-pulse-glow

Toast dwell duration (how long a toast stays on screen before auto-dismissing) is a separate, locked vocabulary — see motion-vocabulary.md for the tier table and the achievement-rarity dwell ruling. The rows above only cover toast enter/exit animation timing, not dwell.


1. Hover states

1.1 Buttons

Every button uses a three-property transition:

css
transition: background-color 200ms cubic-bezier(0.4,0,1,1),
            transform 150ms cubic-bezier(0.4,0,1,1),
            box-shadow 300ms cubic-bezier(0.4,0,0.2,1);

Primary button (bg-primary text-primary-foreground)

StateBackgroundShadow
Defaultbg-primarynone
Hoverbg-primary-lightshadow-glow (brand blue halo)
Active / pressedbg-primary-interactive, translateY(1px)none
Focusunchanged + ring-2 ring-primary ring-offset-2
Disabledopacity-50, pointer-events-none

The blue glow (shadow-glow = 0 0 40px hsla(203,78%,61%,0.30)) renders only on hover and fades on press — it signals "ready" not "active."

Secondary / ghost buttons follow the same timing but use muted surface colors: hover:bg-muted for ghost, hover:bg-secondary/80 for secondary. No glow shadow.

Destructive buttonhover:bg-destructive/90. Never carries a glow shadow.

Touch note: On touch devices (no pointer: fine) hover states do not fire. Buttons must have sufficient visual weight at rest. Never make the rest state invisible and rely on hover to communicate affordance.


1.2 Cards

Cards lift on hover. This is the primary elevation vocabulary — cards do not glow or change color.

css
transition: transform 300ms cubic-bezier(0.4,0,0.2,1),
            box-shadow 300ms cubic-bezier(0.4,0,0.2,1);
StateTransformShadow
Defaultnoneshadow-sm
HovertranslateY(-4px)shadow-xl
Active / pressedtranslateY(-2px)shadow-md

The 4 px lift is deliberate — enough to signal interactivity, not so much it looks cartoonish. Cards that are not interactive should not receive the lift. Add cursor-pointer and the transition only on cards with an @click handler.

Glass cards follow the same transform/shadow pattern. They also gain a subtle brightness overlay on hover: add a pseudo-element or overlay div with bg-white/5 that transitions from opacity-0 to opacity-100 in 200ms.


1.3 Navigation items (bottom tab bar, sidebar)

Navigation items do not lift. They shift color only:

css
transition: color 150ms ease-in, background-color 150ms ease-in;
StateIcon + label colorBackground
Defaulttext-muted-foregroundtransparent
Hover (desktop)text-foregroundbg-muted/50
Active (current route)text-primary-interactivebg-primary/10
Focusunchanged + ring-2 ring-primary ring-offset-2

Active state uses --primary-interactive (hsl(203,78%,35%) in light mode) — 6 points darker than canonical primary — to clear WCAG AA on the composite background. Do not use text-primary here; it fails contrast on the bg-primary/10 tint.


1.4 Icon buttons and FABs

Icon buttons (close, back, icon-only actions) use the same color transition as navigation items. They must be at least 44 × 44 px and carry an aria-label.

FABs (primary and coach variants) get the full glow treatment on hover, same as the primary button — shadow-glow for the primary FAB, shadow-coach for the coach FAB.


css
transition: color 150ms ease-in;
StateColor
Defaulttext-primary
Hovertext-primary-light
Activetext-primary-interactive

Never add text-decoration on hover for navigation links inside the app. Underlines belong in long-form content, not interactive chrome.


1.6 List rows and feed items

Goal rows, feed rows, settings rows — subtle background change only. No lift.

css
transition: background-color 150ms ease-in;
StateBackground
Defaulttransparent
Hoverbg-muted/50
Active / pressedbg-muted

2. Tooltips

2.1 When to use

Use tooltips for:

  • Icon-only controls where the label cannot always be shown (e.g., a toolbar with five unlabeled icons).
  • Supplementary detail that would clutter the primary layout if always visible (e.g., a stat label that expands with a precise definition).
  • Keyboard shortcut hints for power-user paths.

Do not use tooltips for:

  • Primary actions — the label must be visible at rest. Never hide a CTA label in a tooltip.
  • Error messages — use inline validation copy, not a tooltip.
  • Long explanatory copy — if the tooltip needs more than one sentence, use a bottom sheet or an info modal instead.

2.2 Appearance and timing

Tooltips use the UiTooltip.vue component (ionic_frontend/src/components/ui/tooltip/UiTooltip.vue). Do not re-implement tooltip behavior inline.

PropertyValue
Enter delay400ms — prevents tooltips flashing on fast cursor passes
Enter animationopacity 0 → 1 over 200ms ease-out
Exit animationopacity 1 → 0 over 200ms ease
Backgroundhsl(var(--popover))
Texthsl(var(--popover-foreground)), text-xs
Border radiusrounded-md (0.875rem)
Paddingpy-2 px-3
Shadowshadow-lg
Max width200px — overflow wraps
Z-indexz-index: 1000
Pointer eventsnone — tooltips are inert overlays

The 400 ms hover delay is required. Triggering a tooltip immediately feels noisy and makes hover feel fragile. Adjust only if the context is a touch-only device (where the concept of "hover delay" does not apply).


2.3 Placement

Default placement is top. Preference order if top would be clipped: bottomrightleft.

The tooltip arrow (8 × 8 px diamond, rotate(45deg)) centers on the trigger element and adjusts with placement. Gap between trigger and tooltip bubble is 8px (the translate(calc(100% + 8px)) offset in UiTooltip.vue).

Viewport clipping rule: If a tooltip in its default position would render partially off-screen, flip to the opposite side. This is not yet automated — check manually at mobile widths for tooltips on edge-positioned controls.


2.4 Accessibility

  • The trigger element must have aria-describedby="<tooltip-id>" so screen readers announce the tooltip content.
  • UiTooltip.vue auto-generates the id and sets role="tooltip" on the bubble.
  • Tooltips must be keyboard-discoverable: they show on focus and hide on blur, same as mouse.
  • On touch devices, tooltips activate on touchstart and dismiss on touchend. If the content is critical, consider an alternative disclosure pattern (an info icon that opens a sheet) since there is no hover on touch.

2.5 Dark mode

UiTooltip.vue uses hsl(var(--popover)) which automatically resolves to the correct dark-mode surface. No additional classes needed. Verify contrast: text-popover-foreground on bg-popover should meet WCAG AA in both modes.


3. Animations

3.1 Core principle

Every animation must map to a state change. Motion that runs without a corresponding state transition (decorative spin, idle bounce on a non-interactive element) violates the brand's anti-engagement-loop rule. Motion earns its place by making the app feel more responsive, not more alive.

The exception: looping motion on brand marks and marketing illustrations (the floating arc, the streak fire) is decorative but contained — it should never appear inside the main dashboard chrome.


3.2 Duration scale

NameDurationWhen to use
instant100msImmediate state acknowledgement (press, selection)
fast150msMicro-interactions: hover color, press transform
medium200msAccordion, color/shadow transitions, tooltip enter
default300msCard hover, most UI transitions, toast enter
slow400msScale-in, page enter, modal appear
slower500msSlide-in, bottom sheet open

Nothing in the main app UI should take longer than 500ms. If something feels slow at 500ms, the design needs a different reveal pattern — not a longer animation.


3.3 Easing curves

NameCurveWhen to use
standardcubic-bezier(0.4, 0, 0.2, 1)Default for enter/exit, page transitions, most movements
ease-incubic-bezier(0.4, 0, 1, 1)Elements leaving the screen; hover color
ease-outcubic-bezier(0, 0, 0.2, 1)Elements entering; tooltip appear
ease-in-outcubic-bezier(0.4, 0, 0.6, 1)Looping animations (streak fire, float)
bouncecubic-bezier(0.34, 1.56, 0.64, 1)Achievement pops, gold progress dot — the overshoot is the signal of "earned"
linearlinearProgress bars, shimmer, continuous loading

The bounce curve is reserved exclusively for gamification moments. Using it on ordinary UI interactions will dilute the signal. If it doesn't say "you earned something," it should not bounce.


3.4 Named keyframes and when to use them

fade-inopacity: 0 → 1 over 300ms ease-in. Use for: any element appearing on the canvas where positioning is already final. Tooltips, inline hints, score labels fading in after data loads.

scale-intransform: scale(0.9) opacity: 0 → scale(1) opacity: 1 over 400ms standard. Use for: modals and dialogs appearing. The gentle scale-in communicates "this is a new layer appearing above the current one." Do not use for cards in a list — staggered fade-in reads better for list entries.

slide-uptranslateY(20px) opacity: 0 → translateY(0) opacity: 1 over 500ms standard. Use for: bottom sheets, bottom tab transitions, content that arrives from below.

slide-in-righttranslateX(100%) opacity: 0 → translateX(0) opacity: 1 over 500ms standard. Use for: page-level navigation to a detail view (Goal Detail, Community Detail).

page-enteropacity: 0, translateX(20px) → opacity: 1, translateX(0) over 400ms standard. Use for: all route transitions where no specific directional slide is warranted. A subtle 20 px slide communicates forward momentum without being distracting.

pulse-glow — drop-shadow oscillates between 8px and 20px on the accent/streak color over 2s ease-in-out infinite. Use for: the streak fire icon when the user has an active streak. Also for attention anchors (e.g., a "you have an unread Coach insight" indicator). Never on ordinary CTAs.

float — vertical bob ±10px over 3s ease-in-out infinite. Use for: brand mark illustrations in onboarding and empty states, not in app chrome.

shimmer — 200% wide linear gradient translates across the element over 2s linear infinite. Use for: skeleton loaders only. A skeleton should look like content on the way in, not a loading indicator.

confetti — elements translate up 100px, rotate 720°, fade out over 1s ease-out. Use for: rank-up celebration. Full-screen burst, auto-dismisses. Never loops.

point-pop — radius grows from 0 with cubic-bezier(0.34,1.56,0.64,1) overshoot, fades in over 400ms. Use for: the gold progress dot appearing on a milestone ring when it unlocks. The bouncy overshoot is the "earned" signal.

bounce-subtle — vertical bounce ±8px over 1s ease-in-out infinite. Use for: an attention anchor that needs more urgency than pulse-glow — e.g., a "streak at risk" badge. Looping, so use sparingly.

spin360° continuous rotation over 1s linear infinite. Use for: loading spinners and button loading states. Never use spin without a visible loading label or aria-live region update.


3.5 Reduced-motion rule

Always honor prefers-reduced-motion: reduce. Replace the following with a simple opacity fade (200ms ease):

  • All transform-based keyframes (scale-in, slide-up, slide-in-right, page-enter, point-pop, bounce-subtle, float)
  • All looping animations (pulse-glow, float, bounce-subtle, shimmer)

Opacity fades are acceptable under reduced-motion because they convey the state change without triggering vestibular discomfort. Do not remove all visual feedback — just remove the motion.

Tailwind pattern:

html
<div class="transition-opacity duration-200 motion-reduce:transform-none motion-reduce:transition-none">

For custom keyframes, wrap them in a media query:

css
@media (prefers-reduced-motion: no-preference) {
  .animate-point-pop { animation: point-pop 400ms cubic-bezier(0.34,1.56,0.64,1); }
}
@media (prefers-reduced-motion: reduce) {
  .animate-point-pop { animation: fade-in 200ms ease; }
}

3.6 Staggering list animations

When a list of cards appears simultaneously (e.g., the goal list loading after a skeleton), stagger their fade-in by 50ms per item up to a maximum of 200ms total offset (i.e., 5 items). Beyond 5 items, hold the 200 ms offset for all remaining items — the user should not wait for item 12 to animate in.

css
.goal-card:nth-child(1) { animation-delay: 0ms; }
.goal-card:nth-child(2) { animation-delay: 50ms; }
.goal-card:nth-child(3) { animation-delay: 100ms; }
.goal-card:nth-child(4) { animation-delay: 150ms; }
/* 5th item and beyond */ .goal-card:nth-child(n+5) { animation-delay: 200ms; }

4. Modal and overlay interactions

4.1 Types of overlay in the app

TypeMobile behaviorDesktop behavior
DialogBottom sheet (slides up from screen bottom)Centered overlay, max min(600px, 95vw)
SheetFull or half-screen slide-up panelCan also be a side drawer
PopoverNot used on mobile — use a sheetPositioned relative to trigger
AlertCentered dialog regardless of platformSame

4.2 Opening behavior

Enter animation — dialog/sheet:

  • Backdrop fades in: opacity: 0 → 0.45 over 300ms ease-out.
  • Backdrop blur: backdrop-blur(4px) activates simultaneously with the opacity fade.
  • Dialog surface: scale-in + fade-in over 400ms standard for centered dialogs; slide-up over 500ms standard for bottom sheets.

The scale-in communicates layer hierarchy — the user understands a new layer has appeared. Do not skip it.

Scroll lock: When a modal opens, overflow: hidden must be set on <body> to prevent background scroll. Restore on close.

Initial focus: Move focus to the first interactive element inside the modal immediately on open. If the modal has a close button at the top, focus goes there first (it is the escape hatch). If the modal has a primary action, focus the first input in a form flow instead.

Use useFocusTrap (available at ionic_frontend/src/composables/useAccessibility.ts) to trap focus inside the modal for the duration it is open. See BadgeModal.vue for the reference implementation.


4.3 Closing behavior

Exit animation:

  • Dialog surface fades out only: opacity: 1 → 0 over 200ms ease-in. No reverse scale animation — exits should be faster than entrances.
  • Backdrop fades out: opacity: 0.45 → 0 over 200ms ease-in.

Do not play a slide-down or scale-out on close. Reverse animations on exit feel sluggish.

Return focus: On close, return focus to the element that triggered the modal open. Track the triggering element with a ref before activate() is called. useFocusTrap's deactivate() always returns focus to the element that triggered open.


4.4 Backdrop interaction

Clicking or tapping the backdrop should close the modal — with one exception:

Modal typeBackdrop click closes?
Informational (Badge details, Changelog)Yes
Form (Create goal, Edit profile)No — unsaved data requires explicit dismiss
Alert / destructive confirmNo — user must choose an explicit action
Bottom sheet (share, quick actions)Yes

For forms, visually shake the modal (translateX(4px) → translateX(-4px) → 0 over 300ms) when the user clicks the backdrop — signal that the close was blocked without a toast. Include a subtle haptic vibration hint via navigator.vibrate([50]) where the API is available.

Implement backdrop click: bind @click.self="close" on the overlay wrapper, not on the dialog surface itself.


4.5 Keyboard navigation

KeyBehavior
EscapeClose the modal (same as backdrop click — respects the "close-blocked" rules above for forms)
TabCycle forward through focusable elements inside modal
Shift+TabCycle backward
Enter / SpaceActivate focused button

Focusable elements inside a modal must follow a logical reading order (top-to-bottom, left-to-right). The close button is always last in DOM order so it is last in Tab order — this mirrors the spatial position (top-right corner) with the reading flow.


4.6 ARIA

Minimum required ARIA on every dialog:

html
<div
  role="dialog"
  aria-modal="true"
  aria-labelledby="dialog-title-id"
>
  <h2 id="dialog-title-id">...</h2>
  ...
</div>

If the dialog has no visible title (rare), use aria-label on the role="dialog" element instead.

Screen readers announce: "dialog, [title]" when focus enters. This is the user's only orientation signal. A missing or vague title (e.g., aria-label="Modal") fails them.


4.7 Mobile-specific bottom sheet

Bottom sheets on mobile slide in from below with slide-up (500ms standard). Additional rules:

  • Drag handle: Include a 4 × 32 px rounded pill handle at the top of the sheet, bg-border color. The handle communicates dismissability to touch users even before they interact.
  • Swipe to dismiss: Bind a touch-gesture handler to the drag handle and track downward swipe velocity. Dismiss if velocity exceeds 300px/s downward or if displacement exceeds 40% of sheet height. Mirror the position in real-time with translateY().
  • Maximum height: max-h-[90vh] on mobile. Sheets taller than 90vh should become a full-page modal instead.
  • Safe-area bottom: Always apply pb-[env(safe-area-inset-bottom)] to the sheet footer so CTAs clear the iPhone home indicator.

5. Visual flow and polish

5.1 Page transitions

Route transitions use page-enter (opacity: 0, translateX(20px) → opacity: 1, translateX(0) over 400ms standard) for forward navigation and a mirrored slide-in-left for back navigation. The 20 px x-axis shift communicates directionality without overcommitting to a full-screen slide.

Ionic's built-in page transition animation (ios or md) fires by default. If overriding it with a custom transition, disable Ionic's transition by setting animated: false on IonRouter and apply the CSS class transition manually.

Reduced-motion: replace page-enter with fade-in only (no translate). The app should still feel navigable.


5.2 Loading states

Skeleton loaders — use for any content-heavy surface (goal list, feed, dashboard cards) that takes over 200ms to load. The skeleton should approximate the final layout: same card shape, same number of placeholders as the expected content count (cap at 5 skeleton items, not 20).

css
background: linear-gradient(90deg, bg-muted 25%, bg-muted-foreground/10 50%, bg-muted 75%);
background-size: 200%;
animation: shimmer 2s linear infinite;

Spinners — use only for short indeterminate waits (button loading state, file upload) where a skeleton would misrepresent the eventual content shape. A spinner inside a button replaces the button label; set aria-busy="true" on the button and include a visually-hidden Loading… label.

Do not show a spinner for actions that take less than 200ms. Use the button disabled state instead. Flashing a spinner for 100ms and removing it feels broken.


5.3 Gamification feedback

These are the four reserved celebration patterns. Do not reuse them for non-gamification moments.

Streak tick (+1 counter increment)

  • The streak counter increments with a brief bounce-subtle pulse (1s, 1 cycle).
  • Simultaneously: a soft shadow-accent glow fades in on the streak card over 300ms, then fades out over 600ms.
  • Microcopy: "Streak +1. Day N." (do not say "Amazing job!").

XP pop (experience points earned)

  • A +NNN XP label fades in and translates up 16px over 500ms standard, then fades out over 300ms. Triggered from the point of the action (goal card, check-in button).
  • Simultaneously: the XP bar's fill animates to its new value over 500ms cubic-bezier(0.4,0,0.2,1).

Badge unlock

  • BadgeModal.vue opens with scale-in + fade-in (400ms standard).
  • Inside the modal: the badge image appears with point-pop bounce curve (400ms).
  • A rarity-tinted glow shadow activates on the badge icon: shadow-gold for legendary, shadow-coach for epic, primary blue glow for rare.
  • For legendary only: a legendary-pulse CSS animation (scale(1) → scale(1.02) over 2s ease-in-out, infinite) runs on the badge icon to maintain celebratory energy while the modal is open.
  • Toast on dismiss: "<Badge name> unlocked." — not "Amazing! You unlocked...".

Rank up

  • Full-screen confetti burst: confetti keyframe on 40–80 particle elements positioned absolutely. Particles translate up 100px, rotate 720°, fade out over 1s ease-out. Particles are aria-hidden.
  • A rank-up card (glass card, gold accent border border-gold/60, shadow-gold) slides in from below over 500ms.
  • Text: "Level N · <Rank Name>" — brief. Celebratory without excess.
  • Auto-dismiss after 3s; user can also tap anywhere to dismiss. No looping.

5.4 State transitions on interactive elements

Elements should never snap from one state to another. Any property change triggered by user action needs a transition duration. The minimum is 150ms for micro-interactions; 200ms for color/opacity; 300ms for spatial changes.

Checkbox / toggle completion (goal event log)

  • The checkmark icon draws from 0 → 100% stroke-dashoffset over 300ms ease-out.
  • The surrounding circle fills with bg-success/20 over 200ms.
  • The row's text transitions to text-muted-foreground over 300ms to signal completion.

Progress ring (goal completion, momentum bar)

  • stroke-dashoffset animates to its new value over 500ms cubic-bezier(0.4,0,0.2,1).
  • When the ring reaches 100%, a single pulse-glow fires once (non-looping): shadow-accent expands from 0 to 20px and back over 600ms.
  • If a milestone is reached: the gold progress dot pops in with point-pop.

Input focus

  • Border transitions from border-border to border-ring over 200ms.
  • A ring-2 ring-primary ring-offset-2 focus ring appears immediately (no transition on the ring itself — instant visual feedback is required for accessibility).
  • Helper text (below the input) fades in at opacity-0 → 1 over 200ms only if the field has a conditional helper.

5.5 Theme switching (light ↔ dark)

Theme toggle applies a transient html.transitioning class for the duration of the animation:

css
html.transitioning * {
  transition:
    background-color 300ms ease,
    border-color 300ms ease,
    color 200ms ease,
    fill 200ms ease,
    stroke 200ms ease,
    box-shadow 300ms ease !important;
}

The !important ensures component-level transitions are overridden by the global theme transition. Remove the transitioning class after 300ms in JavaScript.

Colored shadows (shadow-glow, shadow-gold, shadow-coach) persist at the same values in both themes — they read well on both the light and dark canvas and do not need per-theme variants.


5.6 Coach / AI surfaces

AI-generated content (insight cards, suggestion banners, the Coach FAB) carries the coach-purple accent and a shadow-coach glow. The FAB opens with a scale-in animation from its resting position (bottom-right corner) at 400ms standard. The insight card slides up from below the fold when new insights are available.

The Coach FAB does not pulse or animate continuously at rest. An unread-insight indicator uses a simple emerald dot (bg-success, 8px) positioned at the top-right of the FAB, with fade-in on appearance. No bounce, no glow, no looping animation — the dot is a signal, not an advertisement.


6. Anti-patterns

These behaviors violate brand principles or degrade the user experience. Do not ship them.

Anti-patternWhy it's wrongCorrect alternative
Animation on an element with no state changeCreates visual noise; feels like engagement baitRemove the animation
bounce curve on ordinary UI (non-gamification)Dilutes the "earned" signal of the bounceUse standard or ease-out
Looping animation in dashboard chromeCompetes for attention; the brand is anti-engagement-loopReserve loops for gamification overlays
Spinner on actions < 200msMakes fast actions feel slowerUse button disabled state
Tooltip immediately on hover (0ms delay)Flickers on fast cursor passesUse 400ms delay
Tooltip with > 1 sentenceInformation overload in a tooltip contextUse a bottom sheet or info modal
Backdrop click always closes a form modalUser loses unsaved dataBlock close; show shake animation
Scale-out / slide-down on modal exitSlows down close; reverses the entry feelFade-out only (200ms)
Hover lift on non-interactive cardMisleads user into thinking the card is clickableRemove transition + translateY from non-interactive cards
No reduced-motion fallbackTriggers vestibular discomfort for affected usersReplace transforms with opacity fades
Focus ring suppressed (outline: none without replacement)Breaks keyboard navigationProvide visible :focus-visible ring
Dark-mode state not testedComponent often breaks at dark-mode boundaryEvery state must be verified in both themes

7. Component cross-reference

ComponentHover patternModal behaviorAnimation notes
GoalCard.vuetranslateY(-4px) lift, shadow-xlOpens Goal Detail view (page transition)slide-in-right on nav
BadgeModal.vueN/ABottom sheet on mobile, centered on desktopscale-in enter; badge uses point-pop
BottomTabBar.vueColor shift text-muted → text-primary-interactiveN/AActive tab: no animation; tap: fast color
UiTooltip.vueN/APositioned bubble400ms delay; 200ms fade enter/exit
Coach FABGlow expand shadow-coachOpens Coach sheetscale-in 400ms
Progress ring (various)N/AN/Astroke-dashoffset 500ms on value change
XP progress barN/AN/AFill animates on XP gain, width 500ms
ToastN/AN/Aslide-up + fade enter; fade exit
Skeleton loadersN/AN/Ashimmer 2s linear infinite

8. Maintenance notes

This document is manually maintained. Update it when:

  • A new interaction pattern is introduced that is not covered here.
  • A token value or timing constant changes in DESIGN.md or design_system/.
  • A component diverges from these patterns and the divergence is intentional (document the exception here).
  • A new gamification mechanic requires a new celebration pattern.

When updating, add your change under the relevant section and note the date in a comment inside the file. Do not change the section numbering — it is referenced from the Storybook story briefs in some component files.

Last substantively updated: 2026-05-22 (Phase 94 audit: motion tokens and interaction patterns verified)


Last updated: 2026-07-09

Loading…