Tulip Logo IconTulip
Blocks

Inline and Sortable Blocks

Compose inline block editors and sortable block lists.

Inline editing uses a list wrapper plus per-block shells.

Sortable List Wrapper

SortableBlocks owns the dnd-kit context, sensors, default vertical strategy, and default parent/vertical modifiers.

<SortableBlocks
  data={blocks}
  queryKey={blocksQueryOptions.queryKey}
  variables={({ activeBlock, toIndex }) => ({
    pageId,
    id: activeBlock.id,
    toIndex,
  })}
  mutation={moveBlockMutation}
>
  <RenderBlocks data={blocks} components={inlineComponents} />
</SortableBlocks>

When queryKey is provided, the package applies an optimistic cache move with moveBlock before dispatching the mutation.

Manual Move Mode

If an app wants to own all side effects, use onMove instead of variables and mutation.

<SortableBlocks
  data={blocks}
  onMove={({ activeBlock, toIndex }) => {
    moveBlockMutation.mutate({ id: activeBlock.id, toIndex });
  }}
>
  <RenderBlocks data={blocks} components={inlineComponents} />
</SortableBlocks>

Inline Block Shell

Each inline component owns its fields, labels, commands, and mutation variables. The package provides the shared collapsible block shell.

export function HeroInlineBlock({ block }: BlockComponentProps<typeof heroBlock>) {
  return (
    <InlineBlockSortable id={block.id}>
      <InlineBlock>
        <InlineBlockHeader>
          <InlineBlockDragHandle>
            <GripIcon className="size-4" />
            <span className="sr-only">Move block</span>
          </InlineBlockDragHandle>

          <InlineBlockTrigger>{heroBlock.label}</InlineBlockTrigger>

          {/* app-owned command menu */}
        </InlineBlockHeader>

        <InlineBlockContent>
          {/* app-owned inline fields */}
        </InlineBlockContent>
      </InlineBlock>
    </InlineBlockSortable>
  );
}

InlineBlockSortable

InlineBlockSortable connects one block item to the nearest SortableBlocks context and shares refs, attributes, listeners, drag state, and transform style with child primitives.

If InlineBlockDragHandle is rendered without a sortable context, it returns null, so the same shell can be used in non-sortable contexts.

Keep this in the package:

  • sortable list mechanics
  • item refs and drag handle wiring
  • collapsible inline shell primitives

Keep this in the app:

  • concrete inline fields
  • command menus
  • permissions and mutation options
  • labels and localized copy

On this page