# Habits

> A daily check-in: the mood scale and a line for the journal, the week's habits, the latest entry and longer goals beside it.

Pattern · Pro kit · [Live page](https://ui.twentyfirst.media/pro/patterns/HabitsScreen/)

A daily check-in: the mood scale and a line for the journal, the week's habits, the latest entry
and longer goals beside it.


The whole screen is glass over `Ambient`: one `<UIProvider surface="glass">` turns the shell, the
sidebar, panels and the Life cards (vitals, goals, insights, journal entries) into glass, with
inputs and chips on them translucent too. Without the provider the same markup is solid.

## Code

```tsx
import * as React from "react";
import {
  Ambient,
  AppShell,
  Button,
  GoalCard,
  HabitTracker,
  JournalEntry,
  MoodPicker,
  PageHeader,
  Panel,
  Stagger,
  Textarea,
  UIProvider,
} from "@twentyfirstmedia/pro";
import type { Habit, MoodLevel, SidebarGroup } from "@twentyfirstmedia/pro";

const NAV: SidebarGroup[] = [
  {
    items: [
      { id: "today", label: "Today", icon: "sun" },
      { id: "assistant", label: "Assistant", icon: "sparkle" },
    ],
  },
  {
    label: "Life",
    items: [
      { id: "health", label: "Health", icon: "heartPulse" },
      { id: "habits", label: "Habits & journal", icon: "check" },
      { id: "money", label: "Money", icon: "wallet" },
      { id: "insurance", label: "Insurance", icon: "shield" },
      { id: "documents", label: "Documents", icon: "folder" },
    ],
  },
];

/** A daily check-in in glass over ambient colour: mood, habits, a journal and longer goals. */
export default function Preview() {
  const [mood, setMood] = React.useState<MoodLevel | null>(null);
  const [habits, setHabits] = React.useState<Habit[]>([
    {
      id: "walk",
      label: "Morning walk",
      icon: "sun",
      tint: "butter",
      days: [true, true, false, true, true, true, false],
      streak: 3,
      goal: "20 min",
    },
    {
      id: "read",
      label: "Read",
      icon: "book",
      tint: "lilac",
      days: [false, true, false, true, false, true, true],
      streak: 2,
      goal: "3× a week",
    },
    {
      id: "screens",
      label: "No screens after 10 pm",
      icon: "moon",
      tint: "mint",
      days: [true, false, false, true, true, false, false],
    },
  ]);
  return (
    <UIProvider scope="subtree" surface="glass">
      <Ambient />
      <AppShell
        navigation="sidebar"
        brand="Life"
        sidebarGroups={NAV}
        active="habits"
        user={{ name: "Mara Ellis", email: "mara@halo.app" }}
        dock={false}
        side={
          <Panel side title="Goals" subtitle="2 active">
            <GoalCard
              title="Read 24 books"
              icon="book"
              tint="lilac"
              value={17}
              target={24}
              unit="books"
              due={new Date(2026, 11, 31)}
            />
            <GoalCard
              title="Emergency fund"
              icon="wallet"
              tint="mint"
              value={3100}
              target={5000}
              format="money"
              due={new Date(2027, 5, 1)}
            />
          </Panel>
        }
      >
        <div className="tf-in">
          <PageHeader title="Habits & journal" subtitle="Friday, 13 November" />
        </div>
        <Stagger className="tf-col" style={{ gap: 16 }}>
          <Panel title="Check in">
            <MoodPicker value={mood} onChange={setMood} />
            <Textarea label="A line about today" placeholder="What stood out?" rows={2} />
            <div className="tf-row" style={{ justifyContent: "flex-end" }}>
              <Button variant="primary" disabled={!mood}>
                Save entry
              </Button>
            </div>
          </Panel>
          <Panel title="Habits" subtitle="This week">
            <HabitTracker
              habits={habits}
              dayLabels={["M", "T", "W", "T", "F", "S", "S"]}
              onToggle={(id, done) =>
                setHabits((all) =>
                  all.map((h) =>
                    h.id === id ? { ...h, days: [...h.days.slice(0, -1), done] } : h,
                  ),
                )
              }
            />
          </Panel>
          <JournalEntry
            date={new Date(2026, 10, 12)}
            mood={4}
            title="Slow morning, good walk"
            tags={["Walk", "Family"]}
          >
            <p>
              Slept through for once. Walked to the harbour before work and the day felt lighter
              after that.
            </p>
          </JournalEntry>
        </Stagger>
      </AppShell>
    </UIProvider>
  );
}
```

## Props

### AppShell

| Prop | Type | Notes |
| --- | --- | --- |
| `active?` | `string` |  |
| `brand?` | `ReactNode` | The product name, or a node (a logo mark with the name; collapsed sidebars show it whole). |
| `tag?` | `string` |  |
| `nav?` | `NavItem[]` |  |
| `user?` | `{ name: string; email?: string }` |  |
| `onNavigate?` | `(label: string) => void` |  |
| `topExtra?` | `ReactNode` |  |
| `sheetStyle?` | `CSSProperties` |  |
| `contentKey?` | `string \| number` |  |
| `side?` | `ReactNode` |  |
| `dock?` | `boolean` |  |
| `dockItems?` | `DockProps["items"]` |  |
| `railTop?` | `RailItem[]` |  |
| `railBottom?` | `RailItem[]` |  |
| `navigation?` | `"top" \| "sidebar"` | `top` (default): navigation in the top bar, tools in the side rail. `sidebar`: a labelled sidebar on the frame, with groups, nesting and a drawer on phones. |
| `sidebarGroups?` | `SidebarGroup[]` | With `navigation="sidebar"`: groups instead of the flat `nav` list. |
| `sidebar?` | `Partial<Omit<SidebarProps, "variant" \| "groups">>` | With `navigation="sidebar"`: more sidebar props (collapsed, storageKey, shortcut, linkAs …). |
| `workspaces?` | `WorkspaceSwitcherProps` | With `navigation="sidebar"`: the workspace switcher at the top instead of the brand. |
| `userMenu?` | `(MenuItem \| "-")[]` | The account menu's entries (with `navigation="sidebar"` at the foot of the sidebar). |
| `children?` | `ReactNode` |  |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |

### MoodPicker

| Prop | Type | Notes |
| --- | --- | --- |
| `value?` | `MoodLevel \| null` |  |
| `defaultValue?` | `MoodLevel \| null` |  |
| `onChange?` | `(mood: MoodLevel) => void` |  |
| `label?` | `ReactNode` | The question (default "How are you feeling?"). |
| `levels?` | `[string, string, string, string, string]` | Words for the five levels, lowest first (default Awful, Low, Okay, Good, Great). |
| `compact?` | `boolean` | Only the faces; the words stay as accessible names. |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |

### HabitTracker

| Prop | Type | Notes |
| --- | --- | --- |
| `habits` | `Habit[]` | Required |
| `dayLabels?` | `string[]` | Labels of the day columns, oldest first (default: weekday initials ending today). |
| `onToggle?` | `(id: string, done: boolean) => void` | Today's box of a habit was toggled. |
| `label?` | `string` | Accessible name of the list (default "Habits"). |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |

### JournalEntry

| Prop | Type | Notes |
| --- | --- | --- |
| `surface?` | `Surface` | `"glass"`: translucent glass over a backdrop (`Ambient`, a photo, a map); `"solid"` opts out inside a glass `UIProvider`. |
| `date` | `Date \| string \| number` | Required |
| `title?` | `string` |  |
| `mood?` | `MoodLevel` |  |
| `tags?` | `string[]` |  |
| `children?` | `ReactNode` |  |
| `actions?` | `ReactNode` | Buttons at the end (edit, share …). |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |

### GoalCard

| Prop | Type | Notes |
| --- | --- | --- |
| `surface?` | `Surface` | `"glass"`: translucent glass over a backdrop (`Ambient`, a photo, a map); `"solid"` opts out inside a glass `UIProvider`. |
| `title` | `string` | Required |
| `value` | `number` | Required |
| `target` | `number` | Required |
| `format?` | `"number" \| "money" \| "percent"` | How to show value and target (default number). |
| `unit?` | `string` | Unit after numbers ("km", "books"). |
| `currency?` | `string` |  |
| `due?` | `Date \| string \| number` |  |
| `icon?` | `IconName` |  |
| `tint?` | `Tint` |  |
| `milestones?` | `{ label: string; done?: boolean }[]` |  |
| `note?` | `ReactNode` | A note under the progress ("€250 a month keeps you on track"). |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |

### PageHeader

| Prop | Type | Notes |
| --- | --- | --- |
| `crumbs?` | `(string \| Crumb)[]` | A breadcrumb above the title (see `Breadcrumb`): labels, or items with `href`/`onClick`. |
| `crumbLinkAs?` | `ElementType` | The element crumb links render as, e.g. Next.js `Link`. |
| `title` | `string` | Required |
| `subtitle?` | `string` |  |
| `actions?` | `ReactNode[]` |  |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |

### Panel

| Prop | Type | Notes |
| --- | --- | --- |
| `side?` | `boolean` |  |
| `title?` | `string` |  |
| `subtitle?` | `string` |  |
| `action?` | `ReactNode` |  |
| `children?` | `ReactNode` |  |
| `surface?` | `Surface` | `"glass"`: translucent glass over what is behind (maps, 3D, video, photos); `"solid"` opts out inside a glass `UIProvider`. |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |

### Stagger

| Prop | Type | Notes |
| --- | --- | --- |
| `as?` | `string` |  |
| `className?` | `string` |  |
| `style?` | `CSSProperties` |  |
| `delay?` | `number` | Extra delay before the first child, in ms. |
| `reveal?` | `boolean` | Below the fold, wait until the group scrolls into view (default true). |
| `children?` | `ReactNode` |  |

### Textarea

| Prop | Type | Notes |
| --- | --- | --- |
| `placeholder?` | `string` |  |
| `value?` | `string` |  |
| `defaultValue?` | `string` |  |
| `rows?` | `number` |  |
| `disabled?` | `boolean` |  |
| `onChange?` | `(e: ChangeEvent<HTMLTextAreaElement>) => void` |  |
| `label?` | `string` |  |
| `hint?` | `string` |  |
| `error?` | `string` |  |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |

### Button

| Prop | Type | Notes |
| --- | --- | --- |
| `variant?` | `"primary" \| "secondary" \| "soft" \| "ghost" \| "danger" \| "danger-solid"` |  |
| `size?` | `"md" \| "sm"` |  |
| `icon?` | `IconName` |  |
| `iconRight?` | `IconName` |  |
| `disabled?` | `boolean` |  |
| `type?` | `"button" \| "submit" \| "reset"` |  |
| `onClick?` | `(e: MouseEvent<HTMLButtonElement>) => void` |  |
| `children?` | `ReactNode` |  |
| `asChild?` | `boolean` | Render the single child element (a link, a router Link) with the button's look instead of a <button>. |
| `id?` | `string` |  |
| `name?` | `string` | Form field name and value, sent with the form when this button submits it. |
| `value?` | `string` |  |
| `form?` | `string` | The id of the form this button belongs to, when it sits outside it. |
| `title?` | `string` |  |
| `tabIndex?` | `number` |  |
| `autoFocus?` | `boolean` |  |
| `aria-label?` | `string` |  |
| `aria-labelledby?` | `string` |  |
| `aria-describedby?` | `string` |  |
| `aria-expanded?` | `boolean \| "true" \| "false"` | For disclosure and menu triggers: whether what the button controls is open. |
| `aria-controls?` | `string` |  |
| `aria-haspopup?` | `boolean \| "true" \| "false" \| "menu" \| "listbox" \| "tree" \| "grid" \| "dialog"` |  |
| `aria-pressed?` | `boolean \| "true" \| "false" \| "mixed"` | For toggle buttons. |
| `aria-current?` | `boolean \| "true" \| "false" \| "page" \| "step" \| "location" \| "date" \| "time"` |  |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |
