How to filter Sanity documents based on checkbox selections in NextJS?

25 replies
Last updated: Jan 27, 2026
I'm trying to create a checkbox filter in my NextJS app. How would I handle filtering out sanity documents that don't meet the checkbox requirements? (I would post code but I don't even know where to begin)
AI Update

I'll help you understand how to filter Sanity documents based on checkbox inputs in your Next.js app! This is a common pattern that combines client-side state management with GROQ queries.

The Basic Approach

The key is to build dynamic GROQ queries based on your checkbox state. Here's a practical example:

1. Set up your checkbox state:

// components/FilteredPosts.jsx
import { useState, useEffect } from 'react';
import { client } from '@/sanity/lib/client';

export default function FilteredPosts() {
  const [filters, setFilters] = useState({
    featured: false,
    published: false,
    hasImage: false
  });
  const [posts, setPosts] = useState([]);

  // Handle checkbox changes
  const handleFilterChange = (filterName) => {
    setFilters(prev => ({
      ...prev,
      [filterName]: !prev[filterName]
    }));
  };

  // Fetch filtered posts
  useEffect(() => {
    const fetchPosts = async () => {
      const query = buildQuery(filters);
      const result = await client.fetch(query);
      setPosts(result);
    };
    
    fetchPosts();
  }, [filters]);

  return (
    <div>
      <div className="filters">
        <label>
          <input
            type="checkbox"
            checked={filters.featured}
            onChange={() => handleFilterChange('featured')}
          />
          Featured Only
        </label>
        <label>
          <input
            type="checkbox"
            checked={filters.published}
            onChange={() => handleFilterChange('published')}
          />
          Published Only
        </label>
        <label>
          <input
            type="checkbox"
            checked={filters.hasImage}
            onChange={() => handleFilterChange('hasImage')}
          />
          Has Image
        </label>
      </div>
      
      <div className="posts">
        {posts.map(post => (
          <div key={post._id}>{post.title}</div>
        ))}
      </div>
    </div>
  );
}

2. Build dynamic GROQ queries:

function buildQuery(filters) {
  // Start with base query
  let conditions = ['_type == "post"'];
  
  // Add conditions based on active filters
  if (filters.featured) {
    conditions.push('featured == true');
  }
  
  if (filters.published) {
    conditions.push('publishedAt != null');
  }
  
  if (filters.hasImage) {
    conditions.push('defined(mainImage)');
  }
  
  // Join conditions with && operator
  const filterString = conditions.join(' && ');
  
  // Return complete GROQ query
  return `*[${filterString}] | order(publishedAt desc) {
    _id,
    title,
    slug,
    publishedAt,
    featured,
    mainImage
  }`;
}

Key GROQ Filtering Concepts

Based on GROQ's syntax structure, here are the filtering patterns you'll use:

  • Equality checks: fieldName == true or fieldName == "value"
  • Existence checks: defined(fieldName) - checks if a field exists (uses the defined() function)
  • Null checks: fieldName != null - ensures field has a value
  • Combining conditions: Use && (AND) or || (OR)
  • Array filtering: "value" in arrayField[]

More Advanced Filtering Example

If you have multiple categories or tags:

function buildAdvancedQuery(filters) {
  let conditions = ['_type == "post"'];
  
  // Filter by multiple selected categories
  if (filters.categories && filters.categories.length > 0) {
    const categoryRefs = filters.categories.map(id => `"${id}"`).join(', ');
    conditions.push(`references(*[_type == "category" && _id in [${categoryRefs}]]._id)`);
  }
  
  // Filter by boolean fields
  if (filters.featured) {
    conditions.push('featured == true');
  }
  
  const filterString = conditions.join(' && ');
  
  return `*[${filterString}] {
    _id,
    title,
    categories[]-> {
      _id,
      title
    }
  }`;
}

Server Components Alternative (Next.js 13+)

If you're using App Router, you can also handle this with URL search params:

// app/posts/page.jsx
import { client } from '@/sanity/lib/client';

export default async function PostsPage({ searchParams }) {
  const params = await searchParams; // await if using Next.js 15
  const featured = params.featured === 'true';
  const published = params.published === 'true';
  
  let conditions = ['_type == "post"'];
  if (featured) conditions.push('featured == true');
  if (published) conditions.push('publishedAt != null');
  
  const query = `*[${conditions.join(' && ')}]`;
  const posts = await client.fetch(query);
  
  return (
    <div>
      <FilterForm /> {/* Client component with checkboxes */}
      <PostsList posts={posts} />
    </div>
  );
}

The beauty of GROQ is that it handles all the filtering server-side, so you only get back the documents that match your criteria. No need to filter arrays in JavaScript after fetching! The pipeline structure means you're querying efficiently at the database level.

Show original thread
25 replies

Sanity – Build the way you think, not the way your CMS thinks

Sanity is the developer-first content operating system that gives you complete control. Schema-as-code, GROQ queries, and real-time APIs mean no more workarounds or waiting for deployments. Free to start, scale as you grow.

Was this answer helpful?