Unlock seamless workflows and faster delivery with our latest releases – get the details

Array of references as checkboxes

By Sigurd Heggemsnes

Tired of pressing "New item" all the time? Render an array of references as checkboxes

Reference checkbox

import { Box, Card, Checkbox, Flex, Stack, Text } from '@sanity/ui';
import { useCallback, useEffect, useMemo, useState } from 'react';
import type { ArrayOfObjectsInputProps } from 'sanity';
import { set, useClient } from 'sanity';

interface ReferenceItem {
  _id: string;
  title?: string;
}

/**
 * Renders a list of checkbox items based on documents that match the schema's "reference" fields.
 */
export function ReferenceCheckbox(props: ArrayOfObjectsInputProps) {
  const [referenceItems, setReferenceItems] = useState<ReferenceItem[]>([]);
  const client = useClient({ apiVersion: '2025-01-12' });

  // Flatten the preview.title fields that exist in the schema.
  const previewProjections = useMemo(() => {
    // Each "of" type could have "to" array. We collect any "preview.select.title".
    const titles = props.schemaType.of.flatMap((type) =>
      'to' in type ? type.to?.flatMap((to) => to.preview?.select?.title ?? []) : []
    );
    // If multiple preview fields exist, we can use `coalesce` with "||" or a fallback approach.
    return titles.length ? titles.join(',') : 'title';
  }, [props.schemaType]);

  // Collect all possible _type names from "to" fields in schema.
  const referenceTypes = useMemo(
    () =>
      props.schemaType.of
        .flatMap((type) => ('to' in type ? type.to?.map((to) => to.name) : []))
        .filter(Boolean),
    [props.schemaType]
  );

  // Fetch reference items on mount or if schema changes
  useEffect(() => {
    const fetchData = async () => {
      if (!referenceTypes.length) return;
      const query = `*[_type in $types] {
        _id,
        "title": coalesce(${previewProjections}, title)
      }`;

      const items: ReferenceItem[] = await client.fetch(query, { types: referenceTypes });
      setReferenceItems(items);
    };

    fetchData();
  }, [client, referenceTypes, previewProjections]);

  /**
   * Toggles a reference in the array (adds if missing, removes if present).
   */
  const handleToggle = useCallback(
    (itemId: string) => {
      const currentValue = props.value ?? [];
      const exists = currentValue.some((val) => '_ref' in val && val._ref === itemId);

      const newValue = exists
        ? currentValue.filter((val) => '_ref' in val && val._ref !== itemId)
        : [
            ...currentValue,
            {
              _key: itemId.slice(0, 10),
              _type: 'reference',
              _ref: itemId,
            },
          ];

      props.onChange(set(newValue));
    },
    [props]
  );

  return (
    <Stack space={2}>
      {referenceItems.map((item) => {
        const isChecked =
          props.value?.some((val) => '_ref' in val && val._ref === item._id) || false;
        return (
          <Card key={item._id} padding={2}>
            <Flex align="center">
              <Checkbox id={item._id} checked={isChecked} onChange={() => handleToggle(item._id)} />
              <Box flex={1} paddingLeft={3}>
                <Text>
                  <label htmlFor={item._id}>{item.title}</label>
                </Text>
              </Box>
            </Flex>
          </Card>
        );
      })}
    </Stack>
  );
}

Some content types can be limited in number but highly reusable across your site. Using the array picker for reference works, but this custom component renders the references as checkboxes to make it faster to check references.

This component use the specified "preview" path on a document to figure out what title to display. It currently does not take into account any filters, but this should be easy to implement.

Obviously this works best with when you are referencing a document type with limited entries.

Contributor

Other schemas by author