Mood Check-in
A daily mood check-in: pick from five faces, add tags for what’s behind it, and see the week as a row of small faces.
$
View as Markdownnpx shadcn@latest add https://sundaeui.dev/r/mood-check-in.json'use client'
import { useState } from 'react'
import './mood-check-in.css'
const LABELS = ['Awful', 'Low', 'Okay', 'Good', 'Great']
const TAGS = ['Sleep', 'Work', 'Exercise', 'People', 'Weather', 'Health']
const Face = ({ m }: { m: number }) => {
const c = [-5, -2.5, 0, 3, 5.5][m - 1]
return <svg viewBox="0 0 36 36" aria-hidden><circle cx="18" cy="18" r="15" /><path d={`M12 ${m === 1 ? 14 : 15}h0.01M24 ${m === 1 ? 14 : 15}h0.01`} strokeWidth="3" /><path d={`M11.5 ${23 - c / 3}Q18 ${23 + c} 24.5 ${23 - c / 3}`} /></svg>
}
export interface MoodCheckInProps {
week?: (number | null)[]
note?: boolean
onSave?: (e: { mood: number; tags: string[]; note: string }) => void
fill?: 'dark' | 'light'
className?: string
}
/** Five-face mood check-in with tags and a week strip. */
export function MoodCheckIn({ week = [3, 4, null, 2, 4, 5], note: showNote = true, onSave, fill = 'dark', className }: MoodCheckInProps) {
const [mood, setMood] = useState(0)
const [tags, setTags] = useState<string[]>([])
const [note, setNote] = useState('')
const [today, setToday] = useState<number | null>(null)
const days = ['Thu', 'Fri', 'Sat', 'Sun', 'Mon', 'Tue', 'Today']
return (
<div className={['sd-mod', className].filter(Boolean).join(' ')} data-fill={fill}>
<h3>{today ? 'Logged. See you tomorrow.' : 'How are you feeling?'}</h3>
<div className="sd-mod__faces" role="radiogroup" aria-label="Mood"
onKeyDown={(e) => { const d = e.key === 'ArrowRight' ? 1 : e.key === 'ArrowLeft' ? -1 : 0; if (d) { e.preventDefault(); setMood((m) => Math.max(1, Math.min(5, (m || 3) + d))) } }}>
{[1, 2, 3, 4, 5].map((m) => <button key={m} type="button" role="radio" aria-checked={mood === m} aria-label={LABELS[m - 1]} tabIndex={mood === m || (!mood && m === 3) ? 0 : -1} className="sd-mod__f" onClick={() => setMood(m)}><Face m={m} /></button>)}
</div>
<p className="sd-mod__lab" aria-live="polite">{mood ? LABELS[mood - 1] : ''}</p>
<div className="sd-mod__tags" role="group" aria-label="What’s behind it">
{TAGS.map((t) => <button key={t} type="button" aria-pressed={tags.includes(t)} onClick={() => setTags((x) => (x.includes(t) ? x.filter((y) => y !== t) : [...x, t]))}>{t}</button>)}
</div>
{showNote && <textarea rows={2} aria-label="Note (optional)" placeholder="Anything on your mind? (optional)" value={note} onChange={(e) => setNote(e.target.value)} />}
<button type="button" className="sd-mod__go" disabled={!mood} onClick={() => { setToday(mood); onSave?.({ mood, tags, note }) }}>{today ? 'Update today' : 'Save check-in'}</button>
<div className="sd-mod__wk" aria-label={`This week: ${[...week, today].map((m, i) => `${days[i]} ${m ? LABELS[m - 1] : 'no entry'}`).join(', ')}`}>
{[...week, today].map((m, i) => <span key={i + '-' + m} data-on={i === 6 && m ? '' : undefined} data-new={i === 6 && m ? '' : undefined} aria-hidden>{m ? <Face m={m} /> : <i />}{days[i]}</span>)}
</div>
</div>
)
}.sd-mod { --bg: #1c1c1c; --fg: #f6f6f6; --mute: #8e8e8e; --line: rgb(255 255 255 / 0.1); --soft: rgb(255 255 255 / 0.07); --strong: #fafafa; --on: #111;
color: var(--fg); font-size: 14px; line-height: 1.4; -webkit-tap-highlight-color: transparent; }
.sd-mod[data-fill='light'] { --bg: #fff; --fg: #121212; --mute: #6f6f6f; --line: rgb(0 0 0 / 0.09); --soft: rgb(0 0 0 / 0.05); --strong: #111; --on: #fff; }
.sd-mod :where(button) { font: inherit; color: inherit; background: none; border: 0; padding: 0; cursor: pointer; }
.sd-mod :focus-visible { outline: 2px solid var(--fg); outline-offset: 2px; }
.sd-mod .sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
.sd-mod { width: min(100%, 330px); padding: 18px; border-radius: 22px; background: var(--bg); box-shadow: inset 0 0 0 1px var(--line); }
.sd-mod h3 { margin: 0 0 14px; font-size: 16px; text-align: center; }
.sd-mod__faces { display: flex; justify-content: space-between; margin-bottom: 6px; }
.sd-mod__f { display: grid; place-items: center; width: 52px; height: 52px; border-radius: 99px; color: var(--mute) !important; transition: transform 220ms cubic-bezier(0.3, 1.6, 0.4, 1), color 150ms, background-color 150ms; }
.sd-mod__f svg { width: 36px; height: 36px; fill: none; stroke: currentColor; stroke-width: 1.7; stroke-linecap: round; }
.sd-mod__f[aria-checked='true'] { background: var(--strong) !important; color: var(--on) !important; transform: scale(1.12); }
.sd-mod__lab { margin: 6px 0 14px; text-align: center; font-size: 14px; font-weight: 600; min-height: 20px; }
.sd-mod__tags { display: flex; flex-wrap: wrap; gap: 6px; justify-content: center; margin-bottom: 12px; }
.sd-mod__tags button { padding: 6px 12px; border-radius: 99px; font-size: 12.5px; box-shadow: inset 0 0 0 1px var(--line); color: var(--mute) !important; }
.sd-mod__tags button[aria-pressed='true'] { background: var(--soft) !important; color: var(--fg) !important; box-shadow: inset 0 0 0 1.5px var(--fg); }
.sd-mod textarea { width: 100%; padding: 10px 12px; border: 0; border-radius: 12px; background: var(--soft); color: var(--fg); font: inherit; font-size: 13px; resize: none; outline: 0; }
.sd-mod textarea:focus { box-shadow: inset 0 0 0 1.5px var(--fg); }
.sd-mod__go { width: 100%; height: 44px; margin-top: 10px; border-radius: 12px; background: var(--strong) !important; color: var(--on) !important; font-weight: 600; }
.sd-mod__go:disabled { opacity: 0.35; }
.sd-mod__wk { display: grid; grid-template-columns: repeat(7, 1fr); gap: 4px; margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--line); text-align: center; font-size: 10.5px; color: var(--mute); }
.sd-mod__wk svg { width: 22px; height: 22px; margin: 0 auto 2px; display: block; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; }
.sd-mod__wk span[data-on] { color: var(--fg); }
.sd-mod__wk span[data-new] svg { animation: sd-mod-pop 400ms cubic-bezier(0.3, 1.6, 0.4, 1); }
.sd-mod__wk i { display: block; width: 22px; height: 22px; margin: 0 auto 2px; border-radius: 99px; box-shadow: inset 0 0 0 1.5px var(--line); }
@keyframes sd-mod-pop { from { transform: scale(0); } }
@media (prefers-reduced-motion: reduce) { .sd-mod *, .sd-mod *::before, .sd-mod *::after { animation: none !important; transition: none !important; } }Overview
Faces morph as you move between them. Tags (sleep, work, exercise, people) add context in one tap; a short note is optional.
Saving drops today’s face into the week strip so patterns show without charts.
When to use
- Wellbeing apps, team pulse checks, journaling.
When not to use
- Clinical assessments: use validated questionnaires.
Installation
$
pnpm dlx shadcn@latest add https://sundaeui.dev/r/mood-check-in.json$
npx shadcn@latest add https://sundaeui.dev/r/mood-check-in.json$
yarn dlx shadcn@latest add https://sundaeui.dev/r/mood-check-in.json$
bunx --bun shadcn@latest add https://sundaeui.dev/r/mood-check-in.jsonWorks in any React project. Files are written to components/sundae/. Read the source in the stage’s Code view.
- No dependencies beyond React 18 or later.
- Copy mood-check-in.tsx and mood-check-in.css into components/sundae/.
- The CSS file is imported by the component, so no extra configuration is needed.
- Import the component and use it as shown below.
Usage
import { MoodCheckIn } from '@/components/sundae/mood-check-in'
<MoodCheckIn />Props
| Prop | Type | Default |
|---|---|---|
weekPast six days, 1–5. | (number | null)[] | — |
onSaveSave callback. | (entry: { mood: number; tags: string[]; note: string }) => void | — |
fillSurface: dark or light. | 'dark' | 'light' | 'dark' |
Accessibility
- Moods are a radiogroup with spoken labels (“Good”), not just faces.
- Tags are toggle buttons; the week strip has a text summary.
- Reduced motion removes the movement and keeps every state change.
Customization
- Colours are CSS custom properties on .sd-mod; override them in your own stylesheet.
- fill="light" switches the built-in light palette.
FAQ
- Is Mood Check-in free?
- Yes. Mood Check-in is free for personal and commercial projects, with full source on this page.
- How do I install Mood Check-in?
- Run npx shadcn@latest add https://sundaeui.dev/r/mood-check-in.json. The files are written to components/sundae/. It has no dependencies beyond React 18 or later.
- Does Mood Check-in support keyboard and screen readers?
- Moods are a radiogroup with spoken labels (“Good”), not just faces. Tags are toggle buttons; the week strip has a text summary.
- Does Mood Check-in respect reduced motion?
- Reduced motion removes the movement and keeps every state change.
More like this
All forms →OTP Field
Forms · Refined- Pro
Drop Zone
Forms · Refined - Pro
Onboarding Flow
Forms · Refined - Pro
Wheel Picker
Forms · Refined Password Strength
Forms · RefinedRating Scale
Forms · Refined