# Eased: docs and free components, in full > The Eased docs, then docs, install commands and usage for all 37 free (MIT) Eased components. The index is at https://eased.dev/llms.txt; Pro components are listed there. --- # Quickstart for agents You copy one prompt, paste it into your coding agent and check that the agent can reach Eased. After that, ask for an interaction, such as "an undoable delete", and your agent finds the component and installs it with the shadcn CLI. ## Before you begin - A React 19 project with Tailwind CSS v4 and a `components.json`. If you don't have a `components.json`, run `npx shadcn init`. - A coding agent that supports MCP: Claude Code, Cursor, VS Code or Codex. ## Give your agent the prompt ```text Set up Eased (https://eased.dev), a motion-first React component library, in this project. 1. Connect the Eased MCP server (https://eased.dev/api/mcp, streamable HTTP). Use the command for this client: - Claude Code: run `claude mcp add --transport http eased https://eased.dev/api/mcp` - Cursor: add `{"mcpServers":{"eased":{"url":"https://eased.dev/api/mcp"}}}` to .cursor/mcp.json - VS Code: add `{"servers":{"eased":{"type":"http","url":"https://eased.dev/api/mcp"}}}` to .vscode/mcp.json - Codex: run `codex mcp add eased --url https://eased.dev/api/mcp` 2. Load the Eased design skill first: call get_skill and follow its rules for all UI work. To keep it in the project, run `npx shadcn add https://eased.dev/r/eased-skill.json` (it installs to .claude/skills/eased/SKILL.md). 3. Find components with search_components or list_components. Call get_component before you use one. 4. Install with the command from get_install_command (npx shadcn add …). Don't copy source by hand. 5. Style with the Eased tokens (bg-surface, text-text-secondary, rounded-panel, h-control-md), not raw colours or radii. Don't wrap Eased components in extra animation. 6. For Pro components: if EASED_PRO_TOKEN is set, add the @eased-pro registry from https://eased.dev/docs/installation#pro-components to components.json, then run npx shadcn add @eased-pro/. If the MCP server isn't available, read https://eased.dev/llms.txt instead, and install the skill with the command above. ``` 1. Click **Copy prompt**. 2. Paste the prompt into your agent's chat in your project. 3. Approve the MCP setup command or config change when your agent asks. The prompt tells your agent to connect the server, load the Eased design skill, search and read components before it uses them, install with the shadcn CLI and style with the Eased tokens. If you have Eased Pro and `EASED_PRO_TOKEN` is set, it also adds the Pro registry. ## Add the design skill The Eased skill is the design thinking behind the library, written for your agent: alignment, spacing ratios, concentric corners, springs per role, focus rings and reduced motion, plus how to install and compose Eased components. It's free. Install it into your project: ```bash npx shadcn add https://eased.dev/r/eased-skill.json ``` It lands in `.claude/skills/eased/SKILL.md`, where Claude Code loads it whenever it works on UI. Commit it so everyone on the project gets it. For Cursor, Codex and other agents, see [The Eased skill](/docs/ai-mcp#the-eased-skill). ## Connect the server yourself To skip the prompt, add the server directly. Pick your client: **Claude Code** (Terminal): ```bash claude mcp add --transport http eased https://eased.dev/api/mcp ``` **Cursor** (.cursor/mcp.json): ```json { "mcpServers": { "eased": { "url": "https://eased.dev/api/mcp" } } } ``` **VS Code** (.vscode/mcp.json): ```json { "servers": { "eased": { "type": "http", "url": "https://eased.dev/api/mcp" } } } ``` **Codex** (Terminal): ```bash codex mcp add eased --url https://eased.dev/api/mcp ``` The server is at `https://eased.dev/api/mcp`. It uses Streamable HTTP and needs no account for free components. For Pro source, see [Pro source for your agent](/docs/ai-mcp#pro-source-for-your-agent). ## What your agent can do The server has five read-only tools: - `get_skill`: returns the Eased design skill. Your agent calls it before it builds UI. - `search_components`: finds components for a need, such as `"loading state"` or `"drag to adjust a number"`. Best match first. - `list_components`: lists every component, optionally only `free` or `pro`. - `get_component`: returns one component as Markdown: docs, props, motion, install command and, for free components, full source and a demo. - `get_install_command`: returns one `npx shadcn add …` command for a list of slugs. ## Verify it works 1. Restart your agent, or reload its MCP servers, so it picks up the new server. 2. Ask your agent: "Use the Eased MCP server to search for a button component." 3. Check that your agent calls `search_components` and lists `button` in the results. If your agent can't find the tools, run its MCP list command, such as `claude mcp list` or `codex mcp list`, and check that `eased` is listed. For a client without MCP, point your agent at [llms.txt](https://eased.dev/llms.txt) instead. ## Next steps - [AI and MCP](/docs/ai-mcp): every tool's input and output, Pro tokens and Markdown pages. - [Installation](/docs/installation): install components by hand. - [Components](/components): browse what your agent can install. --- # Introduction Eased is a library of motion-first React components. You install each component with the shadcn CLI, so its source lands in your project and you can change any line. ```bash npx shadcn add https://eased.dev/r/button.json ``` Every component uses React 19, Tailwind CSS v4, Base UI and Motion. ## What Eased gives you - **Tuned motion.** Three named springs and a small set of curves, chosen per interaction. Exits are faster than entrances, and you can interrupt any animation halfway without a jump. See [Motion](/docs/motion). - **Concentric corners.** An inner corner radius is the outer radius minus the padding between them. Components set `--radius-outer` and `--pad`, so anything you nest inside them gets the right radius. See [Theming](/docs/theming). - **Every state designed.** Hover applies only on pointer devices. Press feedback scales to the control's width. Keyboard focus shows a ring. Disabled and loading states keep their width. Every animated component has a reduced-motion branch. - **Agent support.** An MCP server, a Markdown version of every page and `llms.txt` let your coding agent find, read and install components. See [Quickstart for agents](/docs/agent-quickstart). ## Free and Pro | | Free | Pro | |---|---|---| | Licence | MIT | Eased Pro licence, unlimited projects | | Install | `npx shadcn add https://eased.dev/r/.json` | `npx shadcn add @eased-pro/` with a token | | Source | Yes | Yes | | Light and dark | Yes | Yes | | Agents (MCP) | Docs and source | Docs, plus source with a Pro token | Free components cover the parts every app needs: buttons, inputs, tabs, popovers, tables, the chat composer, thinking states and more. They install from the public registry with one command. Eased Pro adds the showpieces, such as Spotlight Tour, Scrub Label, Group-by List and Gliding Focus Ring, with source included. You pay once for every current and future Pro component, and install them from a private registry with a personal token. See [Pro components](/docs/installation#pro-components). ## What you see is what you install Every demo on eased.dev is the component itself, running. The site reads the same stylesheet that the registry installs into your app, and the library's light and dark themes come from the same tokens. ## Next steps - [Quickstart for agents](/docs/agent-quickstart): give your coding agent one prompt. - [Installation](/docs/installation): add your first component by hand. - [Theming](/docs/theming): change colours, radii and the brand. - [Motion](/docs/motion): the springs, durations and rules every component follows. - [AI and MCP](/docs/ai-mcp): the MCP tools, Pro tokens and Markdown pages. - [Components](/components): every component, grouped by what it's for. --- # Installation Eased components are shadcn registry items. The shadcn CLI copies each component's source into your project, installs its npm dependencies and merges the Eased stylesheet into your global CSS. ## Before you begin - **React 19** and a framework that the shadcn CLI supports, such as Next.js, Vite, React Router or Astro. - **Tailwind CSS v4.** Eased uses `@theme inline`, `@utility` and `@custom-variant`, which need v4. - **A `components.json`.** If your project doesn't have one, run `npx shadcn init`. You don't need to install npm dependencies by hand. Each component brings its own, usually `motion` and `@base-ui/react`. ## Add a component 1. Run `shadcn add` with the component's registry URL: ```bash npx shadcn add https://eased.dev/r/button.json ``` 2. Import the component from `components/eased/`: ```tsx import { Button } from "@/components/eased/button/button"; export function Save() { return ; } ``` Each component page shows its exact install command. To install several components, list their URLs in one command: ```bash npx shadcn add https://eased.dev/r/tabs.json https://eased.dev/r/tooltip.json ``` Most components also have a demo item, `-demo`, which installs the example from the component's page. ## What gets added to your CSS The first component you install brings two shared registry dependencies: | Item | What it adds | |---|---| | `eased-tokens` | The Eased stylesheet, merged into your global CSS: colour, shadow, radius, control-height and motion variables; the utilities `radius-nested`, `squircle`, `fade-x`, `image-ring`, `hit-area` and `focus-ring`; and the variants `hover-fine`, `group-hover-fine` and `reduced` | | `eased-motion` | `components/eased/lib/`: the Motion springs and tweens (`motion.ts`), press classes (`press.ts`) and small DOM and platform helpers | The stylesheet sits beside shadcn/ui without collisions: - Every raw variable starts with `--eased-`, so your `--accent`, `--border` and `--radius` don't change. - The utilities it adds (`bg-surface`, `border-line`, `bg-brand`, `text-ui`, `rounded-panel`, `shadow-raised`, `ease-standard`, `h-control-sm` …) don't exist in Tailwind's default theme or in shadcn/ui, so `text-base`, `rounded-lg` and `bg-accent` keep your values. - It never styles a base element (`html`, `body`, `*`, `::selection`) and never resets a theme namespace. You don't import a CSS file. Light theme follows `:root`. Dark theme follows a `.dark` class or `data-theme="dark"` on an ancestor, so it works with `next-themes` and shadcn's default setup. ## Pro components Pro components install from a private registry, `@eased-pro`, with a personal token. 1. Sign in at [eased.dev/account](/account) and create a token. Copy it: the token starts with `eased_pro_` and is shown once. 2. Add the token to `.env.local`, which the shadcn CLI reads: ```bash EASED_PRO_TOKEN=eased_pro_… ``` 3. Add the registry to `components.json`: ```json { "registries": { "@eased-pro": { "url": "https://eased.dev/r/pro/{name}.json", "headers": { "Authorization": "Bearer ${EASED_PRO_TOKEN}" } } } } ``` 4. Install a Pro component by name: ```bash npx shadcn add @eased-pro/spotlight-tour ``` The CLI fills in the token from your environment, so it never goes into git. You can keep up to 10 active tokens, so create one per machine or CI job, and revoke any token at any time. Pro source opens with early access. Until then, the registry accepts a valid token but answers that Pro source isn't open yet. ## Troubleshooting | Problem | Fix | |---|---| | Classes such as `bg-surface` have no effect | The stylesheet didn't merge. Check that your global CSS imports Tailwind v4 (`@import "tailwindcss"`), then run the install again. The CLI merges into the CSS file named in `components.json`. | | Components render light inside a dark app | Dark follows `.dark` or `data-theme="dark"` on an ancestor. Make sure your theme switcher sets one of them. | | `401` from `/r/pro` | The token is missing, mistyped or revoked. Check `EASED_PRO_TOKEN` and the `Authorization` header in `components.json`. | | `403` from `/r/pro` | The token is valid, but its account has no Pro, or Pro access isn't open yet. | | `429` from `/r/pro` | You sent more than 120 requests in a minute with one token. Wait a minute, then try again. | | Animations don't play | Reduced motion is on in your OS. Every Eased component honours it and replaces movement with a short fade. | --- # Theming Every colour, shadow, radius and duration in Eased is a CSS variable. Components read only semantic names, so you re-theme the whole library by restating a few variables. You don't edit any component. ## How the tokens are layered | Layer | Example | What it's for | |---|---|---| | Raw ramps | `--eased-neutral-1` … `--eased-neutral-12`, `--eased-brand-1` … `--eased-brand-12` | Twelve-step OKLCH scales, authored separately for light and dark. Restate these to re-brand. | | Semantic aliases | `--eased-surface`, `--eased-line-subtle`, `--eased-text-muted`, `--eased-brand` | What a colour is for. Components never read a raw step. | | Tailwind utilities | `bg-surface`, `border-line-subtle`, `text-text-muted`, `bg-brand` | Generated by `@theme inline` from the aliases, so they follow the theme at runtime. | Each layer reads only the layer below it. The neutral ramp follows the Radix step roles: 1 app background, 2 subtle background, 3 component background, 4 hover, 5 pressed, 6 subtle border, 7 border, 8 strong border, 9 solid, 10 solid hover, 11 low-contrast text, 12 text. ## Semantic tokens | Utility | Use it for | |---|---| | `bg-bg` | The page canvas | | `bg-surface`, `bg-surface-sunken`, `bg-surface-raised` | Cards; wells and tracks; popovers and dialogs | | `bg-surface-hover`, `bg-surface-active` | Hover fill; pressed or selected fill | | `border-line-subtle`, `border-line`, `border-line-strong` | Dividers and card edges; interactive edges; inputs at rest | | `text-text`, `text-text-secondary`, `text-text-muted`, `text-text-disabled` | Headings and labels; descriptions; metadata; disabled text | | `bg-brand`, `text-text-on-brand`, `text-brand-text`, `bg-brand-subtle` | The one primary action; text on it; brand-coloured text; a brand tint | | `bg-success-subtle` + `text-success-text` (and `warning`, `danger`, `info`) | Status badges. Use solid fills only on buttons and progress. | | `shadow-resting`, `shadow-raised`, `shadow-floating`, `shadow-overlay` | Resting cards; hover lift; popovers and menus; dialogs | | `inset-shadow-well`, `inset-shadow-highlight` | Sunken inputs and tracks; the top highlight on primary buttons | All colours are OKLCH, so steps are evenly spaced in perceived lightness, and a hue change keeps contrast. ## Switch to dark ```html ``` Light theme is defined on `:root`. Dark theme is defined on `.dark` (shadcn's convention) and on `[data-theme="dark"]`, so either one works on any ancestor. Dark is authored, not inverted. Surfaces get lighter as they rise: `surface` is lighter than `bg`, and `surface-raised` is lighter again. Borders sit above their surface. Shadows carry about four times the alpha plus a faint white ring, so a raised card still reads on a dark canvas. ## Change the brand colour ```css :root { --eased-brand-3: oklch(95% 0.03 265); --eased-brand-9: oklch(52% 0.2 265); --eased-brand-10: oklch(47% 0.2 265); --eased-brand-11: oklch(45% 0.16 265); --eased-text-on-brand: oklch(98% 0.01 265); } .dark { --eased-brand-3: oklch(30% 0.06 265); --eased-brand-9: oklch(72% 0.15 265); --eased-brand-10: oklch(78% 0.13 265); --eased-brand-11: oklch(80% 0.11 265); --eased-text-on-brand: oklch(18% 0.03 265); } ``` 1. Add the brand steps to your global CSS, after the Eased block. 2. Set your own values for both `:root` and `.dark`. Components read steps 9 (fill), 10 (fill hover), 11 (text-safe) and 3 (tint) most. The default brand is warm ink in light and cream in dark. To change the neutrals' tint, set `--eased-neutral-hue`. The default is `80`, a warm tint; around `250` cools them toward blue. The focus ring and selection colour derive from the brand and neutral ramps, so they follow your change. ## Radius roles | Utility | Value | Use it for | |---|---|---| | `rounded-control-xs` | 6px | Chips and badges up to 24px tall | | `rounded-control-sm` | 8px | Controls 28 to 32px tall | | `rounded-control` | 10px | Controls 36 to 40px tall | | `rounded-panel` | 16px | Cards, popovers, menus and toasts | | `rounded-surface` | 24px | Dialogs, sheets and full-bleed media | | `rounded-pill` | 9999px | Chips, switches and avatars only. Never a card. | Components never use a literal radius; they use these roles. To change a role everywhere, restate its variable once, for example `--eased-radius-panel: 12px`. ## Nest rounded boxes ```tsx
``` When one rounded box sits inside another, the inner radius must equal the outer radius minus the space between them. Otherwise the corners look pinched: ```text inner radius = outer radius − padding − border width ``` 1. On the parent, set `--radius-outer` and `--pad`. 2. On the child, add the `radius-nested` utility. The child computes its own radius from the two variables, clamped at zero. In the example, the card is 16px with 6px of padding and a 1px border, so the image gets 9px. If you change the card's radius, the image follows. Every Eased component with a padded shell (Demo Frame, Segmented, Toggle Group, Popover, Hover Card, Composer and Concentric Frame) sets both variables on its root. Anything you put inside gets the right curve without a prop. If the result is under 2px, raise the outer radius, or clip the child with `overflow: clip` instead of rounding it. ## Control heights | Utility | Height | |---|---| | `h-control-xs` | 24px | | `h-control-sm` | 32px | | `h-control-md` | 36px | | `h-control-lg` | 40px | | `h-control-xl` | 48px | Controls share these five heights, so everything in a row lines up. To give a smaller control a 44 by 44px tap target without changing how it looks, add the `hit-area` utility. --- # Motion Every Eased component moves with the same small vocabulary: three springs, four curves and five durations. Each one exists twice, as a CSS variable in the stylesheet and as a Motion object in `components/eased/lib/motion.ts`. Both are sampled from the same physics, so a CSS transition and a Motion animation with the same name feel identical. ## Springs | Spring | Motion | CSS | Use it for | |---|---|---|---| | `snappy` | `{ type: "spring", stiffness: 520, damping: 38 }` | `ease-spring-snappy`, 350ms | Presses, toggles, thumbs, indicators, text swaps, rolling digits and the focus ring | | `smooth` | `{ type: "spring", visualDuration: 0.4, bounce: 0 }` | `ease-spring-smooth`, 450ms | Height and layout changes, panels, and toast and tooltip position. No overshoot. | | `bouncy` | `{ type: "spring", stiffness: 400, damping: 26 }` | `ease-spring-bouncy`, 600ms | Shape morphs and playful reveals only. Never on an exit. | ### Use a spring in CSS ```css .thumb { transition: translate var(--eased-duration-spring-snappy) var(--eased-spring-snappy); } ``` In CSS, the springs are `linear()` curves, so they work in a plain transition. Pair each spring with its duration variable. ### Use a spring in Motion ```tsx import { motion } from "motion/react"; import { snappy } from "@/components/eased/lib/motion"; ``` ## Curves and durations | Name | Value | Use it for | |---|---|---| | `ease-enter` | `cubic-bezier(0.16, 1, 0.3, 1)` | Things that appear | | `ease-standard` | `cubic-bezier(0.22, 1, 0.36, 1)` | Colour and opacity while something stays on screen | | `ease-exit` | `cubic-bezier(0.7, 0, 0.84, 0)` | Things that leave | | `--eased-ease-in-out` | `cubic-bezier(0.65, 0, 0.35, 1)` | Movement that starts and ends on screen | | `--eased-duration-instant` | 100ms | Press-down and the reduced-motion fade | | `--eased-duration-fast` | 150ms | Hover and colour changes | | `--eased-duration-standard` | 200ms | Popovers and tooltips | | `--eased-duration-slow` | 300ms | Dialogs | | `--eased-duration-sheet` | 500ms | Sheets and drawers | Interaction feedback never takes longer than 200ms. The enter curve, `cubic-bezier(0.16, 1, 0.3, 1)`, is the curve in the Eased logo. ## Enter and exit ```tsx import { enter, exit } from "@/components/eased/lib/motion"; ``` Use the `enter` and `exit` pairs from `motion.ts`. An element that appears is something to look at; an element that leaves is in the way. So exits: - take at most three quarters of the enter time - use an ease-in curve - never overshoot Anything that opens from a trigger grows from it. Set its `transform-origin` to the trigger, and enter from 6px toward it at 97% scale. Nothing scales up from zero. ## Swap a label in place ```tsx import { AnimatePresence, motion } from "motion/react"; import { swap, swapIn, swapOut, swapShown } from "@/components/eased/lib/motion"; {label} ``` Use the swap when a label or icon changes in place, such as Save to Saved, or a copy icon to a check. The old text moves up 3px and the new text rises from 4px below, each with a soft blur, so the eye reads one change instead of a flicker. The blur is tweened separately from the spring, so it clears before the movement settles. Text Swap and Button use this pattern. ## Press | Class in `press.ts` | Scale | Use it for | |---|---|---| | `pressIcon` | 0.96 | Icon buttons up to 48px | | `press` | 0.97 | Text buttons | | `pressWide` | 0.985 | Bars and cards wider than 220px | Buttons press down in 100ms and spring back on `snappy`. The scale depends on width, so a wide bar doesn't dip further than a small icon. Triggers that open a popover or menu never scale; they press by colour instead. ## Stagger | `stagger` key | Step | |---|---| | `char` | 16ms per character | | `item` | 35ms per item | | `word` | 40ms per word | | `line` | 80ms per line | Lists that enter together step through `stagger` in `motion.ts`. Keep the whole stagger under 400ms. It runs once when the list mounts, never on a re-render. ## Reduced motion ```tsx import { reducedFade, snappy, useSafeReducedMotion } from "@/components/eased/lib/motion"; const reduced = useSafeReducedMotion(); ``` Every animated component has a reduced branch. When the visitor's system asks for reduced motion, movement becomes a 100ms opacity fade (`reducedFade`) or nothing, and looping animations stop. Read the setting with `useSafeReducedMotion()` from `motion.ts`, not Motion's `useReducedMotion`. It returns `false` on the server and during hydration, then the real value, so reduced-motion visitors don't get a hydration mismatch. In CSS, the `reduced:` variant applies under `prefers-reduced-motion: reduce`, for example `reduced:transition-none`. ## What never animates - Animate only `transform`, `opacity`, `clip-path`, `filter` and colours. Never animate `width`, `height`, `top`, `left`, `margin`, `padding` or `box-shadow` frame by frame, and never use `transition: all`. - Don't animate keyboard-driven, high-frequency actions: opening a command menu, adding or removing list items, opening a context menu or typing. - Hover changes only colour or shadow, by one step. It never changes size, weight or position. - Looping motion pauses off-screen and under reduced motion. - Set `will-change` only during a gesture, and remove it after. --- # AI and MCP Your coding agent can search Eased, read a component's docs and source, and install it. Connect the Eased MCP server, or point your agent at `llms.txt` or any page's Markdown. To get started fast, see [Quickstart for agents](/docs/agent-quickstart). ## Add the MCP server The server is at `https://eased.dev/api/mcp`. It uses Streamable HTTP, keeps no session state and needs no account for free components. ### Claude Code ```bash claude mcp add --transport http eased https://eased.dev/api/mcp ``` ### Cursor ```json .cursor/mcp.json { "mcpServers": { "eased": { "url": "https://eased.dev/api/mcp" } } } ``` Add this to `.cursor/mcp.json` in your project, or to `~/.cursor/mcp.json` for every project. ### VS Code ```json .vscode/mcp.json { "servers": { "eased": { "type": "http", "url": "https://eased.dev/api/mcp" } } } ``` ### Codex ```bash codex mcp add eased --url https://eased.dev/api/mcp ``` ### Other clients Windsurf, Zed and other MCP clients use the same URL. Choose the client's HTTP or Streamable HTTP transport and enter `https://eased.dev/api/mcp`. ## Tools The server has five read-only tools: | Tool | Input | Returns | |---|---|---| | `get_skill` | None | The Eased design skill as Markdown: install, tokens, composition patterns and the design rules. Agents call it first. | | `list_components` | `tier`: `"free"` or `"pro"`, optional | Every component in catalogue order: slug, title, one-line summary, tier, interaction verb and page URL | | `search_components` | `query`: a few words, such as `"loading state"` or `"drag to adjust a number"` | Up to 10 matches, best first, ranked on title, slug, verb, description and README text | | `get_component` | `slug`, such as `"button"` | The component as Markdown: description, tier, docs, install command and dependencies. Free components include the full source of their files and a demo. | | `get_install_command` | `slugs`: one or more slugs | One `npx shadcn add …` command for the free components. Pro and unknown slugs are listed separately. | A typical session: ```text You: Deleting a row should be undoable, like Linear. Agent: search_components("undo delete toast") → undo-pill · Press · free get_component("undo-pill") → docs, props, source npx shadcn add https://eased.dev/r/undo-pill.json Wired UndoPill into FilesTable. ⌘Z restores the row in place. ``` ## The Eased skill The Eased skill packages the design thinking behind the library for your agent: the alignment and grouping rules, the 1:2 spacing ratio, concentric corner radii, the three springs and when to use each, enter and exit timing, focus rings and reduced motion. It also covers how to install Eased components, which tokens to use and how to compose them. It's free and MIT, like the free components. Install it with the shadcn CLI: ```bash npx shadcn add https://eased.dev/r/eased-skill.json ``` The CLI writes `.claude/skills/eased/SKILL.md` at your project root. Claude Code finds it there and loads it when a task touches UI. Commit the file so your whole team's agents use it. Over MCP, `get_skill` returns the same file, so an agent connected to the server can load it without installing anything. ### Cursor Cursor reads project rules from `.cursor/rules/`. Copy the skill there as a rule that applies to UI files: ```bash mkdir -p .cursor/rules cp .claude/skills/eased/SKILL.md .cursor/rules/eased.mdc ``` Then edit the rule's front matter so it attaches to your UI files, for example `globs: ["**/*.tsx"]`, or set `alwaysApply: true`. ### AGENTS.md Codex, Copilot and other agents read `AGENTS.md`. Point it at the skill: ```md AGENTS.md ## UI Before building or changing UI, read `.claude/skills/eased/SKILL.md` and follow its rules. Install Eased components with `npx shadcn add https://eased.dev/r/.json`. ``` ## Pro source for your agent ```bash claude mcp add --transport http eased https://eased.dev/api/mcp \ --header "Authorization: Bearer $EASED_PRO_TOKEN" ``` Without a token, `get_component` returns docs and metadata for Pro components, but not their source. With your Pro token in the `Authorization` header, it returns Pro source too. 1. Create a token at [eased.dev/account](/account). For details, see [Pro components](/docs/installation#pro-components). 2. Export it in your shell as `EASED_PRO_TOKEN`. 3. Add the server with the header, as shown for Claude Code. For clients configured with JSON, add a `headers` entry: ```json { "mcpServers": { "eased": { "url": "https://eased.dev/api/mcp", "headers": { "Authorization": "Bearer eased_pro_…" } } } } ``` For Codex, add the server to `~/.codex/config.toml` and name the token's environment variable: ```toml [mcp_servers.eased] url = "https://eased.dev/api/mcp" bearer_token_env_var = "EASED_PRO_TOKEN" ``` A missing, revoked or invalid token isn't an error. The server behaves as it does without one. Pro source through MCP opens with early access, the same as the Pro registry. ## llms.txt For agents that read the web instead of calling tools: - [`/llms.txt`](https://eased.dev/llms.txt) is the index: what Eased is, the agent setup prompt, how to install, every component with its install command, registry JSON and Markdown link, and these docs. - [`/llms-full.txt`](https://eased.dev/llms-full.txt) is these docs and every free component's page (docs, install and demo) in one file. Both are generated from the registries, so they list a component as soon as it ships. ## Every page as Markdown Add `.md` to any component or docs page URL: - `https://eased.dev/components/button.md` - `https://eased.dev/docs/installation.md` On docs pages, **Copy page** copies the Markdown so you can paste it into a chat. **View as Markdown** opens it. Both are in the **On this page** column. ## Rules for agents - Load the skill with `get_skill`, or install it, before you build UI, and follow its rules. - Read a component with `get_component` before you use it. Its README lists props, keyboard behaviour and motion, and says when to pick it over similar components. - Install with the shadcn CLI, not by copying source, so the shared stylesheet and helpers (`eased-tokens`, `eased-motion`) come with it. - Import components from `@/components/eased//`. Their styles come from the merged stylesheet; there's no CSS file to import. - Don't wrap an Eased component in another animation. Its motion is already tuned, and a second transition fights it. --- # Changelog Changes to Eased, newest first. Each component links to its page, where the demo is the component itself. ## New in Pro New Pro components ship every week. This table lists each release by date, newest first, with the components it adds and whether they are free or Pro. | Date | What shipped | Components | Tier | | --- | --- | --- | --- | | 18 October 2026 | Five components move to Pro | [Hover Card](/components/hover-card), [Search Field](/components/search-field), [Sortable Table](/components/sortable-table), [Bar Chart](/components/bar-chart), [Stat Tile](/components/stat-tile) | Pro | | 18 October 2026 | Line Chart | [Line Chart](/components/line-chart) | Pro | | 18 October 2026 | Thinking states | [Shimmer Text](/components/shimmer-text), [Thinking Bar](/components/thinking-bar), [Agent Orb](/components/agent-orb) | Free | | 18 October 2026 | Composer | [Composer](/components/composer), [Message](/components/message), [Attachment Chip](/components/attachment) | Free | | 18 October 2026 | Tables | [Table](/components/table), [Cells kit](/components/cells) | Free | | 18 October 2026 | Code blocks | [Code Block](/components/code-block), [Code Tabs](/components/code-tabs), [Snippet](/components/snippet) | Free | ## 18 October 2026 **Added** - [Quickstart for agents](/docs/agent-quickstart): one prompt that sets up Eased in Claude Code, Cursor, VS Code or Codex. **Give Eased to your agent** on the homepage copies it. - The library: a sidebar with every component grouped by purpose, search with ⌘K, a light theme alongside dark, and these docs ([Introduction](/docs/introduction), [Installation](/docs/installation), [Theming](/docs/theming), [Motion](/docs/motion), [AI and MCP](/docs/ai-mcp)). Every docs page has a Markdown version at `.md`. - [Squircle](/components/squircle), in Pro. The rest of this release is listed in [New in Pro](#new-in-pro). - Pro accounts: sign in, create and revoke Pro tokens, and manage team seats at [/account](/account). - The private Pro registry, `@eased-pro`, for the shadcn CLI, authorised with `EASED_PRO_TOKEN`. - The Eased MCP server at `https://eased.dev/api/mcp`. It returns Pro source when you add it with a Pro token header. - The components directory at [/components](/components): a page for every component with docs, props and its motion signature, and a generated `llms.txt`. - 27 free components on shared primitives (floating layers, indicator, auto height, text swap, field, number roll, gesture and scroll fade), each with a demo and a README. - Base UI as the behaviour layer under every interactive component. **Changed** - Spinner, Kbd, Avatar and Segmented each share one implementation across every component that uses them, with consistent control heights. - Every component reads reduced motion after hydration, so reduced-motion visitors never get a mismatched first render. **Fixed** - [Undo Pill](/components/undo-pill) closes when its timer ends, and announces itself to screen readers. - Small controls have 44 by 44px hit areas. - The install line wraps instead of overflowing on narrow screens. ## 17 October 2026 **Added** - [Undo Pill](/components/undo-pill), the first component, and the shadcn registry at `/r`. - [Spotlight Tour](/components/spotlight-tour), [Scrub Label](/components/scrub-label), [Group-by List](/components/group-by-list) and [Gliding Focus Ring](/components/gliding-focus-ring) in Pro, and [Concentric Frame](/components/concentric-frame), free. **Changed** - You can install the stylesheet into any shadcn/ui app: every variable starts with `--eased-`, and the utility names (`bg-brand`, `border-line`, `text-ui`) no longer clash with shadcn's. - Shared motion, press and demo-frame helpers, so every component presses and enters the same way. --- # Undo Pill > A pill that rises after a destructive action with a draining countdown ring. Undo (or ⌘Z) reverts it; only expiry commits it. Hover or focus pauses the countdown. - Tier: Free (MIT) - Interaction: Press - Page: https://eased.dev/components/undo-pill - Installs: motion - Registry: https://eased.dev/r/undo-pill.json ## Install ```bash npx shadcn add https://eased.dev/r/undo-pill.json ``` The never-act-immediately pattern: a destructive action shows a floating pill with a draining ring; Undo reverts it, and only expiry commits it. ## When to use - Deleting, archiving or moving things people may regret: files, messages, rows. The action looks instant and stays reversible for five seconds. - Repeated actions in a row: with `useUndoQueue`, everything done while the pill is open joins one batch ("Deleted 3 files") and one Undo restores it all. - Lists and tables where a confirm dialog on every delete would slow people down. ## When not to use - Actions that cannot be reversed once they start (sending an email to a list, charging a card): confirm first, with a dialog. - Confirming a success that needs no action ("Saved"): announce it with a status message instead. - Errors: show them inline beside what failed. ## Usage Remove the item from view at once, and commit the real delete only in `onCommit`. ```tsx files.tsx import { useState } from "react"; import { UndoPill, useUndoQueue } from "@/components/eased/undo-pill/undo-pill"; type File = { id: string; name: string }; export function Files({ initial, remove }: { initial: File[]; remove: (ids: string[]) => Promise }) { const [files, setFiles] = useState(initial); const queue = useUndoQueue({ message: (items) => (items.length === 1 ? `Deleted “${items[0].name}”` : `Deleted ${items.length} files`), onUndo: (items) => setFiles((current) => [...current, ...items]), onCommit: (items) => remove(items.map((f) => f.id)), }); const del = (file: File) => { setFiles((current) => current.filter((f) => f.id !== file.id)); queue.push(file); }; return (
{/* … the list, with a delete button calling del(file) … */}
); } ``` The pill positions itself in its nearest positioned parent, so give that parent `relative`. ## Examples ### At the top For layouts with a bottom bar, place it at the top; it then enters from above. ```tsx ``` ### Without the hook Drive it yourself: keep it mounted and toggle `open`, so the exit animates. ```tsx restore(pending)} onExpire={() => commitArchive(pending)} duration={8000} /> ``` ## API reference ### UndoPill | Prop | Type | Default | Description | |---|---|---|---| | `open` | `boolean` | | Show the pill. Keep it mounted and toggle this so the exit animates. | | `message` | `string` | | What happened, e.g. `Deleted "Q3 roadmap.pdf"`. Changing it restarts the countdown. | | `onUndo` | `() => void` | | Put things back. | | `onExpire` | `() => void` | | The countdown ran out: commit the destructive action here. | | `duration` | `number` | `5000` | Countdown length in ms. Changing it while open restarts the countdown. | | `shortcut` | `boolean` | `true` | Bind ⌘Z / Ctrl+Z to Undo while the pill is open. Ignored while typing in a field. | | `pauseOnHover` | `boolean` | `true` | Pause the countdown while the pointer is over the pill. | | `pauseOnFocus` | `boolean` | `true` | Pause the countdown while focus is inside the pill. | | `onPause` / `onResume` | `() => void` | | Called when hover or focus pauses the countdown, and when it resumes. | | `placement` | `"bottom" \| "top" \| "none"` | `"bottom"` | Centred at the bottom or top of the positioned parent, or `"none"` to place it yourself. | | `labels` | `{ undo?: string; shortcut?: (mac: boolean) => string }` | `"Undo"`, `"⌘ Z"` / `"Ctrl Z"` | Translate the button and the shortcut hint. | | `className` | `string` | | Appended to the pill's classes; placement is kept. | ### useUndoQueue `useUndoQueue({ message, onUndo?, onCommit? })` returns `{ pending, push, undo, commit, pill }`. `push(item)` adds an item to the open batch; spread `pill` onto ``. ## Keyboard | Key | Action | |---|---| | `⌘ Z` / `Ctrl Z` | Undo while the pill is open, unless focus is in a text field. | | `Tab` | Reaches the Undo button; focus inside the pill pauses the countdown. | | `Enter` / `Space` | Undo, on the focused button. | ## Accessibility - A `role="status"` live region, mounted before the pill opens, announces the message, so screen readers hear "Deleted Q3 roadmap.pdf" without focus moving. - Hover and focus pause the countdown, so nobody loses the window while reaching for the button. - The countdown is driven by a timer, not by the animation, so reduced motion or a global animation clamp never shortens it. ## Motion The pill rises 12px from below (or drops from above with `placement="top"`) with a blur-to-sharp fade, and its size springs on the smooth spring as the message changes. A new message swaps with y + blur. The ring drains linearly over `duration`. It leaves faster than it came, travelling 6px with a slight scale-down and no overshoot. Reduced motion keeps opacity fades only; the ring still drains. ## Notes for AI Use Undo Pill for any reversible destructive action in a list or table: remove the item from state immediately, `push` it to `useUndoQueue`, and do the real delete in `onCommit`. Render one pill per surface, inside a `relative` parent, and keep it mounted. Do not call the delete API before expiry, do not add a confirm dialog on top of it, and do not unmount it on close: toggling `open` is what runs the exit and keeps the live region working. ## Related - [Button](/components/button): the delete action that opens the pill. - [Kbd](/components/kbd): the keycaps in the shortcut hint. - [Sortable Table](/components/sortable-table): a table to pair it with. ## Example `components/eased/undo-pill/undo-pill-demo.tsx` ```tsx "use client"; import { AnimatePresence, motion } from "motion/react"; import { useState } from "react"; import { DemoFrame } from "../lib/demo-frame"; import { enter, exit, reducedFade, smooth, useSafeReducedMotion } from "../lib/motion"; import { press, pressIcon } from "../lib/press"; import { UndoPill, useUndoQueue } from "./undo-pill"; type File = { id: string; name: string; size: string; edited: string; kind: "pdf" | "fig" | "doc" | "sheet" }; const FILES: File[] = [ { id: "1", name: "Q3 roadmap.pdf", size: "2.4\u00a0MB", edited: "2h ago", kind: "pdf" }, { id: "2", name: "Onboarding flow.fig", size: "18.1\u00a0MB", edited: "Yesterday", kind: "fig" }, { id: "3", name: "Launch checklist.md", size: "4\u00a0KB", edited: "Mon", kind: "doc" }, { id: "4", name: "Pricing experiments.csv", size: "312\u00a0KB", edited: "Oct 9", kind: "sheet" }, { id: "5", name: "Brand guidelines.pdf", size: "7.8\u00a0MB", edited: "Sep 28", kind: "pdf" }, { id: "6", name: "Investor update.md", size: "9\u00a0KB", edited: "Sep 21", kind: "doc" }, ]; const order = (ids: string[]) => FILES.filter((f) => ids.includes(f.id)); export function UndoPillDemo() { const reduce = useSafeReducedMotion(); const [visible, setVisible] = useState(FILES.map((f) => f.id)); const queue = useUndoQueue({ message: (ids) => ids.length === 1 ? `Deleted “${FILES.find((f) => f.id === ids[0])?.name}”` : `Deleted ${ids.length} files`, onUndo: (ids) => setVisible((v) => [...v, ...ids]), }); const remove = (id: string) => { setVisible((v) => v.filter((x) => x !== id)); queue.push(id); }; const reset = () => setVisible(FILES.map((f) => f.id)); return (
    {order(visible).map((file) => (

    {file.name}

    {file.size} · {file.edited}

    ))}
{visible.length === 0 && queue.pending.length === 0 && (

All files deleted.

)}
); } function FileIcon({ kind }: { kind: File["kind"] }) { const label = { pdf: "PDF", fig: "FIG", doc: "MD", sheet: "CSV" }[kind]; return ( {label} ); } ``` --- # Concentric Frame > A layout primitive that exposes outer, padding and inner radius as CSS variables so nested corners stay concentric. - Tier: Free (MIT) - Interaction: Drag - Page: https://eased.dev/components/concentric-frame - Registry: https://eased.dev/r/concentric-frame.json ## Install ```bash npx shadcn add https://eased.dev/r/concentric-frame.json ``` Written docs for Concentric Frame are on the way; the install command adds its full source to your project. ## Example `components/eased/concentric-frame/concentric-frame-demo.tsx` ```tsx "use client"; import { AnimatePresence, MotionConfig, motion } from "motion/react"; import { useEffect, useId, useRef, useState } from "react"; import { DemoFrame } from "../lib/demo-frame"; import { enter, fade, snappy, swap, swapIn, swapOut, swapShown } from "../lib/motion"; import { press } from "../lib/press"; import { Frame, FrameInner } from "./concentric-frame"; // Fixed geometry of the player, mirrored by the guides so they draw the rendered curves. const BORDER = 1; const GROUP_PAD = 4; const BUTTON_H = 36; const GROUP_H = BUTTON_H + GROUP_PAD * 2; const MIN = 2; // the Frame's default inner-radius floor // What computes: outer − padding − border, floored at MIN, never above the outer. const nested = (outer: number, pad: number, border: number) => Math.min(outer, Math.max(MIN, outer - pad - border)); export function ConcentricFrameDemo() { const [radius, setRadius] = useState(24); const [pad, setPad] = useState(10); const [naive, setNaive] = useState(false); const [playing, setPlaying] = useState(true); // Guides show while a thumb is held, while the pointer is over the card, and briefly after keys. const [dragging, setDragging] = useState(false); const [hovering, setHovering] = useState(false); const [pulsing, setPulsing] = useState(false); const pulseTimer = useRef(0); const pulse = (ms = 900) => { setPulsing(true); window.clearTimeout(pulseTimer.current); pulseTimer.current = window.setTimeout(() => setPulsing(false), ms); }; useEffect(() => () => window.clearTimeout(pulseTimer.current), []); useEffect(() => { if (!dragging) return; const up = () => setDragging(false); window.addEventListener("pointerup", up); window.addEventListener("pointercancel", up); return () => { window.removeEventListener("pointerup", up); window.removeEventListener("pointercancel", up); }; }, [dragging]); const raw = radius - pad - BORDER; const inner = naive ? radius : nested(radius, pad, BORDER); const formula = naive ? "= outer radius" : raw < MIN ? `= max(${MIN}, ${radius} − ${pad} − 1)` : `= ${radius} − ${pad} − 1`; return ( Inner radius px {/* The formula is a bonus; narrow tiles drop it rather than truncate it. */} {formula}

} actions={ { setNaive(v); pulse(1400); }} /> } >
e.pointerType === "mouse" && setHovering(true)} onPointerLeave={() => setHovering(false)} className="relative flex w-full max-w-72 flex-col gap-3 border-line-subtle bg-surface" >

