Advanced Reference Array
Enhanced reference array component for Sanity Studio with search, sort, and bulk operations
Install command
npm i @liiift-studio/sanity-advanced-reference-arraySanity Advanced Reference Array
๐ Enhanced reference array component for Sanity Studio with search, sort, and bulk operations
A powerful, TypeScript-ready component that supercharges Sanity's reference arrays with advanced search capabilities, intelligent sorting, and intuitive bulk operations. Built from real-world usage in production Sanity studios.
Drop it onto any array of reference fields as a custom input component. Editors get a live search box that adds documents with a single click (or all at once), a sort mode that reorders the array by any field on the referenced documents, and a guarded "remove all". Everything is written back through normal Sanity patches, so the stored value stays a plain reference array.
๐ฅ See It In Action
https://github.com/user-attachments/assets/bb53e81b-4475-4510-bd86-452b954e3d2c
Watch the Advanced Reference Array in action - featuring smart search, click-to-add functionality, bulk operations, and dynamic sorting.
โจ Features
๐ Smart Search

- Live GROQ queries with debounced search
- Smart filtering - automatically hides items already in your array
- Individual click-to-add - click any search result to add it instantly
- Bulk "Add All" - add multiple items at once
- Keyboard shortcuts -
Ctrl+Enterto add all,Escapeto clear search
๐ฏ Dynamic Sorting

- Sort by any field in your referenced documents
- Visual sort indicators - see if your list is already sorted
- Toggle sort direction - ascending/descending with one click
- Browser compatible - works across all modern browsers
๐ก๏ธ Safety & UX

