# affectUI > Agent-ready React UI for AI products and SaaS: 96 components, screens, blocks, illustrations and motion effects. Every item is one .tsx file with its styles inside – no Tailwind, no animation library, no other dependencies. React 19, dark mode and reduced motion built in. 39 items are free and open source (MIT). The other 57 are part of affectUI Pro, a one-time purchase (https://www.affectui.com/pricing). ## How to add an item - Free items: `npx shadcn@latest add https://www.affectui.com/r/.json` – the file lands in components/ui. - Short names: register `"@affectui": "https://www.affectui.com/r/{name}.json"` under "registries" in components.json, then `npx shadcn@latest add @affectui/`. - Pro items: the same commands, with the buyer's access key (AFFECTUI_TOKEN, created under https://www.affectui.com/account/downloads) sent as `Authorization: Bearer ` – set it as a header on the @affectui registry in components.json. Without a key the registry answers 401. - Import from `@/components/ui/` and keep the component's own styles and motion; adapt only content and props. - MCP server: https://www.affectui.com/api/mcp (Streamable HTTP) – search items, read docs and fetch source from Claude Code, Cursor, VS Code or Codex. --- ## Agent Plan (Pro) An agent's step-by-step plan to review before it runs – rename, drag to reorder, remove or add steps, then watch each one run with live timers and retry from a failed step - Page: https://www.affectui.com/components/agent-plan - Section: Components · Agents - Install: `npx shadcn@latest add https://www.affectui.com/r/agent-plan.json` (needs the Pro access key) - Import: `@/components/ui/agent-plan` ### When to use - An agent is about to do several things in a row and people should agree to the plan first - Long or expensive runs (tool calls, emails, writes) where a wrong order or an extra step costs real time - Showing the same plan as it executes – which step is running, how long each took, where it failed ### When not to use - A single action that needs a yes or no – use Approval Card - Model reasoning without editable steps – use Thinking State - A user's own onboarding checklist – use Progress Tracker ### Usage ```tsx import { useState } from "react"; import { AgentPlan, type PlanStep } from "@/components/ui/agent-plan"; export function PlanReview({ plan }: { plan: PlanStep[] }) { const [steps, setSteps] = useState(plan); const [status, setStatus] = useState<"draft" | "running" | "done">("draft"); return ( { setStatus("running"); agent.run(final); // stream step updates back into setSteps }} onCancel={() => agent.discard()} onRetry={(id) => agent.retryFrom(id)} /> ); } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | steps | `PlanStep[]` | | Controlled steps: { id, title, detail?, tool?, status?, duration?, error? }. Pair with onChange | | defaultSteps | `PlanStep[]` | `[]` | Initial steps when uncontrolled – edits are kept inside the component | | status | `"draft" \| "running" \| "done"` | `"draft"` | Review and edit, executing, or finished. Editing is only possible in draft | | onChange | `(steps: PlanStep[]) => void` | | Every edit during review: rename, remove, reorder, add | | onRun | `(steps: PlanStep[]) => void` | | "Run plan" pressed – gets the steps in their final order. Set status to "running" here. Also shows "Run again" on the tray once done | | onCancel | `() => void` | | Shows a Cancel button next to Run plan | | onRetry | `(stepId: string) => void` | | Shows "Retry from here" under failed steps | | title | `string` | | The goal of the plan, shown as the heading and used as the list's label | | estimate | `string` | | Estimated run time shown in review, e.g. "~2 min" | | className | `string` | | Extra class on the root | ### Keyboard - Tab: Moves between steps, their remove buttons, the Add step field and the actions - ↑ / ↓: Moves focus to the previous or next step - Alt / ↑ / ↓: Moves the focused step up or down - Enter: Renames the focused step; Enter again saves. In Add step: adds the step at the end and keeps the field ready for the next one - Esc: Cancels a rename - Delete: Removes the focused step ### Accessibility - The steps are an ordered list labelled with the plan's title; each step is focusable in review and describes its shortcuts - Moves, renames, removals and additions are announced in a polite live region ("Moved 'Draft the board memo' to position 3 of 5") - While running, each step carries its status as text, the active one has aria-current="step", and finished or failed steps are announced - The drag grip is pointer-only; Alt+arrow keys do the same from the keyboard - Icon buttons are labelled, focus rings show on keyboard focus only, and the folded-away Add step is inert while running ### Motion - Reordering is live: the dragged step follows the pointer, the others slide aside, and everything settles with a FLIP glide – also for keyboard moves and order changes from props - Removed steps collapse and the list closes the gap; added steps fade up into place - Numbers in a hairline ring turn into a ray spinner, then into a check on a light gray fill that draws itself; the progress line fills segment by segment - Add step folds away when the plan starts, and the tray below the card swaps its actions for the run's status – "Running · 2 of 5" with the elapsed time, then "Done in 14s" - With prefers-reduced-motion everything switches instantly and the spinner stands still ### Responsive - Fills its container up to 460px; titles and details wrap, tool chips stay on one line - Below 400px the footer hint drops away, below 340px gutters tighten – works down to 320px - On touch screens the grip and remove button are always visible - Dark mode built in: switches when an ancestor (usually ) has the class dark or data-theme="dark" ### Notes for AI - Give every step a stable id – reorder animations, timers and announcements follow the id, not the index - Leave duration out to have it measured from when a step turned active; pass it when your agent reports its own timing - On retry, reset the failed step and everything after it to "pending" and keep status "running" - Run the steps onRun hands you, not the original proposal – the user may have renamed, removed or reordered them ### Requirements - React 19 Related: progress-tracker, tool-call-card, approval-card, thinking-state --- ## AI Answer (Pro) Streamed answer with source cards, citation chips that preview their source, copy, regenerate, feedback and follow-ups - Page: https://www.affectui.com/components/ai-answer - Section: Components · Chat - Install: `npx shadcn@latest add https://www.affectui.com/r/ai-answer.json` (needs the Pro access key) - Import: `@/components/ui/ai-answer` ### When to use - Answers from an assistant that searched your docs, the web or connected tools and should show where each claim comes from - Research, support or knowledge-base products where people need to check the source before they act ### When not to use - Casual chat without sources – a plain message bubble is lighter - Long documents or code – the answer is made for a few paragraphs and lists ### Usage ```tsx import { AIAnswer, type AnswerSource } from "@/components/ui/ai-answer"; const sources: AnswerSource[] = [ { name: "Notion", title: "Offsite plan – shortlist and budget", href: "https://notion.so/…", snippet: "Budget €4,500 for three nights…" }, { name: "Slack", title: "#team-offsite – who can make which dates", href: "https://slack.com/…" }, ]; export function Answer({ text, done }: { text: string; done: boolean }) { return ( regenerate()} onFeedback={(value, reason) => track("answer_feedback", { value, reason })} /> ); } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | question | `string` | | Shown as the title above the sources | | sources | `AnswerSource[]` | | Each { title, name, href?, icon?, snippet? }; name is the short label on the citation chip | | answer | `string` | | Blank lines make paragraphs, lines starting with "- " a list, **bold**, [1] or [1,3] cite sources (1-based). It may grow while streaming; a different text starts over | | streaming | `boolean` | `false` | More text is still coming from your model – keeps the cursor going once it has caught up | | animate | `boolean` | `true` | Stream the text in; false shows it at once | | speed | `number` | `38` | Words per second while streaming in | | followUps | `string[]` | | Related questions shown below the finished answer | | onFollowUp | `(question: string) => void` | | A follow-up was clicked | | onRegenerate | `() => void` | | Regenerate was clicked – the answer streams in again; pass a new answer to replace it | | onStop | `() => void` | | Stop was clicked while writing – cancel your request here | | onFeedback | `(value: "up" \| "down", reason?: string) => void` | | Thumbs up, or thumbs down with the reason that was picked | | feedbackReasons | `string[]` | `["Not accurate", "Wrong source", "Too long", "Out of date"]` | Reasons offered after a thumbs down | | onCopy | `(text: string) => void` | | Called with the copied text: the answer without markup plus a numbered source list | | className | `string` | | Extra class on the root | ### Keyboard - Tab: Moves through source cards, citation chips, follow-ups and actions - Esc: Closes the source preview of the focused chip ### Accessibility - Citation chips are buttons named after their sources; the preview is a tooltip linked to the chip - The answer is marked busy while it streams, so screen readers read it once it's complete - Copy, feedback and status changes are announced in a polite live region ### Motion - Source cards pop in one after another while the label reads "Reading 6 sources" - Words fade in out of a light blur, citation chips pop in, a pulsing dot marks where the text grows - Hovering a chip lights up its source card, hovering a card lights up its chips - The card glides to its new height; with prefers-reduced-motion the answer appears at once ### Responsive - Fills its container up to 640px; source cards scroll sideways on narrow screens - Tap a citation chip to open its preview on touch screens - Dark mode built in: switches when an ancestor (usually ) has the class dark or data-theme="dark" ### Requirements - React 19 Related: thinking-state, chat-composer, comment-thread --- ## Approval Card (Pro) An agent asks before it acts – risk badge, preview, Approve and Deny with shortcuts, a pausing countdown, and a smooth morph into a resolved row - Page: https://www.affectui.com/components/approval-card - Section: Components · Agents - Install: `npx shadcn@latest add https://www.affectui.com/r/approval-card.json` (needs the Pro access key) - Import: `@/components/ui/approval-card` ### When to use - An agent is about to do something hard to undo – send email, delete files, spend money, deploy – and a person should say yes first - Human-in-the-loop tool calls in a chat or an agent run log, one card per pending call - Requests that should expire on their own when nobody answers ### When not to use - Many pending requests at once that people triage in bulk – use Decision Inbox - Harmless, reversible actions – just do them and offer undo - Destructive actions the user triggered themselves – a regular confirm dialog fits better ### Usage ```tsx import { ApprovalCard } from "@/components/ui/approval-card"; export function SendApproval({ call }: { call: PendingToolCall }) { return ( call.approve({ always })} // return the promise – the button shows a spinner onDeny={({ reason }) => call.reject(reason)} /> ); } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | title | `string` | | The action, e.g. "Send campaign email" – also the card's accessible name | | description | `ReactNode` | | One line of context below the title; truncates | | risk | `"low" \| "medium" \| "high"` | | Tinted badge in the header (gray, amber, red); leave out to hide it | | icon | `ReactNode` | `shield` | Icon in the 32px header tile; SVGs get a 16px stroke style | | details | `{ label: string; value: ReactNode }[]` | | Key-value rows in the preview panel | | preview | `ReactNode` | | Custom content above the details, e.g. recipients or a diff – children work the same | | children | `ReactNode` | | Used as the preview when preview isn't set | | status | `"pending" \| "approved" \| "denied"` | | Controlled status; setting it back to "pending" starts a fresh request | | defaultStatus | `"pending" \| "approved" \| "denied"` | `"pending"` | Starting status when uncontrolled | | onApprove | `({ always }) => void \| Promise` | | Called on Approve or ⌘/Ctrl+Enter. Return a promise to show a spinner and lock the card; a rejection keeps it pending | | onDeny | `({ reason: "user" \| "timeout" }) => void` | | Called on Deny, Escape, or when the countdown runs out | | onEdit | `() => void` | | Shows an Edit button before Deny | | expiresIn | `number` | | Seconds until the request expires; shown as quiet "Expires in 0:42" text in the tray next to the buttons, amber in the last 10 seconds, paused while hovered or focused | | onExpire | `() => void` | | Called when the countdown runs out, right before onDeny({ reason: "timeout" }) | | allowAlways | `boolean` | `true` | Show the "Always allow this action" checkbox | | alwaysLabel | `string` | `"Always allow this action"` | Label of the checkbox | | approveLabel | `string` | `"Approve"` | Primary button label | | denyLabel | `string` | `"Deny"` | Quiet button label | | editLabel | `string` | `"Edit"` | Edit button label | | shortcuts | `boolean` | `true` | Show the ⌘↵ / Esc hints on the buttons – the shortcuts work either way | | label | `ReactNode` | | Small label on the gray tray above the card, e.g. "Step 4 · gmail.send" | | actions | `"tray" \| "card"` | `"tray"` | Where the buttons sit: on the gray tray below the card, or inside the white card (handy in narrow side panels next to other cards) | | className | `string` | | Extra class on the root | ### Keyboard - ⌘ / Enter: Approve while focus is anywhere in the card (Ctrl+Enter on Windows and Linux) - Escape: Deny while focus is in the card - Tab: Moves through the checkbox, Edit, Deny and Approve ### Accessibility - role="group" named by the title and described by the description and the countdown - Real buttons with aria-keyshortcuts; the kbd hints are aria-hidden - The decision is announced through a polite live region ("Approved: Send campaign email") - The countdown has a full text description ("Expires in 24 seconds unless approved…") linked via aria-describedby; the visible clock is aria-hidden so it isn't read every second, and it pauses while the card is hovered or focused - While an approval is processing the card is aria-busy and the buttons are aria-disabled, so focus is never lost - After a decision focus moves to the resolved row (tabIndex -1) if it was inside the card ### Motion - On a decision the card and its tray footer fade and blur out, the height glides down to a compact row inside the tray and the check or cross draws itself in - The countdown text ticks down once per second without animation and turns amber in the last 10 seconds - Buttons press in slightly; the checkbox tick scales in - With prefers-reduced-motion everything switches instantly and only the spinner keeps turning slowly ### Responsive - Fills its container; below 400px card width the buttons share a full-width row and the shortcut hints hide - Below 340px the risk badge drops the word "risk" and the resolved row drops its time - Titles, descriptions and detail values truncate instead of wrapping; works down to 320px - Shortcut hints are hidden on touch screens - Dark mode built in: switches when an ancestor (usually ) has the class dark or data-theme="dark" ### Notes for AI - Render one card per pending tool call that needs consent; resolve the tool call with the onApprove / onDeny result - Return the real execution promise from onApprove so the card stays locked until the action has actually started - Persist { always: true } per tool name and skip the card next time - Set risk from the action, not the model's confidence: anything external, paid or irreversible is at least medium - Keep title an imperative verb phrase and put the concrete values (recipients, amount, target) in details ### Requirements - React 19 Related: tool-call-card, decision-inbox, thinking-state, thinking-loader --- ## Billing Schedule (Pro) Build a billing phase – start date, duration, products with repeats and timing, discount, minimum and live totals - Page: https://www.affectui.com/components/billing-schedule - Section: Components · Inputs - Install: `npx shadcn@latest add https://www.affectui.com/r/billing-schedule.json` (needs the Pro access key) - Import: `@/components/ui/billing-schedule` ### When to use - Quoting and contract tools where a phase of billing is put together – set-up fees, licences, support - Admin screens for subscriptions with one-off and recurring charges, discounts and minimum commitments ### When not to use - A simple plan picker – use the Pricing Plans block - Usage-based pricing with tiers per unit – this works with fixed prices per line ### Usage ```tsx import { BillingSchedule } from "@/components/ui/billing-schedule"; export function NewContract() { return ( save(schedule)} /> ); } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | title | `string` | `"Create a billing schedule"` | Heading on the tray | | phase | `string` | `"Build-out phase"` | Starting phase name – editable in the summary | | currency | `string` | `"$"` | Currency symbol for prices and totals | | catalog | `CatalogItem[]` | | Products under Add product – { name, repeat, price } | | defaultLines | `ScheduleLine[]` | | Lines to start with – products, a discount or a minimum | | defaultStart | `string` | `"2026-01-01"` | Start date as yyyy-mm-dd | | onChange | `(schedule) => void` | | Called with { phase, start, duration, lines } after every change | | className | `string` | | Extra class on the root | ### Keyboard - Tab: Moves through the phase name, the fields, every line and the buttons - Enter: Confirms a price; Esc closes an open menu ### Accessibility - The lines are a table with column headers; every input and select has a label naming its line - Duration and Add product are buttons that open listboxes; totals are announced politely ### Motion - New lines slide in and the table glides to its new height; the duration menu and catalog scale in - The end date in the summary fades when it changes - With prefers-reduced-motion changes happen instantly ### Responsive - Fills its container up to 560px; on narrow screens the fields stack and the Repeats column hides - Dark mode built in: switches when an ancestor (usually ) has the class dark or data-theme="dark" ### Requirements - React 19 --- ## Breakdown Chart (Pro) Stacked bar chart with measure, slice and segment pickers, a hover breakdown and a linked table - Page: https://www.affectui.com/components/breakdown-chart - Section: Components · Data display - Install: `npx shadcn@latest add https://www.affectui.com/r/breakdown-chart.json` (needs the Pro access key) - Import: `@/components/ui/breakdown-chart` ### When to use - Showing how a set of records splits up – tasks by status and priority, tickets by team, orders by region - Reports where people want to switch the view themselves without leaving the page ### When not to use - Values over time – use the KPI Chart - More than about twelve bars or eight segments – the bars and the legend get hard to read ### Usage ```tsx import { BreakdownChart } from "@/components/ui/breakdown-chart"; const dimensions = [ { key: "status", label: "Status", values: ["Todo", "In progress", "Done"] }, { key: "priority", label: "Priority", values: ["High", "Medium", "Low"], colors: { High: "#e5484d" } }, ]; const measures = [ { key: "count", label: "Task count", unit: "tasks" }, { key: "points", label: "Story points", unit: "points", field: "points" }, ]; export function Sprint({ tasks }) { return ; } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | records | `BreakdownRecord[]` | | Flat records – one per task, ticket or order – with a field per dimension | | dimensions | `Dimension[]` | | key, label, values in order, optional colors and icons per value | | measures | `Measure[]` | | key, label, unit and the field to sum – without a field records are counted | | title | `string` | `"Workload"` | Heading in the gray tray | | defaultMeasure | `string` | | Measure shown first | | defaultSlice | `string` | | Dimension along the bars | | defaultSegment | `string` | | Dimension that stacks the bars, or "none" | | className | `string` | | Extra class on the tray | ### Keyboard - Tab: Move through the pickers, the bars and the legend - ↑ / ↓: Move through an open picker - Escape: Close a picker ### Accessibility - Each bar is a button that reads its slice and total; focusing it shows the breakdown - The table repeats every number, so nothing depends on color or hover - Legend entries are toggle buttons with a pressed state ### Motion - Bars grow and re-stack smoothly when the measure, slice or segment changes - Hovering dims the other bars and highlights the matching table row; reduced motion turns transitions off ### Responsive - Dark mode built in: switches when an ancestor (usually ) has the class dark or data-theme="dark" ### Requirements - React 19 Related: kpi-chart, data-table, change-summary --- ## Button (Free) Primary, secondary, ghost and destructive buttons with icons, sizes and a shimmering loading state - Page: https://www.affectui.com/components/button - Section: Components · Buttons - Install: `npx shadcn@latest add https://www.affectui.com/r/button.json` - Import: `@/components/ui/button` ### When to use - Any action a user triggers: submit a form, send a message, open a dialog - Async actions that take a moment – the loading state keeps the label readable while it works ### When not to use - Navigation to another page – use a link so it can be opened in a new tab - Switching between views – a segmented control or tabs show the current state ### Usage ```tsx import { Button } from "@/components/ui/button"; export function Actions() { return ( <> ); } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | variant | `"primary" \| "secondary" \| "ghost" \| "destructive"` | `"primary"` | Dark filled, white with an outline, text only, or red for deleting | | size | `"sm" \| "md" \| "lg"` | `"md"` | 32, 40 or 48px high | | loading | `boolean` | `false` | Swaps the left icon for a spinner, shimmers the label, sets aria-busy and ignores clicks | | icon | `ReactNode` | | Icon before the label; without children the button becomes square | | iconRight | `ReactNode` | | Icon after the label | | ...props | `button attributes` | | Everything a native button takes, including ref, type and disabled | ### Keyboard - Enter / Space: Activate the button ### Accessibility - A native button element, so focus, keyboard and form submission work as expected - While loading it stays focusable and announces aria-busy instead of becoming disabled - Icon-only buttons need an aria-label – the icon itself is hidden from screen readers ### Motion - Pressing scales the button to 97% - Loading swaps the left icon for a spinner and sweeps a band across the label - When label or icon change, the width glides to the new size and the new content fades in from a soft blur - With prefers-reduced-motion the sweep stops and changes are instant ### Responsive - Dark mode built in: switches when an ancestor (usually ) has the class dark or data-theme="dark" ### Requirements - React 19 Related: chat-composer --- ## Change Summary (Free) What moved since last time – one metric per row with a colored trend - Page: https://www.affectui.com/components/change-summary - Section: Components · Data display - Install: `npx shadcn@latest add https://www.affectui.com/r/change-summary.json` - Import: `@/components/ui/change-summary` ### When to use - A short recap at the top of a dashboard or a weekly report - Explaining in words what changed, next to the charts that show it ### When not to use - Exact values people compare over time – use the KPI Chart or a table - More than about six metrics – the recap stops being a quick read ### Usage ```tsx import { ChangeSummary } from "@/components/ui/change-summary"; const changes = [ { label: "Active users", change: "Up 6.1% to 18.2K", tone: "good" }, { label: "Churn", change: "Down to 2.1% from 2.6%", tone: "good", direction: "down" }, { label: "Support tickets", change: "Up 14% to 312", tone: "bad", direction: "up" }, ]; export function Recap() { return ; } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | changes | `MetricChange[]` | | label, change text, tone (good, bad, neutral) and the arrow direction | | title | `string` | `"What's changed"` | Heading in the gray tray | | period | `string` | | Small text on the right of the heading | | icon | `ReactNode` | | Replaces the icon in front of the heading | | className | `string` | | Extra class on the tray | ### Accessibility - A description list, so each change is read together with its metric - The trend arrow is decorative – the text says what happened ### Motion - Rows fade in one after another; reduced motion shows them at once ### Responsive - Dark mode built in: switches when an ancestor (usually ) has the class dark or data-theme="dark" ### Requirements - React 19 Related: kpi-chart, decision-inbox --- ## Chat Composer (Pro) Prompt composer with model picker, voice input, app connections and approval request - Page: https://www.affectui.com/components/chat-composer - Section: Components · Chat - Install: `npx shadcn@latest add https://www.affectui.com/r/chat-composer.json` (needs the Pro access key) - Import: `@/components/ui/chat-composer` ### When to use - The main input of an AI assistant or agent that can act on connected tools - Flows where the user has to grant extra permissions before a run ### When not to use - Plain chat between people – a simple text field with a send button is enough - Very narrow spaces below ~280px, where the toolbar labels get too small ### Usage ```tsx import { ChatComposer } from "@/components/ui/chat-composer"; export function Assistant() { return console.log(message)} />; } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | placeholder | `string` | `"Start by typing..."` | Placeholder of the message field | | models | `ChatModel[]` | `defaultModels` | Entries of the model picker (Auto, Claude, ChatGPT); the first is selected | | loadMs | `number` | `1300` | Duration of the connect and approval loading states | | dictation | `string` | `"Draft a short update…"` | Demo transcript that appears word by word in gray while recording – replace with real speech-to-text | | onSend | `(message: string, details: { model: string }) => void` | | Called on send or Enter | | onModelChange | `(model: ChatModel) => void` | | Called when a model is picked | | onAttach | `() => void` | | Called when the plus button is clicked – open your own picker here | | onConnectApps | `() => void` | | Called when “Connect apps” is clicked | | onRequestApproval | `() => void` | | Called when “Request approval” is clicked | | onSelectProject | `() => void` | | Called when “Select a project” is clicked | ### Keyboard - Enter: Send the message - Shift + Enter: New line - ↑ / ↓: Open the model picker and move through it - Home / End: Jump to the first or last model - Enter / Space: Pick the highlighted model (in the picker) - Esc: Close the model picker or discard a recording ### Accessibility - Loading and result states are announced through a polite live region - Busy buttons expose aria-busy; the hidden tray row is inert and aria-hidden - Clicking anywhere on the white card focuses the message field - The model picker is a listbox with aria-activedescendant, so screen readers follow the highlight ### Motion - Connecting apps runs two shimmer sweeps while the logos hop, then the card slides down to reveal “Select a project” - Requesting approval waves the hand, then morphs into the “Unrestricted access” pill - The model picker morphs out of the corner next to its button, opens toward the side with more room and stays inside the visible frame - A hover fill fades in under the pointer and glides between rows - While recording, the heard words appear in gray and turn dark once confirmed; a dotted audio trace scrolls next to the controls - All transitions collapse to instant changes with prefers-reduced-motion ### Responsive - Fills its container up to 680px wide; all sizes follow the width via container query units and stop growing at a compact maximum - Dark mode built in: switches when an ancestor (usually ) has the class dark or data-theme="dark" ### Requirements - React 19 --- ## Combobox (Pro) Pick one or many from a searchable, grouped list – chips with avatars, highlighted matches, create-on-the-fly and full keyboard support - Page: https://www.affectui.com/components/combobox - Section: Components · Inputs - Install: `npx shadcn@latest add https://www.affectui.com/r/combobox.json` (needs the Pro access key) - Import: `@/components/ui/combobox` ### When to use - Assigning people, labels, tags or projects – anything picked from a list that's too long to scan - Filters where several values can apply at once, like owners or statuses - Tag fields that should also accept new values (creatable) ### When not to use - Up to five fixed choices – checkboxes or a segmented control show them all at once - Free text with no list behind it – use a plain input - Running actions rather than picking values – use the command menu ### Usage ```tsx import { Combobox, type ComboboxOption } from "@/components/ui/combobox"; const people: ComboboxOption[] = [ { value: "maya", label: "Maya Chen", hint: "Product Designer", avatar: true, group: "Design" }, { value: "sara", label: "Sara Okafor", hint: "Staff Engineer", avatar: "/avatars/sara.jpg", group: "Engineering" }, { value: "elif", label: "Elif Yilmaz", hint: "Head of Product", avatar: true, group: "Product" }, ]; export function Reviewers() { const [reviewers, setReviewers] = useState(["maya"]); return ( ); } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | options | `{ value, label, hint?, avatar?, color?, icon?, group?, keywords? }[]` | | The list. avatar is an image URL or true for initials; options with the same group are listed under one title | | multiple | `boolean` | `true` | Pick many (chips) or one – one closes the list after picking | | value | `string[]` | | Controlled values (also for single choice, with at most one entry) | | defaultValue | `string[]` | `[]` | Starting values when uncontrolled | | onChange | `(value: string[], options: ComboboxOption[]) => void` | | Called on every pick, removal or creation | | placeholder | `string` | `"Search…"` | Shown while nothing is picked | | creatable | `boolean` | `false` | Offer a create row when no option matches the typed text exactly | | onCreate | `(text: string) => ComboboxOption \| void` | | Builds the new option – defaults to { value: text, label: text } | | createLabel | `(text: string) => string` | `Create “…”` | Text of the create row, e.g. Invite “…” | | maxVisibleTags | `number` | | Show at most this many chips, the rest as a "+3" chip | | label | `string` | | Visible label above the field (also names the list) | | defaultOpen | `boolean` | `false` | Open the list right away | | emptyText | `string` | `"No results"` | Shown when nothing matches | | className | `string` | | Extra class on the root | ### Keyboard - ↑ / ↓: Open the list, then move between rows - ↵: Pick or unpick the active row (or create) - ⌫: In the empty field: highlight the last chip, press again to remove it - Esc: Close the list; when closed, clear the typed text - Tab: Close the list and move on ### Accessibility - Follows the ARIA combobox pattern: the input has role combobox with aria-expanded, aria-controls and aria-activedescendant; the list is a listbox with grouped options - Picked rows carry aria-selected, the listbox is aria-multiselectable in multiple mode - Picks and removals are announced in a polite live region, together with the count - Focus stays in the input while clicking rows and chip × buttons; the field shows a soft focus ring ### Motion - Chips grow in with a small pop and fold away after a short flash; the field glides to its new height when chips wrap - A gray highlight glides to the active row; the list glides to its new height while filtering - Check marks pop in with a gentle spring; the "+3" chip and the count roll up or down - With prefers-reduced-motion everything switches instantly ### Responsive - The list is as wide as the field, so it always fits the screen - Chips wrap onto new lines and long names truncate – works down to 320px - Touch screens get 44px rows, bigger chip buttons and 16px input text (no iOS zoom) - Dark mode built in: switches when an ancestor (usually ) has the class dark or data-theme="dark" ### Requirements - React 19 Related: filter-menu, command-menu, integration-picker --- ## Command Menu (Pro) Search and run anything from one field – tool actions with logos and ⌘1–⌘5, everyday commands with icons - Page: https://www.affectui.com/components/command-menu - Section: Components · Navigation - Install: `npx shadcn@latest add https://www.affectui.com/r/command-menu.json` (needs the Pro access key) - Import: `@/components/ui/command-menu` ### When to use - A ⌘K menu that runs actions across the tools your product connects to - Power users who want to reach any page or action without the mouse ### When not to use - The only way to reach something – keep the main navigation visible as well - Fewer than five actions – buttons are easier to discover ### Usage ```tsx import { CommandMenu, type CommandGroup } from "@/components/ui/command-menu"; export function Commands({ onClose }: { onClose: () => void }) { return ( (id === "ask" ? askAssistant(query) : runCommand(id))} /> ); } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | groups | `CommandGroup[]` | `Suggestions, Share, General, Go to` | Each { id, label, commands: { id, label, hint?, logo? | icon?, keys?, keywords? }[] }; the group with id "suggestions" gets ⌘1–⌘5 | | placeholder | `string` | `"Type a command or search…"` | Text in the empty field | | onRun | `(id: string, query?: string) => void` | | Called when a command runs; the assistant fallback sends "ask" and the typed text | | onClose | `() => void` | | Runs on Esc in an empty field – close your dialog here | | autoFocus | `boolean` | | Focus the field when the menu appears | | className | `string` | | Extra class on the root | ### Keyboard - ↑ / ↓: Move row by row through all sections - ↵: Run the highlighted command - Tab: Jump to the next section (Shift Tab goes back) - ⌘ / 1–5: Run a suggestion straight away (Ctrl on Windows) - Esc: Clear the field, then close - ⌘ / K: Focus the field from anywhere ### Accessibility - The field is a combobox that controls the list; the highlighted command is the active option - Groups are labelled, shortcut keys are hidden from screen readers - What just ran is announced in a polite live region ### Motion - The gray highlight glides from row to row - The row that runs gives a short press; a check and its name appear in the tray below - With prefers-reduced-motion everything switches instantly ### Responsive - Fills its container up to 600px; on phones shortcut keys and hints are left out - Dark mode built in: switches when an ancestor (usually ) has the class dark or data-theme="dark" ### Requirements - React 19 --- ## Comment Thread (Free) One comment with replies – an assistant reports what it did, folds open details and answers @mentions - Page: https://www.affectui.com/components/comment-thread - Section: Components · Chat - Install: `npx shadcn@latest add https://www.affectui.com/r/comment-thread.json` - Import: `@/components/ui/comment-thread` ### When to use - A single conversation attached to a selection, a cell or a line of text - Showing what an assistant did after someone asked for a change ### When not to use - A feed of many comments – use the Comments Panel ### Usage ```tsx import { AssistantMark, CommentThread } from "@/components/ui/comment-thread"; const atlas = { name: "Atlas", avatar: , assistant: true }; export function Thread({ messages, me }) { return ( (text.includes("@Atlas") ? await api.ask(text) : undefined)} /> ); } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | messages | `ThreadMessage[]` | | id, author, createdAt, text – assistant replies can add a status line with folding details | | currentUser | `ThreadAuthor` | | Author of new replies | | assistant | `ThreadAuthor` | | Shown with a thinking state while an answer is on its way | | onReply | `(text) => ThreadMessage \| Promise \| void` | | Return a message (or a promise) for the assistant to answer, nothing to just post | | onClose | `() => void` | | Shows a close button | | title | `string` | `"Comment"` | Heading in the gray tray | ### Keyboard - Enter: Send the reply - Tab: Reach the status lines to fold their details open ### Accessibility - Status lines are buttons with aria-expanded; folded details are inert - The thread announces new replies politely ### Motion - Replies fade in, the thinking text shimmers, details fold open – reduced motion turns this off ### Responsive - Dark mode built in: switches when an ancestor (usually ) has the class dark or data-theme="dark" ### Requirements - React 19 Related: comments-panel, chat-composer --- ## Comments Panel (Pro) Comment feed with filters, runnable /edit commands, resolve and a composer - Page: https://www.affectui.com/components/comments-panel - Section: Components · Chat - Install: `npx shadcn@latest add https://www.affectui.com/r/comments-panel.json` (needs the Pro access key) - Import: `@/components/ui/comments-panel` ### When to use - A comments sidebar next to a document, design or record - Collaboration with an assistant that can apply edits people suggest with /edit ### When not to use - Long discussions with nested replies – use the Comment Thread for one conversation ### Usage ```tsx import { CommentsPanel } from "@/components/ui/comments-panel"; export function Sidebar({ comments, me }) { return ( api.applyEdit(c.text)} onAdd={(c) => api.saveComment(c)} /> ); } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | comments | `PanelComment[]` | | id, author ({ name, avatar }), createdAt and text – text starting with /edit becomes a runnable edit | | currentUser | `CommentAuthor` | | Author of new comments | | onRunEdit | `(comment) => Promise \| void` | | Runs an edit; the button shows Running… until the promise settles | | onAdd | `(comment) => void` | | Called with every new comment | | onClose | `() => void` | | Shows a close button | | title | `string` | `"Comments"` | Heading in the gray tray | ### Keyboard - Tab: Move through tabs, sort, comments and the composer - Enter: Send the comment in the composer ### Accessibility - Filters are tabs, sorting is a menu of radio items, resolve is a toggle button - Times are real time elements; the list announces new comments politely ### Motion - New comments fade in, edits show a spinner, the sort menu drops in – reduced motion turns this off ### Responsive - Dark mode built in: switches when an ancestor (usually ) has the class dark or data-theme="dark" ### Requirements - React 19 Related: comment-thread, smart-recommendation --- ## Compact Breakdown (Pro) A total split into its parts in one small card – numbers roll, the bar glides and the ranked rows trade places when the period changes - Page: https://www.affectui.com/components/compact-breakdown - Section: Components · Data display - Install: `npx shadcn@latest add https://www.affectui.com/r/compact-breakdown.json` (needs the Pro access key) - Import: `@/components/ui/compact-breakdown` ### When to use - One number and what it is made of – revenue by channel, usage by feature, tokens by model, tickets by category - Dashboards where the card sits next to others and a full chart would be too much - Whenever people switch between periods and should see what moved ### When not to use - Trends over time – a line chart shows those; this card compares the parts of one total - More than about seven parts – group the small ones into "Other" first - Several measures and slicing options – use Breakdown Chart ### Usage ```tsx import { CompactBreakdown } from "@/components/ui/compact-breakdown"; export function RevenueByChannel() { const [period, setPeriod] = useState("30d"); const { items, delta } = useRevenue(period); // [{ label: "Direct", value: 16900 }, …] return ( `$${Math.round(n).toLocaleString("en-US")}`} totalLabel="Total revenue" /> ); } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | items | `BreakdownItem[]` | | { id?, label, value, color? } – the parts of the shown period; the list ranks them by value. Give an id when labels can change | | title | `string` | `"Breakdown"` | Heading of the card | | periods | `{ id: string; label: string }[]` | | Options of the period switch – no switch when left out | | period | `string` | | Controlled period id | | defaultPeriod | `string` | `first period` | Starting period when uncontrolled | | onPeriodChange | `(id: string) => void` | | Called with the new period – pass the matching items and delta back in | | delta | `number` | | Change against the previous period in percent (12.4 or -3) – a small green or red pill next to the total | | format | `(value: number) => string` | `whole numbers with separators` | Formats every value – the total, the rows and the hovered part | | totalLabel | `string` | `"Total"` | Line under the big number while nothing is hovered | | footer | `ReactNode \| null` | `"Largest: "` | Replaces the line on the gray tray; null hides it | | className | `string` | | Extra class on the root | ### Keyboard - Tab: Moves to the period switch, then through the rows – a focused row is highlighted like a hovered one - ← / →: In the period switch: previous or next period (wraps around) - Home / End: In the period switch: first or last period ### Accessibility - A group named by its title; the period switch is a radiogroup with one tab stop - The rolling numbers are aria-hidden – screen readers get the final total and the change in one sentence, and every row with its value and share - Rows can be focused, so the highlight and the part's value are reachable without a mouse - The bar is decorative: everything it shows is in the list ### Motion - On a new period every number rolls to its value in one shared tween, so the total, the rows and the bar move in step - The bar keeps each part's place and color and only glides to the new widths - Rows slide to their new rank when the order changes; a part that is not in the new period folds away - Hovering a part dims the others, lifts its segment and rolls the big number to that part's value – leaving rolls it back to the total - The pill of the period switch slides to the chosen option; the change pill pops in - With prefers-reduced-motion everything switches instantly ### Responsive - Fills its container up to 360px; below 250px the share column goes and the values stay - Touch screens: taller rows and period options - Dark mode built in: switches when an ancestor (usually ) has the class dark or data-theme="dark" ### Notes for AI - Keep the same id (or label) for a part across periods – that is what lets rows and bar segments animate instead of being replaced - Pass raw numbers and do the formatting in format, so the rolling values stay smooth ### Requirements - React 19 Related: usage-meter, breakdown-chart, kpi-chart --- ## Context Window (Pro) What fills the model's context, as a field of cells – hover a part to light it up, remove files, and compact the conversation while its cells shimmer and clear - Page: https://www.affectui.com/components/context-window - Section: Components · Agents - Install: `npx shadcn@latest add https://www.affectui.com/r/context-window.json` (needs the Pro access key) - Import: `@/components/ui/context-window` ### When to use - Chat and agent products where people should see what the model currently has in mind – and why answers get slow, expensive or forgetful - Next to a composer or in a session panel, to clean up before the window is full: drop files, compact the conversation - Debug and admin views that explain a request: how much went to the system prompt, tools, files and history ### When not to use - Billing and plan usage over a month – use Usage Meter - A plain "72% used" – a single progress bar says that with less - More than about six parts – group the small ones, the cells need colors people can tell apart ### Usage ```tsx import { ContextWindow } from "@/components/ui/context-window"; export function SessionContext() { return ( ); } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | parts | `ContextPart[]` | | { id, label, tokens?, items?, locked?, compact?, color? } – they fill the field in this order | | limit | `number` | | Size of the context window in tokens | | title | `string` | `"Context window"` | Heading of the card | | model | `string` | | Small tag in the head, e.g. the model name | | modelIcon | `ReactNode` | | Logo in front of the model name, shown at 12px | | onRemoveItem | `(partId: string, itemId: string) => void` | | Called when an item is removed. With it the card shows exactly what you pass; without it the removal lives inside the card | | onCompact | `(partId: string) => void \| Promise` | | Called when Compact is pressed; the cells shimmer until the promise settles. Without it the part shrinks on its own to its compact share | | action | `{ label: string; onClick: () => void }` | | A button on the tray, e.g. New chat | | warnAt | `number` | `0.8` | Share of the limit from which the percentage turns amber and the tray says how much is left; from 0.95 the window counts as full | | cells | `number` | `216` | Number of cells in the field | | columns | `number` | `36` | Cells per row | | className | `string` | | Extra class on the root | ### Keyboard - Tab: Through the parts that can be opened, the Compact button, the items' remove buttons and the action - Enter / Space: Opens or closes a part with items, presses Compact or removes an item ### Accessibility - A group named by its title; the rolling number is aria-hidden and screen readers get the final "used of limit" in one sentence - The field of cells is decorative – every number it shows is in the list below - Parts with items are buttons with aria-expanded; closed lists are inert, remove buttons are named after their item - The line on the tray is announced politely: compacting, how much was saved, and how much is left once the window fills up ### Motion - Any change sweeps through the field cell by cell, so removing a file empties its cells from left to right and everything behind it moves up - Compacting makes the part's cells shimmer in a wave while it works; then most of them clear, the numbers roll down and the button morphs into a green check with "Compacted" for a moment and then fades away – it only comes back once the part has grown again - Hovering a part (row or cells) fades the other cells and rolls the big number to that part - Items unfold in place; a removed item folds away before it goes - With prefers-reduced-motion everything switches without movement ### Responsive - Fills its container up to 360px. Below 310px the field puts fewer cells in a row so they stay readable, the model tag shortens and Compact keeps only its icon - Touch screens: taller rows and buttons - Dark mode built in: switches when an ancestor (usually ) has the class dark or data-theme="dark" ### Notes for AI - A part with items takes its tokens from the items' sum – leave tokens out there - One cell stands for limit / cells tokens; any part with tokens gets at least one cell so it never disappears - The card does not count tokens – pass the numbers your provider or tokenizer returns ### Requirements - React 19 Related: usage-meter, tool-call-card, chat-composer --- ## Cover Flow (Free) Browse covers in 3D – drag, flick or scrub the row, it springs to the nearest cover; images drift in their frames over a soft reflection - Page: https://www.affectui.com/components/cover-flow - Section: Components · Cards - Install: `npx shadcn@latest add https://www.affectui.com/r/cover-flow.json` - Import: `@/components/ui/cover-flow` ### When to use - Six to twenty visual items where looking at one at a time is the point – albums, places, products, case studies, templates - A showcase row that should feel physical: drag, flick, sideways trackpad scroll ### When not to use - Text-heavy slides or many items to compare side by side – use a plain carousel or a grid - Large libraries – a grid with a lightbox scales better ### Usage ```tsx import { CoverFlow } from "@/components/ui/cover-flow"; export function Albums() { return ( openAlbum(item.id)} /> ); } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | items | `CoverFlowItem[]` | | { id, title?, subtitle?, image?, alt?, cover? } – pass an image URL or any ReactNode as cover | | index | `number` | | Controlled front cover – pair with onIndexChange | | defaultIndex | `number` | `middle cover` | Starting cover when uncontrolled | | onIndexChange | `(index: number) => void` | | Called whenever a different cover reaches the front, also while dragging | | onSelect | `(item, index) => void` | | Clicking the front cover or pressing Enter – e.g. open a detail view | | itemWidth | `number` | `280` | Cover width in px – the whole row scales down in narrow containers (to half at most) | | itemHeight | `number` | `280` | Cover height in px | | angle | `number` | `50` | How far the side covers turn away, in degrees | | reflection | `boolean` | `true` | Mirror the covers on the floor | | caption | `boolean` | `true` | Title and subtitle of the front cover | | renderCaption | `(item, index) => ReactNode` | | Replaces the default caption | | controls | `boolean` | `true` | Previous and next buttons beside the caption | | ticks | `boolean` | `true` | Small position ticks under the caption – clicking one jumps there | | sound | `boolean` | `false` | A soft click each time a new cover reaches the front (Web Audio, no file) | | aria-label | `string` | `"Cover flow"` | Names the carousel | | className | `string` | | Extra class on the root | ### Keyboard - ← / →: Previous or next cover while the row has focus - Home / End: First or last cover - ↵: Selects the front cover (calls onSelect) ### Accessibility - A region with aria-roledescription="carousel"; each cover is a group with aria-roledescription="slide" and a label like "3 of 10: Alpenglow" - Covers away from the front are aria-hidden; the reflections are aria-hidden and inert - The caption is a polite live region, so the new front cover is announced - Previous / next are real buttons; the ticks are labelled "Show " with aria-current and stay out of the tab order ### Motion - A spring carries the row: drag 1:1, flick and it travels with your speed, then lands on a cover – a quick flick always moves at least one - Sideways trackpad scrolling scrubs the row and settles in the direction of the swipe; vertical scrolling stays with the page - Covers turn, recede, scale down and dim with distance; a light sheen sits on the side facing the center and each image drifts inside its frame - The ends rubber-band; the caption rises in from the direction of travel - With prefers-reduced-motion covers jump into place and the drift is off ### Responsive - Scales with its container down to half size; touch-action pan-y keeps vertical swipes scrolling the page - Dark mode built in: switches when an ancestor (usually <html>) has the class dark or data-theme="dark" ### Requirements - React 19 Related: tilt-card, logo-marquee, segmented-control --- ## Data Table (Pro) Spreadsheet-style table: select cells and ranges, edit in place, copy and paste, undo and sort - Page: https://www.affectui.com/components/data-table - Section: Components · Data display - Install: `npx shadcn@latest add https://www.affectui.com/r/data-table.json` (needs the Pro access key) - Import: `@/components/ui/data-table` ### When to use - Small databases people edit in place – tools, vendors, budgets, contacts - Data that moves to and from Excel or Google Sheets with copy and paste - Lists where people pick several rows to act on ### When not to use - Thousands of rows – add virtualisation, filters and server paging with a grid library - Editing one record with many fields – a form is clearer ### Usage ```tsx import { DataTable, TextColumnIcon, TagColumnIcon } from "@/components/ui/data-table"; const columns = [ { key: "tool", label: "Tool", icon: <TextColumnIcon />, type: "title" }, { key: "category", label: "Category", icon: <TagColumnIcon />, type: "tag" }, { key: "website", label: "Website", type: "link" }, { key: "seats", label: "Seats", type: "number" }, ]; export function Stack() { return ( <DataTable title="Design Stack" columns={columns} rows={rows} tagTones={{ Design: "violet" }} onRowsChange={save} /> ); } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | title | `string` | | Heading in the gray tray, also the grid's accessible name | | icon | `ReactNode` | | Icon in front of the heading | | columns | `DataColumn[]` | | key, label, icon, width, type (title, tag, link, number, text), editable and options for tags | | rows | `DataRow[]` | | Starting rows – an id, an optional logo and a value per column key | | tagTones | `Record<string, TagTone>` | | Tag color per tag text: blue, violet, pink, green, amber or gray | | editable | `boolean` | `true` | false keeps selection and copy, but turns off editing, paste, delete and Add row | | onRowsChange | `(rows: DataRow[]) => void` | | Called with all rows after every edit, paste, clear, delete, undo or redo | | onSelectionChange | `(ids: string[]) => void` | | Called with the ids of the checked rows | | className | `string` | | Extra class on the tray | ### Keyboard - Arrow keys: Move the active cell - Shift / Arrow keys: Grow the selected range - ⌘ / Ctrl / Arrow keys: Jump to the first or last row or column - Tab: Move to the next cell, Shift + Tab to the previous one - Enter / F2: Edit the active cell; Enter saves and moves down - Any character: Start editing with that character - Escape: Cancel an edit or collapse the range - Delete / Backspace: Clear the selected cells - ⌘ / Ctrl / C / X / V: Copy, cut and paste – works with Excel and Google Sheets - ⌘ / Ctrl / Z: Undo, add Shift to redo - ⌘ / Ctrl / A: Select every cell ### Accessibility - The table is a grid with selected cells marked, so screen readers follow the active cell and the range - Column headers are sort buttons and report the sort direction - Row checkboxes have labels; the header checkbox shows a mixed state when only some rows are checked ### Motion - Tag picker drops in, sort arrows turn, checkboxes pop – reduced motion turns all of it off ### Responsive - The table scrolls sideways inside the card on narrow screens instead of squeezing the columns - Dark mode built in: switches when an ancestor (usually <html>) has the class dark or data-theme="dark" ### Requirements - React 19 Related: kpi-chart, change-summary --- ## Date Range Picker (Pro) Presets and two calendars with a hover preview, comparison with the previous period or year, full keyboard control - Page: https://www.affectui.com/components/date-range-picker - Section: Components · Inputs - Install: `npx shadcn@latest add https://www.affectui.com/r/date-range-picker.json` (needs the Pro access key) - Import: `@/components/ui/date-range-picker` ### When to use - Analytics, reports and dashboards where people switch between periods and compare them - Filters for bookings, invoices or logs that need a start and an end date ### When not to use - A single date, like a birthday – a plain date field is quicker - Ranges that span many years – type the dates instead ### Usage ```tsx import { DateRangePicker, type DateRange } from "@/components/ui/date-range-picker"; export function ReportFilter() { return ( <DateRangePicker onChange={(range, compare) => { loadReport(range.start, range.end); if (compare) loadComparison(compare.start, compare.end); }} /> ); } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | value | `{ start: Date; end: Date }` | | Controlled range | | defaultValue | `{ start: Date; end: Date }` | `last 30 days` | Starting range when uncontrolled | | onChange | `(range, compare, mode) => void` | | Called on Apply with the range, the comparison range (or null) and the compare mode | | presets | `{ id, label, range: (today) => DateRange }[]` | `Today … Year to date` | The list on the left (chips on phones) | | compare | `boolean` | `true` | Show "Compare: Off / Previous period / Previous year" | | defaultCompare | `"off" \| "previous" \| "year"` | `"previous"` | Comparison selected at the start | | weekStartsOn | `0 \| 1` | `1` | 0 = Sunday, 1 = Monday | | min / max | `Date` | `max: today` | Days outside can't be picked | | defaultOpen | `boolean` | `false` | Open the panel right away | | today | `Date` | `now` | Reference day for the presets – handy for tests and screenshots | | className | `string` | | Extra class on the root | ### Keyboard - ← / → / ↑ / ↓: Move day by day and week by week - PageUp / PageDown: Previous / next month - Home / End: Start / end of the week - ↵: Pick the start, then the end - Esc: Close without applying ### Accessibility - The trigger opens a dialog; each month is a grid and every day button has its full date as name - Today is marked with aria-current, picked days with aria-selected; the month name is announced when it changes - Focus moves with the arrow keys and returns to the trigger when the panel closes ### Motion - The panel opens with a short scale and fade; months slide in from the side you're moving to - After the first click the band follows the pointer as a lighter preview until the second click - The comparison period appears as a gray band with outlined ends - With prefers-reduced-motion everything switches instantly ### Responsive - Below 600px the presets turn into a scrolling row of chips and only the month the range ends in is shown; the footer stacks - The panel is as wide as the screen allows (up to 652px); set --ui-drp-panel-width, e.g. to 100%, to match your layout - Bigger day cells and buttons on touch screens - Dark mode built in: switches when an ancestor (usually <html>) has the class dark or data-theme="dark" ### Requirements - React 19 Related: kpi-chart, filter-menu, breakdown-chart --- ## Decision Inbox (Free) Open decisions with two quick answers each – answered ones confirm and fold away - Page: https://www.affectui.com/components/decision-inbox - Section: Components · Feedback - Install: `npx shadcn@latest add https://www.affectui.com/r/decision-inbox.json` - Import: `@/components/ui/decision-inbox` ### When to use - Dashboards or agents that need a quick yes / no from a person before they continue - Approvals, renewals and alerts that can be answered with one click ### When not to use - Decisions that need a form or a longer explanation – link to a page instead - Plain notifications without an answer – use a toast or an activity list ### Usage ```tsx import { DecisionInbox } from "@/components/ui/decision-inbox"; const decisions = [ { id: "renewal", title: "Renew Figma for the design team?", detail: "12 seats · $1,800 per year · renews in 5 days", primary: "Renew", secondary: "Change seats", }, ]; export function Inbox() { return <DecisionInbox decisions={decisions} onDecide={(d, answer) => save(d.id, answer)} />; } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | decisions | `Decision[]` | | id, title, detail, primary and secondary answer, and collapsed to start folded | | onDecide | `(decision, answer: "primary" \| "secondary") => void` | | Called when someone picks an answer | | className | `string` | | Extra class on the tray | ### Keyboard - Tab: Move between the answers and folded decisions - Enter / Space: Pick an answer or open a folded decision ### Accessibility - The heading with the count is a live region, so the new count is announced - The confirmation after an answer is a status message; folded-away rows are inert ### Motion - An answer turns into a green confirmation, then the row folds away and the next folded decision opens - Reduced motion skips the folding ### Responsive - Dark mode built in: switches when an ancestor (usually <html>) has the class dark or data-theme="dark" ### Requirements - React 19 Related: change-summary, setup-guide --- ## Dropzone (Pro) Drag files in and watch them upload – fanning documents, colored type badges, progress with time left, errors with retry - Page: https://www.affectui.com/components/dropzone - Section: Components · Inputs - Install: `npx shadcn@latest add https://www.affectui.com/r/dropzone.json` (needs the Pro access key) - Import: `@/components/ui/dropzone` ### When to use - Uploading documents, images or media – attachments, imports, asset libraries, onboarding - Several files at once, where people want to see what's done, what's still running and what failed ### When not to use - A single avatar or logo – a simple picker with a preview is lighter - Huge uploads that run in the background for minutes – show them in a separate transfer panel ### Usage ```tsx import { Dropzone } from "@/components/ui/dropzone"; export function Attachments() { return ( <Dropzone accept={[".pdf", ".png", ".jpg", ".xlsx"]} maxSize={25 * 1024 * 1024} onUpload={(file, onProgress) => new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.upload.onprogress = (e) => onProgress(e.loaded / e.total); xhr.onload = () => (xhr.status < 300 ? resolve() : reject(new Error("Upload failed"))); xhr.onerror = () => reject(new Error("Upload failed – connection lost")); xhr.open("POST", "/api/upload"); xhr.send(file); }) } onChange={(items) => setAttachments(items.filter((i) => i.status === "done"))} /> ); } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | accept | `string[]` | | Allowed extensions, e.g. [".pdf", ".png"]; any file when left out. Other files get a row with an error | | maxSize | `number` | `25 MB` | Largest file in bytes; bigger files get a "Too large" row | | onUpload | `(file: File, onProgress: (p: number) => void) => Promise<void>` | | Uploads one file and reports progress from 0 to 1; reject to mark it as failed (the message is shown, Retry calls it again). Simulated when left out | | onChange | `(items: UploadItem[]) => void` | | Every change of the list: { id, name, size, status, progress, error?, file? } | | hint | `string` | | Text under the title; defaults to the accepted types and the size limit | | autoplay | `boolean` | `false` | Plays a sample drop of four files – for previews and landing pages | | className | `string` | | Extra class on the root | ### Keyboard - Tab: Reaches the drop area, then Retry and Remove in every row - ↵: On the drop area: opens the file picker (Space works too) ### Accessibility - The drop area is a button that opens the file picker, described by the accepted types and size - Every row has a progress bar with its value; Retry and Remove are named after the file - Finished, failed and rejected files are announced in a polite live region ### Motion - Dragging files over the area fans the three little documents out, the outline turns solid blue and the title counts the files - Rows grow in one after another; the bar fills in one solid color, a small check pops in when a file is done - A failed row gives a short shake and its bar turns red; removed rows fold away - With prefers-reduced-motion everything switches instantly ### Responsive - Fills its container up to 520px; long file names are cut with an ellipsis - Dark mode built in: switches when an ancestor (usually <html>) has the class dark or data-theme="dark" ### Requirements - React 19 Related: chat-composer, ai-answer, glass-folder --- ## Email Draft (Free) Agent-written email for review – switch recipient and tone, edit inline, remove the attachment, send or discard with undo - Page: https://www.affectui.com/components/email-draft - Section: Components · Cards - Install: `npx shadcn@latest add https://www.affectui.com/r/email-draft.json` - Import: `@/components/ui/email-draft` ### When to use - Human-in-the-loop flows where an agent drafts an email and someone approves it – renewals, reminders, follow-ups - Inboxes or task lists of pending outreach, one card per record ### When not to use - Writing long emails from scratch – use a full editor - Messages that go out without review – then a simple activity row is enough ### Usage ```tsx import { EmailDraft } from "@/components/ui/email-draft"; export function RenewalReview() { return ( <EmailDraft reference="SUB-1182" account="Northwind Logistics" amount="$8,900" status="Renews in 5 days" agent="Renewal agent" contacts={[{ name: "Jonas Weber", email: "jonas@northwind.io" }]} attachment="Renewal-quote.pdf" link={{ label: "Review quote", href: "/quotes/1182" }} onSend={(email) => api.send(email)} /> ); } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | reference | `string` | `"SUB-1182"` | Reference of the record the email is about | | account | `string` | `"Northwind Logistics"` | Account or company under the reference | | amount | `string` | `"$8,900"` | Amount on the right | | status | `string` | `"Renews in 5 days"` | Status chip next to the reference | | agent | `string` | `"Renewal agent"` | Who wrote the draft, shown in the footer | | contacts | `DraftContact[]` | | People the email can go to – the first is preselected | | attachment | `string \| null` | | File name of the attachment; it can be removed and added back | | tones | `DraftTone[]` | | Text variants { id, label, body(contact) } – Friendly, Short and Formal by default | | link | `{ label, href }` | | Link appended to the email text | | onSend | `({ to, body, attachment }) => void` | | Called after Send with the final email | | onDiscard | `() => void` | | Called when the draft is discarded | | className | `string` | | Extra class on the root | ### Keyboard - ⌘ / Enter: Sends the email while editing - Esc: Leaves editing, or closes the recipient menu ### Accessibility - The recipient is a button that opens a listbox; tones are toggle buttons with aria-pressed - The text is a plain paragraph; clicking it or the pencil swaps in a labelled text field - Sent and discarded states are announced as a status with an Undo button ### Motion - Switching tone fades the new text in; the recipient menu scales in from its corner - Sending shows a spinner, then the card folds into a one-line receipt – the height glides between them - With prefers-reduced-motion changes happen instantly ### Responsive - Fills its container up to 520px; on narrow screens the email address and the shortcut hint hide - Dark mode built in: switches when an ancestor (usually <html>) has the class dark or data-theme="dark" ### Requirements - React 19 --- ## Experiment Card (Pro) An A/B test in two lines that quietly unfolds on hover – variants linked to a tiny chart, a metric switch with rolling numbers, and a winner you pick with a click - Page: https://www.affectui.com/components/experiment-card - Section: Components · Data display - Install: `npx shadcn@latest add https://www.affectui.com/r/experiment-card.json` (needs the Pro access key) - Import: `@/components/ui/experiment-card` ### When to use - A/B tests, prompt or model comparisons, pricing experiments – anything with a few variants measured the same way - Lists of running experiments where the result should be one hover away - When people should not only read the result but act on it: pick the winner and ship it ### When not to use - More than about five variants – the head chart and the rows get crowded; use a table - Results over time – a line chart shows when a variant pulled ahead; this card shows where things stand - A single number without variants – use KPI Chart or Compact Breakdown ### Usage ```tsx import { ExperimentCard } from "@/components/ui/experiment-card"; export function SignupCopyTest() { return ( <ExperimentCard title="Signup button copy" status={{ label: "Running", tone: "green" }} note="Day 9 of 14" metrics={[ { id: "conversion", label: "Conversion", format: (n) => `${n.toFixed(1)}%` }, { id: "revenue", label: "Revenue", format: (n) => `$${n.toFixed(2)}` }, ]} variants={[ { id: "a", key: "A", name: "Start free trial", control: true, visitors: 4790, values: { conversion: 4.2, revenue: 18.4 } }, { id: "b", key: "B", name: "Try it free", visitors: 4812, values: { conversion: 4.7, revenue: 19.1 }, confidence: { conversion: 96, revenue: 81 } }, ]} action={{ label: (v) => `Ship ${v.key}`, onClick: (v) => ship(v.id) }} /> ); } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | title | `string` | | Name of the experiment | | variants | `ExperimentVariant[]` | | { id, key, name, values, control?, visitors?, confidence?, color? } – values and confidence hold one number per metric id; key is the short letter in the badge | | metrics | `ExperimentMetric[]` | | { id, label, format?, lowerIsBetter? } – the switch above the rows; no switch with a single metric | | metric | `string` | | Controlled metric id | | defaultMetric | `string` | `first metric` | Starting metric when uncontrolled | | onMetricChange | `(id: string) => void` | | Called with the new metric | | winner | `string \| null` | | Controlled id of the picked variant | | defaultWinner | `string \| null` | `null` | Picked variant when uncontrolled | | onWinnerChange | `(id: string \| null) => void` | | Called when a variant is picked, or null when the pick is taken back | | status | `{ label: string; tone?: ExperimentTone }` | | The tag under the title – tone is red, amber, green, blue, violet or gray | | note | `string` | | Short text next to the status, e.g. "Day 9 of 14" | | action | `{ label: string \| (variant) => string; onClick: (variant) => void }` | | The button that appears on the tray once a winner is picked | | hint | `ReactNode` | `"Pick a winner"` | The line on the tray while nothing is hovered or picked | | trigger | `"hover" \| "click"` | `"hover"` | Hover opens under the mouse; taps and the keyboard always toggle | | open | `boolean` | | Controlled open state | | defaultOpen | `boolean` | `false` | Starts open when uncontrolled | | onOpenChange | `(open: boolean) => void` | | Called when the card opens or closes | | className | `string` | | Extra class on the root | ### Keyboard - Enter / Space: On the card: opens or closes it - ← / →: In the metric switch: previous or next metric (wraps around) - Tab: Through the metric switch, the variants and the action - Space: On a variant: picks it as the winner, or takes the pick back - Esc: Closes the card and returns focus to it ### Accessibility - The head is one button with aria-expanded; its name carries the status and who is ahead - The metric switch and the variants are radio groups; every variant's name includes its value and its change against the baseline - The small chart in the head is decorative – everything it shows is in the rows - While the card is closed, everything inside is inert; the line on the tray is announced politely ### Motion - Nothing travels: the head stays where it is and the comparison unfolds below it while it fades in - Switching the metric rolls every number in one shared tween while bars and columns glide to their new size; the pill of the switch slides along - Hovering a variant widens its column in the head and fades the others – and the other way round - Picking a winner turns its badge into a check and pops the action onto the tray - With prefers-reduced-motion everything switches without movement ### Responsive - Fills its container up to 340px; below 300px the note next to the status goes and the chips tighten - Touch screens: a tap opens, a tap outside closes, rows and the metric switch are taller - Dark mode built in: switches when an ancestor (usually <html>) has the class dark or data-theme="dark" ### Notes for AI - Changes are measured against the control (the variant with control: true, else the first). With lowerIsBetter a drop is shown as it is (−7.5%) but colored green and counted as the lead - Pass raw numbers and do the formatting in the metric's format, so the rolling values stay smooth - The card does not compute confidence – pass what your stats engine gives you ### Requirements - React 19 Related: project-card, compact-breakdown, kpi-chart --- ## Filter Menu (Free) Grouped filters with removable chips, collapsible checkbox groups, Clear all and full keyboard control - Page: https://www.affectui.com/components/filter-menu - Section: Components · Inputs - Install: `npx shadcn@latest add https://www.affectui.com/r/filter-menu.json` - Import: `@/components/ui/filter-menu` ### When to use - Filtering a list, table or board by a few groups of options – stage, owner, region and the like - Side panels or popovers where people pick several filters and want to see them all at a glance ### When not to use - A single choice from a short list – use a select or segmented control - Free-text search or numeric ranges with a slider – this menu works with predefined options ### Usage ```tsx import { FilterMenu } from "@/components/ui/filter-menu"; const groups = [ { id: "stage", label: "Stage", options: [{ id: "new", label: "New lead" }, { id: "won", label: "Won" }] }, { id: "size", label: "Deal size", chipPrefix: "Deal: ", options: [{ id: "s", label: "Under $5k" }, { id: "l", label: "Over $50k" }] }, ]; export function DealFilters() { const [filters, setFilters] = useState({}); return <FilterMenu groups={groups} value={filters} onChange={setFilters} />; } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | groups | `FilterGroup[]` | | Required. Each { id, label, options: { id, label }[], chipPrefix? } | | value | `Record<string, string[]>` | | Picked option ids per group – makes it controlled | | defaultValue | `Record<string, string[]>` | `{}` | Starting filters when uncontrolled | | onChange | `(value) => void` | | Called with the new filters after every change | | defaultOpen | `string[]` | `the first group` | Groups that start open | | title | `string` | `"Filters"` | Heading on top | | hints | `boolean` | `true` | Shows the keyboard hints on the tray | | className | `string` | | Extra class on the root | ### Keyboard - ↑ / ↓: Moves through group headers and the options of open groups - Enter / Space: Picks or unpicks an option, or opens and closes a group - → / ←: Opens a group, or closes it and jumps back to its header - Home / End: Jumps to the first or last row ### Accessibility - Group headers are buttons with aria-expanded; options are role=checkbox with aria-checked - One roving tab stop for the list, so Tab moves past it in one step - Each chip has a remove button labelled with the filter; changes to the chips are announced politely ### Motion - Groups open and close by animating their height; the chevron turns along - Chips pop in, and their box glides to its new height when a row of chips appears or goes - Checkmarks draw themselves in - With prefers-reduced-motion everything switches instantly ### Responsive - Fills its container up to 420px wide; chips wrap onto new lines - Dark mode built in: switches when an ancestor (usually <html>) has the class dark or data-theme="dark" ### Requirements - React 19 --- ## Inline Diff (Pro) AI edits to prose shown inline – struck and added words you accept or reject one by one, with keys, a popover and undo - Page: https://www.affectui.com/components/inline-diff - Section: Components · Agents - Install: `npx shadcn@latest add https://www.affectui.com/r/inline-diff.json` (needs the Pro access key) - Import: `@/components/ui/inline-diff` ### When to use - An AI rewrote a piece of prose – an email, a paragraph, a product description – and people should approve each edit - "Make it shorter", "fix the tone", "fix grammar" actions where most edits are small and scattered through the text - Review steps in an agent flow where the result should only change with an explicit yes ### When not to use - Code or line-based text – use a line diff with gutters instead - Full rewrites where almost every word changes – show the new version next to the old one - Live typing suggestions inside an editor – this reviews a finished suggestion, it isn't a text field ### Usage ```tsx import { InlineDiff } from "@/components/ui/inline-diff"; export function ReviewRewrite({ draft, rewrite }: { draft: string; rewrite: string }) { return ( <InlineDiff label="Suggested by AI · Make it shorter" original={draft} suggested={rewrite} onChange={(text) => saveDraft(text)} onDone={(text) => setFinal(text)} /> ); } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | original | `string` | `""` | The text as it is now | | suggested | `string` | `""` | The text the AI suggests – diffed word by word against original | | changes | `DiffSegment[]` | | Pre-computed segments ({ type: "equal", text } or { type: "change", remove, insert }) instead of original and suggested | | onChange | `(text: string) => void` | | Called on every decision and undo with the text so far – pending changes keep the original words | | onDone | `(text: string) => void` | | Called when the last pending change is decided, with the final text | | label | `string` | | Small line on the gray tray above the card, e.g. "Suggested by AI · Make it shorter" | | ref | `Ref<InlineDiffHandle>` | | highlight(index | null), accept(index), reject(index), acceptAll(), rejectAll(), undo(), reset() | | className | `string` | | Extra class on the root | ### Keyboard - Tab / Shift+Tab: Move between pending changes - ‹ / ›: Toolbar arrows: highlight the previous / next pending change (wrapping around) and open its popover - Y / Enter: Accept the focused change and move to the next one - N / Backspace: Reject the focused change and move to the next one - Esc: Leave the change or clear the arrow highlight ### Accessibility - Every pending change is a focusable button labelled like "Suggestion 2 of 4: replace “available” with “live”. Press Y to accept, N to reject" - Removed words are a <del>, new words an <ins>; the side that was dropped is hidden from screen readers once decided - Each decision, Accept all, Reject all and Undo are announced in a polite live region with the number left - After the last decision focus moves to Undo, so the keyboard is never stranded; the hidden toolbar is inert - Popover buttons are labelled and keep focus on the change; visible focus rings throughout - The prev/next arrows never steal focus: a focused change hands focus to the next one, otherwise the change is highlighted while focus stays on the arrow - Deleted words are red and struck, added words green – color is never the only cue (strike-through, <del> and <ins>) ### Motion - Accepting shrinks the struck words to nothing while the green tint fades and the new words settle into the text - Rejecting does the opposite – the new words shrink away, the strike-through and red tint fade - The popover fades up above a change and glides from one change to the next - The arrows only scroll the page when the next change is out of view (smoothly, or instantly with reduced motion) - The toolbar cross-fades into "All changes reviewed" and the check draws itself - With prefers-reduced-motion every change happens instantly ### Responsive - Fills its container; the paragraph wraps freely and changes wrap across lines with their tints - The popover stays inside the card horizontally, down to 320px wide - Below 400px the prev/next arrows step aside; on touch screens the popover buttons grow and drop the key hints - Dark mode built in: switches when an ancestor (usually <html>) has the class dark or data-theme="dark" ### Notes for AI - Pass the user's text as original and the model's rewrite as suggested – diffWords groups adjacent removals and insertions into one change - Ask the model for a rewrite that keeps unchanged sentences verbatim; the fewer stray edits, the easier the review - Write the result back with onDone (or onChange for drafts); undecided changes keep the original words - diffWords is exported, so you can count edits or build the same segments on the server ### Requirements - React 19 Related: writing-assistant, change-summary, approval-card --- ## Integration Picker (Pro) Integrations page of a command menu – search, a list of services and the picked one with its connection on two tiles - Page: https://www.affectui.com/components/integration-picker - Section: Components · Navigation - Install: `npx shadcn@latest add https://www.affectui.com/r/integration-picker.json` (needs the Pro access key) - Import: `@/components/ui/integration-picker` ### When to use - The integrations step of a command menu or settings dialog - Letting people find a service quickly and switch it on without leaving what they're doing ### When not to use - A full marketplace with categories and many details per app – use a page instead - Two or three integrations – a simple list with switches is enough ### Usage ```tsx import { IntegrationPicker } from "@/components/ui/integration-picker"; export function Integrations() { return ( <IntegrationPicker appName="affectUI" onToggle={(id, connected) => saveIntegration(id, connected)} onExplore={(id) => router.push(`/integrations/${id}`)} onBack={() => setPage("root")} onClose={closeMenu} /> ); } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | integrations | `Integration[]` | `10 sample services` | Each { id, name, domain, category, description, features, path + color or icon, connected } | | appName | `string` | `"affectUI"` | Your product's name in the title, e.g. “affectUI + Linear” | | appIcon | `ReactNode` | `the affectUI mark` | Your product's mark on the black tile | | defaultSelected | `string` | `the first item` | Id of the integration that is picked first | | onToggle | `(id: string, connected: boolean) => void` | | Called when an integration is switched on or off | | onExplore | `(id: string) => void` | | Called by “Explore integration” | | onBack | `() => void` | | Shows the back button and runs on ← in an empty search | | onClose | `() => void` | | Runs on Esc in an empty search | | className | `string` | | Extra class on the root | ### Keyboard - ↑ / ↓: Move through the list - ↵: Switch the picked integration on or off - Esc: Clear the search, then close - ←: Back to the parent page when the search is empty - ⌘ / /: Jump into the search from anywhere (Ctrl / on Windows) ### Accessibility - The search is a combobox that controls the list; the picked row is announced as the active option - The connection switch is a real switch with a label that says what it will do - Details update in a polite live region when the pick changes ### Motion - The gray highlight glides from row to row, also across the Connected and Available groups - The service tile swaps in with a short blur and lift; the arrows between the tiles turn once when connected - While connected, a ring keeps spreading out from the tiles – paused when the picker is off screen - With prefers-reduced-motion everything switches instantly ### Responsive - Follows its own width: below 560px the list sits above the details; the illustration and capabilities are left out - Dark mode built in: switches when an ancestor (usually <html>) has the class dark or data-theme="dark" ### Requirements - React 19 --- ## Integration Settings (Pro) Settings card for a connected service – on/off switch, API key, validated site address, time zone and a save bar - Page: https://www.affectui.com/components/integration-settings - Section: Components · Inputs - Install: `npx shadcn@latest add https://www.affectui.com/r/integration-settings.json` (needs the Pro access key) - Import: `@/components/ui/integration-settings` ### When to use - The settings of one connected service – a store, an ad account, a data source - Forms that should only save when someone confirms, with a clear way to discard ### When not to use - Long settings pages with many sections – split them into separate cards or pages ### Usage ```tsx import { IntegrationSettings } from "@/components/ui/integration-settings"; export function Store() { return ( <IntegrationSettings name="Shopify store" logo={<ShopifyLogo />} count={128} meta="Connected on March 4, 2026" defaultValues={{ siteUrl: "lumen-studio.com", timeZone: "(GMT +01:00) Berlin" }} onGenerateKey={() => api.createKey()} onSave={(values) => api.saveSettings(values)} /> ); } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | name | `string` | | Name of the service | | logo | `ReactNode` | | Logo in the white tile | | count | `number` | | Small number next to the name | | meta | `string` | | Line under the name; switching off shows a paused note instead | | defaultValues | `Partial<IntegrationValues>` | | enabled, siteUrl, timeZone and apiKey to start with | | siteHint | `string` | | Helper text under the address field | | timeZones | `string[]` | | Choices in the time zone picker | | onGenerateKey | `() => string \| Promise<string>` | | Creates a key – a random demo key without it | | onSave | `(values) => void \| Promise<void>` | | Called with all values from the save bar | | onToggle | `(enabled: boolean) => void` | | Called when the switch changes | ### Keyboard - Space: Toggle the switch - Tab: Move through the key actions, the address and the time zone - Escape: Close the time zone picker ### Accessibility - The toggle is a real switch; the address field reports invalid input and points to its hint - A new key is announced together with the reminder to copy it ### Motion - Switching off folds the settings away, the save bar slides open on changes – reduced motion turns this off ### Responsive - Dark mode built in: switches when an ancestor (usually <html>) has the class dark or data-theme="dark" ### Requirements - React 19 Related: app-directory, data-table --- ## KPI Chart (Pro) Metric card with a multi-line chart and a tooltip that follows the pointer - Page: https://www.affectui.com/components/kpi-chart - Section: Components · Data display - Install: `npx shadcn@latest add https://www.affectui.com/r/kpi-chart.json` (needs the Pro access key) - Import: `@/components/ui/kpi-chart` ### When to use - Dashboards that show one headline number and how its parts moved over a short period - Comparing a handful of channels, pages or plans day by day ### When not to use - More than about six lines – the chart and the tooltip get hard to read - Long time ranges with hundreds of points – use a chart library with zoom and sampling ### Usage ```tsx import { KpiChart } from "@/components/ui/kpi-chart"; export function Revenue() { return ( <KpiChart title="Revenue" value="$96.4k" delta={7900} format={(n) => `$${(n / 1000).toFixed(1)}k`} labels={["Mar 3", "Mar 4", "Mar 5"]} series={[ { label: "Organic", color: "#2f6bff", values: [4600, 4900, 5200] }, { label: "Paid ads", color: "#ec4899", values: [3100, 3400, 3200] }, ]} footer="Top channels" /> ); } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | title | `string` | | Label above the number | | value | `string` | | Headline value, already formatted | | delta | `number` | | Change against the previous period – green when positive, red when negative | | labels | `string[]` | | One label per point; first, middle and last are shown on the axis | | tooltipLabels | `string[]` | | Longer labels for the tooltip header, falls back to labels | | series | `KpiSeries[]` | | Lines with a label, a color and one value per label | | format | `(n: number) => string` | `compact, e.g. 1.2k` | Formats axis, tooltip and delta values | | footer | `string` | | Line under the chart | | defaultIndex | `number` | | Point that stays highlighted while nobody hovers | | className | `string` | | Extra class on the card | ### Keyboard - Tab: Focus the chart - ← / →: Move to the previous or next point - Esc: Close the tooltip ### Accessibility - The chart is focusable and readable with the arrow keys - The tooltip is a polite live region, so screen readers announce the values of the current point ### Motion - Lines draw in from the left on mount - Guide, dots and tooltip glide between points; reduced motion turns this off ### Responsive - The card fills its container up to 460px; the plot scales, line widths stay crisp - Touch works too – tap or drag across the chart - Dark mode built in: switches when an ancestor (usually <html>) has the class dark or data-theme="dark" ### Requirements - React 19 Related: progress-tracker --- ## Notification Stack (Free) Notice cards stacked behind each other – dismiss the front one to reveal the next - Page: https://www.affectui.com/components/notification-stack - Section: Components · Feedback - Install: `npx shadcn@latest add https://www.affectui.com/r/notification-stack.json` - Import: `@/components/ui/notification-stack` ### When to use - A few announcements or notices in a corner of an app – releases, maintenance, invites - Toast-style messages that stay until someone dismisses them ### When not to use - Messages that need an answer before people continue – use a dialog - Many notifications at once – past three or four, use an inbox or activity list ### Usage ```tsx import { NotificationStack } from "@/components/ui/notification-stack"; const notices = [ { id: "maintenance", badge: "Notice", title: "Scheduled maintenance", text: "We'll be offline on Sunday from 02:00 to 03:00 UTC.", action: { label: "View status", href: "/status", external: true }, pattern: "grid", }, ]; export function Notices() { return <NotificationStack notices={notices} onDismiss={(n) => markRead(n.id)} />; } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | notices | `Notice[]` | | id, badge, title, text, action ({ label, href, onClick, external }) and pattern | | onDismiss | `(notice: Notice) => void` | | Called when a notice is dismissed with its close button | | className | `string` | | Extra class on the stack | ### Keyboard - Tab: Move through the front card's buttons - Enter / Space: Run the action or dismiss the notice ### Accessibility - The stack is a labelled region; each card is an article named by its title - Only the front card is read out and focusable; the next one takes its place after a dismiss ### Motion - A dismissed card slides away and the next one moves up into its place; reduced motion turns this off ### Responsive - Dark mode built in: switches when an ancestor (usually <html>) has the class dark or data-theme="dark" ### Requirements - React 19 Related: decision-inbox, tips-panel --- ## OTP Input (Free) The verification code field done right – a gliding focus ring, paste and SMS autofill, a calm shake on errors and a morph into Verified - Page: https://www.affectui.com/components/otp-input - Section: Components · Inputs - Install: `npx shadcn@latest add https://www.affectui.com/r/otp-input.json` - Import: `@/components/ui/otp-input` ### When to use - Two-step sign-in, email or phone verification, confirming a sensitive action with a one-time code - Any short code people copy from an email or SMS – paste and autofill fill every box at once ### When not to use - Codes longer than about 8 characters or with free-form text – a plain text field is easier to read and correct - Passwords or PINs that must stay hidden – the boxes show what's typed ### Usage ```tsx import { useState } from "react"; import { OtpInput, type OtpStatus } from "@/components/ui/otp-input"; export function VerifyEmail() { const [code, setCode] = useState(""); const [status, setStatus] = useState<OtpStatus>("idle"); const [triesLeft, setTriesLeft] = useState(3); async function verify(value: string) { setStatus("verifying"); const res = await fetch("/api/verify", { method: "POST", body: JSON.stringify({ code: value }) }); if (res.ok) { setStatus("success"); router.push("/dashboard"); } else { setTriesLeft((n) => n - 1); setStatus("error"); } } return ( <OtpInput value={code} onChange={(next) => { setCode(next); if (status === "error" && next) setStatus("idle"); }} onComplete={verify} status={status} errorMessage={`That code didn't work. ${triesLeft} tries left.`} separator autoFocus onResend={() => fetch("/api/send-code", { method: "POST" })} resendIn={30} /> ); } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | length | `number` | `6` | Number of boxes | | separator | `boolean \| number` | `false` | A small dash between the boxes – true puts it in the middle, a number after that many boxes | | numeric | `boolean` | `true` | Digits only with the numeric keyboard on phones; false allows letters too, shown in capitals | | value | `string` | | The code – makes the field controlled | | defaultValue | `string` | `""` | Starting code when uncontrolled | | onChange | `(code: string) => void` | | Every change: typing, Backspace, paste, autofill and the clearing after an error | | onComplete | `(code: string) => void` | | Called once the last box is filled by the user – the place to start verifying | | status | `"idle" \| "verifying" \| "error" \| "success"` | `"idle"` | Verifying shimmers and locks the field, error shakes and shows the message, success morphs into the pill | | errorMessage | `ReactNode` | `"That code didn't work. Try again."` | Shown below the boxes after an error, until the user types again | | clearOnError | `boolean` | `true` | Clears the boxes shortly after the shake, ready for the next try | | successLabel | `string` | `"Verified"` | Text in the pill on success | | onResend | `() => void` | | Shows "Didn't get a code?" below the boxes with a countdown that becomes a Resend button; clicking restarts the countdown | | resendIn | `number` | `30` | Seconds until the code can be resent | | align | `"start" \| "center"` | `"start"` | Where the boxes and the lines below them sit | | disabled | `boolean` | `false` | Dims the field and ignores input | | autoFocus | `boolean` | `false` | Focuses the field on mount (without scrolling) | | label | `string` | `"Verification code"` | Accessible name; the number of digits is added | | className | `string` | | Extra class on the root | ### Keyboard - 0–9: Fills the active box and moves on; typing on a filled box replaces it - ⌫: Clears the box and goes back one - ← / →: Moves between the filled boxes and the next empty one - Home / End: Jumps to the first box, or to the next empty one - ⌘ / V: Pastes a whole code – spaces and dashes are ignored ### Accessibility - One real text input carries the whole code, so screen readers hear one field ("Verification code, 6 digits") instead of six - autocomplete="one-time-code" lets iOS and Android offer the code from an SMS; inputMode="numeric" opens the number pad - Errors set aria-invalid and point to the message with aria-describedby; verifying, errors, success and resend are announced in a polite live region ### Motion - A single focus ring glides from box to box with a softly blinking caret; digits pop in out of a short blur - Pasted or autofilled codes fill the boxes one after another - Verifying: a soft light passes over the boxes. Error: a calm shake, red borders, the message grows in below and the code fades out for the next try - Success: the boxes slide together while one frame morphs its width into a "Verified" pill and the check draws in - With prefers-reduced-motion everything switches instantly ### Responsive - Container query: the boxes shrink to fit their container – six boxes with a separator fit a 320px phone - Inputs use 16px text under the hood, so iOS doesn't zoom in on focus - Dark mode built in: switches when an ancestor (usually <html>) has the class dark or data-theme="dark" ### Requirements - React 19 Related: combobox, segmented-control, button, toaster --- ## Profile Menu (Free) Account card with the signed-in user, menu actions and profile setup progress - Page: https://www.affectui.com/components/profile-menu - Section: Components · Navigation - Install: `npx shadcn@latest add https://www.affectui.com/r/profile-menu.json` - Import: `@/components/ui/profile-menu` ### When to use - The dropdown behind the avatar in an app header - Nudging people to finish their profile, right next to the account actions ### When not to use - Long menus with nested levels – use a full dropdown menu with submenus - Long onboarding flows with many steps – use a checklist like the Setup Guide instead ### Usage ```tsx import { ProfileMenu, SettingsIcon, LogoutIcon } from "@/components/ui/profile-menu"; export function Account() { return ( <ProfileMenu name="Lena Hartmann" email="lena.hartmann@fastmail.com" items={[ { id: "settings", label: "Settings", icon: <SettingsIcon />, onSelect: openSettings }, { id: "logout", label: "Logout", icon: <LogoutIcon />, onSelect: signOut }, ]} setup={{ done: 3, total: 5, next: "Add a profile photo", onContinue: openProfile }} /> ); } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | name | `string` | | Name of the signed-in user | | email | `string` | | Email under the name, cut off with an ellipsis when long | | avatar | `ReactNode` | | Replaces the default avatar, e.g. an <img> | | items | `ProfileMenuItem[]` | | Actions with label, icon, optional badge and onSelect | | setup | `ProfileSetup` | | Profile setup progress: done, total, the next step and onContinue – leave empty to hide | | className | `string` | | Extra class on the card | ### Keyboard - Tab: Move into the menu - ↑ / ↓: Move between the actions - Home / End: Jump to the first or last action - Enter / Space: Run the focused action ### Accessibility - Actions are buttons in a menu role, so screen readers announce them as menu items - The setup bar is a progressbar with its percentage as value ### Motion - The setup bar fills up when the card comes into view - Reduced motion shows it at once ### Responsive - Dark mode built in: switches when an ancestor (usually <html>) has the class dark or data-theme="dark" ### Requirements - React 19 Related: progress-tracker --- ## Progress Tracker (Free) Onboarding checklist with an animated progress bar, ticks that draw themselves and auto play - Page: https://www.affectui.com/components/progress-tracker - Section: Components · Feedback - Install: `npx shadcn@latest add https://www.affectui.com/r/progress-tracker.json` - Import: `@/components/ui/progress-tracker` ### When to use - Onboarding checklists that guide new users through their first steps - Any short list of tasks where progress should feel rewarding ### When not to use - Long task lists – past roughly eight steps the card gets hard to scan - Long step labels – each step stays on one line and cuts off with an ellipsis - Steps that must happen in a fixed order – use a stepper that locks the next step ### Usage ```tsx import { ProgressTracker } from "@/components/ui/progress-tracker"; const steps = [ { id: "tools", label: "Connect your imported tools" }, { id: "notify", label: "Turn on notifications" }, { id: "project", label: "Create a project", done: true }, ]; export function Onboarding() { return <ProgressTracker title="Getting started" steps={steps} onChange={save} />; } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | steps | `ProgressStep[]` | | Each step has an id, a label, an optional done flag and icon | | title | `string` | `"Getting started"` | Heading above the bar | | autoPlay | `boolean` | `false` | Ticks the open steps one by one and starts over – stops when someone clicks or types | | interval | `number` | `2200` | Pause between two automatic steps, in ms | | onChange | `(steps: ProgressStep[]) => void` | | Called with every step and its state | | className | `string` | | Extra class on the card | ### Keyboard - Tab: Move between the steps - Space / Enter: Tick or untick the focused step ### Accessibility - Each step is a checkbox with aria-checked; the bar is a progressbar with the count as text - Any click or key press stops the automatic play, so it never fights the user ### Motion - The bar fills smoothly and a light sweeps across it each time a step is done - The check ring fills and pops, then the tick draws itself - A line strikes through the label, line by line, while it fades to gray - On mount the card settles in and the rows fade up one after another - Steps stay in place so nothing jumps; the counter rolls to the new number - With prefers-reduced-motion every change is instant ### Responsive - Dark mode built in: switches when an ancestor (usually <html>) has the class dark or data-theme="dark" ### Requirements - React 19 Related: button, chat-composer --- ## Project Card (Pro) A two-line project summary that quietly unfolds on hover – a progress ring with one segment per subtask, a people filter and a status picker, all linked by hover - Page: https://www.affectui.com/components/project-card - Section: Components · Cards - Install: `npx shadcn@latest add https://www.affectui.com/r/project-card.json` (needs the Pro access key) - Import: `@/components/ui/project-card` ### When to use - Boards, lists and sidebars where many projects or tasks sit next to each other and the details should be one hover away - Anything with a short summary and a longer truth behind it – a project, a ticket, a deal, an agent run - When opening a separate page or drawer would be too heavy for a quick look and a small edit ### When not to use - Long content or forms – open a drawer or a page instead; this card is for a glance and a tick - Dense tables – a row that grows on hover pushes everything below it around - When the details must stay visible – show the open card as a static layout instead ### Usage ```tsx import { ProjectCard } from "@/components/ui/project-card"; export function OnboardingCard() { return ( <ProjectCard title="Onboarding flow" tasks={[ { id: "welcome", label: "Welcome screen", done: true, assignee: "Mara" }, { id: "workspace", label: "Workspace setup", done: true, assignee: "Jonas" }, { id: "invite", label: "Invite teammates", assignee: "Mara" }, ]} status="progress" due="Oct 24" members={[{ name: "Mara" }, { name: "Jonas" }]} action={{ label: "Open", onClick: () => router.push("/projects/onboarding") }} /> ); } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | title | `string` | | Name of the project | | tasks | `ProjectTask[]` | | { id, label, done?, assignee? } – one ring segment and one row each; assignee is a member's name | | onTasksChange | `(tasks: ProjectTask[]) => void` | | Called with the full list after a tick. With it the card shows exactly what you pass; without it the ticks live inside the card | | status | `string` | `first option` | Id of the chosen status | | statuses | `ProjectOption[]` | `To do, In progress, In review, Done` | { id, label, tone? } – tone is red, amber, green, blue, violet or gray and also picks the icon; icon replaces it | | onStatusChange | `(id: string) => void` | | Called with the picked status | | due | `string` | | Ready-to-show due date, e.g. "Oct 24" – shown as "Due Oct 24" | | members | `ProjectMember[]` | | { name, avatar? } – the avatars in the head; hovering one shows only that person's subtasks. Initials on a soft color without an image | | action | `{ label: string; onClick: () => void }` | | A button on the tray of the open card, e.g. Open | | hint | `ReactNode` | `"2 left" / "All done"` | The line on the tray while nothing is hovered | | trigger | `"hover" \| "click"` | `"hover"` | Hover opens under the mouse; taps and the keyboard always toggle | | open | `boolean` | | Controlled open state | | defaultOpen | `boolean` | `false` | Starts open when uncontrolled | | onOpenChange | `(open: boolean) => void` | | Called when the card opens or closes | | className | `string` | | Extra class on the root | ### Keyboard - Enter / Space: On the card: opens or closes it - Tab: Through the status, the people, the subtasks and the action - Space: On a subtask: ticks or unticks it. On a person: keeps or releases their filter. On the status: unfolds the options - Esc: Closes the status options first, then the card ### Accessibility - The head is one button with aria-expanded whose name carries the summary: title, progress, status and due date - Subtasks are checkboxes, the people are toggle buttons, the status options are a radio group - While the card is closed, everything inside is inert – nothing hidden can be focused or read - Focusing a subtask or a person does what hovering does, and the line on the tray is announced politely ### Motion - Nothing travels: the head stays where it is and the list unfolds below it while it fades in - The status options unfold in the same way, in place - Hovering a subtask thickens its ring segment and brings in the assignee's avatar; ticking fills the circle, draws the check and turns the segment dark - Hovering a person fades everyone else's subtasks and segments - The line on the tray swaps with a short rise; the number in the ring pops when it changes - With prefers-reduced-motion everything switches without movement ### Responsive - Fills its container up to 320px. Below 290px the date drops its word ("Oct 24") and the avatars move closer; a long title is cut with an ellipsis and nothing runs under the avatars - Touch screens: a tap opens, a tap outside closes, rows and options are taller and the assignee avatars are always visible - Dark mode built in: switches when an ancestor (usually <html>) has the class dark or data-theme="dark" ### Notes for AI - The card grows downward when it opens – in a list, give it room or lay it over the next items with position absolute in a fixed-height slot - A task's assignee must match a member's name exactly – that is what links rows, avatars and the people filter - More than about twelve subtasks make the ring segments too small to point at – group them first ### Requirements - React 19 Related: progress-tracker, agent-plan, compact-breakdown --- ## Reaction Bar (Free) Like, comment, repost and share in one pill – icons pop and fill, counts roll in digit by digit - Page: https://www.affectui.com/components/reaction-bar - Section: Components · Buttons - Install: `npx shadcn@latest add https://www.affectui.com/r/reaction-bar.json` - Import: `@/components/ui/reaction-bar` ### When to use - Reactions under posts, comments or updates in a feed - Any toggle with a public count, like saves or upvotes ### When not to use - Actions that need a confirmation – a toggle gives none ### Usage ```tsx import { ReactionBar } from "@/components/ui/reaction-bar"; export function PostActions({ post }) { return ( <ReactionBar reactions={{ like: { count: post.likes, active: post.liked }, comment: { count: post.comments } }} onReact={(key, state) => api.react(post.id, key, state.active)} /> ); } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | reactions | `Partial<Record<ReactionKey, { count; active? }>>` | | Starting count and pressed state per action | | onReact | `(key, state) => void` | | Called after a toggle with the new count and state | | actions | `ReactionKey[]` | `["like", "comment", "repost", "share"]` | Which actions to show, in order | | className | `string` | | Extra class on the pill | ### Keyboard - Tab: Move between the actions - Enter / Space: Toggle the focused action ### Accessibility - Every action is a toggle button with aria-pressed and a label that includes its count - The rolling digits are decorative – the label carries the number ### Motion - The icon shrinks, springs and fills with its color; the count glides to its new width and changed digits roll in - Reduced motion turns this off ### Responsive - Dark mode built in: switches when an ancestor (usually <html>) has the class dark or data-theme="dark" ### Requirements - React 19 Related: button, comments-panel --- ## Segmented Control (Free) Switch between two to six options – a raised pill or underline that glides, stretches a little and can be dragged across, with icons, counts and badges - Page: https://www.affectui.com/components/segmented-control - Section: Components · Navigation - Install: `npx shadcn@latest add https://www.affectui.com/r/segmented-control.json` - Import: `@/components/ui/segmented-control` ### When to use - Switching between two to six views of the same content – sections of a dashboard, list / board / calendar, monthly / yearly - A choice that applies right away and where every option should stay visible ### When not to use - More than six options or long labels – use a select or a menu - Choices that are confirmed with a Save button inside a form – radio buttons read clearer there - Turning one thing on or off – that's a switch ### Usage ```tsx import { SegmentedControl } from "@/components/ui/segmented-control"; export function DashboardTabs() { const [section, setSection] = useState("overview"); return ( <SegmentedControl aria-label="Dashboard section" kind="tabs" value={section} onChange={setSection} options={[ { value: "overview", label: "Overview", controls: "panel-overview" }, { value: "analytics", label: "Analytics", controls: "panel-analytics" }, { value: "reports", label: "Reports", controls: "panel-reports" }, { value: "notifications", label: "Notifications", count: unread, controls: "panel-notifications" }, ]} /> ); } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | options | `SegmentOption[]` | | { value, label?, icon?, count?, badge?, disabled?, ariaLabel?, controls? } – two to six work best; give icon-only options an ariaLabel | | value | `string` | | Controlled value | | defaultValue | `string` | `first enabled option` | Starting value when uncontrolled | | onChange | `(value: string) => void` | | Called with the new value – on click, while dragging the pill and on arrow keys | | size | `"sm" \| "md" \| "lg"` | `"md"` | 26, 30 or 36px tall options (40px+ on touch screens) | | variant | `"tray" \| "underline"` | `"tray"` | A gray track with a raised white pill, or text tabs on a hairline with a sliding line | | fullWidth | `boolean` | `false` | Fills its container with equal segments, each as wide as the widest one needs | | kind | `"radio" \| "tabs"` | `"radio"` | A radiogroup, or a tablist that switches panels (pair with controls on each option) | | disabled | `boolean` | `false` | Disables the whole control | | aria-label | `string` | | Names the group – or use aria-labelledby | | className | `string` | | Extra class on the root | ### Keyboard - Tab: Focuses the active option (one tab stop for the whole control) - ← / →: Moves to and selects the previous or next option, wrapping around; ↑ ↓ too as a radiogroup - Home / End: Selects the first or last option - ↵: Selects the focused option (Space works too) ### Accessibility - A radiogroup with radios (aria-checked), or a tablist with tabs (aria-selected, aria-controls) with kind="tabs" - Roving tabindex: only the active option is in the tab order; disabled options are skipped by the arrow keys - The semibold copy of each label is aria-hidden, so names are read once; counts and badges are part of the name - Visible focus ring on the focused option ### Motion - The indicator glides to the picked option; the edge facing the target leads, so it stretches a little and settles – calm, no bounce - Labels crossfade from regular to semibold while the semibold copy holds the width, so neighbours never shift - When a label or count changes, the option and the whole control morph to the new width and the pill follows; a new count blurs in - Pressing gives the pill a little (scale 0.97); dragging it across the options selects as you go - With prefers-reduced-motion everything switches instantly ### Responsive - If it doesn't fit, it scrolls sideways with soft edges on the sides that hide options and keeps the active one in view - Touch screens: options are at least 40px tall; a tap picks on lift, so swiping to scroll never picks by accident - Dark mode built in: switches when an ancestor (usually <html>) has the class dark or data-theme="dark" ### Requirements - React 19 Related: date-range-picker, filter-menu, data-table --- ## Setup Guide (Pro) Onboarding panel with a progress bar and collapsible task groups that strike through when done - Page: https://www.affectui.com/components/setup-guide - Section: Components · Feedback - Install: `npx shadcn@latest add https://www.affectui.com/r/setup-guide.json` (needs the Pro access key) - Import: `@/components/ui/setup-guide` ### When to use - Onboarding with several areas, each made of a few steps that build on each other - A floating guide in a dashboard corner that people can fold away and come back to ### When not to use - A single short list of steps – the Progress Tracker is lighter - Steps that can be done in any order – here each task unlocks the next ### Usage ```tsx import { SetupGuide } from "@/components/ui/setup-guide"; const groups = [ { id: "workspace", title: "Set up your workspace", tasks: [ { id: "name", label: "Choose a workspace name" }, { id: "invite", label: "Invite your teammates" }, ], }, { id: "verify", title: "Verify your account", tasks: [{ id: "email", label: "Confirm your email", done: true }] }, ]; export function Onboarding() { return <SetupGuide groups={groups} onChange={save} onClose={hideGuide} />; } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | groups | `SetupGroup[]` | | Groups with an id, a title and tasks; each task has an id, a label and an optional done flag | | title | `string` | `"Setup guide"` | Heading above the bar | | onEdit | `() => void` | | Shows an Edit link in the header | | onClose | `() => void` | | Shows a close button in the header | | onChange | `(done: string[]) => void` | | Called with the ids of all finished tasks | | className | `string` | | Extra class on the panel | ### Keyboard - Tab: Move between groups, tasks and header buttons - Enter / Space: Open or close a group, tick or untick a task ### Accessibility - Group headers are buttons with aria-expanded, the closed panels are inert - Tasks that are not unlocked yet are disabled; finished ones are pressed toggle buttons - The bar is a progressbar with its percentage as value ### Motion - Groups unfold smoothly; a finished group is struck through, folds away and opens the next one - Reduced motion turns the transitions off ### Responsive - Dark mode built in: switches when an ancestor (usually <html>) has the class dark or data-theme="dark" ### Requirements - React 19 Related: progress-tracker, profile-menu --- ## Smart Recommendation (Free) Suggestion card with a confidence signal, alternatives to switch to and accept with undo - Page: https://www.affectui.com/components/smart-recommendation - Section: Components · Cards - Install: `npx shadcn@latest add https://www.affectui.com/r/smart-recommendation.json` - Import: `@/components/ui/smart-recommendation` ### When to use - Suggesting the next step an app or assistant thinks is best – with a way out - Settings people rarely change but should get a nudge on, like sync modes or plans ### When not to use - Choices without a clear favorite – show them side by side instead ### Usage ```tsx import { SmartRecommendation } from "@/components/ui/smart-recommendation"; const options = [ { id: "incremental", label: "Incremental sync", reason: "Only 4% of rows changed.", signal: 3 }, { id: "full", label: "Full refresh", reason: "Rebuilds every table.", signal: 1 }, ]; export function Sync() { return <SmartRecommendation options={options} onAccept={(o) => api.setMode(o.id)} />; } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | options | `Recommendation[]` | | The first is recommended, the rest are alternatives – id, label, reason and signal (0–3) | | onAccept | `(option) => void` | | Called when an option is accepted | | onUndo | `(option) => void` | | Called when an accept is undone | | defaultOpen | `boolean` | `true` | Starts with the reason visible | | title | `string` | `"Smart recommendation"` | Heading of the card | ### Keyboard - Enter / Space: Fold the card, open alternatives or accept - Escape: Close the alternatives ### Accessibility - The heading is a button with aria-expanded; alternatives are a menu of radio items - The signal is written out in words next to the bars ### Motion - The reason swaps softly when an alternative is picked; reduced motion turns this off ### Responsive - Dark mode built in: switches when an ancestor (usually <html>) has the class dark or data-theme="dark" ### Requirements - React 19 Related: decision-inbox, comment-thread --- ## Stepper Form (Pro) A multi-step flow for onboarding and setup – steps that fill and check off, sliding steps, a card that morphs to each step, inline validation and a success state - Page: https://www.affectui.com/components/stepper-form - Section: Components · Inputs - Install: `npx shadcn@latest add https://www.affectui.com/r/stepper-form.json` (needs the Pro access key) - Import: `@/components/ui/stepper-form` ### When to use - Onboarding, checkout or setup wizards where one long form would feel like too much at once - Flows whose later steps depend on earlier answers, ending in a review before anything is created ### When not to use - Two or three fields – a single form is faster - Settings people jump around in freely – use tabs or sections instead ### Usage ```tsx import { StepperForm, StepperField, StepperInput, StepperTags, StepperChoices, StepperReview, type Step } from "@/components/ui/stepper-form"; type Values = { name: string; slug: string; invites: string[]; plan: string }; const steps: Step<Values>[] = [ { id: "workspace", title: "Workspace", heading: "Name your workspace", render: ({ values, setValues }) => ( <> <StepperField name="name" label="Workspace name"> <StepperInput value={values.name} onChange={(e) => setValues({ name: e.target.value, slug: slugify(e.target.value) })} /> </StepperField> <StepperField name="slug" label="Workspace URL"> <StepperInput prefix="acme.app/" value={values.slug} onChange={(e) => setValues({ slug: e.target.value })} /> </StepperField> </> ), validate: (v) => ({ name: v.name.trim() ? undefined : "Give your workspace a name" }), }, { id: "team", title: "Team", render: ({ values, setValues }) => ( <StepperField name="invites" label="Email addresses" optional> <StepperTags value={values.invites} onChange={(invites) => setValues({ invites })} /> </StepperField> ), }, { id: "plan", title: "Plan", render: ({ values, setValues }) => ( <StepperChoices label="Plan" value={values.plan} options={plans} onChange={(plan) => setValues({ plan })} /> ), }, { id: "review", title: "Review", render: ({ values, goTo }) => ( <StepperReview onEdit={goTo} rows={[ { label: "Workspace", value: values.name, step: "workspace" }, { label: "Plan", value: values.plan, step: "plan" }, ]} /> ), }, ]; export function Onboarding() { return ( <StepperForm<Values> steps={steps} initialValues={{ name: "", slug: "", invites: [], plan: "pro" }} submitLabel="Create workspace" onComplete={(values) => api.createWorkspace(values)} /> ); } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | steps | `Step<V>[]` | | { id, title, heading?, description?, render(ctx), validate?(values) } – render gets values, setValues, errors, goTo and next; validate returns messages by field name | | initialValues | `V` | | Values shared by all steps, so Back and Edit keep what was entered | | onComplete | `(values: V) => void \| Promise<void>` | | Runs on the last step; a promise shows the submitting label until it settles, a thrown error is shown next to the button | | initialStep | `number` | `0` | Index of the first step shown | | step | `number` | | Controlled step index; pair with onStepChange | | onStepChange | `(step: number) => void` | | Called whenever the step changes | | submitLabel | `string` | `"Finish"` | Label of the button on the last step | | submittingLabel | `string` | `"Creating…"` | Label while onComplete runs | | success | `(values: V, reset: () => void) => ReactNode` | | Replaces the steps once done – StepperSuccess gives the popping check, a heading and a line | | className | `string` | | Extra class on the root | | StepperField | `{ name, label, hint?, optional?, children }` | | Label, control and inline message for the field name in validate; the control shakes when Continue finds a problem | | StepperInput | `InputHTMLAttributes & { prefix? }` | | A text field wired to its StepperField; prefix shows fixed text like "acme.app/" | | StepperTags | `{ value, onChange, validate?, max?, placeholder? }` | | Entries as chips – emails by default; Enter, comma or a pasted list add, × or Backspace remove | | StepperChoices | `{ value, onChange, options, label }` | | Selectable cards { value, title, aside?, description?, badge? } as a radio group with a gliding ring | | StepperReview | `{ rows, onEdit? }` | | A summary { label, value, step? } – rows with a step get an Edit link (pass goTo) | ### Keyboard - ↵: Continues (in a chip field: adds the typed entry first, Continue when it's empty) - Tab: Moves through the fields, then Back and Continue - ← / →: In the plan cards: moves and selects; ↵ picks and continues - , / ⌫: In a chip field: comma adds, Backspace in the empty field removes the last chip ### Accessibility - The steps are an ordered list with aria-current="step"; finished steps are buttons that jump back - On narrow screens the list stays for screen readers while the compact "Step 2 of 4" text shows on screen - The new step's first field (or its heading) gets the focus; after a failed Continue the first invalid field does - Fields carry aria-invalid and point to their message with aria-describedby; "Step 2 of 4: Team" and errors are announced in a polite live region - Continue looks dimmed while the step isn't valid but still answers, so keyboard and screen reader users hear what's missing ### Motion - A finished step swaps its number for a check that draws itself in - Steps slide in from the side you're heading to with a short fade, while the old one slides out the other way - The card glides to the height of each step; the primary button glides to the width of its new label - An invalid field shakes once and its message folds in; chips pop in and fold away - The plan ring glides between cards; the success check pops in - With prefers-reduced-motion everything switches instantly ### Responsive - Fills its container up to 520px and works down to 320px - Below 420px the steps collapse to "Step 2 of 4 · Team", plan cards stack and review rows wrap - Touch screens get 16px fields (no zoom on iOS) and buttons of at least 40px; the footer sticks to the bottom in long pages - Dark mode built in: switches when an ancestor (usually <html>) has the class dark or data-theme="dark" ### Requirements - React 19 Related: setup-guide, progress-tracker, combobox, segmented-control --- ## Thinking Loader (Free) A small working pill for AI replies – an eight-ray spinner and a shimmering label that rolls through phases and settles on a check - Page: https://www.affectui.com/components/thinking-loader - Section: Components · Chat - Install: `npx shadcn@latest add https://www.affectui.com/r/thinking-loader.json` - Import: `@/components/ui/thinking-loader` ### When to use - The wait between sending a prompt and the first streamed token – one line that says what the model is doing - Agent and pipeline steps with real labels: searching, reading, running a tool, writing - Inline in a chat bubble or next to a button where a full panel would be too much ### When not to use - When people should see the sources and every step – use Thinking State - Page or data loading with no phases – a skeleton or a plain spinner reads calmer - Waits under half a second – the loader would only flash ### Usage ```tsx import { ThinkingLoader } from "@/components/ui/thinking-loader"; export function AssistantPending({ streaming }: { streaming: boolean }) { return ( <ThinkingLoader phases={["Thinking", "Searching the web", "Reading sources", "Writing response"]} interval={2200} done={streaming} /> ); } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | phases | `string[]` | `["Thinking", "Searching the web", "Reading sources", "Writing response"]` | Labels to walk through, one after another | | interval | `number` | `2200` | Time per phase in ms when the phase isn't controlled | | phase | `number` | | Controlled index into phases – the timer stops and the loader shows this phase | | loop | `boolean` | `false` | Start over after the last phase instead of holding it | | done | `boolean` | `false` | Turns the spinner into a check and the label into doneLabel; setting it back to false starts over | | doneLabel | `string \| ((seconds: number) => string)` | `(s) => `Thought for ${s}s`` | Label once done – a function gets the seconds since the loader started | | size | `"sm" \| "md"` | `"md"` | 28 or 34px tall | | variant | `"pill" \| "plain"` | `"pill"` | A bordered capsule, or just the spinner and the label | | className | `string` | | Extra class on the root | ### Accessibility - role="status" with aria-live="polite": screen readers hear each new phase and the done label once - The spinner, the check and the label rolling out are aria-hidden; the shimmer copy has empty alt text, so labels are read once ### Motion - Eight rays fade one after another, so the spinner turns without rotating - A light sweeps over the label while it works - On a new phase the old label rolls up and blurs out, the new one rolls in, and the pill glides to its new width - Done: the spinner shrinks away while the check grows in and draws itself; the label stops shimmering - With prefers-reduced-motion the labels switch instantly and the spinner and shimmer stand still ### Responsive - Sizes to its label; long phases stay on one line, so keep them short (two to four words) - Dark mode built in: switches when an ancestor (usually <html>) has the class dark or data-theme="dark" ### Notes for AI - Map phases to real events (tool calls, retrieval, first token) with the phase prop when you have them; the timer is a fallback - Set done when the first token arrives and swap the loader for the streamed message shortly after ### Requirements - React 19 Related: thinking-state, ai-answer, chat-composer --- ## Thinking State (Pro) AI working panel with a live timer, searched sources and thinking steps that appear one by one - Page: https://www.affectui.com/components/thinking-state - Section: Components · Chat - Install: `npx shadcn@latest add https://www.affectui.com/r/thinking-state.json` (needs the Pro access key) - Import: `@/components/ui/thinking-state` ### When to use - Showing what an AI assistant is doing while it works: searching, reading, drafting - Long-running tasks where a visible trail of steps builds trust and keeps people waiting patiently ### When not to use - Short waits under a second – a simple spinner or shimmer is enough - Exact progress you can measure – use a progress bar with a percentage ### Usage ```tsx import { ThinkingState } from "@/components/ui/thinking-state"; const steps = [ { kind: "search", label: "Searched", sources: [{ label: "acme.com" }, { label: "+4 more" }] }, { kind: "step", label: "Reading the sources" }, { kind: "step", label: "Drafting your answer" }, ]; export function AssistantMessage() { return <ThinkingState steps={steps} onDone={(seconds) => console.log(seconds)} />; } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | steps | `ThinkingStep[]` | | Searches and thinking steps, revealed in order. Sources can link out (href) and show a tooltip; moreSources hide behind a “+N more” chip | | doneLabel | `string` | `"Finished thinking"` | Label of the final row | | stepMs | `number` | `1300` | Pause between two steps, in ms | | loop | `boolean` | `false` | Starts over a few seconds after finishing | | onDone | `(seconds: number) => void` | | Called once all steps are done | | className | `string` | | Extra class on the root | ### Keyboard - Enter / Space: Collapse or expand the steps (on the header), or show more sources - Tab: Move through the source links – each shows its tooltip on focus ### Accessibility - The header is a button with aria-expanded, so the steps can be collapsed - New steps are announced politely through an aria-live list ### Motion - The header shimmers with a live timer while thinking, then turns into “Worked for …” - Steps fade up one after another and the timeline grows down to each new node - Sources pop in one by one; the active step pulses and shimmers - Hovering a source shows its link as a tooltip; “+N more” reveals the remaining sources - At the end a blue check pops in and draws its tick - With prefers-reduced-motion everything appears without animation ### Responsive - Dark mode built in: switches when an ancestor (usually <html>) has the class dark or data-theme="dark" ### Requirements - React 19 Related: chat-composer, progress-tracker --- ## Tips Panel (Free) Grid of short tips with one action each – taken tips turn into a done state - Page: https://www.affectui.com/components/tips-panel - Section: Components · Cards - Install: `npx shadcn@latest add https://www.affectui.com/r/tips-panel.json` - Import: `@/components/ui/tips-panel` ### When to use - Pointing new users to features they haven't tried yet - A “What's new” or getting-started panel in a dashboard or a dialog ### When not to use - Steps that build on each other – use the Setup Guide - A single announcement – a banner or toast is lighter ### Usage ```tsx import { Kbd, TipsPanel } from "@/components/ui/tips-panel"; const tips = [ { id: "search", title: "Quick search", description: <>Press <Kbd>⌘K</Kbd> to jump anywhere.</>, action: "Try it" }, { id: "pro", title: "Ready for Pro?", description: "Unlimited projects and priority support.", action: "See plans", primary: true }, ]; export function Tips() { return <TipsPanel tips={tips} onClose={() => setOpen(false)} />; } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | tips | `Tip[]` | | id, title, description, action, doneLabel, primary and onAction | | title | `string` | `"Tips"` | Heading in the gray tray | | onClose | `() => void` | | Shows a Close button in the footer | | closeLabel | `string` | `"Close"` | Text of the Close button | | className | `string` | | Extra class on the tray | ### Keyboard - Tab: Move between the tip actions and Close - Enter / Space: Take the focused action ### Accessibility - Each tip is a list item with a real button; the done state is announced as a status - The counter in the heading is a polite live region ### Motion - Tips fade in one after another; a taken tip pops into its done state - Reduced motion turns both off ### Responsive - Set --ui-tp-max-height on the panel to change how tall the list grows before it scrolls (420px by default) - Three columns, two below 560px panel width, one below 380px – based on the panel's own width - Dark mode built in: switches when an ancestor (usually <html>) has the class dark or data-theme="dark" ### Requirements - React 19 Related: setup-guide, decision-inbox --- ## Toaster (Pro) Calm toasts from anywhere – a stack that fans out on hover, promise toasts that morph from spinner to check, swipe to dismiss - Page: https://www.affectui.com/components/toaster - Section: Components · Feedback - Install: `npx shadcn@latest add https://www.affectui.com/r/toaster.json` (needs the Pro access key) - Import: `@/components/ui/toaster` ### When to use - Short confirmations after an action – saved, copied, sent, moved to trash (with Undo) - Background work people started and want to hear back from – uploads, syncs, exports (toast.promise) - Errors that don't block the page, e.g. a sync that failed and can be retried later ### When not to use - Errors in a form – show them next to the field - Anything people must read or decide on – use a dialog or an inline banner - Long-running progress with details – a progress panel or tracker fits better ### Usage ```tsx import { Toaster, toast } from "@/components/ui/toaster"; // Once, near the root of the app export function App() { return ( <> <Routes /> <Toaster position="bottom-right" /> </> ); } // Anywhere – no hook or context needed function save() { toast.success("Draft saved", { description: "Q3 Board Report · all changes kept" }); } function remove(file: File) { trash(file.id); toast("File moved to trash", { description: file.name, action: { label: "Undo", onClick: () => restore(file.id) }, }); } function upload(file: File) { toast.promise(api.upload(file), { loading: { title: "Uploading report…", description: file.name }, success: (res) => ({ title: "Report uploaded", description: `Shared with ${res.people} people` }), error: (err) => ({ title: "Upload failed", description: String(err) }), }); } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | position | `"top-left" \| "top-center" \| "top-right" \| "bottom-left" \| "bottom-center" \| "bottom-right"` | `"bottom-right"` | Corner or edge the stack sits in; new toasts come in from that edge | | inline | `boolean` | `false` | Positions the stack inside the nearest positioned ancestor instead of the viewport | | id | `string` | | Only shows toasts sent with { toaster: id } – for a second, separate stack | | duration | `number` | `4000` | Default time on screen in ms; each toast can override it, Infinity keeps it open | | visibleToasts | `number` | `3` | Toasts visible at once; older ones wait (with a paused timer) until there's room | | expand | `boolean` | `false` | Always shows the stack fanned out instead of on hover and focus | | offset | `number` | `24 (20 inline)` | Distance from the edges in px; phones always use 12px gutters | | gap | `number` | `10` | Space between toasts when fanned out, in px | | hotkey | `string \| null` | `"F8"` | Key (KeyboardEvent.key or .code) that moves focus to the newest toast | | hairline | `boolean` | `true` | A 1px line along the bottom that runs down with the timer and stops when it pauses | | closeButton | `boolean` | `true` | Small close button on hover and focus – always visible on touch screens | | label | `string` | `"Notifications"` | Name of the region for screen readers (the hotkey is added) | | className | `string` | | Extra class on the region | | toast(title, options?) | `(ReactNode, ToastOptions) => ToastId` | | Shows a plain toast; also toast.success, toast.error, toast.info and toast.loading. Returns the id | | toast.promise(promise, messages, options?) | `<T>(Promise<T> \| () => Promise<T>, { loading, success, error }) => Promise<T>` | | A loading toast that turns into success or error; each message is a title, { title, description } or a function of the result | | toast.dismiss(id?) | `(ToastId?) => void` | | Closes one toast, or all of them without an id | | ToastOptions | `{ id?, description?, action?: { label, onClick }, duration?, icon?, toaster?, onDismiss? }` | | Pass an existing id to update that toast in place (the timer restarts, the text crossfades). action.onClick closes the toast unless it calls event.preventDefault() | ### Keyboard - F8: Moves focus to the newest toast and fans the stack out - Tab: Goes from toast to toast, through their action and close buttons - Esc: Closes the focused toast (or the newest) and moves focus to the next one; focus returns where it was after the last ### Accessibility - The stack is a labelled region ("Notifications (F8)"); every toast is focusable - New and updated toasts are read out in a polite status region – errors in an alert region, so they come first - The timer pauses while the pointer or keyboard focus is in the stack and while the tab is hidden, so nothing vanishes mid-read - Swiping is never the only way out: there's Esc and a named close button - Icons are decorative; color is limited to the success and error icons, the text says what happened ### Motion - New toasts lift in from the edge; the ones behind move back, shrink a little and peek out - Hover or focus fans the stack out into a list – every toast is measured, so positions and heights glide - Promise toasts: the spinning arc closes into a ring while a check or cross draws in; the text crossfades and the card resizes - Closed toasts fade and sink a few pixels while the rest of the stack moves up; swiped toasts fly out in the swipe direction, short swipes rubber-band back - With prefers-reduced-motion everything switches instantly – only the spinner keeps turning ### Responsive - 356px wide on larger screens; under 600px it spans the width with 12px gutters (and respects the home indicator) - Touch: tap the stack to fan it out, swipe sideways or down to dismiss; close and action buttons get bigger hit areas - Dark mode built in: switches when an ancestor (usually <html>) has the class dark or data-theme="dark" ### Notes for AI - Render <Toaster /> once; call toast() from event handlers or effects, not during render - Use toast.promise for async work instead of separate loading and success toasts - Keep titles to a few words and put details in description ### Requirements - React 19 Related: notification-stack, progress-tracker, dropzone --- ## Tool Call Card (Free) An agent's tool call in one line – live timer, status and summary, folds open to highlighted arguments and result with copy, retry on errors and a group that sums them up - Page: https://www.affectui.com/components/tool-call-card - Section: Components · Agents - Install: `npx shadcn@latest add https://www.affectui.com/r/tool-call-card.json` - Import: `@/components/ui/tool-call-card` ### When to use - Tool and function calls in an agent chat or run log – what was called, with what, and what came back - Agents that call several tools per turn – wrap them in ToolCallGroup so the answer isn't buried under cards - Developer-facing views where people need to copy the exact arguments or result to debug ### When not to use - Plain reasoning with no tool calls – use Thinking State or Thinking Loader - End users who never care about JSON – show a one-line status instead and keep the details in a log - Huge payloads (megabytes of JSON) – trim or paginate them first; the card renders the whole string ### Usage ```tsx import { ToolCallCard, ToolCallGroup } from "@/components/ui/tool-call-card"; export function ToolCalls({ calls }: { calls: ToolCall[] }) { return ( <ToolCallGroup> {calls.map((call) => ( <ToolCallCard key={call.id} name={call.name} summary={call.summary} status={call.status} args={call.args} result={call.result} error={call.error} startedAt={call.startedAt} duration={call.durationMs} onRetry={() => retryToolCall(call.id)} /> ))} </ToolCallGroup> ); } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | name | `string` | | Tool name as the agent calls it, shown in mono – e.g. "search_web" | | status | `"running" \| "success" \| "error"` | | Spinner with a live timer, check with the duration, or a red "Failed" | | summary | `ReactNode` | | Short human summary next to the name; truncates with an ellipsis | | icon | `ReactNode` | `wrench icon` | Icon in the tile on the left – a 16px stroke SVG fits best | | args | `unknown` | | Arguments – objects are pretty-printed and highlighted, JSON strings are parsed; the section is hidden when undefined | | result | `unknown` | | What the tool returned – like args; other strings are shown as plain text. While running without a result, "Waiting for result…" is shown | | error | `string` | | Error message, shown in the body when status is "error" | | startedAt | `number` | | Start time in ms since epoch – the live timer counts from here instead of from when the card turned running | | duration | `number` | | Final duration in ms – otherwise the time measured while the card was running | | defaultOpen | `boolean` | `false` | Start expanded (uncontrolled) | | open | `boolean` | | Expanded state (controlled) | | onOpenChange | `(open: boolean) => void` | | Called when the header is toggled | | openOnError | `boolean` | `true` | Opens the card when the call fails and closes it on the next run, unless someone toggled it by hand (uncontrolled only) | | onRetry | `() => void` | | Shows a Retry button next to the error message | | maxCodeHeight | `number` | `220` | Height in px after which arguments and result are capped with a fade and a "Show all" toggle | | className | `string` | | Extra class on the root | | ToolCallGroup · children | `ToolCallCard elements` | | The cards – the group reads their icon, status and duration props for its header, so pass them as direct children | | ToolCallGroup · label | `string` | `"Used N tools"` | Header label once nothing is running | | ToolCallGroup · runningLabel | `string` | `"Using tools…"` | Shimmering header label while any card is running | | ToolCallGroup · duration | `number` | | Total in ms – otherwise the sum of the cards' duration props, or the time measured while any card was running | | ToolCallGroup · defaultOpen / open / onOpenChange | `boolean / boolean / (open) => void` | `false` | Expanded state, uncontrolled or controlled | | ToolCallGroup · className | `string` | | Extra class on the root | ### Keyboard - Tab: Moves through the headers, copy buttons, Show all and Retry; content of closed cards is skipped - Enter / Space: On a header: expands or collapses the card or the group ### Accessibility - Headers are real buttons with aria-expanded and aria-controls pointing at the body - Closed bodies are inert, so they can't be focused or read until opened - A polite live region per card announces "search_web running", "finished" or "failed: …"; the group announces "Using tools…" and "Used 3 tools" - Copy buttons are labelled "Copy arguments" / "Copy result" and switch to "Copied"; icons are aria-hidden - Errors use text and an icon, not only red ### Motion - The body grows with grid-template-rows 0fr → 1fr and fades in; the chevron turns - Status changes blur in; the success check draws itself; the spinner turns while running - Copy morphs into a check for 1.4s; Show all opens the capped block to its full height - In the group, new tool icons pop into the stack and "Using tools…" shimmers while anything runs - With prefers-reduced-motion everything switches instantly and the shimmer is off ### Responsive - Fills its container; name and summary truncate with an ellipsis, the status never wraps – works down to ~320px - Code wraps long lines instead of scrolling sideways - Dark mode built in: switches when an ancestor (usually <html>) has the class dark or data-theme="dark" ### Notes for AI - Map your agent's tool-call events 1:1: status running on the call, success or error on the result; pass startedAt from the event for an exact timer - Write summary in past tense when done ("Searched…") and present progressive while running ("Searching…") - Pass args and result as objects – stringify nothing yourself; redact secrets before they reach the card - Wrap all calls of one assistant turn in a single ToolCallGroup and keep cards as direct children ### Requirements - React 19 Related: thinking-loader, thinking-state, ai-answer --- ## Usage Meter (Pro) Plan usage and AI credits – a stacked credits meter by category, daily sparkline, limits and a button that speaks up near the limit - Page: https://www.affectui.com/components/usage-meter - Section: Components · Data display - Install: `npx shadcn@latest add https://www.affectui.com/r/usage-meter.json` (needs the Pro access key) - Import: `@/components/ui/usage-meter` ### When to use - The usage or billing page of a SaaS or AI product – credits, tokens, minutes or messages per billing cycle - Showing what the usage went into (chat, search, images, API) next to plan limits like seats and storage - Nudging people to top up or upgrade right where they see they're running out ### When not to use - Long-term trends over many months – a line chart like the KPI Chart reads better - More than about five categories – the bar gets too thin; group the small ones into "Other" ### Usage ```tsx import { useState } from "react"; import { UsageMeter } from "@/components/ui/usage-meter"; export function Usage({ usage }) { const [period, setPeriod] = useState("month"); const data = usage[period]; // { used, limits, daily } per period from your API return ( <UsageMeter title="Acme AI" plan="Pro plan" total={10000} used={data.used} // [{ label: "Chat", value: 3980 }, { label: "Search", value: 1640 }, …] limits={[ { label: "Seats", used: 8, total: 10 }, { label: "Storage", used: 3.2, total: 5, unit: "GB" }, { label: "Projects", used: 12, total: null }, // unlimited ]} daily={data.daily} // [{ label: "Oct 1", value: 312 }, …, { label: "Oct 17", value: null }] periods={[ { id: "month", label: "This month" }, { id: "last", label: "Last month" }, { id: "7d", label: "7 days" }, ]} period={period} onPeriodChange={setPeriod} resetAt="2026-10-17" onUpgrade={(state) => (state === "ok" ? openPlans() : openTopUp())} /> ); } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | total | `number` | | Credits included in the plan – the bar's full width | | used | `{ label: string; value: number; color?: string }[]` | | What the credits went into in the shown period. Without colors the parts get shades of the accent, then grays; each label keeps its shade | | limits | `{ label; used; total?: number \| null; unit?; decimals? }[]` | | Thin meters below, e.g. seats and storage. Leave total out (or null) for unlimited – shown as "of ∞" on a striped track | | periods | `{ id: string; label: string }[]` | | Options of the period switch; no switch when left out | | period | `string` | | Controlled period id – pass the matching used, limits and daily with it | | defaultPeriod | `string` | | Starting period when uncontrolled; defaults to the first option | | onPeriodChange | `(id: string) => void` | | Called when a period is picked | | resetAt | `Date \| string \| number` | | When the credits reset – shown as "Resets in 9 days", the date on hover | | daily | `{ label: string; value: number \| null }[]` | | Usage per day for the sparkline; null for days still to come, shown as flat stubs | | onUpgrade | `(state: "ok" \| "warn" \| "limit") => void` | | Called by the button with the current state | | title | `string` | `"Usage"` | Card title, e.g. the workspace or product name | | plan | `string` | | Plan name in front of the reset date | | unit | `string` | `"credits"` | Unit of the main meter | | warnAt | `number` | `0.8` | Share (0–1) from which the meter and the limits warn | | upgradeLabel | `string` | `"Upgrade"` | Button text while there is room left (a quiet button) | | topUpLabel | `string` | `"Add credits"` | Button text near and at the limit (a solid button) | | className | `string` | | Extra class on the root | ### Keyboard - Tab: Reaches the period switch, the bar, the sparkline and the button - ← / →: Switch: picks the previous or next period. Bar: steps through the categories. Sparkline: steps through the days - Esc: Hides the category or day readout ### Accessibility - A section labelled by its title; the bar and every limit are meters with their value as text ("7,420 of 10,000 credits") - The period switch is a radio group with a roving tab stop - The category tooltip is a polite live region, so stepping through the bar reads each part - Running low and reaching the limit are announced in a polite live region; amber is never the only signal – the text says it too ### Motion - On first view the bar fills from zero and every number counts up, in one frame loop so numbers and widths stay in step - Switching periods: the white pill glides to the new option, numbers roll to their new values and the parts glide to their new widths - A category that drops out shrinks to nothing on the bar while its legend row folds away; the card glides to its new height - Hovering a part dims the others and highlights its legend row; the tooltip glides along the bar - Near the limit a small amber dot pops in and the button turns solid with a gentle nudge, its width gliding to the new label - With prefers-reduced-motion everything switches instantly ### Responsive - Fills its container up to 460px; at 400px and below the switch gets its own row and the limits stack as rows - Works down to 320px wide; long labels end with an ellipsis, numbers and units never split - Touch: tapping a part or a legend row shows its value, targets grow to 36–38px - Dark mode built in: switches when an ancestor (usually <html>) has the class dark or data-theme="dark" ### Requirements - React 19 Related: kpi-chart, breakdown-chart, billing-schedule, progress-tracker --- ## Voice Orb (Pro) The face of a voice assistant – a sphere of dots that breathes, catches sound, collapses into a spinning ring while thinking and ripples when it speaks - Page: https://www.affectui.com/components/voice-orb - Section: Components · Chat - Install: `npx shadcn@latest add https://www.affectui.com/r/voice-orb.json` (needs the Pro access key) - Import: `@/components/ui/voice-orb` ### When to use - Voice mode of an assistant: one calm object that shows whether the AI listens, thinks or talks - Hero of an AI product page, an empty state before the first prompt, or a call screen - Anywhere a spinner would undersell what is happening – the orb says which phase it is ### When not to use - Inline in a chat thread or next to text – use Thinking Loader or Thinking State, they are made for small sizes - As decoration that never changes state – a still image costs nothing; the orb draws every frame - Below about 96px – the dots merge into a blob ### Usage ```tsx import { VoiceOrb } from "@/components/ui/voice-orb"; export function VoiceMode() { const { state, level, toggle } = useVoiceSession(); // "idle" | "listening" | "thinking" | "speaking", 0–1 return <VoiceOrb state={state} level={level} onPress={toggle} />; } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | state | `"idle" \| "listening" \| "thinking" \| "speaking"` | `"idle"` | What the assistant is doing – the orb melts from one state into the next | | level | `number` | | Loudness from 0 to 1 while listening or speaking. Without it the orb makes up a natural-looking level | | size | `number` | `220` | Edge length in px | | dots | `number` | `520` | Number of dots – raise it for large sizes | | accent | `string` | `blue` | Color the moving dots take on; resting dots use the text color | | label | `boolean \| Partial<Record<state, string>>` | `true` | The line under the orb – false hides it, an object replaces single texts | | interactive | `boolean` | `true` | Dots give way to the mouse pointer | | onPress | `() => void` | | Makes the orb a button, e.g. to start and stop listening | | className | `string` | | Extra class on the root | | style | `CSSProperties` | | Inline styles on the root – set color here to change the resting dots | ### Keyboard - Enter / Space: With onPress: presses the orb ### Accessibility - The canvas is decorative; the state is in the label, which is announced politely when it changes - With onPress the orb is a real button named by its label; without it, an image named after the state - With prefers-reduced-motion nothing moves: the orb shows one still frame per state (sphere, raised dots, ring, waves) - The colors follow the text color and adapt to dark mode on their own ### Motion - Idle: the sphere turns slowly and breathes - Listening: dots jump outward with the level and take on the accent color; a soft glow follows the loudness - Thinking: the sphere collapses into a tilted, fast spinning ring with a light running around it - Speaking: waves travel from pole to pole with the level - States are blended, never switched – every dot glides from one shape to the next in about a third of a second - The animation pauses while the orb is off screen or the tab is in the background ### Responsive - Fixed square of size px – pass a smaller size on phones; it is sharp on high-density screens - Touch screens: no pointer effect, the press works as usual - Dark mode built in: switches when an ancestor (usually <html>) has the class dark or data-theme="dark" ### Notes for AI - Changing state or level never restarts the animation – only size and dots rebuild the canvas - Smooth a raw microphone level a little before passing it, or pass it as is: the orb eases towards it anyway ### Requirements - React 19 Related: thinking-loader, thinking-state, chat-composer --- ## Writing Assistant (Pro) AI chat card with a soft gradient glow, suggestions, streaming replies and live voice dictation - Page: https://www.affectui.com/components/writing-assistant - Section: Components · Chat - Install: `npx shadcn@latest add https://www.affectui.com/r/writing-assistant.json` (needs the Pro access key) - Import: `@/components/ui/writing-assistant` ### When to use - A small AI helper inside an editor, CRM or sidebar – rewrite, shorten, translate or draft - Landing pages that should show an assistant you can actually try, including voice input ### When not to use - Long conversations with files, tools and model settings – use the Chat Composer or the AI Workspace block - When you can't send voice to the browser's speech service – set speech="simulate" or hide the mic ### Usage ```tsx import { WritingAssistant } from "@/components/ui/writing-assistant"; export function Helper() { return ( <WritingAssistant name="Quill" tagline="Your writing companion." respond={async (message) => { const res = await fetch("/api/write", { method: "POST", body: JSON.stringify({ message }) }); return (await res.json()).text; }} /> ); } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | name | `string` | `"Quill"` | Assistant name on top | | tagline | `string` | `"Your writing companion."` | Line under the name | | greeting | `string` | `"What should we write?"` | Big line on the welcome screen | | suggestions | `string[]` | | Chips that send themselves as a message | | placeholder | `string` | `"Ask anything…"` | Placeholder of the prompt field | | respond | `(message) => string \| Promise<string>` | | Your answer for a message; a sample writer is used by default | | speech | `"auto" \| "simulate"` | `"auto"` | auto uses the browser's speech recognition when available, simulate always plays a sample take | | dictation | `string` | | Sentence used for simulated dictation | | className | `string` | | Extra class on the root | ### Keyboard - Enter: Sends what's in the prompt field - Shift / Enter: Starts a new line – the field grows up to four lines, then scrolls - Tab: Moves through the suggestions, the field, Send, the mic and New chat ### Accessibility - The mic is a toggle button with aria-pressed and a label that says what it does next - New messages are announced politely; the typing dots carry a label - Everything works with the keyboard; dictation is optional ### Motion - The glow keeps drifting in the lower half; the welcome view hands over to the chat with a short rise - While listening, the mic turns dark with three calm bars and a soft ring breathing outwards; words appear live in the field - When you stop speaking, the finished sentence moves into the chat as your message; replies stream in word by word - With prefers-reduced-motion the glow stands still and replies appear at once ### Responsive - Fills its container up to 380px wide - Dictation uses the Web Speech API (Chrome, Edge, Safari) with the page language; elsewhere a short simulated take plays - Dark mode built in: switches when an ancestor (usually <html>) has the class dark or data-theme="dark" ### Requirements - React 19 --- ## Agent Chat (Pro) Chat with an orchestrating agent, the agents at work, and a brief to fill in beside it - Page: https://www.affectui.com/screens/agent-chat - Section: Screens · App screens - Install: `npx shadcn@latest add https://www.affectui.com/r/agent-chat.json` (needs the Pro access key) - Import: `@/components/ui/agent-chat` ### When to use - The main view of an AI agent or copilot product - Chats that end in a concrete task – a brief, a draft, a report ### When not to use - Plain support chat – a lighter chat layout is enough ### Usage ```tsx import { AgentChatScreen } from "@/components/ui/agent-chat"; export default function Page() { return <AgentChatScreen workspace="Acme" user={{ name: "Alex Morgan" }} onNavigate={(label) => router.push(routes[label])} />; } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | onNavigate | `(label: string) => void` | | Called with the label of a sidebar item | | workspace | `string` | `"Acme"` | Workspace name in the sidebar | | user | `{ name: string; avatar?: string }` | `{ name: "Alex Morgan" }` | Account at the bottom of the sidebar | | className | `string` | | Extra class on the root | ### Accessibility - Sidebar items, tabs and toolbar actions are real buttons with labels - Icons are decorative and hidden from screen readers ### Responsive - Fills its container – give it the full viewport height in your app - Dark mode built in: switches when an ancestor (usually <html>) has the class dark or data-theme="dark"; colors are CSS variables on .ui-as ### Requirements - React 19 - @hugeicons/react - @hugeicons/core-free-icons - App Shell, Assistant Home (for its composer) and Button (installed with it) --- ## Agent Run (Pro) One autonomous agent run: the plan ticking through its steps, the tool calls, an approval it waits on and the draft it wrote - Page: https://www.affectui.com/screens/agent-run - Section: Screens · App screens - Install: `npx shadcn@latest add https://www.affectui.com/r/agent-run.json` (needs the Pro access key) - Import: `@/components/ui/agent-run` ### When to use - The detail view of one autonomous agent run – a background task, a scheduled job, a long workflow - Runs that pause for a human before they send, pay or publish something ### When not to use - Back-and-forth conversations with the agent – use Agent Chat - A list of many runs – use a table with a status column ### Usage ```tsx import { AgentRunScreen } from "@/components/ui/agent-run"; export default function Page() { return ( <AgentRunScreen title="Prepare the Q3 board update" steps={run.steps} draft={{ original: memo.before, suggested: memo.after, label: "Board memo – Q3" }} onApprove={(stepId) => api.approve(run.id, stepId)} onDeny={(stepId) => api.deny(run.id, stepId)} onNavigate={(label) => router.push(routes[label])} /> ); } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | title | `string` | `"Prepare the Q3 board update"` | Goal of the run, in the header and above the plan | | steps | `RunStep[]` | `defaultRunSteps` | Steps with id, title, detail, tool, their tool calls ({ name, summary, icon?, args?, result?, duration }) and an optional approval ({ title, description, risk, details }) the run waits on | | draft | `{ original: string; suggested: string; label?: string }` | `defaultRunDraft` | Text the agent wrote, shown as an inline diff in the side panel | | agent | `string` | `"Board prep"` | Name of the agent in the run details | | autoRun | `boolean` | `true` | Plays the built-in simulation on mount – turn off and drive the steps from your own events | | onApprove | `(stepId: string) => void` | | An approval was given; the run carries on | | onDeny | `(stepId: string) => void` | | An approval was denied; the run stops at that step | | onStop | `() => void` | | Stop run pressed | | onDone | `() => void` | | Every step is done | | onNavigate | `(label: string) => void` | | Called with the label of a sidebar item | | workspace | `string` | `"Acme"` | Workspace name in the sidebar | | user | `{ name: string; avatar?: string }` | `{ name: "Alex Morgan" }` | Account at the bottom of the sidebar | | className | `string` | | Extra class on the root | ### Accessibility - Sidebar items, the run actions and the panel toggle are real buttons with labels - The plan marks the current step with aria-current and announces progress; tool calls and approvals announce their status - Approve and deny work from the keyboard (⌘↵ / Esc), the diff with Tab, Y and N - Icons are decorative and hidden from screen readers ### Responsive - Fills its container – give it the full viewport height in your app; the run and the side panel scroll on their own - Below 1000px of main area the side panel becomes a layer that slides in from the right - Dark mode built in: switches when an ancestor (usually <html>) has the class dark or data-theme="dark"; colors are CSS variables on .ui-as ### Requirements - React 19 - @hugeicons/react - @hugeicons/core-free-icons - App Shell, Agent Plan, Tool Call Card, Approval Card, Inline Diff, Thinking Loader and Button (installed with it) --- ## Analytics (Pro) Revenue dashboard: key figures, revenue by channel, breakdown, top products and weekly trends - Page: https://www.affectui.com/screens/analytics - Section: Screens · App screens - Install: `npx shadcn@latest add https://www.affectui.com/r/analytics.json` (needs the Pro access key) - Import: `@/components/ui/analytics` ### When to use - Revenue, product or marketing dashboards - Overview pages that combine key figures and charts ### When not to use - Live monitoring with many changing values – use a denser layout ### Usage ```tsx import { AnalyticsScreen } from "@/components/ui/analytics"; export default function Page() { return <AnalyticsScreen workspace="Acme" user={{ name: "Alex Morgan" }} onNavigate={(label) => router.push(routes[label])} />; } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | onNavigate | `(label: string) => void` | | Called with the label of a sidebar item | | workspace | `string` | `"Acme"` | Workspace name in the sidebar | | user | `{ name: string; avatar?: string }` | `{ name: "Alex Morgan" }` | Account at the bottom of the sidebar | | className | `string` | | Extra class on the root | ### Accessibility - Sidebar items, tabs and toolbar actions are real buttons with labels - Icons are decorative and hidden from screen readers ### Responsive - Fills its container – give it the full viewport height in your app - Dark mode built in: switches when an ancestor (usually <html>) has the class dark or data-theme="dark"; colors are CSS variables on .ui-as ### Requirements - React 19 - @hugeicons/react - @hugeicons/core-free-icons - App Shell, KPI Chart and Button (installed with it) --- ## App Shell (Pro) Collapsible sidebar with workspace, menu, projects and account – the frame every screen sits in - Page: https://www.affectui.com/screens/app-shell - Section: Screens · App screens - Install: `npx shadcn@latest add https://www.affectui.com/r/app-shell.json` (needs the Pro access key) - Import: `@/components/ui/app-shell` ### When to use - The base of an AI assistant, internal tool or SaaS dashboard - Building your own screens on the same frame as the other affectUI screens ### When not to use - Marketing pages – use a block or a plain layout - Apps with deep, multi-level navigation trees ### Usage ```tsx import { AppShell } from "@/components/ui/app-shell"; export function Layout({ children }: { children: React.ReactNode }) { return ( <AppShell active="New chat" workspace="Acme" user={{ name: "Alex Morgan" }} onNavigate={(label) => router.push(routes[label])}> <div className="ui-as-page">{children}</div> </AppShell> ); } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | children | `ReactNode` | | The page shown next to the sidebar | | active | `string` | | Label of the menu item or project that is open | | onNavigate | `(label: string) => void` | | Called with the label of a menu item or project when it is clicked | | workspace | `string` | `"Acme"` | Workspace name; its first letter becomes the logo | | user | `{ name: string; avatar?: string }` | `{ name: "Alex Morgan" }` | Account at the bottom; without an avatar it shows initials | | nav | `ShellNavItem[]` | `New chat, Folders, Integrations` | Menu items, each { label, icon } | | projects | `string[]` | | Project list under the menu | | className | `string` | | Extra class on the root | ### Accessibility - Sidebar items, projects and the collapse toggle are real buttons with labels - Icons are decorative and hidden from screen readers ### Responsive - Fills its container – give it the full viewport height in your app - Dark mode built in: switches when an ancestor (usually <html>) has the class dark or data-theme="dark"; colors are CSS variables on .ui-as ### Requirements - React 19 - @hugeicons/react - @hugeicons/core-free-icons --- ## Assistant Home (Pro) AI assistant start page: greeting, prompt box with tools, connected apps and suggestions - Page: https://www.affectui.com/screens/assistant-home - Section: Screens · App screens - Install: `npx shadcn@latest add https://www.affectui.com/r/assistant-home.json` (needs the Pro access key) - Import: `@/components/ui/assistant-home` ### When to use - The first screen of an AI assistant or copilot - A start page that leads straight into a prompt ### When not to use - Apps where the chat is not the main action ### Usage ```tsx import { AssistantHomeScreen } from "@/components/ui/assistant-home"; export default function Page() { return <AssistantHomeScreen workspace="Acme" user={{ name: "Alex Morgan" }} onNavigate={(label) => router.push(routes[label])} />; } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | onNavigate | `(label: string) => void` | | Called with the label of a sidebar item or project | | workspace | `string` | `"Acme"` | Workspace name in the sidebar | | user | `{ name: string; avatar?: string }` | `{ name: "Alex Morgan" }` | Account at the bottom of the sidebar | | className | `string` | | Extra class on the root | ### Accessibility - Sidebar items, projects and the collapse toggle are real buttons with labels - Icons are decorative and hidden from screen readers ### Responsive - Fills its container – give it the full viewport height in your app - Dark mode built in: switches when an ancestor (usually <html>) has the class dark or data-theme="dark"; colors are CSS variables on .ui-as ### Requirements - React 19 - @hugeicons/react - @hugeicons/core-free-icons - App Shell (installed with it) --- ## CRM (Pro) Company list with pipelines, open views as tabs and an editable data table - Page: https://www.affectui.com/screens/crm - Section: Screens · App screens - Install: `npx shadcn@latest add https://www.affectui.com/r/crm.json` (needs the Pro access key) - Import: `@/components/ui/crm` ### When to use - Sales, customer or partner lists in an internal tool - Any record list people edit like a spreadsheet ### When not to use - Read-only reports – use a static table or the Analytics screen ### Usage ```tsx import { CrmScreen } from "@/components/ui/crm"; export default function Page() { return <CrmScreen workspace="Acme" user={{ name: "Alex Morgan" }} onNavigate={(label) => router.push(routes[label])} />; } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | onNavigate | `(label: string) => void` | | Called with the label of a sidebar item | | workspace | `string` | `"Acme"` | Workspace name in the sidebar | | user | `{ name: string; avatar?: string }` | `{ name: "Alex Morgan" }` | Account at the bottom of the sidebar | | className | `string` | | Extra class on the root | ### Accessibility - Sidebar items, tabs and toolbar actions are real buttons with labels - Icons are decorative and hidden from screen readers ### Responsive - Fills its container – give it the full viewport height in your app - Dark mode built in: switches when an ancestor (usually <html>) has the class dark or data-theme="dark"; colors are CSS variables on .ui-as ### Requirements - React 19 - @hugeicons/react - @hugeicons/core-free-icons - App Shell and Data Table (installed with it) --- ## Evals (Pro) Prompt evaluation dashboard: pass rate by version, reviewable prompt changes and test cases you can re-run - Page: https://www.affectui.com/screens/evals - Section: Screens · App screens - Install: `npx shadcn@latest add https://www.affectui.com/r/evals.json` (needs the Pro access key) - Import: `@/components/ui/evals` ### When to use - Eval dashboards for prompts, agents or LLM features - Comparing prompt or model versions against a fixed test suite ### When not to use - Live production monitoring of single requests – use a log or trace view ### Usage ```tsx import { EvalsScreen } from "@/components/ui/evals"; export default function Page() { return <EvalsScreen workspace="Acme" user={{ name: "Alex Morgan" }} onNavigate={(label) => router.push(routes[label])} />; } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | onNavigate | `(label: string) => void` | | Called with the label of a sidebar item | | workspace | `string` | `"Acme"` | Workspace name in the sidebar | | user | `{ name: string; avatar?: string }` | `{ name: "Alex Morgan" }` | Account at the bottom of the sidebar | | className | `string` | | Extra class on the root | ### Accessibility - Sidebar items, the version menu, filters and Run eval are real buttons with labels - Test case rows are focusable and select with Enter or Space - Prompt changes are reviewed with the keyboard: Tab between changes, Y accepts, N rejects - Icons are decorative and hidden from screen readers ### Responsive - Fills its container – give it the full viewport height in your app - Below 1000px the right column moves under the table; the table scrolls sideways on phones - Dark mode built in: switches when an ancestor (usually <html>) has the class dark or data-theme="dark"; colors are CSS variables on .ui-as ### Requirements - React 19 - @hugeicons/react - @hugeicons/core-free-icons - App Shell, KPI Chart, Inline Diff, Segmented Control and Button (installed with it) --- ## Folder (Pro) Inside one folder: breadcrumbs, file cards with previews, and an empty state - Page: https://www.affectui.com/screens/folder - Section: Screens · App screens - Install: `npx shadcn@latest add https://www.affectui.com/r/folder.json` (needs the Pro access key) - Import: `@/components/ui/folder` ### When to use - Detail view of a project, folder or collection - Pairs with the Folders screen ### When not to use - Nested trees deeper than one level ### Usage ```tsx import { FolderScreen } from "@/components/ui/folder"; export default function Page() { return <FolderScreen folder="Q3 Campaign" workspace="Acme" user={{ name: "Alex Morgan" }} onNavigate={(label) => router.push(routes[label])} />; } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | folder | `string` | `"Q3 Campaign"` | Folder that is open – the title, crumbs and files follow it | | onNavigate | `(label: string) => void` | | Called with the label of a sidebar item or project | | workspace | `string` | `"Acme"` | Workspace name in the sidebar | | user | `{ name: string; avatar?: string }` | `{ name: "Alex Morgan" }` | Account at the bottom of the sidebar | | className | `string` | | Extra class on the root | ### Accessibility - Sidebar items, projects and the collapse toggle are real buttons with labels - Icons are decorative and hidden from screen readers ### Responsive - Fills its container – give it the full viewport height in your app - Dark mode built in: switches when an ancestor (usually <html>) has the class dark or data-theme="dark"; colors are CSS variables on .ui-as ### Requirements - React 19 - @hugeicons/react - @hugeicons/core-free-icons - App Shell (installed with it) --- ## Folders (Pro) Folder overview with tabs, search, sort, grid/list toggle and file templates - Page: https://www.affectui.com/screens/files - Section: Screens · App screens - Install: `npx shadcn@latest add https://www.affectui.com/r/files.json` (needs the Pro access key) - Import: `@/components/ui/files` ### When to use - File, knowledge or document areas in an app - Overviews of projects or workspaces as cards ### When not to use - Thousands of files – use a virtualised table ### Usage ```tsx import { FilesScreen } from "@/components/ui/files"; export default function Page() { return <FilesScreen workspace="Acme" user={{ name: "Alex Morgan" }} onNavigate={(label) => router.push(routes[label])} />; } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | onNavigate | `(label: string) => void` | | Called with the label of a sidebar item or project | | workspace | `string` | `"Acme"` | Workspace name in the sidebar | | user | `{ name: string; avatar?: string }` | `{ name: "Alex Morgan" }` | Account at the bottom of the sidebar | | className | `string` | | Extra class on the root | ### Accessibility - Sidebar items, projects and the collapse toggle are real buttons with labels - Icons are decorative and hidden from screen readers ### Responsive - Fills its container – give it the full viewport height in your app - Dark mode built in: switches when an ancestor (usually <html>) has the class dark or data-theme="dark"; colors are CSS variables on .ui-as ### Requirements - React 19 - @hugeicons/react - @hugeicons/core-free-icons - App Shell (installed with it) --- ## Integrations (Pro) App directory with a sticky header, categories, search and connect states - Page: https://www.affectui.com/screens/integrations - Section: Screens · App screens - Install: `npx shadcn@latest add https://www.affectui.com/r/integrations.json` (needs the Pro access key) - Import: `@/components/ui/integrations` ### When to use - Settings or marketplace pages that list apps to connect - Showing which tools are connected at a glance ### When not to use - Fewer than six apps – a simple list is enough ### Usage ```tsx import { IntegrationsScreen } from "@/components/ui/integrations"; export default function Page() { return <IntegrationsScreen workspace="Acme" user={{ name: "Alex Morgan" }} onNavigate={(label) => router.push(routes[label])} />; } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | onNavigate | `(label: string) => void` | | Called with the label of a sidebar item or project | | workspace | `string` | `"Acme"` | Workspace name in the sidebar | | user | `{ name: string; avatar?: string }` | `{ name: "Alex Morgan" }` | Account at the bottom of the sidebar | | className | `string` | | Extra class on the root | ### Accessibility - Sidebar items, projects and the collapse toggle are real buttons with labels - Icons are decorative and hidden from screen readers ### Responsive - Fills its container – give it the full viewport height in your app - Dark mode built in: switches when an ancestor (usually <html>) has the class dark or data-theme="dark"; colors are CSS variables on .ui-as ### Requirements - React 19 - @hugeicons/react - @hugeicons/core-free-icons - App Shell (installed with it) --- ## Onboarding (Pro) First-run flow of an AI workspace: workspace setup, tool connections, team invites and a first agent, with a getting-started checklist - Page: https://www.affectui.com/screens/onboarding - Section: Screens · App screens - Install: `npx shadcn@latest add https://www.affectui.com/r/onboarding.json` (needs the Pro access key) - Import: `@/components/ui/onboarding` ### When to use - The first minutes after sign-up in a workspace or AI product - Guided setup that people can skip and pick up again later ### When not to use - Long forms with many required fields – use the Stepper Form - A single setting – show it inline instead of a whole flow ### Usage ```tsx import { OnboardingScreen } from "@/components/ui/onboarding"; export default function Page() { return ( <OnboardingScreen workspace="Acme" user={{ name: "Alex Morgan" }} defaultStep={1} onComplete={(values) => api.finishOnboarding(values)} onNavigate={(label) => router.push(routes[label])} /> ); } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | defaultStep | `1 \| 2 \| 3 \| 4` | `2` | Step shown first; the steps before it count as done | | onComplete | `(values: OnboardingValues) => void` | | Called after Create agent (or skipping the last step) with workspace, slug, team size, tools, invites, role and template | | onNavigate | `(label: string) => void` | | Called with the label of a sidebar item or a next step (Chat, Integrations, Analytics) | | workspace | `string` | `"Acme"` | Workspace name in the sidebar, the greeting and the first field | | user | `{ name: string; avatar?: string }` | `{ name: "Alex Morgan" }` | Account at the bottom of the sidebar; the first name is used in the greeting | | className | `string` | | Extra class on the root | ### Accessibility - Each step is a form: Enter continues, the primary button submits - Progress is a progressbar with the step name as its value text; the checklist marks the current step with aria-current - Tool toggles are switches, templates a radio group; email chips can be removed with their button or Backspace - Step transitions and checkmarks turn off with prefers-reduced-motion ### Responsive - Fills its container – give it the full viewport height in your app - The checklist sits beside the step card on wide panels and moves below it on narrow ones - Dark mode built in: switches when an ancestor (usually <html>) has the class dark or data-theme="dark"; colors are CSS variables on .ui-as ### Requirements - React 19 - @hugeicons/react - @hugeicons/core-free-icons - App Shell, Segmented Control and Button (installed with it) --- ## Settings & Billing (Pro) Settings page with tabs: workspace, members, API keys and billing with plan, usage, payment method, plan comparison and invoices - Page: https://www.affectui.com/screens/settings-billing - Section: Screens · App screens - Install: `npx shadcn@latest add https://www.affectui.com/r/settings-billing.json` (needs the Pro access key) - Import: `@/components/ui/settings-billing` ### When to use - The settings area of a SaaS or AI product – workspace, team, billing and API access in one place - A billing page that shows plan, usage, payment method and invoices together ### When not to use - A public pricing page – use a dedicated pricing layout - Checkout or entering card details – hand that off to your payment provider ### Usage ```tsx import { SettingsBillingScreen } from "@/components/ui/settings-billing"; export default function Page() { return <SettingsBillingScreen defaultTab="Billing" workspace="Acme" user={{ name: "Alex Morgan" }} onNavigate={(label) => router.push(routes[label])} />; } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | defaultTab | `"General" \| "Members" \| "Billing" \| "API keys"` | `"Billing"` | Tab that is open first | | onNavigate | `(label: string) => void` | | Called with the label of a sidebar item | | workspace | `string` | `"Acme"` | Workspace name in the sidebar and the General tab | | user | `{ name: string; avatar?: string }` | `{ name: "Alex Morgan" }` | Account at the bottom of the sidebar | | className | `string` | | Extra class on the root | ### Accessibility - Tabs, the monthly/yearly switch, menus and every row action are real buttons with labels - Invoices are a real table with column headers; download buttons name the invoice they download - Toasts are announced as a status; icons are decorative and hidden from screen readers ### Responsive - Fills its container – give it the full viewport height in your app - Overview and plan cards stack on narrow panels; the invoice table scrolls sideways on phones - Dark mode built in: switches when an ancestor (usually <html>) has the class dark or data-theme="dark"; colors are CSS variables on .ui-as ### Requirements - React 19 - @hugeicons/react - @hugeicons/core-free-icons - App Shell, Usage Meter, Segmented Control and Button (installed with it) --- ## Agent Gallery (Pro) Slim gradient panels with one open product screen in the middle – pick a panel and it glides open - Page: https://www.affectui.com/blocks/agent-gallery - Section: Blocks · Feature sections - Install: `npx shadcn@latest add https://www.affectui.com/r/agent-gallery.json` (needs the Pro access key) - Import: `@/components/ui/agent-gallery` ### When to use - Showing several agents, workflows or product areas on a landing page, one at a time with a real-looking screen - Feature sections that should feel explorable rather than a static grid ### When not to use - Two or three items – a simple tab switch reads faster - Content people need to compare side by side ### Usage ```tsx import { AgentGallery } from "@/components/ui/agent-gallery"; export function AgentsSection() { return <AgentGallery title="Agents that run your playbook" />; } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | title | `string` | | Heading above the panels | | subtitle | `string` | | Line under the heading | | items | `GalleryItem[]` | | Each { id, name, description, colors: [strong, soft], trigger, steps, skills, guardrail } – drives the panel colors, the label and the screen | | defaultIndex | `number` | `the middle item` | Panel that starts open | | className | `string` | | Extra class on the root | ### Keyboard - ← / →: With focus in the panels, opens the previous or next one - Tab: Moves through the side panels and the arrow buttons ### Accessibility - Marked as a carousel; side panels are buttons labelled “Show …” - The caption under the open panel is announced when it changes ### Motion - The picked panel widens into the middle while the open one folds into a slim panel – one smooth width transition - Side panels show the artwork muted and come to full color on hover; the screen slides in after the panel has opened - With prefers-reduced-motion panels switch instantly ### Responsive - Follows its own width: below 760px the open panel fills the width with one slim panel peeking on each side - Every panel shares one gradient artwork drawn in CSS – only two colors per item change, so it scales crisply - Dark mode built in: switches when an ancestor (usually <html>) has the class dark or data-theme="dark" ### Requirements - React 19 --- ## AI Workspace (Pro) Complete assistant app – sidebar, welcome, streaming chat and projects, built on the Chat Composer - Page: https://www.affectui.com/blocks/ai-workspace - Section: Blocks · Productivity - Install: `npx shadcn@latest add https://www.affectui.com/r/ai-workspace.json` (needs the Pro access key) - Import: `@/components/ui/ai-workspace` ### When to use - The main screen of an AI assistant or copilot inside your product - Internal tools where people chat with your data and group work into projects ### When not to use - A small chat widget in a corner – use the Chat Composer on its own ### Usage ```tsx import { AiWorkspace } from "@/components/ui/ai-workspace"; export function Assistant() { return ( <AiWorkspace userName="Lena" respond={async (message, history) => { const res = await fetch("/api/chat", { method: "POST", body: JSON.stringify({ message, history }) }); return (await res.json()).answer; }} /> ); } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | respond | `(message, history) => Promise<string>` | | Your model call – the answer streams in word by word. Demo answers without it | | userName | `string` | `"Lena"` | Shown in the greeting and the sidebar | | userAvatar | `string` | | Image for the sidebar footer | | workspace | `ReactNode` | `"N"` | Initial or logo in the top-left tile | | pinned | `string[]` | | Pinned assistants in the sidebar – pinned projects are added automatically | | conversations | `Conversation[]` | | Recent chats with their messages | | projects | `Project[]` | | Projects with name, description, days since update and pinned | ### Keyboard - Enter: Send a message in the composer - Escape: Close menus and the create-project dialog - Tab: Move through sidebar, header, chat and projects ### Accessibility - Menus are real menus with radio items, the dialog is modal and labelled, the collapsed sidebar is inert - New answers are announced politely while they stream in ### Motion - The sidebar collapses smoothly, answers stream in, views fade in, menus drop in - Reduced motion turns these off ### Responsive - Below 720px of width the sidebar becomes an overlay and the project grid turns into one column - Dark mode built in: switches when an ancestor (usually <html>) has the class dark or data-theme="dark" ### Requirements - React 19 - Chat Composer – installed automatically with the CLI Related: chat-composer, thinking-state, comments-panel --- ## App Directory (Pro) Browse, search and connect integrations – categories, filters and app cards with a connect flow - Page: https://www.affectui.com/blocks/app-directory - Section: Blocks · Integrations - Install: `npx shadcn@latest add https://www.affectui.com/r/app-directory.json` (needs the Pro access key) - Import: `@/components/ui/app-directory` ### When to use - An integrations or marketplace page in a SaaS product - Settings screens where people connect and disconnect third-party tools ### When not to use - Two or three integrations – a short list in settings is enough - Hundreds of apps – add server-side search and paging ### Usage ```tsx import { AppDirectory } from "@/components/ui/app-directory"; export function Integrations() { return <AppDirectory apps={apps} onToggle={(app, connected) => save(app.id, connected)} />; } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | apps | `App[]` | | id, name, vendor, description, categories, logo, tile color, essential and connected | | categories | `AppCategory[]` | | Sidebar entries with id, label and icon – essentials and all are built in | | title | `string` | `"Integrations"` | Heading inside the card | | subtitle | `string` | | Line under the heading | | onToggle | `(app: App, connected: boolean) => void` | | Called after an app was connected or disconnected | | className | `string` | | Extra class on the block | ### Keyboard - Tab: Move through search, filter, categories and the connect buttons - Escape: Close the filter menu ### Accessibility - Search has a label, the filter is a menu of radio items, the current category is marked - Connect buttons say which app they connect; the result heading is a live region ### Motion - Cards fade in, connecting shows a spinner, the filter menu drops in – reduced motion turns this off ### Responsive - Three columns with a sidebar on wide containers; below 820px the categories become a row of chips and the grid two columns; below 560px one column - Dark mode built in: switches when an ancestor (usually <html>) has the class dark or data-theme="dark" ### Requirements - React 19 --- ## Client Context (Pro) A client workspace with live numbers and a shared context timeline – switch clients, search, filter and add notes or files - Page: https://www.affectui.com/blocks/client-context - Section: Blocks · Productivity - Install: `npx shadcn@latest add https://www.affectui.com/r/client-context.json` (needs the Pro access key) - Import: `@/components/ui/client-context` ### When to use - Agency, accounting and CRM tools where a team keeps goals, files and decisions per client - Product pages for AI tools – show the context that makes every answer fit the client ### When not to use - A full activity log with hundreds of events – page or virtualise it instead - Real-time collaboration – the timeline adds entries locally and leaves syncing to you ### Usage ```tsx import { ClientContext } from "@/components/ui/client-context"; export function ClientPage() { return ( <ClientContext me={{ name: "Lena Hart", avatar: "/avatars/lena.jpg" }} onAddContext={(clientId, text, files) => api.addContext(clientId, { text, files })} onCreateClient={(name) => api.createClient(name)} /> ); } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | clients | `ContextClient[]` | | Each { id, name, meta, mark, kpis, reports, context } – the switcher lists them all | | context entries | `ContextEntry[]` | | note ("@Report name" becomes a mention), report, files (type pdf · xlsx · csv · doc) or integration { app, connected } | | me | `{ name, avatar? }` | | Author of new context | | onAddContext | `(clientId, text, files) => void` | | Called when context is added | | onCreateClient | `(name) => void` | | Called when a client is created from the switcher | | className | `string` | | Extra class on the root | ### Keyboard - ⌘ / Enter: Adds the context you typed - @: Suggests the client's reports – Enter or Tab inserts the first - Esc: Closes the switcher, the filter and the mention suggestions - Tab: Moves through the switcher, tabs, search, filter and composer ### Accessibility - Tabs are a tablist with a labelled panel; the client switcher, filter and mention suggestions are listboxes - KPI cards and financial rows are toggle buttons; reports are disclosure buttons with aria-expanded - Search, composer and attachment buttons are labelled; settings are switches ### Motion - KPI cards rise in one after another and their sparklines draw in; new context slides in at the top - The tab thumb glides with a little spring, reports fold open, menus pop from their button, sync icons spin while working - With prefers-reduced-motion everything switches instantly ### Responsive - Follows its own width: below 680px the KPIs wrap into two rows, the tabs stretch and search moves below them - Dark mode built in: switches when an ancestor (usually <html>) has the class dark or data-theme="dark" ### Requirements - React 19 --- ## Invoice & Credit Note (Pro) An invoice and its credit note stacked like paper – switch between them, pick lines to credit, adjust amounts and issue - Page: https://www.affectui.com/blocks/invoice-credit - Section: Blocks · Billing - Install: `npx shadcn@latest add https://www.affectui.com/r/invoice-credit.json` (needs the Pro access key) - Import: `@/components/ui/invoice-credit` ### When to use - Billing portals and admin tools that show an invoice and let a team credit it - Product pages for invoicing software – a document that looks real and can be tried ### When not to use - Printing or PDF generation – render a dedicated print layout instead - Invoices with dozens of lines – the credit tray is built for a handful ### Usage ```tsx import { InvoiceCredit } from "@/components/ui/invoice-credit"; export function Billing() { return ( <InvoiceCredit invoiceNo="INV-2041" creditNo="CN-0117" lines={[ { id: "plan", name: "Growth plan", qty: 1, rate: 1250, tax: 0.2 }, { id: "api", name: "API calls (per 1k)", qty: 18420, rate: 0.42, tax: 0.2, group: "Metered usage" }, ]} onIssue={(credits, total) => api.issueCreditNote(credits, total)} /> ); } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | seller / buyer | `{ name, lines: string[] }` | | From and To blocks on the invoice | | invoiceNo / creditNo | `string` | | Document numbers shown under Terms | | invoiceDate / dueDate / period | `string` | | Dates shown on both documents | | lines | `InvoiceLine[]` | | Each { id, name, qty, rate, tax, group? } – lines with a group are listed under a heading | | currency | `string` | `"$"` | Currency symbol | | onIssue | `(credits, total) => void` | | Called when the credit note is issued, with the credited amount per line and the total incl. tax | | className | `string` | | Extra class on the root | ### Keyboard - Tab: Moves through the switch, the actions, every line and amount - Enter: Confirms an amount ### Accessibility - The switch is a tablist; the sheet in the back is hidden from screen readers and can't be focused - Credit lines are checkboxes with labels; amounts are labelled per line and stay within the line total ### Motion - Switching slides the sheets: the chosen one comes forward, the other tucks behind it with only its top edge showing - Checkmarks draw in; the segmented thumb glides between Invoice and Credit note - With prefers-reduced-motion everything switches instantly ### Responsive - Follows its own width: below 560px the parties stack, the rate and tax columns hide and the credit tray drops the total column - Dark mode built in: switches when an ancestor (usually <html>) has the class dark or data-theme="dark" ### Requirements - React 19 --- ## Logo Marquee (Free) The endless “Trusted by” strip – a seamless loop that eases to a stop on hover, scrubs with inertia and lifts the hovered logo to colour - Page: https://www.affectui.com/blocks/logo-marquee - Section: Blocks · Social proof - Install: `npx shadcn@latest add https://www.affectui.com/r/logo-marquee.json` - Import: `@/components/ui/logo-marquee` ### When to use - A “Trusted by” or “Used by teams at” strip under a hero or above pricing - Showing more customer, partner or integration logos than fit in one static row ### When not to use - Three to five logos – a static, centred row reads calmer and needs no motion - Logos people need to compare or click through – use a grid; a moving target is hard to hit ### Usage ```tsx import { LogoMarquee } from "@/components/ui/logo-marquee"; import { VercelMark, LinearMark, StripeMark, FigmaMark, NotionMark, GitHubMark } from "./marks"; const logos = [ { name: "Vercel", svg: <VercelMark />, scale: 0.86 }, { name: "Linear", svg: <LinearMark />, color: "#5E6AD2" }, { name: "Stripe", svg: <StripeMark />, color: "#635BFF", href: "/customers/stripe" }, { name: "Figma", svg: <FigmaMark />, color: "#F24E1E" }, { name: "Notion", svg: <NotionMark /> }, { name: "GitHub", svg: <GitHubMark /> }, ]; export function TrustedBy() { return ( <section className="py-16"> <p className="mb-6 text-center text-sm text-neutral-500">Trusted by teams at</p> <LogoMarquee logos={logos} rows={2} speed={36} labels="beside" /> </section> ); } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | logos | `{ name, svg?, src?, color?, scale?, href? }[]` | | The logos. svg is a ReactNode – use fill="currentColor" so it follows the muted colour; src takes an image instead. color is the brand colour it lifts to on hover, scale corrects optical size, href makes it a link | | speed | `number` | `40` | Pixels per second – the same pace on any width | | direction | `"left" \| "right"` | `"left"` | Which way the first row runs | | rows | `1 \| 2` | `1` | Two rows split the logos alternately and run in opposite directions | | gap | `number` | `56` | Space between logos in px; about 60 % of it on narrow containers | | pauseOnHover | `boolean` | `true` | Ease to a stop while hovered or focused with the keyboard, and speed back up on leave | | grayscale | `boolean` | `true` | Muted, colourless logos; the hovered one lifts to its brand colour. Off shows full colour and dims the others on hover | | labels | `"none" \| "below" \| "beside"` | `"none"` | Show the brand name as a small caption under the mark, or beside it like a wordmark | | label | `string` | `"Logos of companies using the product"` | Accessible name of the strip | | className | `string` | | Extra class on the wrapper | ### Keyboard - Tab: Moves through linked logos; the strip stops and the focused logo is nudged into view ### Accessibility - A labelled region; each name is read once – the repeated copies that make the loop are aria-hidden and inert - Keyboard focus stops the strip so a logo never runs away from the focus ring - Hover and focus stop it too, which covers the WCAG pause requirement for moving content - Reduced motion shows every logo in a static, wrapping row ### Motion - The loop repeats a measured set and wraps by exactly its width – no gap, no jump - One requestAnimationFrame loop eases a velocity per row: hover slows it to a stop, leaving winds it back up - Drag or swipe to scrub; letting go flings it with inertia before it settles back into its own pace - Only runs while on screen; edges fade out with a mask ### Responsive - Container queries make logos and gaps smaller in narrow containers, down to 320px without page overflow - Touch: horizontal swipes scrub the strip, vertical swipes still scroll the page - Dark mode built in: switches when an ancestor (usually <html>) has the class dark or data-theme="dark" ### Requirements - React 19 Related: logo-cluster, text-reveal, gradient-text --- ## Metrics Dashboard (Pro) Sales overview – KPI cards pick the metric, one chart switches between dotted line and bars, with top products and channels - Page: https://www.affectui.com/blocks/metrics-dashboard - Section: Blocks · Analytics - Install: `npx shadcn@latest add https://www.affectui.com/r/metrics-dashboard.json` (needs the Pro access key) - Import: `@/components/ui/metrics-dashboard` ### When to use - A compact sales or business overview with a handful of headline metrics - Dashboards where one chart, picked by KPI cards and shown as a line or as bars, says more than a grid of small charts ### When not to use - Large data sets or drill-downs – the chart shows up to 30 points per period - A single number – use a KPI card instead ### Usage ```tsx import { MetricsDashboard } from "@/components/ui/metrics-dashboard"; export function Overview() { return ( <MetricsDashboard title="Sales" site="Northwind Store" defaultRange="30d" onExport={(range, metric) => console.log("export", range, metric)} /> ); } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | title | `string` | `"Sales"` | Title in the bar on top | | site | `string` | `"Northwind Store"` | Site or workspace shown under the title | | defaultRange | `"7d" \| "30d" \| "90d"` | `"30d"` | Period to start on – shown as Week, Month and Quarter | | defaultMetric | `"revenue" \| "orders" \| "aov" \| "refunds"` | `"revenue"` | Metric to start on | | onExport | `(range, metric) => void` | | Called when Export is pressed; the button confirms locally either way | | className | `string` | | Extra class on the root | ### Keyboard - Tab: Moves through the period switch, Export, the KPI cards, the chart type, Compare and the chart - ← / →: With the chart focused, moves the inspected point - Home / End: Jumps to the first or last point ### Accessibility - Period and chart-type buttons use aria-pressed; the KPI cards are tabs with aria-selected - The chart has a text label with the range and values; the inspected point is announced through a status tooltip - Deltas say up or down in text, not only with color – and a falling refund rate counts as good ### Motion - KPI values and list counts count to their new amounts with tabular numerals - The chosen KPI card turns fully opaque while the others dim; the line draws in with its dots popping on, bars grow up one after another - With prefers-reduced-motion values and views switch instantly ### Responsive - Follows the block's own width with container queries: below 640px the KPI cards form a 2 × 2 grid and the lists stack - Below 460px the period switch fills the width, figures shrink and the chart is 168px tall - Dark mode built in: switches when an ancestor (usually <html>) has the class dark or data-theme="dark" ### Requirements - React 19 --- ## Pricing Plans (Pro) Pricing section with a monthly / yearly switch, three plan cards and collapsible details - Page: https://www.affectui.com/blocks/pricing-plans - Section: Blocks · Pricing - Install: `npx shadcn@latest add https://www.affectui.com/r/pricing-plans.json` (needs the Pro access key) - Import: `@/components/ui/pricing-plans` ### When to use - A pricing page or section with up to three plans and a monthly / yearly choice - Plans that differ in a few limits plus a longer list of features ### When not to use - One-time purchases – drop the billing switch or use a simpler card - More than three or four plans – use a comparison table instead ### Usage ```tsx import { PricingPlans } from "@/components/ui/pricing-plans"; export function Pricing() { return <PricingPlans plans={plans} defaultBilling="yearly" />; } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | plans | `Plan[]` | | Name, monthly and yearly price, limits, sync chip, features and an onSelect callback | | title | `string` | `"Choose your plan"` | Section heading | | subtitle | `string` | | Line under the heading | | defaultBilling | `"monthly" \| "yearly"` | `"yearly"` | Billing period shown first | | currency | `string` | `"$"` | Symbol in front of the prices | | className | `string` | | Extra class on the section | ### Keyboard - Tab: Move between the switch, the buttons and the detail toggles - Enter / Space: Switch the billing period, start a plan or show / hide details ### Accessibility - The billing switch is a radio group - Details toggles use aria-expanded; hidden details are inert - Features that are not included are announced as such ### Motion - The switch thumb slides; prices roll digit by digit after a switch - Details fold open and closed; reduced motion turns this off ### Responsive - Three columns on wide screens, one column below 860px - Dark mode built in: switches when an ancestor (usually <html>) has the class dark or data-theme="dark" ### Requirements - React 19 --- ## Task Board (Pro) Kanban board with drag and drop, quick add, a detail panel, search, label filters and a compact view - Page: https://www.affectui.com/blocks/task-board - Section: Blocks · Productivity - Install: `npx shadcn@latest add https://www.affectui.com/r/task-board.json` (needs the Pro access key) - Import: `@/components/ui/task-board` ### When to use - Sprint and project boards in a product, with tasks moving through a few statuses - Internal tools where people sort work by dragging it ### When not to use - Hundreds of tasks per column – add virtualisation and server-side filters - Touch-first apps – native drag and drop needs a mouse; offer the detail panel's status buttons instead ### Usage ```tsx import { TaskBoard } from "@/components/ui/task-board"; export function Sprint({ tasks }) { return ( <TaskBoard tasks={tasks} statuses={statuses} labels={labels} people={people} path={["Northwind", "Launch", "Sprint 12"]} onChange={save} /> ); } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | tasks | `Task[]` | | key, title, status, priority (0–4), estimate, labels, assignee, dueIn, subtasks, pullRequest, created | | statuses | `TaskStatus[]` | | Columns in order: id, label and icon | | labels | `TaskLabel[]` | | id, label and color – used on cards, in the filter and in the panel | | people | `TaskPerson[]` | | id, name and avatar for assignees | | path | `string[]` | | Breadcrumb above the board | | onChange | `(tasks: Task[]) => void` | | Called with all tasks after every move, edit, add or delete | ### Keyboard - Tab: Move through the toolbar, columns and cards - Enter / Space: Open the focused card in the detail panel - Escape: Close the detail panel or cancel a new task ### Accessibility - Every card is focusable and opens a panel where status, priority, labels and assignee are plain toggle buttons – no dragging needed - The star, compact view and filter are pressed or checked toggles; the breadcrumb marks the current page ### Motion - Cards fade in, a blue line shows where a dragged card lands, the panel slides in from the right - Reduced motion turns these off ### Responsive - Columns keep a minimum width and the board scrolls sideways on narrow screens - Dark mode built in: switches when an ancestor (usually <html>) has the class dark or data-theme="dark" ### Requirements - React 19 --- ## Testimonial Wall (Free) The landing-page wall of quotes – columns that scroll endlessly in opposite directions, ease to a stop on hover and scrub with inertia - Page: https://www.affectui.com/blocks/testimonial-wall - Section: Blocks · Social proof - Install: `npx shadcn@latest add https://www.affectui.com/r/testimonial-wall.json` - Import: `@/components/ui/testimonial-wall` ### When to use - A “Loved by” section on a landing or pricing page with eight or more quotes - Social proof that should feel alive without asking for attention – it runs slowly and stops the moment someone reads ### When not to use - Two or three quotes – show them side by side; a loop of three repeats too obviously - A single hero quote or a case study – it deserves a static, larger layout - Reviews people need to search or filter – use a list or grid ### Usage ```tsx import { TestimonialWall } from "@/components/ui/testimonial-wall"; const items = [ { quote: "We replaced three half-finished internal kits with it in a sprint.", name: "Lena Hartmann", role: "Design Lead", company: "Fieldnote", avatar: "/avatars/lena.jpg", rating: 5, }, { quote: "The motion is the part nobody notices – which is exactly the point.", name: "Jonas Weber", role: "Staff Engineer", company: "Ledgerline", verified: true, }, // … at least two quotes per column, ideally four or more ]; export function Testimonials() { return ( <section className="py-24"> <p className="text-center text-sm text-neutral-500">Testimonials</p> <h2 className="mb-10 text-center text-3xl font-semibold tracking-tight">Loved by product teams</h2> <TestimonialWall items={items} speed={26} className="[--ui-tw-height:640px]" /> </section> ); } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | items | `{ quote, name, role?, company?, avatar?, rating?, verified? }[]` | | The quotes. avatar is a photo URL – without one the initials sit on a soft gray circle. rating (1–5) adds a small gray star row, verified a gray check beside the name | | columns | `1 \| 2 \| 3` | `3` | Most columns on a wide container; the wall itself drops to 2 below 740px and 1 below 480px of its own width | | speed | `number` | `28` | Pixels per second of the first column; the others run about 15 % slower and faster, in alternating directions | | pauseOnHover | `boolean` | `true` | Ease the hovered column to a stop and wind it back up on leave. Keyboard focus always stops its column | | label | `string` | `"What customers say"` | Accessible name of the wall | | className | `string` | | Extra class on the wrapper. Set the height with --ui-tw-height (600px by default) and the edge fade with --ui-tw-fade (96px) | ### Keyboard - Tab: Moves through the quotes; the focused card's column stops and the card is nudged into view - Shift / Tab: Moves back; leaving the wall lets the columns run again ### Accessibility - A labelled region; every quote is a figure with a blockquote and a figcaption, read out once – the copies that make the loop are aria-hidden and inert - Keyboard focus stops the column, so a card never runs away from the focus ring - Hover, focus and press-and-hold all stop the motion, which covers the WCAG pause requirement for moving content - Star ratings are an image with a text label (“Rated 5 out of 5”), the check is labelled “Verified customer” - Reduced motion shows every quote once in a static masonry grid ### Motion - Each column repeats a measured set and wraps by exactly its height – no gap, no jump - Columns alternate up and down at slightly different speeds and start at different points, so rows never line up - One requestAnimationFrame loop eases a velocity per column: hover slows it to a stop over about a third of a second, leaving winds it back up - Drag a column with the mouse to scrub it; letting go flings it with inertia before it settles back into its own pace - The hovered card lifts 3px with a soft shadow while the rest of the wall dims a little - Only runs while on screen; top and bottom edges fade out with a mask ### Responsive - Columns follow the wall's own width (3 / 2 / 1), so it works in a sidebar as well as full width - One column on phones with 15px quotes; names win over role and company when space is tight – down to 320px without overflow - Touch: vertical swipes keep scrolling the page (no scroll trap); pressing and holding a column pauses it to read - Dark mode built in: switches when an ancestor (usually <html>) has the class dark or data-theme="dark" ### Requirements - React 19 Related: logo-marquee, tilt-card, text-reveal --- ## Agent Team (Free) A prompt types itself and branches out into three agents, each with a role and overlapping tool logos - Page: https://www.affectui.com/illustrations/agent-team - Section: Illustrations · Card illustrations - Install: `npx shadcn@latest add https://www.affectui.com/r/agent-team.json` - Import: `@/components/ui/agent-team` ### When to use - AI and automation products where one prompt sets up several agents or roles - Feature cards about teams, delegation or workflows that span several tools ### When not to use - Real org charts or live agent status – the scene is decorative and loops on its own - More than three agents – the layout is built for exactly three ### Usage ```tsx import { AgentTeam } from "@/components/ui/agent-team"; export function FeatureCard() { return ( <article style={{ maxWidth: 560 }}> <AgentTeam prompt="Build a team to work my inbound pipeline" members={[ { role: "Lead Researcher", avatar: "/team/lena.jpg", tools: ["linkedin", "hubspot"] }, { role: "Inbound Qualifier", avatar: "/team/theo.jpg", tools: ["hubspot", "slack"] }, { role: "Outbound SDR", avatar: "/team/noah.jpg", tools: ["gmail", "salesforce"] }, ]} /> </article> ); } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | prompt | `string` | `"Build a team to work my inbound pipeline"` | Text that types itself into the prompt pill | | members | `[AgentTeamMember, AgentTeamMember, AgentTeamMember]` | | Required. Three agents, left to right – { role, avatar, tools? } with up to two of linkedin, hubspot, slack, gmail, salesforce | | label | `string` | | Accessible description – by default built from the prompt and the roles | | className | `string` | | Extra class on the root, e.g. for width or margins | ### Accessibility - Exposed as one image with a label built from the prompt and the roles; everything inside is hidden from screen readers - Nothing in the scene is interactive ### Motion - The prompt types itself, the send button presses and a soft pink-to-violet glow blooms behind it, dashed lines draw down and the three agents pop in one after another – then it starts over - Each status dot pops in last; the tool circles overlap slightly - With prefers-reduced-motion it shows the finished team and stays still ### Responsive - Transparent background, so it sits on any light or dark surface - Keeps a 3 : 2 aspect ratio and scales to the width of its container – text and shapes are sized in container units - Dark mode built in: switches when an ancestor (usually <html>) has the class dark or data-theme="dark" ### Requirements - React 19 --- ## DACH Region (Free) DACH map with fading neighbours and a pastel band along the border, switchable to the US, China, India, Brazil or Japan - Page: https://www.affectui.com/illustrations/region-dach - Section: Illustrations · Maps - Install: `npx shadcn@latest add https://www.affectui.com/r/region-dach.json` - Import: `@/components/ui/region-dach` ### When to use - Showing a service area, sales territory or market focus in Germany, Austria and Switzerland - Sections about regional exclusivity or local presence in one of the larger markets (US, China, India, Brazil, Japan), next to a short text ### When not to use - Maps that need cities, regions or exact borders – the shapes are simplified and there are no labels ### Usage ```tsx import { RegionDach } from "@/components/ui/region-dach"; export function TerritorySection() { return ( <section style={{ maxWidth: 420 }}> <RegionDach /> </section> ); } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | region | `"dach" \| "us" \| "cn" \| "in" \| "br" \| "jp"` | | Shown region (controlled). Pair it with onRegionChange to keep the menu working | | defaultRegion | `Region` | `"dach"` | Region shown first when region is not set | | onRegionChange | `(region: Region) => void` | | Called when the viewer picks a region in the menu | | showSwitcher | `boolean` | `true` | Show the region menu in the top-left corner | | label | `string \| ((region: Region) => string)` | `"Map of the DACH region: Germany, Austria and Switzerland" / "Map of Japan" …` | Accessible description of the map; by default it follows the current region | | className | `string` | | Extra class on the root, e.g. for width or margins | ### Keyboard - Enter / Space: On the menu button: open the region list. In the list: pick the highlighted region - ArrowDown / ArrowUp: On the menu button: open the list. In the list: move through the regions - Home / End: Jump to the first or last region - Escape: Close the list and return focus to the menu button - Tab: Close the list and move on ### Accessibility - The map is one image whose label names the current region; the shapes inside are hidden from screen readers - The menu button reports the current region and its expanded state; the list is a listbox with options, the selected one marked with aria-selected ### Motion - A soft pastel band travels along the outer border in a continuous 14-second loop: a blurred glow that only spreads outwards, with three sharp layers on top - Switching regions fades and slightly grows the old map away while the new one settles in from slightly smaller; the band starts anew - The menu morphs open out of its button, and the hover fill glides between rows - With prefers-reduced-motion the band stays still, regions swap instantly and the menu opens without the morph ### Responsive - Transparent background; the neighbouring countries fade out towards the edges, so it sits on any light or dark surface - Keeps a 540 : 440 aspect ratio and scales to the width of its container – set a width or max-width via className - The menu scales down with the illustration (to about 60 %) so it still fits in small previews - The file embeds the map data for all six regions (about 155 KB) - Dark mode built in: switches when an ancestor (usually <html>) has the class dark or data-theme="dark" ### Requirements - React 19 --- ## Empty Calendar (Free) Stack of calendar cards marked empty, fanning out on hover - Page: https://www.affectui.com/illustrations/empty-calendar - Section: Illustrations · Card illustrations - Install: `npx shadcn@latest add https://www.affectui.com/r/empty-calendar.json` - Import: `@/components/ui/empty-calendar` ### When to use - A problem or pain-point card about an empty pipeline or no booked meetings - Bento grids and feature cards that need a small, friendly illustration above the text ### When not to use - Showing a real calendar – the cards are decorative and contain placeholder lines only ### Usage ```tsx import { EmptyCalendar } from "@/components/ui/empty-calendar"; export function FeatureCard() { return ( <article style={{ maxWidth: 360 }}> <EmptyCalendar /> <h3>Card title</h3> <p>Short description of the problem or step.</p> </article> ); } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | label | `string` | `"Stacked calendar cards marked empty…"` | Accessible description of the illustration | | className | `string` | | Extra class on the root, e.g. for width or margins | ### Accessibility - Exposed as one image with a descriptive label; the cards and texts inside are hidden from screen readers - Hover effects are purely decorative, nothing needs to be reached with the keyboard ### Motion - The placeholder lines shimmer softly and the colored glow under the front card breathes - Hover the front card to lift and tilt it; hover a back card to slide it further out - With prefers-reduced-motion the shimmer and glow stop and hover changes happen instantly ### Responsive - Transparent background, so it sits on any light or dark surface - Keeps a 3 : 2 aspect ratio and scales to the width of its container – set a width or max-width via className - Drawn on a 320 × 240 viewBox, so text and cards scale crisply at any size - Dark mode built in: switches when an ancestor (usually <html>) has the class dark or data-theme="dark" ### Requirements - React 19 --- ## Enrichment Orbit (Free) Grok tile inside two rings with comets and data nodes orbiting around it - Page: https://www.affectui.com/illustrations/enrichment-orbit - Section: Illustrations · Card illustrations - Install: `npx shadcn@latest add https://www.affectui.com/r/enrichment-orbit.json` - Import: `@/components/ui/enrichment-orbit` ### When to use - A step or feature card about enriching, scoring or syncing data around a central record - Bento grids that need a calm, continuously moving illustration ### When not to use - Places where constant motion would distract from nearby content ### Usage ```tsx import { EnrichmentOrbit } from "@/components/ui/enrichment-orbit"; export function FeatureCard() { return ( <article style={{ maxWidth: 360 }}> <EnrichmentOrbit /> <h3>Card title</h3> <p>Short description of the problem or step.</p> </article> ); } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | label | `string` | `"A Grok tile with data nodes orbiting…"` | Accessible description of the illustration | | className | `string` | | Extra class on the root, e.g. for width or margins | ### Accessibility - Exposed as one image with a descriptive label; the rings and nodes inside are hidden from screen readers ### Motion - Two colored comets run around the inner and outer ring in opposite directions (one turn every 7 s) - Three data nodes orbit the outer ring 120° apart (one orbit every 22 s) and stay upright while they travel - With prefers-reduced-motion everything is paused, the nodes keep their staggered spots on the ring ### Responsive - Transparent background, so it sits on any light or dark surface - Keeps a 3 : 2 aspect ratio and scales to the width of its container – set a width or max-width via className - Drawn on a 320 × 240 viewBox, so rings and nodes scale crisply at any size - Dark mode built in: switches when an ancestor (usually <html>) has the class dark or data-theme="dark" ### Requirements - React 19 --- ## Frost Cloud (Free) A frosted white cloud with eyes cut out of it, on a red-orange or blue sky – it floats, turns to look around, blinks, follows the pointer and bounces with a wink when clicked - Page: https://www.affectui.com/illustrations/frost-cloud - Section: Illustrations · Card illustrations - Install: `npx shadcn@latest add https://www.affectui.com/r/frost-cloud.json` - Import: `@/components/ui/frost-cloud` ### When to use - A friendly face for an assistant, an AI feature or a cloud product – as an app icon, an empty state or a feature card - Landing pages that want a calm, living mascot instead of a static logo ### When not to use - Tiny sizes below ~48px – the glow and the eyes get lost - Brands far from blue or orange – the colors are part of the look ### Usage ```tsx import { FrostCloud } from "@/components/ui/frost-cloud"; export function AssistantCard() { return ( <article style={{ maxWidth: 320 }}> <FrostCloud color="blue" /> <h3>Meet your assistant</h3> </article> ); } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | color | `"blue" \| "orange"` | `"orange"` | Blue-to-cyan or red-to-orange sky; the cloud's colors follow | | background | `boolean` | `true` | Fill the whole box with the sky gradient; false leaves the cloud on a transparent background with a soft colored edge | | followPointer | `boolean` | `true` | The eyes follow the pointer while it's over the illustration | | label | `string` | | Accessible description of the illustration | | className | `string` | | Extra class on the root | ### Accessibility - Exposed as one image with a descriptive label - Clicking only plays an animation – nothing else happens ### Motion - Frosted white and a little see-through, the sky shining through softly towards the bottom, eyes cut out of it - The cloud floats and breathes; its shadow shrinks and fades as it rises - Every few seconds it glances left, right, up or down: the body tilts a little, the eyes move further and the shine slides the other way, so it seems to turn - It blinks at random moments, sometimes twice - The eyes follow the pointer; a click gives a squishy bounce and a wink - Runs only while on screen; with prefers-reduced-motion it keeps still and only reacts to clicks ### Responsive - Square by default; give it a height and the sky fills the whole box while the cloud stays centered, up to 320px wide - Light and dark mode via a .dark class or data-theme="dark": a deeper sky in dark mode ### Requirements - React 19 Related: glow-cloud, model-wheel --- ## Ghosted Chat (Free) Unanswered chat messages marked as read, with a thumbs-down reply on hover - Page: https://www.affectui.com/illustrations/ghosted-chat - Section: Illustrations · Card illustrations - Install: `npx shadcn@latest add https://www.affectui.com/r/ghosted-chat.json` - Import: `@/components/ui/ghosted-chat` ### When to use - A problem card about cold outreach that gets ignored or messages left on read - Bento grids and feature cards that need a small illustration above the text ### When not to use - A real chat UI – the bubbles are a fixed, decorative scene - Very small sizes below ~220px wide, where the bubble text gets hard to read ### Usage ```tsx import { GhostedChat } from "@/components/ui/ghosted-chat"; export function FeatureCard() { return ( <article style={{ maxWidth: 360 }}> <GhostedChat /> <h3>Card title</h3> <p>Short description of the problem or step.</p> </article> ); } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | label | `string` | `"A chat with three unanswered messages…"` | Accessible description of the illustration | | className | `string` | | Extra class on the root, e.g. for width or margins | ### Accessibility - Exposed as one image with a descriptive label; the messages inside are hidden from screen readers - The hover reply is purely decorative, nothing needs to be reached with the keyboard ### Motion - A red shine sweeps through the last message every few seconds; the last bubble has a soft colored glow - On hover the status texts fade out, the messages move up and a blue thumbs-down reply pops in with a small bounce - With prefers-reduced-motion the shine stops and hover changes happen instantly ### Responsive - Transparent background, so it sits on any light or dark surface - Laid out on a fixed 360 × 240 artboard and scaled to the width of its container with a ResizeObserver, so the bubbles never reflow - Dark mode built in: switches when an ancestor (usually <html>) has the class dark or data-theme="dark" ### Requirements - React 19 --- ## Glass Folder (Pro) Glassy blue folder with documents behind a frosted pocket that tips open on hover - Page: https://www.affectui.com/illustrations/glass-folder - Section: Illustrations · Card illustrations - Install: `npx shadcn@latest add https://www.affectui.com/r/glass-folder.json` (needs the Pro access key) - Import: `@/components/ui/glass-folder` ### When to use - Feature cards about files, uploads, storage or a knowledge base; empty states for documents - Bento grids that need a calm, friendly illustration above the text ### When not to use - Showing real files – the documents are decorative and contain placeholder lines only - Dark sections – the folder and documents are drawn for a light theme ### Usage ```tsx import { GlassFolder } from "@/components/ui/glass-folder"; export function FeatureCard() { return ( <article style={{ maxWidth: 360 }}> <GlassFolder /> <h3>Card title</h3> <p>Short description of the feature.</p> </article> ); } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | label | `string` | `"Blue folder holding three documents…"` | Accessible description of the illustration | | className | `string` | | Extra class on the root, e.g. for width or margins | ### Accessibility - Exposed as one image with a descriptive label; the folder parts inside are hidden from screen readers - The hover effect is purely decorative, nothing needs to be reached with the keyboard ### Motion - Hover the folder: the frosted pocket tips forward and each document shifts and turns on its own - With prefers-reduced-motion the changes happen instantly ### Responsive - Transparent background, so it sits on any light surface - Keeps a 3 : 2 aspect ratio and scales to the width of its container – every detail is sized in container units ### Requirements - React 19 --- ## Glow Cloud (Free) A glowing cloud with eyes in blue or red-orange – it floats, turns to look around, blinks, follows the pointer and bounces with a wink when clicked - Page: https://www.affectui.com/illustrations/glow-cloud - Section: Illustrations · Card illustrations - Install: `npx shadcn@latest add https://www.affectui.com/r/glow-cloud.json` - Import: `@/components/ui/glow-cloud` ### When to use - A friendly face for an assistant, an AI feature or a cloud product – as an app icon, an empty state or a feature card - Landing pages that want a calm, living mascot instead of a static logo ### When not to use - Tiny sizes below ~48px – the glow and the eyes get lost - Brands far from blue or orange – the colors are part of the look ### Usage ```tsx import { GlowCloud } from "@/components/ui/glow-cloud"; export function AssistantCard() { return ( <article style={{ maxWidth: 320 }}> <GlowCloud color="orange" /> <h3>Meet your assistant</h3> </article> ); } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | color | `"blue" \| "orange"` | `"blue"` | Blue-to-cyan or red-to-orange sky; the cloud's colors follow | | background | `boolean` | `true` | Fill the whole box with the sky gradient; false leaves the cloud on a transparent background with a soft colored edge | | followPointer | `boolean` | `true` | The eyes follow the pointer while it's over the illustration | | label | `string` | | Accessible description of the illustration | | className | `string` | | Extra class on the root | ### Accessibility - Exposed as one image with a descriptive label - Clicking only plays an animation – nothing else happens ### Motion - A bright colored body inside a white glow, with white eyes shaded lightly on the inside - The cloud floats and breathes; its shadow shrinks and fades as it rises - Every few seconds it glances left, right, up or down: the body tilts a little, the eyes move further and the shine slides the other way, so it seems to turn - It blinks at random moments, sometimes twice - The eyes follow the pointer; a click gives a squishy bounce and a wink - Runs only while on screen; with prefers-reduced-motion it keeps still and only reacts to clicks ### Responsive - Square by default; give it a height and the sky fills the whole box while the cloud stays centered, up to 320px wide - Light and dark mode via a .dark class or data-theme="dark": a deeper sky in dark mode ### Requirements - React 19 Related: frost-cloud, model-wheel --- ## Glow Keyboard (Pro) AI chat input that types itself over a glass keyboard – every key press sends a wave up the gradient floor - Page: https://www.affectui.com/illustrations/glow-keyboard - Section: Illustrations · Product scenes - Install: `npx shadcn@latest add https://www.affectui.com/r/glow-keyboard.json` (needs the Pro access key) - Import: `@/components/ui/glow-keyboard` ### When to use - Hero sections for AI assistants, chat or writing tools – the prompt types itself and invites people to try - Landing pages that want an interactive scene instead of a video ### When not to use - As your real chat input – it is a showcase; use Chat Composer for a working input - Small cards on mobile – the keyboard legends get tiny below ~360px width ### Usage ```tsx import { GlowKeyboard } from "@/components/ui/glow-keyboard"; export function Hero() { return ( <section style={{ maxWidth: 1100, margin: "0 auto" }}> <GlowKeyboard /> </section> ); } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | prompts | `string[]` | `three hiking prompts` | Prompts the demo types one after another; each one is sent with Enter | | placeholder | `string` | `"Ask AI anything..."` | Placeholder of the chat input | | onSubmit | `(text: string) => void` | | Called when a prompt is sent with Enter – by the demo or the user | | label | `string` | `"AI chat input above a glass keyboard…"` | Accessible name of the scene | | className | `string` | | Extra class on the root, e.g. for width or margins | ### Keyboard - Click: Anywhere on the scene puts the cursor into the input and stops the demo - Any key: Types and lights up the same key on the drawn keyboard - Enter: Sends the prompt and clears the input ### Accessibility - The input is a real text field with a label; the drawn keyboard and the floor are hidden from screen readers - The demo stops as soon as the input gets focus, so it never types over the user ### Motion - The demo types each prompt with an uneven, human rhythm, holds Shift for capitals and corrects one typo - Every key press: the key sinks in, a wave rises out of the gradient below it and the input glow flashes at the same spot - The floor's two layers – colour and white plus-lighter – drift and morph slowly - Runs only while on screen; with prefers-reduced-motion the scene stays still and the demo does not type, typing yourself still works ### Responsive - Keeps an 11 : 6 frame and scales to the width of its container – every detail is sized in container units - Set the background with --ui-gk-bg (default #fafafa, #111113 in dark mode) - Dark mode built in: switches when an ancestor (usually <html>) has the class dark or data-theme="dark" ### Requirements - React 19 Related: gradient-floor, chat-composer --- ## Integration Beam (Free) Your product between the tools it connects – light pulses flow along curved beams, hover to focus a tool, click to pause it - Page: https://www.affectui.com/illustrations/integration-beam - Section: Illustrations · Product scenes - Install: `npx shadcn@latest add https://www.affectui.com/r/integration-beam.json` - Import: `@/components/ui/integration-beam` ### When to use - A hero or integrations section that shows your product sitting between the tools a team already uses - Landing pages where a calm, self-playing data flow explains “we connect to X and Y” faster than a logo wall ### When not to use - A real integration settings page – pausing a tool here is only part of the scene, nothing is saved - More than four tools per side – the beams get crowded; use Integration Grid or Logo Cluster for long lists ### Usage ```tsx import { IntegrationBeam } from "@/components/ui/integration-beam"; export function IntegrationsSection() { return ( <section style={{ maxWidth: 640, margin: "0 auto" }}> <IntegrationBeam /> </section> ); } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | left | `{ name: string; logo: ReactNode }[]` | `Slack, Notion, Gmail` | Tools that send data in – on the left, or above the hub on narrow widths. The logo fills a 28 × 28 box | | right | `{ name: string; logo: ReactNode }[]` | `HubSpot, Linear, Google Drive` | Tools that receive data – on the right, or below the hub on narrow widths | | center | `{ name: string; logo?: ReactNode }` | `{ name: "Acme" }` | Your product. The logo is drawn white into a 36 × 36 box on the dark tile (currentColor); a neutral flower mark when left out | | label | `string` | `"Acme connected to Slack, Notion, …"` | Accessible name of the scene | | className | `string` | | Extra class on the root, e.g. for width or margins | ### Keyboard - Tab: Moves between the tool tiles; the focused tool's beam comes forward and its name shows - Enter / Space: Pauses or reconnects the focused tool ### Accessibility - The scene is a labelled group; every tool is a toggle button with aria-pressed and a name like “Slack, connected” - Pausing or reconnecting is announced in a polite live region, with the new count (“5 of 6 connected”) - The beams, pulses and logos are decorative and hidden from screen readers - Visible focus ring on the tiles; the tooltip also shows on keyboard focus ### Motion - Sync cycles: pulses leave the left tools 280 ms apart, travel 1.5 s along their beam and, as they land, the hub glows and the next wave leaves for the right tools - Each pulse is a short gradient – clear tail, accent body, pale tip – with a soft blurred glow; one accent color only - The status chip under the hub slides between “Syncing…” and “6 connected”, and its width glides with the text - When the layout switches between side by side and stacked, the tiles glide to their new spots and the curves morph with them (460 ms) - The loop only runs while the scene is on screen (IntersectionObserver) - With prefers-reduced-motion the beams are drawn as static lines – no pulses, no glides; hover, focus and pausing still work ### Responsive - The beams are measured from the real tile positions (ResizeObserver), so the scene reflows with its container - Below 460px of container width the senders stack above the hub and the receivers below, with vertical beams; fits 320px screens - On touch screens a tap pauses a tool and shows its name for a moment instead of a hover tooltip - Dark mode built in: switches when an ancestor (usually <html>) has the class dark or data-theme="dark" ### Requirements - React 19 Related: integration-grid, integration-flow, logo-cluster, agent-team --- ## Integration Flow (Pro) Integration pills rotating along a dashed arc, each beaming its data into a central analysis hub - Page: https://www.affectui.com/illustrations/integration-flow - Section: Illustrations · Product scenes - Install: `npx shadcn@latest add https://www.affectui.com/r/integration-flow.json` (needs the Pro access key) - Import: `@/components/ui/integration-flow` ### When to use - A hero or feature section that shows your product pulling data in from the tools teams already use - Integration or data-platform pages where a short, self-playing loop explains the idea better than a logo wall ### When not to use - A real list of connected integrations – the pills are decorative, not interactive - Very small spaces below ~320px wide, where the pill text gets hard to read - Brands that clash with the warm coral background ### Usage ```tsx import { IntegrationFlow } from "@/components/ui/integration-flow"; export function IntegrationsSection() { return ( <section style={{ maxWidth: 1100, margin: "0 auto" }}> <IntegrationFlow /> </section> ); } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | items | `{ name: string; icon: ReactNode; title?: string; mono?: boolean }[]` | `Okta, Snowflake, Slack, ClickUp, Zendesk` | Integrations that rotate through the pills (5 or more look best). The icon fills a 64 × 64 box; mono logos turn black in the centre | | label | `string` | `"Integrations like Slack, Okta and Snowflake…"` | Accessible description of the scene | | className | `string` | | Extra class on the root, e.g. for width or margins | ### Accessibility - The scene is exposed as one image with a descriptive label; the pills and cards inside are hidden from screen readers ### Motion - The pills travel along the dashed arc one slot at a time (0.9 s); the one landing in the centre turns light and shows its logo in color - The centre pill charges up, a beam sweeps over to the Data Analysis hub (1.1 s) and the hub lights up as it arrives - The coral background blobs drift and breathe slowly on a 16 s loop - With prefers-reduced-motion everything stands still in the centre state, with the beam drawn in ### Responsive - Drawn on a 2400 × 1490 stage and scaled to the width of its container, so the layout never reflows - Keeps its 2400 : 1490 aspect ratio; set a width or max-width via className - Set --ui-if-bg and --ui-if-fade to change the stage background and the color the bottom fades into - Dark mode built in: switches when an ancestor (usually <html>) has the class dark or data-theme="dark" ### Requirements - React 19 --- ## Integration Grid (Free) Honeycomb-like wall of integration logos around a bright centre tile with your product mark, fading out toward the edges - Page: https://www.affectui.com/illustrations/integration-grid - Section: Illustrations · Card illustrations - Install: `npx shadcn@latest add https://www.affectui.com/r/integration-grid.json` - Import: `@/components/ui/integration-grid` ### When to use - Integration sections and feature cards like “Works with the tools you already use” - Landing pages that show your product at the centre of an ecosystem ### When not to use - A real, clickable list of integrations – the tiles are decorative and not links - More than 22 logos – the grid has 22 outer tiles, extra logos are left out ### Usage ```tsx import { IntegrationGrid } from "@/components/ui/integration-grid"; export function IntegrationsSection() { return ( <section style={{ maxWidth: 760 }}> <IntegrationGrid mark={<img src="/logo.svg" alt="" width={40} height={40} />} /> <h3>Plays well with your stack</h3> <p>Connect the tools your team already relies on.</p> </section> ); } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | logos | `IntegrationGridLogo[]` | `22 well-known tools` | Logos filled row by row around the centre – each { name, path, color?, darkColor? } with a single 24 × 24 SVG path (e.g. from simple-icons); without color the mark follows the ink color | | mark | `ReactNode` | `a neutral flower mark` | Content of the larger centre tile – usually your product logo | | columns | `5 \| 7 \| 9` | `9` | Tiles in the middle row – the rows above and below get two fewer. Fewer columns make the tiles larger | | label | `string` | | Accessible description – by default lists the integrations | | className | `string` | | Extra class on the root, e.g. for width or margins | ### Accessibility - Exposed as one image whose label lists every integration; tiles and logos are hidden from screen readers - Nothing in the scene is interactive ### Motion - Still by default; on hover a tile turns fully white and its logo grows a little from the centre - With prefers-reduced-motion the hover lift is switched off ### Responsive - Transparent background, so it sits on any light or dark surface - Keeps a 1520 : 560 aspect ratio and scales to the width of its container – every size is in container units - Tile fills fade toward the edges while the logos keep their full brand colors - Dark mode built in: switches when an ancestor (usually <html>) has the class dark or data-theme="dark" ### Requirements - React 19 --- ## Layer Stack (Free) Isometric stack of five layers with a list beside it – the chosen entry lifts its layer out in color - Page: https://www.affectui.com/illustrations/layer-stack - Section: Illustrations · Card illustrations - Install: `npx shadcn@latest add https://www.affectui.com/r/layer-stack.json` - Import: `@/components/ui/layer-stack` ### When to use - Explaining a layered product or platform – each entry is one layer of the stack - Feature sections that walk through capabilities one by one, with a visual that follows along ### When not to use - More than five layers – the stack and the list are sized for up to five - Navigation that changes page content – this is an illustration with its own small state ### Usage ```tsx import { LayerStack } from "@/components/ui/layer-stack"; export function PlatformSection() { return ( <section style={{ maxWidth: 640 }}> <LayerStack items={[ { title: "Triggered by events", hint: "Webhooks", icon: "bolt" }, { title: "Grounded in your data", hint: "Context", icon: "database" }, { title: "Connected to your apps", hint: "APIs", icon: "plug" }, { title: "Works with any model", hint: "Routing", icon: "spark" }, { title: "Traced at every step", hint: "Logs", icon: "pulse" }, ]} /> </section> ); } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | items | `LayerStackItem[]` | `five sample layers` | Up to five entries, top layer first – { title, hint?, icon? } with icon one of bolt, database, plug, spark, pulse | | defaultIndex | `number` | `0` | Entry to start on | | interval | `number` | `2600` | Time per entry while it plays on its own, in ms | | onChange | `(index: number) => void` | | Called when the user picks an entry | | className | `string` | | Extra class on the root, e.g. for width or margins | ### Keyboard - Tab: Moves focus to the chosen entry - ↑ / ↓: Picks the previous or next entry ### Accessibility - The list is a vertical tablist with real buttons; the chosen entry has aria-selected - The stack itself is decorative and hidden from screen readers ### Motion - The chosen layer and the ones above it glide up to open a gap; its face, edge and icon turn blue and a soft glow fades in beneath it - A white card glides behind the chosen entry in the list - Steps through the entries on its own and pauses while hovered or focused; after a click or key press it stays put - With prefers-reduced-motion it doesn't play on its own and changes happen instantly ### Responsive - Transparent background, so it sits on any light or dark surface - Keeps a 3 : 2 aspect ratio and scales to the width of its container – text and layers are sized in container units - Dark mode built in: switches when an ancestor (usually <html>) has the class dark or data-theme="dark" ### Requirements - React 19 --- ## Lead Sourcing (Free) ChatGPT tile linked to Gmail and Grok, with a shine running along the connection - Page: https://www.affectui.com/illustrations/lead-sourcing - Section: Illustrations · Card illustrations - Install: `npx shadcn@latest add https://www.affectui.com/r/lead-sourcing.json` - Import: `@/components/ui/lead-sourcing` ### When to use - A step or feature card about collecting leads or data from several sources - Integration sections that show your product connecting a few well-known apps ### When not to use - Communicating real integrations you don't offer – swap the logos for your own sources ### Usage ```tsx import { LeadSourcing } from "@/components/ui/lead-sourcing"; export function FeatureCard() { return ( <article style={{ maxWidth: 360 }}> <LeadSourcing /> <h3>Card title</h3> <p>Short description of the problem or step.</p> </article> ); } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | label | `string` | `"A ChatGPT tile connected to Gmail and Grok…"` | Accessible description of the illustration | | className | `string` | | Extra class on the root, e.g. for width or margins | ### Accessibility - Exposed as one image with a descriptive label; the logos and tiles inside are hidden from screen readers ### Motion - A colored shine sweeps along the connecting line from left to right, then the line rests for a few seconds before the next pass - With prefers-reduced-motion the line stays gray and still ### Responsive - Transparent background, so it sits on any light or dark surface - Keeps a 3 : 2 aspect ratio and scales to the width of its container – set a width or max-width via className - Drawn on a 320 × 240 viewBox, so logos and tiles scale crisply at any size - Dark mode built in: switches when an ancestor (usually <html>) has the class dark or data-theme="dark" ### Requirements - React 19 --- ## Logo Cluster (Free) Your mark on a white disc in the middle – company logos bounce out of it and settle around it - Page: https://www.affectui.com/illustrations/logo-cluster - Section: Illustrations · Card illustrations - Install: `npx shadcn@latest add https://www.affectui.com/r/logo-cluster.json` - Import: `@/components/ui/logo-cluster` ### When to use - Integration or ecosystem sections – "works with the tools you already use" - Feature cards that need a lively, friendly picture around your own brand ### When not to use - Listing your real integrations – the logos are examples - Places where people need to read or click the logos ### Usage ```tsx import { LogoCluster } from "@/components/ui/logo-cluster"; export function IntegrationsCard() { return ( <article style={{ maxWidth: 360 }}> <LogoCluster /> <h3>Connects with everything</h3> </article> ); } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | label | `string` | `"The affectUI mark surrounded by…"` | Accessible description of the illustration | | className | `string` | | Extra class on the root, e.g. for width | ### Accessibility - Exposed as one image with a descriptive label; the bubbles are hidden from screen readers ### Motion - When the cluster comes into view the middle circle appears, then the logos quickly bounce out of it one after another - Once they land, the ring turns slowly clockwise while every logo stays upright – paused while off screen - Hovering a bubble grows it slightly with a springy ease and a softer shadow - With prefers-reduced-motion everything is in place right away ### Responsive - Square, scales with the width of its container (max 420px) – every size is in container units - Light and dark mode via a .dark class or data-theme="dark" ### Notes for AI - Swap the mark in the middle for your own logo: replace the MARK path and its viewBox - Company logos belong to their owners – check you may show them before you ship ### Requirements - React 19 Related: integration-grid, model-wheel --- ## Model Wheel (Free) AI model names on a turning drum – the rows bend away at the edges and each model lands on a soft gray fill - Page: https://www.affectui.com/illustrations/model-wheel - Section: Illustrations · Card illustrations - Install: `npx shadcn@latest add https://www.affectui.com/r/model-wheel.json` - Import: `@/components/ui/model-wheel` ### When to use - Feature cards about model choice, routing or "bring your own model" - Landing pages that list supported AI providers in a calm, moving way ### When not to use - A real model picker – the wheel can be spun, but it doesn't report a choice - Long lists – the drum shows about seven rows at a time ### Usage ```tsx import { ModelWheel } from "@/components/ui/model-wheel"; export function FeatureCard() { return ( <article style={{ maxWidth: 360 }}> <ModelWheel /> <h3>Every model, one workspace</h3> </article> ); } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | models | `{ name, viewBox, color, d }[]` | `8 popular models` | Models on the wheel, each with its brand mark as one SVG path; use at least 7 to keep the drum full | | hold | `number` | `1500` | Milliseconds a model stays in the middle | | label | `string` | `"AI models on a turning wheel…"` | Accessible description of the illustration | | className | `string` | | Extra class on the root | ### Accessibility - Exposed as one image with a descriptive label; the rows are hidden from screen readers - Hovering holds the wheel, so nothing moves while someone reads it - Spinning by hand also works with touch; the automatic turn starts again after a short pause ### Motion - The rows sit on a cylinder: towards the top and bottom they tilt away, shrink and fade - Every turn runs on a soft spring: the drum gathers speed, overshoots a touch and locks onto the next model like a magnet - Grab the wheel and drag it up or down to spin it; a quick flick keeps it turning, then it snaps onto the closest model - The wheel turns downwards; the model in the middle sits on a gray fill that grows or shrinks to its name - Runs only while on screen; with prefers-reduced-motion it doesn't turn by itself, but can still be spun by hand ### Responsive - Fixed height of seven rows; the width follows its container - Light and dark mode via a .dark class or data-theme="dark" ### Notes for AI - Brand marks belong to their owners – check you may show them before you ship ### Requirements - React 19 Related: glow-keyboard, chat-composer --- ## Outreach Branches (Pro) Lines branching out to Gmail, Slack and Notion with colored pulses flowing down - Page: https://www.affectui.com/illustrations/outreach-branches - Section: Illustrations · Card illustrations - Install: `npx shadcn@latest add https://www.affectui.com/r/outreach-branches.json` (needs the Pro access key) - Import: `@/components/ui/outreach-branches` ### When to use - A step or feature card about sending messages across several channels - Integration sections that show one action fanning out to a few apps ### When not to use - Communicating real integrations you don't offer – swap the logos for your own channels ### Usage ```tsx import { OutreachBranches } from "@/components/ui/outreach-branches"; export function FeatureCard() { return ( <article style={{ maxWidth: 360 }}> <OutreachBranches /> <h3>Card title</h3> <p>Short description of the problem or step.</p> </article> ); } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | label | `string` | `"Lines branching out to Gmail, Slack and Notion…"` | Accessible description of the illustration | | className | `string` | | Extra class on the root, e.g. for width or margins | ### Accessibility - Exposed as one image with a descriptive label; the logos and lines inside are hidden from screen readers ### Motion - Short colored pulses flow down each of the three lines, staggered by one second (3 s loop) - With prefers-reduced-motion the pulses are hidden and only the gray lines remain ### Responsive - Transparent background, so it sits on any light or dark surface - Keeps a 3 : 2 aspect ratio and scales to the width of its container – set a width or max-width via className - Drawn on a 320 × 240 viewBox, so logos and tiles scale crisply at any size - Dark mode built in: switches when an ancestor (usually <html>) has the class dark or data-theme="dark" ### Requirements - React 19 --- ## Project Flow (Pro) Animated scene of a client task branching into subtasks, notes and assigned roles - Page: https://www.affectui.com/illustrations/project-flow - Section: Illustrations · Product scenes - Install: `npx shadcn@latest add https://www.affectui.com/r/project-flow.json` (needs the Pro access key) - Import: `@/components/ui/project-flow` ### When to use - A feature section that shows how tasks, clients and roles connect in your product - Landing pages where a short, self-playing scene explains a workflow better than text ### When not to use - Interactive product UI – the scene is a decorative illustration, not working components - Very small spaces below ~320px wide, where the card text gets hard to read ### Usage ```tsx import { ProjectFlow } from "@/components/ui/project-flow"; export function FeatureSection() { return ( <section style={{ maxWidth: 960, margin: "0 auto" }}> <ProjectFlow /> </section> ); } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | theme | `"light" \| "dark"` | `"light"` | Light or dark cards | | label | `string` | `"A client task branching…"` | Accessible description of the scene | | className | `string` | | Extra class on the root, e.g. for width or margins | ### Accessibility - The scene is exposed as one image with a descriptive label; its inner elements are hidden from screen readers ### Motion - The cards build up one after another: client, task, notes, roles and team, connected by lines that draw in - A hand drags the subtask cards in, drops them with a small bounce and lets go - With prefers-reduced-motion everything appears in its final state right away ### Responsive - The background is transparent; set --ui-pf-fade to your page color so the soft edges blend in - Drawn on a 2312 × 1644 artboard and scaled to the width of its container, so the layout never reflows ### Requirements - React 19 --- ## Side Folder (Free) Glassy blue folder on its side, documents sliding out to the right as the pocket swings open on hover - Page: https://www.affectui.com/illustrations/side-folder - Section: Illustrations · Card illustrations - Install: `npx shadcn@latest add https://www.affectui.com/r/side-folder.json` - Import: `@/components/ui/side-folder` ### When to use - Feature cards about files, uploads, storage or a knowledge base; empty states for documents - Bento grids that need a calm, friendly illustration above the text ### When not to use - Showing real files – the documents are decorative and contain placeholder lines only - Dark sections – the folder and documents are drawn for a light theme ### Usage ```tsx import { SideFolder } from "@/components/ui/side-folder"; export function FeatureCard() { return ( <article style={{ maxWidth: 360 }}> <SideFolder /> <h3>Card title</h3> <p>Short description of the feature.</p> </article> ); } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | label | `string` | `"Blue folder on its side…"` | Accessible description of the illustration | | className | `string` | | Extra class on the root, e.g. for width or margins | ### Accessibility - Exposed as one image with a descriptive label; the folder parts inside are hidden from screen readers - The hover effect is purely decorative, nothing needs to be reached with the keyboard ### Motion - Hover the folder: the frosted pocket swings forward on its hinge and each document slides out to the right on its own - With prefers-reduced-motion the changes happen instantly ### Responsive - Transparent background, so it sits on any light surface - Keeps a 3 : 2 aspect ratio and scales to the width of its container – every detail is sized in container units ### Requirements - React 19 --- ## Tab Overload (Free) Overlapping browser windows where the highlight follows the hovered tab - Page: https://www.affectui.com/illustrations/tab-overload - Section: Illustrations · Card illustrations - Install: `npx shadcn@latest add https://www.affectui.com/r/tab-overload.json` - Import: `@/components/ui/tab-overload` ### When to use - A problem card about context switching, research in too many tabs or manual busywork - Bento grids and feature cards that need a small illustration above the text ### When not to use - Showing real browser content – the windows only contain placeholder blocks - Places where the right and bottom edge must stay sharp – the scene fades out there by design ### Usage ```tsx import { TabOverload } from "@/components/ui/tab-overload"; export function FeatureCard() { return ( <article style={{ maxWidth: 360 }}> <TabOverload /> <h3>Card title</h3> <p>Short description of the problem or step.</p> </article> ); } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | label | `string` | `"Three overlapping browser windows…"` | Accessible description of the illustration | | className | `string` | | Extra class on the root, e.g. for width or margins | ### Accessibility - Exposed as one image with a descriptive label; the windows inside are hidden from screen readers - The hover highlight is purely decorative, nothing needs to be reached with the keyboard ### Motion - The placeholder blocks shimmer softly - The front window is highlighted with colored dots, an outline and a glow; hover a window behind it and the highlight moves over - With prefers-reduced-motion the shimmer stops and the highlight switches instantly ### Responsive - Transparent background; the whole scene fades out towards the right and bottom edge with a CSS mask, so it blends into any surface - Keeps a 3 : 2 aspect ratio and scales to the width of its container – set a width or max-width via className - The hover swap uses the CSS :has() selector; in browsers without it the front window simply stays highlighted - Dark mode built in: switches when an ancestor (usually <html>) has the class dark or data-theme="dark" ### Requirements - React 19 --- ## Team Invite (Free) Connected teammates – a cursor drags a new one into the empty slot, the line turns solid and they go online - Page: https://www.affectui.com/illustrations/team-invite - Section: Illustrations · Card illustrations - Install: `npx shadcn@latest add https://www.affectui.com/r/team-invite.json` - Import: `@/components/ui/team-invite` ### When to use - Feature cards about inviting teammates, collaboration, sharing or multiplayer - Onboarding steps like “Invite your team” where a friendly, moving scene helps ### When not to use - Showing your real team list – the scene is decorative and loops on its own - Places that need to stay completely still – use prefers-reduced-motion or a static image ### Usage ```tsx import { TeamInvite } from "@/components/ui/team-invite"; export function FeatureCard() { return ( <article style={{ maxWidth: 420 }}> <TeamInvite avatars={[ { src: "/team/maya.png", name: "Maya" }, { src: "/team/finn.png", name: "Finn" }, { src: "/team/lea.png", name: "Lea" }, ]} /> <h3>Invite your team</h3> <p>Bring everyone into the same workspace in seconds.</p> </article> ); } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | avatars | `[TeamInviteAvatar, TeamInviteAvatar, TeamInviteAvatar]` | | Required. The two connected teammates, then the one being invited – each { src, name? } with a square portrait | | label | `string` | | Accessible description – by default built from the names | | className | `string` | | Extra class on the root, e.g. for width or margins | ### Accessibility - Exposed as one image with a descriptive label built from the names; avatars, lines and cursor are hidden from screen readers - Nothing in the scene is interactive ### Motion - A 4.4 s loop with soft ease-in-out: the cursor picks up the third avatar, carries it into the dashed slot, the line draws solid and the green status dot pops in - Only transforms, opacity and a stroke offset are animated – no layout work - With prefers-reduced-motion it stays a still frame of the waiting state ### Responsive - Transparent background, so it sits on any light or dark surface - Keeps a 3 : 2 aspect ratio and scales to the width of its container – every size is in container units - Avatars are square portraits shown in white-ringed circles; photos, 3D faces or illustrations all work - Dark mode built in: switches when an ancestor (usually <html>) has the class dark or data-theme="dark" ### Requirements - React 19 --- ## World Map Continental (Pro) World map with one highlighted continent and a pastel band along its coastline, switchable between all six - Page: https://www.affectui.com/illustrations/world-map - Section: Illustrations · Maps - Install: `npx shadcn@latest add https://www.affectui.com/r/world-map.json` (needs the Pro access key) - Import: `@/components/ui/world-map` ### When to use - Showing that you serve, operate in or focus on one continent - Market, coverage or expansion sections on a landing page, where a calm map says more than a list of countries ### When not to use - Data maps with values per country – this is a decorative illustration, not a chart - Showing individual countries – each continent is one merged shape without borders, simplified from Natural Earth 1:110m ### Usage ```tsx import { WorldMap } from "@/components/ui/world-map"; export function MarketSection() { return ( <section style={{ maxWidth: 640 }}> <WorldMap /> </section> ); } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | continent | `"north-america" \| "south-america" \| "europe" \| "africa" \| "asia" \| "oceania"` | | Highlighted continent (controlled). Pair it with onContinentChange to keep the menu working | | defaultContinent | `Continent` | `"europe"` | Continent highlighted first when continent is not set | | onContinentChange | `(continent: Continent) => void` | | Called when the viewer picks a continent in the menu | | showSwitcher | `boolean` | `true` | Show the continent menu in the top-left corner | | label | `string \| ((continent: Continent) => string)` | `"World map with Europe highlighted" …` | Accessible description of the map; by default it follows the current continent | | className | `string` | | Extra class on the root, e.g. for width or margins | ### Keyboard - Enter / Space: On the menu button: open the continent list. In the list: pick the highlighted continent - ArrowDown / ArrowUp: On the menu button: open the list. In the list: move through the continents - Home / End: Jump to the first or last continent - Escape: Close the list and return focus to the menu button - Tab: Close the list and move on ### Accessibility - The map is one image whose label names the highlighted continent; the shapes inside are hidden from screen readers - The menu button reports the current continent and its expanded state; the list is a listbox with options, the selected one marked with aria-selected ### Motion - A soft pastel band travels along the coastline of the continent's main landmass in a continuous 14-second loop: a blurred glow that only spreads outwards, with three sharp layers on top – the same band as in the DACH Region map - Switching continents fades the old highlight back into the gray world while the new one settles in from slightly smaller with its shadow; the band starts anew - The menu morphs open out of its button, and the hover fill glides between rows - With prefers-reduced-motion the band stays still, continents swap instantly and the menu opens without the morph ### Responsive - Transparent background; the solid gray world and the lighter highlighted continent read on light and dark surfaces alike - Keeps a 540 : 242 aspect ratio and scales to the width of its container – set a width or max-width via className - The menu scales down with the illustration (to about 60 %); on small sizes the open list may extend below the map - Dark mode built in: switches when an ancestor (usually <html>) has the class dark or data-theme="dark" ### Requirements - React 19 --- ## Blur Crossfade (Pro) Swaps content with a soft blur while the box glides smoothly to the size of what comes next - Page: https://www.affectui.com/motion/blur-crossfade - Section: Motion · Transitions - Install: `npx shadcn@latest add https://www.affectui.com/r/blur-crossfade.json` (needs the Pro access key) - Import: `@/components/ui/blur-crossfade` ### When to use - Button labels and status text that change in place – Save → Saving… → Saved - Panels, tips or steps that swap content of different lengths without a jump in the layout ### When not to use - Navigating between pages or large views – use a route transition instead - Content that updates many times per second, like counters or live prices ### Usage ```tsx import { BlurCrossfade } from "@/components/ui/blur-crossfade"; export function SaveButton({ status }) { return ( <button> <BlurCrossfade id={status} morph="both" as="span"> {status === "saving" ? "Saving…" : status === "saved" ? "Saved" : "Save"} </BlurCrossfade> </button> ); } // Height glides between messages of different length <BlurCrossfade id={step}> <p>{steps[step]}</p> </BlurCrossfade> ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | id | `string \| number` | | Key of the current content – changing it starts the swap | | children | `ReactNode` | | Content for the current id | | duration | `number` | `420` | Length of the swap and the size morph in ms | | blur | `number` | `10` | Blur in px at the outer ends of the fade | | morph | `"height" \| "both" \| "none"` | `"height"` | Which sides glide to the new size; both makes the box as wide as its content | | as | `"div" \| "span"` | `"div"` | Root element – span inside buttons and other inline content | | className | `string` | | Extra class on the box | ### Accessibility - Only the current content is exposed – the leaving copy is aria-hidden and inert - Put aria-live on a surrounding element when the change should be announced, e.g. a status button ### Motion - The old content blurs, fades and grows slightly as the new one sharpens in from 98 %, while the box glides to the new height (and width) - Quick changes stay clean: only the latest leaving copy and the current content are ever on screen - Reduced motion swaps instantly ### Responsive - Dark mode built in: switches when an ancestor (usually <html>) has the class dark or data-theme="dark" - Content keeps its natural size; the box only clips during the swap, so shadows and focus rings show otherwise ### Requirements - React 19 Related: text-reveal, gradient-text --- ## Gradient Floor (Pro) A blurred colour gradient that glows up from the bottom edge – two layers that drift and morph - Page: https://www.affectui.com/motion/gradient-floor - Section: Motion · Backgrounds - Install: `npx shadcn@latest add https://www.affectui.com/r/gradient-floor.json` (needs the Pro access key) - Import: `@/components/ui/gradient-floor` ### When to use - Hero sections, AI features or call-to-action blocks that need a warm, living backdrop - Behind a chat input or a prompt, to make it feel like the start of something ### When not to use - Behind long body text – the bright floor competes with small type - Several floors on one screen – each one runs its own animation ### Usage ```tsx import { GradientFloor } from "@/components/ui/gradient-floor"; export function Hero() { return ( // Give the floor a height, e.g. with a class <GradientFloor className="hero"> <h1>Ideas start to glow</h1> </GradientFloor> ); } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | children | `ReactNode` | | Content rendered above the floor | | colors | `[string, string, string]` | `["#FF7700", "#FF0055", "#0073FF"]` | Gradient from the crest down | | speed | `number` | `3` | Seconds per morph loop; 0 keeps the shape still | | className | `string` | | Extra class on the root, e.g. for height or rounded corners | ### Accessibility - The floor layers are hidden from screen readers; only your children are announced - Keep text above the bright part of the floor at a readable contrast ### Motion - Two blurred layers – colour and a white plus-lighter layer – whose peaks and valleys rise and fall against each other - Both layers drift sideways on the same beat, the light one a little less - Runs only while on screen; with prefers-reduced-motion the floor stays still ### Responsive - Scales with the width of its container and stays anchored to the bottom edge – give the root a height - Set the background with --ui-gf-bg (default #fafafa, dark mode #111113) ### Requirements - React 19 Related: glow-keyboard, grid-background --- ## Gradient Text (Pro) Letters cut from a liquid, ever-flowing gradient – the color sweeps across the word the first time it's seen - Page: https://www.affectui.com/motion/gradient-text - Section: Motion · Text - Install: `npx shadcn@latest add https://www.affectui.com/r/gradient-text.json` (needs the Pro access key) - Import: `@/components/ui/gradient-text` ### When to use - One word or a short phrase in a hero headline that should carry the brand - A highlight inside a heading, like a product name or the payoff of a sentence ### When not to use - Body copy or long passages – a moving gradient makes them harder to read - Several highlights on one screen – the effect works because it's rare ### Usage ```tsx import { GradientText } from "@/components/ui/gradient-text"; export function Hero() { return ( <h1> Interfaces people actually <GradientText>feel.</GradientText> </h1> ); } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | children | `ReactNode` | | The text to fill with the gradient | | colors | `string[]` | `["#dd0674", "#5e3ee0", "#ff5fae", "#7c5cff"]` | Gradient start, end and up to two blob colors; replaces the dark-mode colors too | | speed | `number` | `8` | Seconds per flow cycle | | sweep | `boolean` | `true` | Sweep the color in from left to right when the text scrolls into view | | as | `"span" \| "h1" \| "h2" \| "p"` | `"span"` | Element to render | | className | `string` | | Extra class on the root | ### Accessibility - The text stays real, selectable text – the gradient copy on top is aria-hidden - Pick colors with enough contrast against your background; the defaults are tuned for white and near-black ### Motion - Soft color blobs drift across a two-color base in a slow loop; the sweep wipes the color in once, the first time the text is 60% visible - Reduced motion shows the finished gradient, standing still ### Responsive - Font, size and weight come from the parent, so it scales with your headline - Dark mode built in: switches when an ancestor (usually <html>) has the class dark or data-theme="dark" ### Requirements - React 19 Related: text-reveal --- ## Grid Background (Free) A fine line grid that drifts slowly behind your content and fades out towards the edges - Page: https://www.affectui.com/motion/grid-background - Section: Motion · Backgrounds - Install: `npx shadcn@latest add https://www.affectui.com/r/grid-background.json` - Import: `@/components/ui/grid-background` ### When to use - Hero sections and feature headers that need a quiet, technical backdrop - Empty states or panels that feel too flat on a plain background ### When not to use - Behind dense text or data tables – the lines compete with the content - Several moving backgrounds on one screen – keep it to one per view ### Usage ```tsx import { GridBackground } from "@/components/ui/grid-background"; export function Hero() { return ( <GridBackground size={48} direction="diagonal"> <section style={{ display: "grid", placeItems: "center", minHeight: 480 }}> <h1>Build on a steady grid</h1> </section> </GridBackground> ); } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | children | `ReactNode` | | Content rendered above the grid | | size | `number` | `48` | Cell size in px | | color | `string` | `rgba(0,0,0,.07)` | Line color; dark mode defaults to rgba(255,255,255,.07) | | speed | `number` | `3` | Seconds per cell; 0 keeps the grid still | | direction | `"up" \| "down" \| "left" \| "right" \| "diagonal"` | `"diagonal"` | Drift direction; diagonal runs from top right to bottom left | | reveal | `boolean` | `true` | Uncover the grid with a circle growing from the center when it first comes into view | | fade | `number` | `70` | Radius of the edge fade, in % of the distance to the corners | | className | `string` | | Extra class on the container – give it a height or let the content size it | ### Accessibility - The grid layer is decorative: aria-hidden and pointer-events: none - Content sits in its own layer above the grid and stays fully interactive ### Motion - The lines move by exactly one cell per loop, so the drift is seamless; only transform is animated - The reveal grows a circular mask from the center once, when the grid is 20% in view - Reduced motion: the grid stays still and appears without the reveal ### Responsive - Fills its container at any size; the fade follows the container's shape - Dark mode built in: switches when an ancestor (usually <html>) has the class dark or data-theme="dark" ### Requirements - React 19 --- ## Number Ticker (Free) Odometer-style numbers – every digit rolls to its new value and counts in from zero when it scrolls into view - Page: https://www.affectui.com/motion/number-ticker - Section: Motion · Numbers - Install: `npx shadcn@latest add https://www.affectui.com/r/number-ticker.json` - Import: `@/components/ui/number-ticker` ### When to use - KPIs, prices and counters that change while the user is looking, like dashboards or live stats - Headline numbers on a landing page that should count in when they scroll into view ### When not to use - Dense tables with many numbers – rolling digits everywhere becomes noise - Values that change many times per second; the roll never settles ### Usage ```tsx import { NumberTicker } from "@/components/ui/number-ticker"; export function RevenueKpi({ revenue }: { revenue: number }) { return ( <p className="text-4xl font-semibold"> <NumberTicker value={revenue} format={{ style: "currency", currency: "USD", maximumFractionDigits: 0 }} /> </p> ); } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | value | `number` | | The number to show – change it and the digits roll to the new value | | format | `Intl.NumberFormatOptions` | | Formatting, e.g. currency, percent, compact notation or fraction digits | | locale | `string` | `browser locale` | Locale passed to Intl.NumberFormat, e.g. "en-US" or "de-DE" | | duration | `number` | `900` | Length of a roll in milliseconds | | delay | `number` | `0` | Wait before the first count-in, in milliseconds | | className | `string` | | Extra class on the wrapper – font size, weight and color are inherited | ### Accessibility - The formatted value is in a visually hidden text inside an aria-live="polite" region, so screen readers hear the final number once - The rolling digit columns are aria-hidden ### Motion - Each digit rolls up when the value grows and down when it shrinks; new columns (9,999 → 10,000) slide in - Counts in from 0 the first time it scrolls into view - Reduced motion jumps straight to the value ### Responsive - Inherits font size and weight from its parent and uses tabular numbers, so the width stays steady while rolling - Dark mode built in: switches when an ancestor (usually <html>) has the class dark or data-theme="dark" ### Requirements - React 19 --- ## Shimmer Loader (Free) Skeleton blocks that share one soft light sweep, plus shimmering text for “Thinking…” states - Page: https://www.affectui.com/motion/shimmer-loader - Section: Motion · Loaders - Install: `npx shadcn@latest add https://www.affectui.com/r/shimmer-loader.json` - Import: `@/components/ui/shimmer-loader` ### When to use - Placeholders while cards, lists or profiles load, shaped like the content that will arrive - A shimmering status label while an AI answer or a long task is being generated ### When not to use - Waits under ~300 ms – a flash of skeletons feels slower than showing nothing - Progress you can measure; a progress bar tells the user how long is left ### Usage ```tsx import { Skeleton, ShimmerText } from "@/components/ui/shimmer-loader"; export function PostSkeleton() { return ( <div aria-busy="true" className="flex items-center gap-3"> <Skeleton circle width={36} /> <div className="grid gap-2"> <Skeleton width={120} height={10} /> <Skeleton width={80} height={8} /> </div> </div> ); } export function Thinking() { return <ShimmerText>Generating summary…</ShimmerText>; } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | Skeleton · width | `number \| string` | `"100%"` | Width – numbers are px, strings any CSS length | | Skeleton · height | `number \| string` | `12` | Height – numbers are px | | Skeleton · radius | `number \| string` | `6` | Corner radius | | Skeleton · circle | `boolean` | `false` | Round shape that uses width for both sides, e.g. for avatars | | Skeleton · className / style | `string / CSSProperties` | | Extra class or inline styles on the block | | ShimmerText · children | `ReactNode` | | The label the highlight runs through | | ShimmerText · duration | `number` | `2` | Seconds for one pass of the highlight | | ShimmerText · className | `string` | | Extra class – font size and weight are inherited | ### Accessibility - Skeletons are aria-hidden – put aria-busy="true" on the container that is loading - ShimmerText stays real text, so screen readers read the label normally ### Motion - The skeleton highlight is pinned to the viewport, so all blocks share one sweep that travels across them together - Reduced motion shows static placeholders and plain text ### Responsive - Skeletons fill their container by default; set width for fixed shapes - Dark mode built in: switches when an ancestor (usually <html>) has the class dark or data-theme="dark" ### Requirements - React 19 --- ## Text Reveal (Free) Text that arrives word by word or letter by letter – each piece rises out of a soft blur as it scrolls into view - Page: https://www.affectui.com/motion/text-reveal - Section: Motion · Text - Install: `npx shadcn@latest add https://www.affectui.com/r/text-reveal.json` - Import: `@/components/ui/text-reveal` ### When to use - Headlines and short intros on landing pages that should arrive as you scroll - A key sentence you want read in order, like a mission statement or a quote ### When not to use - Long paragraphs or anything people need to read right away – waiting for words gets tiring - Text that changes often, like live data – the reveal plays only when it enters the view ### Usage ```tsx import { TextReveal } from "@/components/ui/text-reveal"; export function Intro() { return <TextReveal as="h2" text="Words that arrive one at a time." />; } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | text | `string` | | The text to reveal | | by | `"word" \| "char"` | `"word"` | Reveal word by word or letter by letter | | stagger | `number` | `40` | Time between two pieces, in ms | | delay | `number` | `0` | Wait before the first piece, in ms | | duration | `number` | `700` | Time each piece takes to arrive, in ms | | as | `"p" \| "h1" \| "h2" \| "h3" \| "span"` | `"p"` | Element to render | | once | `boolean` | `true` | Reveal only the first time; otherwise it plays again each time it enters the view | | className | `string` | | Extra class on the root | ### Accessibility - Screen readers get the full sentence from a visually hidden copy; the animated pieces are aria-hidden - Words never break in the middle, and spaces stay real, so lines wrap like normal text ### Motion - Each piece fades in out of a blur while it rises a little, one after another, once 30% of the text is visible - Reduced motion shows the text right away ### Responsive - Font, size and color come from the parent, so it fits any heading or paragraph - Dark mode built in: switches when an ancestor (usually <html>) has the class dark or data-theme="dark" ### Requirements - React 19 Related: gradient-text --- ## Tilt Card (Pro) A card that tilts in 3D towards the pointer, with a soft glare that follows it and a smooth spring back - Page: https://www.affectui.com/motion/tilt-card - Section: Motion · Hover - Install: `npx shadcn@latest add https://www.affectui.com/r/tilt-card.json` (needs the Pro access key) - Import: `@/components/ui/tilt-card` ### When to use - Showcase cards: membership or pricing cards, product shots, featured items - A single hero object that should feel tangible on hover ### When not to use - Grids of many cards – the motion becomes noise when every card tilts - Cards full of controls or text to read – the moving surface makes them harder to use ### Usage ```tsx import { TiltCard } from "@/components/ui/tilt-card"; export function MembershipCard() { return ( <TiltCard maxTilt={10} scale={1.02}> <div className="membership"> <span>affectUI Pro</span> <span>•••• 4821</span> </div> </TiltCard> ); } ``` ### Props | Name | Type | Default | Description | | --- | --- | --- | --- | | children | `ReactNode` | | The card's content – it fills the tilting surface | | maxTilt | `number` | `10` | Maximum tilt in degrees | | glare | `boolean` | `true` | Show a soft glare that follows the pointer | | scale | `number` | `1.02` | Scale while the pointer is over the card | | perspective | `number` | `900` | Perspective distance in px – smaller is more dramatic | | className | `string` | | Extra class on the wrapper; set --ui-tc-radius to match your card's corners | ### Accessibility - Purely visual: the tilt adds no roles or tab stops, and the glare is aria-hidden - Keyboard focus on anything inside the card lifts it slightly, so focus is visible in the motion too ### Motion - Pointer moves are batched with requestAnimationFrame and written to CSS variables; only transform and opacity animate - On leave the card springs back to rest with a soft overshoot - Touch pointers don't tilt - Reduced motion: no tilt or glare, only the shadow changes ### Responsive - Sizes to its content; the demo card shrinks to the available width - Dark mode built in: switches when an ancestor (usually <html>) has the class dark or data-theme="dark" ### Requirements - React 19