Slow Light

Halden Rowe

2:14 / 3:48

setPlaying((p) => !p)}> {playing ? : }
setDragging(true)} onKey={() => pulse()} /> setDragging(true)} onKey={() => pulse()} />
); } /* ---------- Guides: each corner's arc and centre, so concentricity is visible ---------- */ type Arc = { c: number; r: number }; function Guides({ radius, pad, naive, visible }: { radius: number; pad: number; naive: boolean; visible: boolean }) { // Rendered radii: the browser shrinks any radius past half the box, so the guides do too. const inner = naive ? radius : nested(radius, pad, BORDER); const group = Math.min(inner, GROUP_H / 2); const button = Math.min(naive ? radius : nested(inner, GROUP_PAD, 0), BUTTON_H / 2); // Centres measured from the padding box, which is what this overlay fills. const outer: Arc = { c: radius - BORDER, r: radius }; const top = [outer, { c: pad + inner, r: inner }]; const bottom = [outer, { c: pad + group, r: group }, { c: pad + GROUP_PAD + button, r: button }]; return ( ); } function Corner({ arcs, className }: { arcs: Arc[]; className: string }) { return ( {arcs .filter((a) => a.r > 0.5) .map(({ c, r }, i) => { const quarter = `M ${c - r} ${c} A ${r} ${r} 0 0 1 ${c} ${c - r}`; return ( ); })} ); } /* ---------- Controls ---------- */ function Slider({ label, min, max, value, onChange, onGrab, onKey, }: { label: string; min: number; max: number; value: number; onChange: (v: number) => void; onGrab: () => void; onKey: () => void; }) { const id = useId(); const pct = (value - min) / (max - min); return (
{value} px
{/* The native range owns input, keys and a11y; its 16px thumb matches the drawn one. */} onChange(Number(e.target.value))} onPointerDown={onGrab} onKeyDown={onKey} className="absolute inset-x-0 top-1/2 z-10 h-11 w-full -translate-y-1/2 cursor-pointer appearance-none bg-transparent opacity-0 [&::-moz-range-thumb]:size-4 [&::-moz-range-thumb]:border-0 [&::-webkit-slider-thumb]:size-4 [&::-webkit-slider-thumb]:appearance-none" />
{/* A fixed cap plus a scaled run, so the fill ends under the thumb's centre. */}
); } function NaiveSwitch({ checked, onChange }: { checked: boolean; onChange: (v: boolean) => void }) { return ( ); } /* ---------- Rolling digits ---------- */ function RollingNumber({ value }: { value: number }) { const tens = Math.floor(value / 10) % 10; return ( <> {value} ); } function Digit({ digit, hidden = false }: { digit: number; hidden?: boolean }) { return ( ); } /* ---------- Player parts ---------- */ function PlayerButton({ label, primary = false, onClick, children, }: { label: string; primary?: boolean; onClick?: () => void; children: React.ReactNode; }) { return ( {children} ); } function Cover() { const id = useId().replace(/[^\w-]/g, ""); // safe inside url(#…) return ( ); } function SkipIcon({ className = "" }: { className?: string }) { return ( ); } function PlayIcon() { // Drawn ~1.5px right of centre so the triangle reads optically centred. return ( ); } function PauseIcon() { return ( ); } ``` --- # Accordion > Disclosures whose height springs open with no overshoot, content fading in as the height lands and the chevron turning on the same spring. Base UI Accordion behaviour. - Tier: Free (MIT) - Interaction: Press - Page: https://eased.dev/components/accordion - Installs: @base-ui/react - Registry: https://eased.dev/r/accordion.json ## Install ```bash npx shadcn add https://eased.dev/r/accordion.json ``` Disclosures on Base UI's Accordion (buttons in headings, `aria-expanded`/`aria-controls`, arrow keys, Home/End), animated by Auto Height. ```tsx import { Accordion, AccordionItem } from "@/components/eased/accordion/accordion"; On the day your trial ends, then on the same date every month. Within 14 days, in full. ``` | Prop | Type | Default | | |---|---|---|---| | `value` / `defaultValue` | `string[]` | | Open items, controlled or uncontrolled. | | `onValueChange` | `(value: string[]) => void` | | | | `multiple` | `boolean` | `false` | Allow several open at once. | | `disabled` | `boolean` | | | | `AccordionItem` | `value`, `title`, `children`, `headingLevel?` (default 3), `disabled?` | | | **Motion signature:** the height springs open with no overshoot, the content fades in as the height lands, and the chevron turns on the same spring; closing fades the content first and is quicker than opening. ## Example `components/eased/accordion/accordion-demo.tsx` ```tsx "use client"; import { DemoFrame } from "../lib/demo-frame"; import { Accordion, AccordionItem } from "./accordion"; export function AccordionDemo() { return (
On the day your trial ends, then on the same date every month. If that date doesn’t exist in a month, such as the 31st, we charge on the last day instead. Yes. Upgrades apply at once and you pay the difference for the days left in the cycle. Downgrades take effect at your next renewal, so you keep what you paid for. Within 14 days of any charge, in full, no questions asked.

Go to Settings → Billing → Tax details and enter it with its country prefix, for example DE123456789. We check it against VIES before saving.

New invoices show it from then on. To reissue a past invoice with the number on it, open the invoice and choose Regenerate.

); } ``` --- # Attachment Chip > A chat attachment: images as a thumbnail that sharpens from a blur once decoded, files as a typed chip. Upload progress is a ring that springs between reports; a failure shakes once and offers Retry; removing one lets its neighbours slide into the gap. - Tier: Free (MIT) - Interaction: Loading - Page: https://eased.dev/components/attachment - Installs: motion - Registry: https://eased.dev/r/attachment.json ## Install ```bash npx shadcn add https://eased.dev/r/attachment.json ``` An image thumbnail or a typed file chip, with upload progress, errors and remove. **Motion signature:** the thumbnail sharpens from a 4px blur once it decodes, progress is a ring that springs between reports (spinning when the size is unknown), a failure shakes once, and a removed chip shrinks away while its neighbours slide into the gap. ```tsx const { attachments, add, remove, retry } = useAttachments({ upload }); ``` ## Props - `AttachmentChip`: `attachment`, `onRemove?`, `onRetry?`, `variant` (`auto` | `file`). - `AttachmentList`: `attachments`, `onRemove?`, `onRetry?`, `label` ("Attachments"), `children`. - `ProgressRing`: `value?` (0–1), `name`. The remove button shows on hover and focus with a mouse, and always on touch. Each chip is a labelled group that reads out its name and upload state. ## Example `components/eased/attachment/attachment-demo.tsx` ```tsx "use client"; import { useEffect, useRef } from "react"; import { useAttachments, type Attachment, type UploadFn } from "../_primitives/chat-core/chat-core"; import { Button } from "../button/button"; import { DemoFrame } from "../lib/demo-frame"; import { AttachmentList } from "./attachment-chip"; /** A small landscape as an SVG data URL, so the demo ships no image files. */ export function scene(sky: string, sun: string, far: string, near: string, sunX = 64) { const svg = ``; return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`; } /** * A pretend upload: progress in uneven steps over ~1.5–2.5s (like a real network), failing * when the name contains "fail". Respects the abort signal. */ export const fakeUpload: UploadFn = (file, { onProgress, signal }) => new Promise((resolve, reject) => { let p = 0; const id = window.setInterval(() => { p = Math.min(1, p + 0.06 + Math.random() * 0.14); onProgress(p); if (/fail/i.test(file.name) && p > 0.55) { clearInterval(id); reject(new Error("Connection lost")); } else if (p >= 1) { clearInterval(id); resolve({ url: URL.createObjectURL(file) }); } }, 220); signal.addEventListener("abort", () => clearInterval(id)); }); const SEED: Attachment[] = [ { id: "a1", name: "harbour-dawn.png", kind: "image", mediaType: "image/png", previewUrl: scene("#f6d6b8", "#f08a5d", "#7a8fb0", "#3e4f6e", 70), status: "done" }, { id: "a2", name: "Q3 board deck.pdf", kind: "file", mediaType: "application/pdf", size: 4_812_000, status: "done" }, { id: "a3", name: "dunes.jpg", kind: "image", mediaType: "image/jpeg", previewUrl: scene("#fbe7c6", "#e9a15a", "#d8a46c", "#a7683a", 40), status: "uploading", progress: 0.42 }, { id: "a4", name: "churn-by-cohort.csv", kind: "file", mediaType: "text/csv", size: 218_400, status: "error", error: "Connection lost" }, ]; /** Uploads in flight, a finished deck, a failed CSV with Retry; add your own files to watch the ring. */ export function AttachmentChipDemo() { const input = useRef(null); const { attachments, add, remove, retry, update } = useAttachments({ upload: fakeUpload, defaultValue: SEED, maxSize: 20_000_000 }); const timers = useRef([]); // Seeded chips have no File, so drive their "upload" here. const drive = (id: string, from: number) => { update(id, { status: "uploading", progress: from, error: undefined }); let p = from; const t = window.setInterval(() => { p = Math.min(1, p + 0.08 + Math.random() * 0.1); update(id, p >= 1 ? { status: "done", progress: 1 } : { progress: p }); if (p >= 1) clearInterval(t); }, 260); timers.current.push(t); }; useEffect(() => { const start = window.setTimeout(() => drive("a3", 0.42), 900); const list = timers.current; return () => { clearTimeout(start); list.forEach(clearInterval); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); return (