- Danger mode - prevents accidental bulk deletions
- Confirmation dialogs for destructive operations
- Loading states and error handling
- Responsive design - works on mobile and desktop
- Accessibility ready - keyboard navigation support
โก Performance
- Debounced search - no unnecessary API calls
- Smart caching - efficient data fetching
- TypeScript support - full type safety
- Collapsible list - long arrays auto-collapse so the form stays navigable
๐ฆ Installation
npm install @liiift-studio/sanity-advanced-reference-arrayPeer dependencies (sanity, react, @sanity/ui, @sanity/icons) are already present in any Sanity Studio โ see Compatibility.
๐ Quick Start
Attach the component as the input for an array of references. No configuration is required โ the defaults are the behaviour shown in the video above.
import { defineType, defineField } from 'sanity'
import { AdvancedRefArray } from '@liiift-studio/sanity-advanced-reference-array'
export const myDocument = defineType({
name: 'myDocument',
type: 'document',
fields: [
defineField({
name: 'relatedItems',
title: 'Related Items',
type: 'array',
of: [
{ type: 'reference', to: [{ type: 'product' }, { type: 'article' }] },
],
components: {
input: AdvancedRefArray,
},
}),
],
})Data shape
The stored value is an ordinary Sanity reference array โ this component changes the editing experience, not the data. Sorting rewrites the array order; nothing else about the shape changes.
"relatedItems": [
{ "_type": "reference", "_key": "a1b2c3d4", "_ref": "product-oxford" },
{ "_type": "reference", "_key": "e5f6g7h8", "_ref": "article-on-serifs" }
]Query it exactly as you would any reference array:
*[_type == "myDocument"]{
relatedItems[]->{ _id, title }
}โ๏ธ Configuration
Read this before copying a config example. There are two different mechanisms, and most options only work through one of them.
Sanity passes a fixed set of props to a custom input component โ you cannot pass arbitrary settings through the field's options object and have them arrive as props. So this component reads its configuration from two places:
flowchart TB
W["Wrapper component<br/>(props you pass)"] -->|"most options"| C["AdvancedRefArray"]
S["Field schema<br/>options: { โฆ }"] -->|"collapsibleList<br/>collapseThreshold only"| C
D["Built-in defaults"] --> C
C --> R["Rendered input"]Resolution order for the two schema-readable options is explicit prop โ schema options โ default. Every other option is prop-only.
Configure via a wrapper (the general case)
Wrap the component to bind props, then use the wrapper as your input:
import { defineType, defineField } from 'sanity'
import { AdvancedRefArray } from '@liiift-studio/sanity-advanced-reference-array'
// Bind options once, reuse across fields
const CuratedRefArray = (props) => (
<AdvancedRefArray
{...props}
maxSearchResults={20}
searchPlaceholder="Search products..."
allowBulkAdd={false}
showItemCount={false}
/>
)
export const collection = defineType({
name: 'collection',
type: 'document',
fields: [
defineField({
name: 'featuredItems',
type: 'array',
of: [{ type: 'reference', to: [{ type: 'article' }] }],
components: { input: CuratedRefArray },
}),
],
})Configure via schema options (collapse only)
defineField({
name: 'relatedItems',
type: 'array',
of: [{ type: 'reference', to: [{ type: 'product' }] }],
components: { input: AdvancedRefArray },
options: {
collapsibleList: true, // read from schema options
collapseThreshold: 10, // read from schema options
},
})Option reference
| Option | Type | Default | Settable via | What it does |
|---|---|---|---|---|
searchPlaceholder | string | 'Find items to add...' | prop | Placeholder text for the search input |
allowIndividualAdd | boolean | true | prop | Click a search result to add it |
allowBulkAdd | boolean | true | prop | Show the "Add All" button |
filterExisting | boolean | true | prop | Hide results already in the array |
maxSearchResults | number | 50 | prop | Slice applied to the search query |
showItemCount | boolean | true | prop | Show result counts under the list |
enableKeyboardShortcuts | boolean | true | prop | โ/Ctrl+Enter add all, Escape clear |
filterGroq | string | โ | prop | Extra GROQ condition appended to the search with && |
filterParams | (document) => object | โ | prop | Returns GROQ params derived from the current document |
collapsibleList | boolean | true | prop or schema options | Enable the collapse/expand toggle |
collapseThreshold | number | 20 | prop or schema options | Auto-collapse once the array exceeds this many items |
Document-scoped search with filterGroq / filterParams
The most powerful pairing: narrow the search to documents related to the one being edited. filterParams receives the current document and returns GROQ parameters; filterGroq is appended to the search condition.
// Only offer fonts that belong to the typeface currently being edited
const typefaceParams = (doc) => ({ typefaceName: doc?.title || '' })
const FontsRefArray = (props) => (
<AdvancedRefArray
{...props}
filterGroq="lower(typefaceName) == lower($typefaceName)"
filterParams={typefaceParams}
/>
)This is exactly how @liiift-studio/sanity-font-manager scopes its font pickers.
How search and sort actually resolve fields
Two behaviours worth knowing, because they are automatic rather than configured:
- Search matches the
titlefield only. The generated query is*[_type in $types && title match $search][0...maxSearchResults]. Referenced documents therefore need atitleto be findable. There is nosearchFieldsoption โ searching additional fields is not currently implemented. - Sortable fields are auto-discovered. On entering sort mode the component expands the referenced documents and offers every non-underscore-prefixed key of the first result as a sort field. There is no
sortableFieldsoption โ the list comes from your data.
Earlier drafts of this README documented
searchFieldsandsortableFieldsoptions. They were never implemented; the behaviour above is what ships. Both remain reasonable feature requests โ see Contributing.
๐ฏ Real-World Examples
E-commerce Product Relations
// Perfect for related products, cross-sells, upsells
const ProductRefArray = (props) => (
<AdvancedRefArray {...props} maxSearchResults={20} searchPlaceholder="Search products..." />
)
defineField({
name: 'relatedProducts',
title: 'Related Products',
type: 'array',
of: [{ type: 'reference', to: [{ type: 'product' }] }],
components: { input: ProductRefArray },
})Content Collections
// Great for curated article collections, featured content.
// Curated content should be added individually, so bulk add is off.
const CuratedArticles = (props) => (
<AdvancedRefArray {...props} allowBulkAdd={false} filterExisting />
)
defineField({
name: 'featuredArticles',
title: 'Featured Articles',
type: 'array',
of: [{ type: 'reference', to: [{ type: 'article' }] }],
components: { input: CuratedArticles },
})Team & Author Management
// Perfect for assigning multiple team members, contributors
const PeopleRefArray = (props) => (
<AdvancedRefArray {...props} searchPlaceholder="Search team members..." />
)
defineField({
name: 'contributors',
title: 'Contributors',
type: 'array',
of: [{ type: 'reference', to: [{ type: 'person' }] }],
components: { input: PeopleRefArray },
options: { collapseThreshold: 15 },
})๐ Relationship to sanity-font-manager
This package is a required peer dependency of @liiift-studio/sanity-font-manager (declared as ">=1").
sanity-font-manager imports AdvancedRefArray at the top level of its createStylesField schema module, which its package entry re-exports. That is a static ESM import, so if this package is not installed, importing anything from sanity-font-manager throws at module load โ the failure looks like a Studio that will not boot, not a missing-feature warning.
flowchart LR
FM["@liiift-studio/sanity-font-manager"] -->|"peer >=1 ยท static import<br/>in createStylesField"| ARA["@liiift-studio/<br/>sanity-advanced-reference-array"]
ARA -->|"renders the styles /<br/>collections pickers"| ST["Studio typeface document"]If you use the font manager, install both:
npm install @liiift-studio/sanity-font-manager @liiift-studio/sanity-advanced-reference-arrayThis package is also useful entirely on its own โ it has no dependency on the font manager.
๐งฉ Compatibility
Supports Sanity Studio v3, v4, v5 and v6 from a single build.
| Package | Supported range |
|---|---|
sanity | >=3 <7 (Studio v3 โ v6) |
react | >=18 |
@sanity/ui | >=2 <5 |
@sanity/icons | >=2 <6 |
How one build spans four majors
The peer ranges look inconsistent at a glance, so here is the reasoning:
@sanity/uiv4 moved components to subpath entries.Tooltip,Menu,MenuButton,MenuItem,Code,Popover,Autocomplete,ToastanduseToastare no longer on the package root.@sanity/iconsv5 removed every named*Iconexport.- Both still declare the removed names in their
.d.ts, typednever. A named import therefore type-checks, compiles, and only then fails at runtime โ the breakage is invisible totscand to a green build. - So this package imports no
@sanity/uior@sanity/iconssymbol directly. Everything routes through@liiift-studio/sanity-ui-compat(a real runtime dependency, installed for you), which resolves the installed namespace at runtime and works against either layout.
The @sanity/ui peer is >=2 <5, and that is correct for Sanity v6 โ Studio v6 ships @sanity/ui v4, not v5. It is not a stale upper bound.
Verification status
v3 โ v6 support is established by the declared peer ranges, green builds, and the runtime-resolving compat layer. Beyond that, this component has been exercised in three in-house Studios. It has not been broadly tested in a running Sanity 6 Studio outside those. Please open an issue if you hit a version-specific problem.
Packaging
- Ships CJS (
dist/index.js) and ESM (dist/index.esm.js) with bundled TypeScript declarations (dist/index.d.ts) and source maps. - Uses the Studio client via
useClient({ apiVersion: '2023-01-01' }), so it must render inside a Sanity Studio React tree.
A note on GROQ interpolation
filterGroq and maxSearchResults are interpolated directly into the generated GROQ query. They are intended to come from trusted schema/wrapper configuration only โ never wire them to end-user input. Search text itself is correctly passed as a bound $search parameter.
๐ง Development
# Clone the repository
git clone https://github.com/Liiift-Studio/sanity-advanced-reference-array.git
# Install dependencies
npm install
# Build the package
npm run build
# Run type checking
npm run type-check
# Run linting
npm run lint๐ค Contributing
We welcome contributions! This component was built from real-world usage across multiple Sanity studios and continues to evolve based on community needs.
Areas for Contribution:
- ๐ Bug fixes - Help us squash issues
- โจ Feature requests - Suggest new capabilities (configurable
searchFieldsandsortableFieldsare both open ideas) - ๐ Documentation - Improve examples and guides
- ๐งช Testing - Add test coverage
- ๐จ UI/UX - Enhance the user experience
๐ License
MIT ยฉ Quinn Keaveney
๐ Acknowledgments
This component combines the best features from multiple implementations used in production Sanity studios:
- Darden Studio - Original advanced search and sort functionality
- The Designer's Foundry - Individual item selection and UX improvements
- Community feedback - Ongoing enhancements and bug fixes
๐ Links
- NPM Package
- GitHub Repository
@liiift-studio/sanity-font-manager- depends on this package@liiift-studio/sanity-ui-compat- the v3โv6 compatibility layer- Sanity.io
- Report Issues
Made with โค๏ธ for the Sanity community
Transform your reference arrays from basic lists into powerful, searchable, sortable content management tools.