SavedViews
A saved-view switcher — Attio's "All Companies ▾" menu.
A generic saved-view switcher: a trigger chip (the active view's own icon + name, or your
default label when nothing is active) opens a popover on a "Search views..." box, then
every view as a role="option" row. Each row reveals a kebab on hover with Rename /
Duplicate / Delete (and Add to favorites, when you pass onToggleFavorite),
optionally followed by a "Created by / date" footer, plus a trailing Create new view
row. It has no object/table concept of its own — pass whatever SavedView[] your app's
own views are.
Installation
npm install @crisp-ui-kit/crispUsage
import { SavedViews, type SavedView } from "@crisp-ui-kit/crisp";
const views: SavedView[] = [
{ id: "all", name: "All Companies", icon: <Building2 color="#00D17E" /> },
];
<SavedViews
label="All Companies"
views={views}
activeId={activeId}
onSelect={setActiveId}
onCreate={createView}
onRename={renameView}
onDuplicate={duplicateView}
onDelete={deleteView}
/>;Props
Prop
Type
SavedViewsSaveBar
The "unsaved changes" bar for a currently loaded saved view — Attio's own "Discard changes"
- a split "Save for everyone ▾" button, measured live. Not part of
SavedViews' own trigger/panel: Attio renders this in the page toolbar's trailing slot (TableToolbar'schildren), replacing whatever sits there at rest (Import/Export, "+ New …"). Swap it in once your own dirty check (live filters/sort/columns vs. the loaded view's snapshot) is true, and back out once it's false — this component carries no dirty state of its own.onSaveAsNewis optional; omit it to render a plain button with no chevron/menu.
Prop
Type
Default view: Discard / Save for everyone
SavedViewsSaveBar has no dirty logic of its own: you decide when it shows. isViewDirty(live, saved) is that check. It deep-compares two { filters, sorts, shownKeys, columnOrder, ...anything else you store } objects and returns true when they differ. It ignores ToolbarFilter.id (the toolbar generates ids per mount, so a plain JSON.stringify compare of an untouched view is always dirty) and treats a missing key, undefined, null and [] as the same. Array order still counts, and so does every other filter field, including join and group.
The default view is the one no one has saved yet: keep its saved snapshot in state, let the toolbar edit the live copy, and show the bar while the two differ. onSave persists the live state as the new default (for everyone); onDiscard resets the live state to the snapshot.
SavedView = { id, name, icon?, shared?, keywords?, createdBy?, createdAt? } — icon is
the view's own 16×16 leading icon (coloured, per view); shared renders a small
shared-view indicator next to the name; keywords is matched against the search box when
name isn't a plain string; createdBy/createdAt render as the kebab menu's own
"Created by / date" footer (each independently optional — the footer renders only when at
least one is given).
Behavior
onToggleFavoriteis optional — omit it and each row's kebab menu has no "Add to favorites" item.- Search: the panel always opens on a "Search views..." box (real focus moves there,
not the trigger) — typing filters the view list by
name(orkeywords, for a non-stringname); a query matching nothing shows "No results", and the trailing "Create new view" row always stays regardless of the filter. - Keyboard: with the panel open, focus lives on the search box — ↑/↓ move a virtual highlight over the (possibly filtered) list, Enter chooses it, Escape closes — a still-open kebab menu closes first, the panel closes on the next Escape.
- A row's kebab click never also selects the row (it stops the click from bubbling into the row's own handler).
- The trigger's own chrome is white with a soft 3-layer shadow, not a flat grey fill (#772, #784, re-measured live in the trigger's actual view-bar position):
elevation.field's exact shadow (inset 0 0 0 1px transparent, 0 0 2px rgba(28,40,64,.18), 0 1px 3px rgba(0,0,0,.04)) overbgSurface/white — the same raised chromeTableToolbar's View settings and Import/Export triggers carry. Supersedes the original #555 measurement (a flatbgRaisedgrey), which was never re-checked once the trigger moved into its real position next to View settings. - Attio's view bar is
SavedViewsplusTableToolbar'sviewSettingsRef/importExportRef(#766, #772, measured live): the view switcher and View settings share the LEFT of one row; Import/Export and a primary action share its RIGHT — the two pairs sit far apart, not next to each other — with a separate row below holding only Sort and Filter.SavedViewsdoesn't render those other three controls itself — it stays a standalone switcher, unaware ofTableToolbar— pair them by rendering two separate<span>s (one next toSavedViews' own trigger, one next to your own primary action) and passing each toTableToolbarasviewSettingsRef/importExportRef; seeTableToolbar's own docs for the full composition.