To

Finance team

(attachments.find((a) => a.id === id)?.file ? retry(id) : drive(id, 0.05))} />

Up to 20 MB per file

{ add(Array.from(e.target.files ?? [])); e.target.value = ""; }} />
); } ``` --- # Avatar > Avatar and AvatarGroup on Base UI Avatar: images fade in from a 4px blur once loaded so initials never flash; overlapping stacks with a cut-out ring spread apart on hover; the overflow count rolls. - Tier: Free (MIT) - Interaction: Hover - Page: https://eased.dev/components/avatar - Installs: @base-ui/react - Registry: https://eased.dev/r/avatar.json ## Install ```bash npx shadcn add https://eased.dev/r/avatar.json ``` Avatar and AvatarGroup on Base UI Avatar. ```tsx import { Avatar, AvatarGroup } from "@/components/eased/avatar/avatar"; {people.map((p) => )} ``` ## Props `Avatar`: `name` (accessible name and initials), `src`, `initials` (override), `size` (`xs` 20 · `sm` 24 · `md` 32 · `lg` 40 · `xl` 48, default `md`), `shape` (`circle` or `square`; 48px squares get a superellipse), `className`. `AvatarGroup`: `children` (Avatars), `max` (the rest collapse into a rolling `+N`), `size`, `shape`, `aria-label`, `cutout` (the separating ring's colour; match the surface behind, default `var(--eased-surface)`), `className`. **Motion signature:** an image fades in from a 4px blur once it has loaded, and the initials wait 400ms (`duration.fallback`) before showing, so a quick image never flashes the fallback (a failed or missing image shows initials at once); a stack spreads apart on the snappy spring under a fine pointer or keyboard focus, and its overflow count rolls. ## Example `components/eased/avatar/avatar-demo.tsx` ```tsx "use client"; import { AnimatePresence, motion } from "motion/react"; import { useEffect, useRef, useState } from "react"; import { Badge } from "../badge/badge"; import { Button } from "../button/button"; import { DemoFrame } from "../lib/demo-frame"; import { useScrollFades } from "../lib/dom"; import { enter, reducedFade, smooth, useSafeReducedMotion } from "../lib/motion"; import { Avatar, AvatarGroup } from "./avatar"; /** Generated portraits (soft gradient blobs), inlined so the demo needs no image host. */ function portrait(a: string, b: string, c: string, x: number, y: number) { const svg = ``; return `data:image/svg+xml,${encodeURIComponent(svg)}`; } type Member = { id: string; name: string; email: string; role: "Owner" | "Admin" | "Member"; src?: string }; const TEAM: Member[] = [ { id: "1", name: "Maya Okafor", email: "maya@northwind.studio", role: "Owner", src: portrait("#f4c9a8", "#e0795b", "#f7e3b5", 18, 20) }, { id: "2", name: "Jonas Weber", email: "jonas@northwind.studio", role: "Admin", src: portrait("#b9d3e8", "#4e7fb3", "#d7ecd9", 44, 16) }, { id: "3", name: "Priya Raman", email: "priya@northwind.studio", role: "Member" }, { id: "4", name: "Leo Martín", email: "leo@northwind.studio", role: "Member", src: portrait("#d9cff1", "#8a6fd1", "#f2c6dc", 24, 40) }, // A broken image: the initials take over at once instead of waiting out the delay. { id: "5", name: "Hana Sato", email: "hana@northwind.studio", role: "Member", src: "data:image/png;base64,broken" }, ]; const INVITES: Member[] = [ { id: "6", name: "Amara Diallo", email: "amara@northwind.studio", role: "Member", src: portrait("#cfe7d2", "#3f9466", "#f1e6a6", 40, 38) }, { id: "7", name: "Tomás Rivera", email: "tomas@northwind.studio", role: "Member" }, { id: "8", name: "Elif Kaya", email: "elif@northwind.studio", role: "Member", src: portrait("#f6d0d6", "#d0566e", "#fbe2c4", 20, 44) }, { id: "9", name: "Noah Fischer", email: "noah@northwind.studio", role: "Member", src: portrait("#d2dcf0", "#5368b5", "#c9eef0", 46, 28) }, ]; const ROLE_TONE = { Owner: "brand", Admin: "info", Member: "neutral" } as const; export function AvatarDemo() { const reduced = useSafeReducedMotion(); const [members, setMembers] = useState(TEAM); const next = INVITES.find((m) => !members.some((x) => x.id === m.id)); const list = useRef(null); useScrollFades(list); // Bring a new member into view once it has been added (skipped on mount). const count = useRef(members.length); useEffect(() => { if (members.length > count.current) { list.current?.scrollTo({ top: list.current.scrollHeight, behavior: reduced ? "auto" : "smooth" }); } count.current = members.length; }, [members.length, reduced]); return ( next && setMembers((m) => [...m, next])}> Invite } >
{members.map((m) => ( ))}

