How to Build Conditional Editor Fields in a Headless CMS
You ship a schema where an "Article" can be a standard post, a sponsored placement, or a gated whitepaper.
You ship a schema where an "Article" can be a standard post, a sponsored placement, or a gated whitepaper. An editor opens it and sees forty fields at once: sponsor disclosure, paywall tier, embargo date, syndication partners, all rendered whether or not they apply. They fill in the embargo on a piece that will never be embargoed, leave the sponsor field blank on a sponsored post, and your frontend renders a half-configured page. Every unconditional field is a place for a mistake, and reviewers stop trusting the form.
Conditional fields fix this by showing a field only when it is relevant, but most headless CMSes treat conditional logic as an afterthought: a dropdown-driven rule builder, a fixed set of operators, or nothing at all. Sanity takes the opposite stance. Because the Studio is a React application you configure in code, conditional visibility is just a function of the current document, and the same schema drives your types end to end. Sanity is the Content Operating System, the intelligent backend where the editing experience adapts to your content model instead of forcing your model to fit a fixed form.
This guide walks through the failure modes of naive schemas, how conditional fields actually work, and how to keep the logic honest as your model grows.
Why a flat schema breaks down at scale
A content model that starts clean rarely stays that way. The first version of a document type has eight fields and every one applies to every document. Then marketing needs a sponsored variant, so you add a sponsor name, a disclosure line, and a campaign ID. Then legal needs a gated variant with a paywall tier and an access-expiry date. Then a syndication team wants partner IDs and an embargo timestamp. None of these apply to a routine editorial post, but a flat schema shows all of them to every editor, every time.
The cost is not cosmetic. Each field an editor can see but should not touch is a defect waiting to happen. Someone sets an embargo on a post that ships immediately. Someone leaves the sponsor disclosure blank on a paid placement, and the frontend renders a page that violates an advertising-disclosure obligation. Validation can catch some of this, but validation fires at save time and produces error messages, which is a worse experience than never showing the field at all. The reviewer who has to approve the document now has to mentally filter which of the forty fields were even supposed to be in play.
The deeper problem is that a flat form encodes no relationship between fields. It cannot express "the sponsor block only matters when this is a sponsored post," so it leaks that logic into three places: the editor's head, the validation layer, and the frontend rendering code. Conditional fields move that relationship into the schema itself, which is the one place all three can share it. That is the reframe: field visibility is not a UI nicety, it is where your business rules about content should live.
How conditional fields actually work
Conditional visibility answers one question at render time: given the current state of this document, should this field appear? In Sanity Studio that question is answered by a `hidden` property on a field definition, which you can set to a plain boolean or, more usefully, to a callback. The callback receives the current document and the parent object, so you can branch on any value the editor has already entered. A sponsor block that reads `hidden: ({document}) => document?.contentType !== 'sponsored'` simply does not render until the editor picks the sponsored type, and it disappears again if they change their mind.
Because the callback is ordinary JavaScript running inside a React application, the condition is not limited to a fixed set of operators the way a dropdown rule builder is. You can check multiple fields, compare against arrays, read a value from a referenced document, or call a helper. The same pattern applies to the `readOnly` property when you want a field visible but locked, for example showing a computed price to an editor who is not allowed to change it. Fields hidden this way are collapsed from the form entirely, so the editing surface shrinks to exactly what is relevant to the document in front of the person.
The important mental model is that these `defineType` and `defineField` schemas are portable code, not configuration living in a vendor console. They live in your repository, they are reviewed in pull requests, and they are the same objects TypeGen reads to generate your TypeScript types. Conditional logic written once in the schema is the logic your query layer and your frontend inherit, rather than three separate implementations drifting apart.
Pattern one: variant-driven field groups
The most common conditional pattern is a single discriminator field that switches whole groups of fields on and off. You model a `contentType` string field with options like `standard`, `sponsored`, and `gated`, then attach a `hidden` callback to every field that belongs to a specific variant. Pick `sponsored` and the sponsor name, disclosure, and campaign ID appear together; the paywall tier and access-expiry stay hidden. This keeps a single document type flexible without exploding into three near-duplicate types that share ninety percent of their fields and drift apart over time.
To keep a variant form from becoming its own wall of fields, pair the discriminator with Studio field groups, which render tabs across the top of the editor. You can put shared fields in a General tab and variant-specific fields in their own tabs, then combine group membership with conditional hiding so a tab's contents collapse when the variant does not apply. Structure Builder gives you a further layer of control: you can present sponsored and gated documents as separate lists in the desk, so an editor working a campaign never wades through routine posts.
The counter-example worth naming is the temptation to model each variant as its own document type. That feels tidy until you need a query that spans all articles regardless of variant, or until a shared field changes and you have to edit it in three schemas. A discriminator plus conditional fields keeps one type, one GROQ query shape, and one place to evolve the common fields, while still giving each variant an editing experience as focused as a bespoke form.
Pattern two: conditions that read other documents
Simple conditions branch on a sibling field in the same document. Harder ones need to look outside it. Suppose a product page should only expose a `regionalPricing` block when the linked brand is configured to sell in multiple regions, or a campaign field should appear only while a referenced campaign document is still active. The value that drives the condition lives in another document entirely, and a naive `hidden` callback cannot see it because the callback only receives the current document and parent.
The pattern here is to resolve the external state before the condition needs it. A custom input component built with the App SDK can query the Content Lake with GROQ, following the reference with the `->` operator to read the brand's region list or the campaign's status, and then drive the field's visibility from that live result. Because Content Lake queries are schema-aware and real time, the component reflects the referenced document's current state rather than a value cached at page load, so a field can appear or disappear as the upstream document changes. This is the point where Sanity's editor being real code, not a fixed form, stops being a talking point and becomes the only reason the requirement is buildable at all.
Keep two guardrails on this pattern. First, a hidden field is a UI affordance, not a security boundary, so any rule that genuinely must hold should also be a validation rule and, where it protects a real obligation, enforced in your publishing logic with Functions. Second, resist reaching across documents for cosmetic conditions; every cross-document read is a dependency, and the clearest schemas keep most conditions local and reserve reference-driven visibility for the cases that truly need it.
Keeping conditional logic honest as the model grows
Conditional fields are easy to add and hard to audit. Six months in, a document type can carry a dozen `hidden` callbacks, some contradicting each other, some referencing a discriminator value that no longer exists because someone renamed an option. The failure mode is a field that can never appear, or worse, one that appears in a state where its value is meaningless and then gets published. Because the logic is code, the discipline that keeps it honest is also code discipline.
Extract condition callbacks into named, testable functions rather than inlining anonymous arrows in every field. A function called `isSponsored(document)` reads clearly at the call site, can be unit tested against sample documents, and gives you one place to change the rule when the discriminator changes. Because the schema is TypeScript, TypeGen turns your `defineType` definitions into generated types, so a query that selects a field which only exists conditionally is visible to the compiler, and a renamed field surfaces as a type error rather than a silent blank on a live page. Pair the visibility rule with a matching validation rule so the two never contradict: if a field is required when visible, express both the `hidden` and the `validation` in terms of the same helper.
Governance closes the loop. Content Releases let you stage a schema-driven change and review it as a batch before it goes live, and Audit logs record who changed what, so a conditional rule that starts hiding a compliance-critical field can be traced. The reframe from the introduction holds all the way down: the value of putting business rules in the schema is only realized if you treat the schema as software, with reviews, tests, and version history, rather than as settings someone clicks together in a console.
Conditional field logic across headless platforms
| Feature | Sanity | Contentful | Storyblok | Strapi |
|---|---|---|---|---|
| Where conditions are defined | In code: a `hidden` callback on `defineField` in your repo, reviewed in pull requests alongside the rest of the schema. | In the web app UI via appearance settings and validations; some conditional logic requires custom UI extensions built with the App Framework. | Field visibility rules configured in the block schema UI, with more advanced logic handled through custom field-type plugins. | Conditional display is configured in the Content-Type Builder and admin panel; deeper rules need custom admin customization in React. |
| Expressiveness of the condition | Arbitrary JavaScript over the current document and parent, so you can branch on multiple fields, arrays, or helpers, not a fixed operator set. | UI-driven rules cover common comparisons; branching on several fields at once or computed logic pushes you into custom app code. | Rule-based visibility handles typical show/hide cases; complex multi-field logic generally needs a plugin. | Attribute-level conditions cover standard cases; multi-field or computed logic means customizing the admin build. |
| Reading another document's state | Custom input via App SDK queries Content Lake with GROQ, following `->` to read a referenced doc's live value and drive visibility from it. | Possible by fetching linked entries in a custom UI extension via the CMA; not available in the built-in conditional rules. | Achievable inside a custom field plugin that calls the API; not part of the standard visibility configuration. | Requires custom admin code calling the API to read related entries; not offered by the built-in conditional display. |
| Type safety of conditional fields | TypeGen generates TypeScript from the same `defineType` schema, so a query touching a conditional field is checked and renamed fields become compile errors. | Types are generated from the API/content model via community and official tooling; conditional visibility itself is not reflected in types. | TypeScript types can be generated from the space schema; visibility rules are not represented in the generated types. | Generates TypeScript types for content types; conditional display settings are not encoded in those types. |
| Grouping and desk organization | Field groups render tabs, and Structure Builder can present each variant as its own list, so editors see only the relevant slice. | Fields can be grouped and validated in the entry editor; list organization is handled through views and search rather than custom desk code. | Field groups and tabs are supported in the block editor; list organization uses folders and filters. | Content-Type Builder supports components and groups; list views are configured in the admin panel. |
| Backing a rule with enforcement | Pair `hidden` with a `validation` rule on the same helper, then enforce in publishing with Functions for compliance-critical fields. | Field validations enforce required/format rules at save; server-side enforcement lives in your own webhook or app logic. | Field validations cover required and format checks; deeper enforcement is handled in your delivery or webhook layer. | Model-level and lifecycle-hook validation can enforce rules server-side within the self-hosted app. |