Building sections
Theme editor
The four rules every interactive component follows.
React sections work in the editor properly, and keeping them that way is a requirement for any new component — not a nice-to-have.
Everything here is inert on the live storefront: shopify_attributes renders
empty and the events are never dispatched.
Selection
Blocks carry Shopify's own shopify_attributes, passed through verbatim rather
than reconstructed. Spread it with editorProps():
<SwiperSlide {...editorProps(block.shopify_attributes)}>That's what makes Inspect land on the right block. If you render blocks with
<Blocks> this is already done for you.
Reacting to selection
Rule 2 of the checklist — selecting a block reveals it — is the same job in every component. The trigger is identical; only the reveal differs. So it's one hook:
1import { useRevealOnSelect } from '@/framework'23useRevealOnSelect(blocks, (index) => setOpenIndex(index)) // accordion4useRevealOnSelect(blocks, (index) => setActiveTab(index)) // tabs5useRevealOnSelect(blocks, (index) => swiper.slideTo(index)) // carousel6useRevealOnSelect(blocks, (_, block) => scrollTo(refs[block.id]))It fires only for blocks in the list you pass, so it's already scoped to your component — and it fires every time, including when the merchant re-selects the block that is already selected.
If you need the state rather than a callback:
1const selected = useSelectedIndex(blocks)23selected.index // -1 when the selection isn't one of yours4selected.item // the block itself, or null5selected.isSelected // pause autoplay, highlight, whateveruseSelectedBlock() is the raw page-wide selection underneath both, as
{ id, seq }. Reach for it only when you're not working from a list.
Depend on the whole object, not
.id.seqincrements on every selection event. Without it a component goes dead after the first click: the merchant selects slide 3, scrolls away, clicks slide 3 again, and nothing happens because the id never changed.useRevealOnSelecthandles this for you.
Selection is deliberately not scoped by section id. Block ids are unique
across the page, so finding the id among your own blocks already scopes it — and
comparing section ids on top of that silently breaks sections inside groups,
whose ids are prefixed (sections--…__announcement_bar) and never match.
Surviving edits
Changing a setting makes Shopify replace the section's markup, which remounts React and resets every piece of component state.
This is the single thing that breaks editor support, and it isn't obvious — the component works perfectly until a merchant edits the thing they just selected.
Selection itself is held in a module-level store, not component state, so a
remounted component reads the selection that is still current and comes back
pinned to the same block. Anything else that should survive goes in
useSectionState:
const [openTab, setOpenTab] = useSectionState(id, 'tab', 0)Reads and writes exactly like useState, including the updater form. The
difference is only in what happens across a remount: the value comes back. Keyed
by section id, so two instances of a section don't share one.
It is module scope, so it lasts one page load. This is for editor ergonomics —
carousel position, open accordion, active tab — not persistence. Anything that
should outlive a navigation belongs in sessionStorage or the cart, and anything
the shopper can link to belongs in the URL.
Checklist for any new interactive component
- Blocks carry
shopify_attributesviaeditorProps(), so Inspect and selection can find them. - Selecting a block reveals it — a carousel slides to it, a rotator stops on
it, an accordion opens it. Depend on the whole
useSelectedBlock()object, not.id, or re-clicking the same block does nothing. - Autoplay pauses while a block is selected. Otherwise the merchant is editing a moving target.
- State that matters survives a remount. If the answer to "what happens when
this section re-renders mid-edit" is "it resets", it needs
useSectionState.
Section-level selection
useSectionSelected(id) is true while this section is the selected one — for
editor-only affordances, like showing an empty-state hint where a merchant has
not added blocks yet.
useDesignMode() tells you whether you are in the editor at all. Use it for
hints and warnings, not for changing what shoppers see.