Hover the stack

    {members.map((m) => (

    {m.name}

    {m.email}

    {m.role}
    ))}
); } ``` --- # Cells kit > Table cells that make rows look finished: avatar with name, status badge, number with a rolling value and coloured delta, a progress meter that fills on view, a self-updating relative time, and a mono ID with a copy button. - Tier: Free (MIT) - Interaction: Loading - Page: https://eased.dev/components/cells - Installs: motion - Registry: https://eased.dev/r/cells.json ## Install ```bash npx shadcn add https://eased.dev/r/cells.json ``` Cells that make table rows look finished. They work in any table and size themselves to an Eased table's density. ```tsx import { DeltaCell, IdCell, PersonCell, ProgressCell, RelativeTimeCell, StatusCell } from "@/components/eased/cells/cells"; Active ``` | Cell | Props | | |---|---|---| | `PersonCell` | `name`, `src?`, `detail?` | 24px avatar when dense, 32px when comfortable. The second line shows only in comfortable rows. | | `StatusCell` | `children`, `tone?`, `live?` | A Badge with a dot. `live` makes the dot breathe. | | `DeltaCell` | `value`, `delta` (a fraction), `format?`, `locale?`, `invert?` | The value rolls. The delta sits in a fixed track: a rise is success and a fall is warning (`invert` swaps them). | | `ProgressCell` | `value`, `max?`, `label?`, `locale?` | A `role="meter"` bar with fixed widths for the track and the label. | | `RelativeTimeCell` | `date`, `now?`, `locale?` | `