Crisp is LettuceAI's React component library. It provides application layouts, form controls, navigation, overlays, and media components, with shared design tokens and motion presets.
Components are written in TypeScript and styled with Tailwind CSS v4. Themes support light and dark palettes, custom colors, corner radii, and compact density.
bun add @lettuceai/crispYour application must also provide these peer dependencies:
| Package | Required version |
|---|---|
react, react-dom |
19 or later |
tailwindcss |
4 or later |
framer-motion |
12 or later |
lucide-react |
1 or later |
Crisp requires a working Tailwind CSS v4 build in your application. Import its stylesheet after Tailwind and include the package in Tailwind's source scanning:
@import "tailwindcss";
@import "@lettuceai/crisp/styles.css";
@source "../node_modules/@lettuceai/crisp/dist";The @source path is relative to your stylesheet; the example assumes it lives in src/. Crisp supplies tokens and CSS utilities, while your application's Tailwind build generates the component classes.
The stylesheet also sets global typography, page colors, and focus styles. It uses Inter when available, with system font fallbacks; it does not load a font file.
Import components from @lettuceai/crisp. This example opens a sheet containing a labeled input:
import { useState } from "react";
import { Button, Field, Input, Sheet, ThemeProvider } from "@lettuceai/crisp";
export function App() {
const [open, setOpen] = useState(false);
return (
<ThemeProvider>
<main className="p-6">
<Button variant="primary" onClick={() => setOpen(true)}>
Edit profile
</Button>
<Sheet open={open} onClose={() => setOpen(false)} title="Edit profile">
<Field label="Display name">
<Input name="displayName" autoComplete="nickname" />
</Field>
</Sheet>
</main>
</ThemeProvider>
);
}For notifications, mount Toaster once at the application root and call toast.success(), toast.error(), or the other toast helpers. To use await confirm(...), mount ConfirmHost there as well. Both hosts are optional.
| Area | Examples |
|---|---|
| Layout | Box, Stack, Flex, Grid, Container, AppShell |
| Navigation | AppBar, NavRail, TabBar, Dock, Tabs, Breadcrumb |
| Forms | Field, Input, Select, Combobox, Checkbox, Switch, DatePicker |
| Overlays | Sheet, Dialog, Popover, Menu, CommandPalette |
| Feedback | Alert, Banner, Toaster, Progress, Skeleton, EmptyState |
| Data and content | Table, Tree, ConnectedTree, VirtualList, Chart, Prose, CodeBlock |
| Media and chat | MediaCard, ImageViewer, AudioItem, Message, Composer, AvatarPicker |
| Flows and setup | Wizard, ActionBar, Page, ChoiceCard, SecretInput, ModelSelect, ReorderableList |
See the component exports and layout exports for the full list. Component source files include their TypeScript prop definitions.
Prose renders Markdown and leaves raw HTML as text by default. Opt in with <Prose html>{message}</Prose> for mixed Markdown and HTML. Content is parsed and sanitized as a syntax tree before React renders it; no raw HTML is inserted into the DOM.
The policy supports inline emphasis, headings, lists, quotes, code, tables (including captions and cell spans), details/summary, definition lists, figures, ruby, time and bidirectional text. Images use the normal media tile/viewer callback, and links accept HTTP, HTTPS or mailto. Image sources accept HTTP and HTTPS. GFM checkboxes remain disabled.
Inline styles support text colors and emphasis, bounded font sizes, alignment, solid/gradient backgrounds, borders, rounding, bounded spacing/sizing, shadows and in-flow flex/grid layouts. CSS values are checked against the property syntax; network URLs, indirect values and custom properties are rejected. Nested relative font sizes are clamped. Script, style, iframe, form, SVG/MathML and embedded content are removed, as are event handlers, IDs, arbitrary classes, positioning, transforms and animations. The HTML content root clips paint to its bounds. This restriction does not apply to an image viewer explicitly opened by the user.
Supported styling is deliberately narrower than browser CSS. Keep html disabled for consumers that only need Markdown.
ThemeProvider applies theme variables to the document root and stores the selected theme in localStorage under crisp.theme. It restores the saved theme on startup, falling back to Midnight. Pass an initial theme to override that starting selection.
Use useTheme() inside the provider to change colors, radius, or density:
import { Button, useTheme } from "@lettuceai/crisp";
export function AccentPicker() {
const { setColor } = useTheme();
return (
<Button onClick={() => setColor("accent", "#38bdf8")}>
Use blue accent
</Button>
);
}The provider exposes six presets through presets: Midnight, Ink, Vellum, Paper, Slate, and Ember. Select one with setTheme(). Radius options are sharp, default, and round; density options are default and compact.
Text, fill, border, and elevated surface colors are derived from the theme palette. For custom integrations, themeToVars() returns the CSS variables and applyTheme() applies a theme to an HTML element. The theme definitions document the available values.
Every string crisp renders on its own (aria-labels, default prop text, status lines) lives in one typed object, CrispStrings. Wrap the app in CrispStringsProvider and pass only the strings you translate; the rest stay English. Entries that need values are functions.
import { CrispStringsProvider } from "@lettuceai/crisp";
<CrispStringsProvider
locale="de"
strings={{
alertDismiss: "Schließen",
paginationPage: (page) => `Seite ${page}`,
}}
>
<App />
</CrispStringsProvider>locale is a BCP 47 tag. It drives every Intl call, including DatePicker, and the format helpers. Props that already take text (placeholder, backLabel, emptyMessage, …) still win over the provider. Without a provider everything is English and "en". englishStrings lists every key, and useCrispStrings() returns the active { strings, locale } for your own components.
relativeTime(), compactNumber() and fileSize() take an optional trailing locale. English keeps its compact house style ("4 min ago", "1.2k", "1.5 MB"); other locales use Intl.RelativeTimeFormat and Intl.NumberFormat, so they need no extra strings.
cn()merges conditional class names and resolves Tailwind class conflicts.icondefines five icon sizes, fromicon.xs(12px) toicon.xl(20px).motionprovides Framer Motion transition presets such asmotion.settleandmotion.sheetIn.relativeTime(),compactNumber()andfileSize()format the numbers a list shows, one way everywhere.
Hooks and color helpers are also available from the package entry point.
bun install
bun run typecheck
bun run lint
bun run buildThe build writes an ES module, source map, and TypeScript declarations to dist/. Runtime dependencies remain external. The stylesheet is distributed separately as @lettuceai/crisp/styles.css.
MIT © 2026 LettuceAI.
