How to Use
Guide on how to use Schema UI components in your project
Using Schema UI components
This starter is a monorepo: the Next.js app lives under frontend/ and Sanity Studio (schemas) under studio/. When you add a block end to end:
| Piece | Location |
|---|---|
| React component | frontend/components/blocks/ (and frontend/components/ui/ for primitives) |
| GROQ query | frontend/sanity/queries/ |
| Sanity schema | studio/schemas/ |
| Register schema types | studio/schema-types.ts |
| Page builder list | studio/schemas/documents/page.ts (blocks of array) |
| Render map | frontend/components/blocks/index.tsx |
After changing schemas or queries, run pnpm typegen from the repository root (or npx sanity typegen generate from studio/) so frontend/sanity.types.ts stays in sync.
1. Copy the component
Copy the desired component from the Schema UI library into frontend/components/ui or frontend/components/blocks/... (match the folder layout used by similar blocks in the starter).
2. Copy the schema
Copy the corresponding schema into studio/schemas/ (e.g. studio/schemas/blocks/hero/hero-1.ts), alongside the existing block and shared object files.
3. Copy the query
Copy the related GROQ fragment or query file into frontend/sanity/queries/ (e.g. frontend/sanity/queries/hero/hero-1.ts).
4. Register the schema in studio/schema-types.ts
Import your new type and add it to the schemaTypes array (this file is what sanity.config.ts uses—not a separate schema.ts at the repo root):
// studio/schema-types.ts
// documents
import page from "./schemas/documents/page";
import post from "./schemas/documents/post";
import author from "./schemas/documents/author";
import category from "./schemas/documents/category";
import faq from "./schemas/documents/faq";
import testimonial from "./schemas/documents/testimonial";
import navigation from "./schemas/documents/navigation";
import settings from "./schemas/documents/settings";
// Schema UI shared objects
import blockContent from "./schemas/blocks/shared/block-content";
import link from "./schemas/blocks/shared/link";
import { colorVariant } from "./schemas/blocks/shared/color-variant";
import { buttonVariant } from "./schemas/blocks/shared/button-variant";
import sectionPadding from "./schemas/blocks/shared/section-padding";
// Schema UI objects
import hero1 from "./schemas/blocks/hero/hero-1";
import hero2 from "./schemas/blocks/hero/hero-2";
import sectionHeader from "./schemas/blocks/section-header";
import splitRow from "./schemas/blocks/split/split-row";
import splitContent from "./schemas/blocks/split/split-content";
import splitCardsList from "./schemas/blocks/split/split-cards-list";
import splitCard from "./schemas/blocks/split/split-card";
import splitImage from "./schemas/blocks/split/split-image";
import splitInfoList from "./schemas/blocks/split/split-info-list";
import splitInfo from "./schemas/blocks/split/split-info";
import gridCard from "./schemas/blocks/grid/grid-card";
import pricingCard from "./schemas/blocks/grid/pricing-card";
import gridPost from "./schemas/blocks/grid/grid-post";
import gridRow from "./schemas/blocks/grid/grid-row";
import carousel1 from "./schemas/blocks/carousel/carousel-1";
import carousel2 from "./schemas/blocks/carousel/carousel-2";
import timelineRow from "./schemas/blocks/timeline/timeline-row";
import timelinesOne from "./schemas/blocks/timeline/timelines-1";
import cta1 from "./schemas/blocks/cta/cta-1";
import logoCloud1 from "./schemas/blocks/logo-cloud/logo-cloud-1";
import faqs from "./schemas/blocks/faqs";
import newsletter from "./schemas/blocks/forms/newsletter";
import allPosts from "./schemas/blocks/all-posts";
export const schemaTypes = [
// documents
page,
post,
author,
category,
faq,
testimonial,
navigation,
settings,
// shared objects
blockContent,
link,
colorVariant,
buttonVariant,
sectionPadding,
// blocks
hero1,
hero2,
heroMarketing,
sectionHeader,
splitRow,
splitContent,
splitCardsList,
splitCard,
splitImage,
splitInfoList,
splitInfo,
gridCard,
pricingCard,
gridPost,
gridRow,
carousel1,
carousel2,
timelineRow,
timelinesOne,
cta1,
logoCloud1,
faqs,
newsletter,
allPosts,
];5. Allow the block on the page document
Add your block type to the blocks field’s of array in studio/schemas/documents/page.ts. The starter also uses an SEO meta object and optional insertMenu grouping—copy patterns from the full file in the repo.
import { defineField, defineType } from "sanity";
import { Files } from "lucide-react";
import { orderRankField } from "@sanity/orderable-document-list";
import meta from "../blocks/shared/meta";
export default defineType({
name: "page",
type: "document",
title: "Page",
icon: Files,
groups: [
{ name: "content", title: "Content" },
{ name: "seo", title: "SEO" },
{ name: "settings", title: "Settings" },
],
fields: [
defineField({ name: "title", type: "string", group: "content" }),
defineField({
name: "slug",
title: "Slug",
type: "slug",
group: "settings",
options: { source: "title", maxLength: 96 },
validation: (Rule) => Rule.required(),
}),
defineField({
name: "blocks",
type: "array",
group: "content",
of: [
{ type: "hero-1" },
{ type: "hero-2" },
{ type: "section-header" },
{ type: "split-row" },
{ type: "grid-row" },
{ type: "carousel-1" },
{ type: "carousel-2" },
{ type: "timeline-row" },
{ type: "cta-1" },
{ type: "logo-cloud-1" },
{ type: "faqs" },
{ type: "form-newsletter" },
{ type: "all-posts" },
],
}),
meta,
orderRankField({ type: "page" }),
],
});6. Extend the page GROQ query
Import your query fragment in frontend/sanity/queries/page.ts and add it inside blocks[]{ ... }, for example:
import { groq } from "next-sanity";
import { metaQuery } from "./shared/meta";
import { hero1Query } from "./hero/hero-1";
import { hero2Query } from "./hero/hero-2";
import { sectionHeaderQuery } from "./section-header";
import { splitRowQuery } from "./split/split-row";
import { gridRowQuery } from "./grid/grid-row";
import { carousel1Query } from "./carousel/carousel-1";
import { carousel2Query } from "./carousel/carousel-2";
import { timelineQuery } from "./timeline";
import { cta1Query } from "./cta/cta-1";
import { logoCloud1Query } from "./logo-cloud/logo-cloud-1";
import { faqsQuery } from "./faqs";
import { formNewsletterQuery } from "./forms/newsletter";
import { allPostsQuery } from "./all-posts";
export const PAGE_QUERY = groq`
*[_type == "page" && slug.current == $slug][0]{
blocks[]{
${hero1Query},
${hero2Query},
${heroMarketingQuery},
${sectionHeaderQuery},
${splitRowQuery},
${gridRowQuery},
${carousel1Query},
${carousel2Query},
${timelineQuery},
${cta1Query},
${logoCloud1Query},
${faqsQuery},
${formNewsletterQuery},
${allPostsQuery},
},
${metaQuery},
}
`;
export const PAGES_SLUGS_QUERY = groq`*[_type == "page" && defined(slug)]{slug}`;7. Map block _type to React components
Wire components in frontend/components/blocks/index.tsx:
import { PAGE_QUERY_RESULT } from "@/sanity.types";
import Hero1 from "@/components/blocks/hero/hero-1";
import Hero2 from "@/components/blocks/hero/hero-2";
import SectionHeader from "@/components/blocks/section-header";
import SplitRow from "@/components/blocks/split/split-row";
import GridRow from "@/components/blocks/grid/grid-row";
import Carousel1 from "@/components/blocks/carousel/carousel-1";
import Carousel2 from "@/components/blocks/carousel/carousel-2";
import TimelineRow from "@/components/blocks/timeline/timeline-row";
import Cta1 from "@/components/blocks/cta/cta-1";
import LogoCloud1 from "@/components/blocks/logo-cloud/logo-cloud-1";
import FAQs from "@/components/blocks/faqs";
import FormNewsletter from "@/components/blocks/forms/newsletter";
import AllPosts from "@/components/blocks/all-posts";
type Block = NonNullable<NonNullable<PAGE_QUERY_RESULT>["blocks"]>[number];
const componentMap: {
[K in Block["_type"]]: React.ComponentType<Extract<Block, { _type: K }>>;
} = {
"hero-1": Hero1,
"hero-2": Hero2,
"section-header": SectionHeader,
"split-row": SplitRow,
"grid-row": GridRow,
"carousel-1": Carousel1,
"carousel-2": Carousel2,
"timeline-row": TimelineRow,
"cta-1": Cta1,
"logo-cloud-1": LogoCloud1,
faqs: FAQs,
"form-newsletter": FormNewsletter,
"all-posts": AllPosts,
};
export default function Blocks({ blocks }: { blocks: Block[] }) {
return (
<>
{blocks?.map((block) => {
const Component = componentMap[block._type];
if (!Component) {
console.warn(
`No component implemented for block type: ${block._type}`,
);
return <div data-type={block._type} key={block._key} />;
}
return <Component {...(block as any)} key={block._key} />;
})}
</>
);
}8. Skip if you use the Next.js Sanity Starter
If you are not using our Next.js Sanity Starter:
- Add the shared object schemas referenced above under shared objects in your Studio schema registration.
- Add
section-container.tsxtofrontend/components/ui/:
import { cn } from "@/lib/utils";
import { SectionPadding, ColorVariant } from "@/sanity.types";
interface SectionContainerProps {
color?: ColorVariant | null;
padding?: SectionPadding | null;
children: React.ReactNode;
className?: string;
}
export default function SectionContainer({
color = "background",
padding,
children,
className,
}: SectionContainerProps) {
return (
<div
className={cn(
`bg-${color} relative`,
padding?.top ? "pt-16 xl:pt-20" : undefined,
padding?.bottom ? "pb-16 xl:pb-20" : undefined,
className
)}
>
<div className="container">{children}</div>
</div>
);
}