Rebuilding GitBook’s sidebar: A deep dive into performance, animations, and user experience

Rebuilding GitBook’s sidebar: A deep dive into performance, animations, and user experience

Product updates

Product updates

Product updates

21 Jul, 2025

Author

Author

Author

It’s so easy to get a sidebar wrong.

The sidebar is arguably the most visible part of any documentation platform — always there, guiding users through content hierarchies and serving as the primary navigation tool.

When we decided to completely redesign GitBook’s sidebar, it wasn’t because people were complaining — they rarely do about things like this unless they’re really broken — but because we knew it could be better.

We wanted to improve the visual consistency of the sidebar, and also make it performant, accessible, and smooth.

But we quickly realized that redesigning a sidebar came with a set of technical challenges.

Initial problems

We faced four core problems that shaped our approach:

  • Complex layout animations: Standard CSS animations couldn’t handle the coordinated movements we needed between the sidebar and content area.

  • Custom drag-and-drop requirements: Our specific business rules for content organization didn’t match what existing libraries provided.

  • API performance optimization: Loading data efficiently required rethinking our entire request strategy for organizations with varying scales.

  • Persistent state management: Maintaining user preferences across sessions while keeping animations smooth presented coordination challenges.

Here’s how we solved each challenge and what we learned about building performant, user-focused interface components.

Solving layout animations with Motion

The first major technical decision was choosing our animation library. We chose Motion over CSS animations for several technical reasons that go beyond simple preference.

Animating height changes that affect layout is notoriously difficult with CSS. While CSS has recently introduced better support for this, browser compatibility is still inconsistent while I am writing this article. Motion handles height animations performantly by design and provides physics-based animations with controllable velocity and damping. This creates more natural movement that responds appropriately to user interaction speed.

When an element needs to “push” other elements around — like our sidebar expanding and shifting the content area — CSS can’t handle this smoothly. Motion’s layout animation system coordinates these complex movements seamlessly.

But animations are all about perception. They have look great to users — and sometimes that requires some tricks. In our case, we animated the sidebar position in absolute and then artificially animated the shift of the content with another div.

{/* Animate the sidebar position */}
<motion.div
  className="z-10 flex min-h-0 w-[--sidebar-width] flex-1"
  initial={false}
  style={{ position: 'relative' }}
  animate={
        isCollapsed
            ? isOpen
                ? {
                      position: 'absolute',
                      top: '0.75rem',
                      bottom: '0.75rem',
                      translateX: 0,
                      left: '0.75rem',
                  }
                : {
                      position: 'absolute',
                      top: '0.75rem',
                      bottom: '0.75rem',
                      translateX: '-100%',
                      left: 0,
                  }
            : {
                  position: 'absolute',
                  top: 0,
                  bottom: 0,
                  translateX: 0,
                  left: 0,
                  transitionEnd: {
                      position: 'relative',
                  },
              }
    }
/>

{/* Push the content to the right */}
<motion.div
  transition={transition}
  initial={false}
  animate={isCollapsed ? { width: 0 } : { width: 'var(--sidebar-width)' }}
/>

By combining two animations we ensure it looks natural, as if the sidebar were anchored to the page. But in reality the sidebar never really pushes the content to the right.

Building custom drag and drop

The next challenge was implementing drag and drop functionality for our tree structure. The React tree component didn’t support drag and drop when we implemented this — they’ve since added it. So we built our own implementation. This turned out to be more complex than anticipated due to our specific requirements.

This kind of custom business logic is why off-the-shelf solutions often fall short for complex applications. Sometimes you need to build exactly what your users need rather than what a generic component provides.

One good thing about React Aria’s implementation is that you have complete control over the drag and drop rules you want to implement. In GitBook they are actually quite complex — for example, you can’t or drop a space inside another space but you can drop a space inside a collection.

All of these rules can be implemented in the getDropOperation :

getDropOperation(target, _types, allowedOperations) {
    // Support only item drops, not root.
    if (target.type !== 'item') {
        return 'cancel';
    }

    // Pick the best allowed operation.
    const bestAllowedOperation = allowedOperations[0];

    // If we drag on the same item, cancel the operation.
    if (draggedKeys.has(target.key)) {
        return 'cancel';
    }

    // If we want to drag just after an expanded item, cancel the operation.
    if (target.dropPosition === 'after' && expandedKeys.has(target.key)) {
        return 'cancel';
    }

    // We support only one dragged item.
    if (draggedKeys.size !== 1) {
        return 'cancel';
    }

    const targetItem = getItem(target.key, items);

    const draggedKey = Array.from(draggedKeys)[0];
    const draggedItem = getItem(draggedKey, items);

    // If we can't find the item, cancel the operation.
    if (!targetItem || !draggedItem) {
        return 'cancel';
    }

    if (draggedItem.type === 'space') {
        if (!draggedItem.space.space.permissions.admin) {
            return 'cancel';
        }

        // Disallow to drop a space on its parent.
        if (
            target.dropPosition === 'on' &&
            targetItem.type === 'collection' &&
            draggedItem.space.space.parent === targetItem.collection.collection.id
        ) {
            return 'cancel';
        }
    }

    if (draggedItem.type === 'collection') {
        if (!draggedItem.collection.collection.permissions.admin) {
            return 'cancel';
        }

        const targetAncestors = getAncestors(content, targetItem);

        if (targetAncestors === null) {
            return 'cancel';
        }

        // If we try to drop on a collection that is a child of the dragged item, cancel the operation.
        if (targetAncestors.some((ancestor) => ancestor.collection.id === draggedItem.id)) {
            return 'cancel';
        }

        // Disallow to drop a collection on its parent.
        if (
            target.dropPosition === 'on' &&
            targetItem.type === 'collection' &&
            draggedItem.collection.collection.parent === targetItem.collection.collection.id
        ) {
            return 'cancel';
        }
    }

    // Drag on an item.
    if (target.dropPosition === 'on') {
        // We can only drop on a collection.
        if (targetItem.type !== 'collection') {
            return 'cancel';
        }

        return bestAllowedOperation;
    }

    if (targetItem.type !== draggedItem.type) {
        // We can only move items of the same type.
        return 'cancel';
    }

    return bestAllowedOperation;
},

Optimizing API performance

Our third major challenge was data loading performance. Instead of making individual requests for each site and space — which could easily become dozens of requests for active organizations — we created a single dedicated API endpoint. This returns everything needed for sidebar rendering in a single call.

This optimization makes sense for GitBook because most organizations have a manageable number of sites and spaces. The data structure is relatively flat, so we can optimize the payload specifically for sidebar rendering needs. For edge cases with hundreds of sites, this might be slower than a paginated approach. But we optimized for the 90% case — organizations with one to two sites and a handful of spaces.

get:
    operationId: getOrganizationAllContent
    summary: List all content in an organization
    description: >
        Lists all spaces, collections, sites accessible by current user in the current organization.
        This endpoint is internal for now as the data structure is optimized for the sidebar.
    tags:
        - organizations
        - critical
        - internal
    security:
        - user-internal: []
    parameters:
        - $ref: '../../../parameters/path/organizationId.yaml'
    responses:
        '200':
            description: OK
            content:
                application/json:
                    schema:
                        type: object
                        properties:
                            sites:
                                type: array
                                items:
                                    $ref: './OrganizationAllSite.yaml'
                            spaces:
                                type: array
                                items:
                                    oneOf:
                                        - $ref: './OrganizationAllCollection.yaml'
                                        - $ref: './OrganizationAllSpace.yaml'
                            deletedSpaces:
                                description: List of soft-deleted spaces.
                                type: array
                                items:
                                    $ref: './OrganizationAllSpace.yaml'
                        required:
                            - sites
                            - spaces
                            - deletedSpaces

This single API endpoint allows us to display the whole sidebar in one query — and to optimize performance.

To improve things even more,we also made sure the data fetching is optimized to be done in parallel, to avoid fetching the same resource twice.

The UX improvements that emerged

A new approach to space management

Instead of a permanently visible icon-only sidebar, we adopted an approach inspired by Linear — a sidebar that appears when you hover near the left edge of the screen and disappears when you don’t need it.

This gives users the best of both worlds: full access to navigation when needed, and maximum content real estate when focused on reading or editing. When it’s visible, they can access everything without having to expand or click through additional UI elements. And they can adjust the width if they prefer it wider or narrower while visible.

Improved information hierarchy

We restructured the sidebar to better reflect our platform’s information architecture. The connection between sites and their constituent spaces is now more visually obvious. And instead of a separate selector, organization switching is now integrated into the settings panel.

We also moved settings, invites, and other actions into floating elements that don’t consume precious vertical space. We reduced spacing between elements to a single pixel, which creates visual separation without wasted space.

Animation philosophy: purposeful motion

Our philosophy is simple: animations should help users understand what’s happening, show progress, or bring joy without compromising function. For the sidebar, animation serves several key purposes. When the sidebar expands or collapses, users see the content area adjust. This helps them understand the spatial relationship and makes it clear that the same element is moving, not appearing and disappearing.

At the same time, we deliberately chose not to animate every possible interaction. For example, expanding and collapsing individual sections within the tree doesn’t use animation. It would be overwhelming and slow down power users who are quickly navigating through content.

Managing persistent state

The sidebar’s state — expanded/collapsed and which sections are open — persists across sessions using local storage. This seems simple, but coordinating animation states with persistence while maintaining smooth performance required careful state management.

// Create an "atom" that represents the expanded state
// of the space section globally in the app
const disclosureExpandedAtom = atom<boolean>({
    key: 'sidebarSpacesDisclosureExpanded',
    default: true,
    // Persist it into the local storage
    effects: [persistRecoilEffect()],
});

function SidebarSpaces() {
    const [isExpanded, setExpanded] = useRecoilState(disclosureExpandedAtom);
}

We use Recoil to manage global states in the GitBook app. Combining this with local storage allows us to persist small interactions like collapsing the spaces section in the sidebar. However, since React 19 does not support Recoil, we plan to move to Zustand soon.

The results: more than just aesthetics

The new sidebar delivers measurable improvements. We gained 30% more content space when the sidebar is collapsed. Navigation feels smoother with purposeful animations. The information architecture better reflects our platform structure. We improved performance with optimized data loading. And we enhanced accessibility with proper keyboard navigation support.

But perhaps most importantly, it creates a foundation for future improvements. The modular architecture and clean animation system make it easier to iterate and add new features without accumulating technical debt.

Technical learnings

Building this sidebar reinforced several important principles:

  • Animation should serve users, not delight developers. Every animation should have a clear purpose in helping users understand the interface.

  • Performance optimization is about understanding your users. Optimizing for the common case — few sites and spaces — rather than the edge case of hundreds of sites delivered better results for 90% of users.

  • Custom implementations aren’t always over-engineering. Sometimes building exactly what you need provides a better experience than adapting a generic solution.

  • Small details compound. Single-pixel spacing, carefully tuned easing curves, and thoughtful state persistence might seem minor individually. But together they create a noticeably better experience.

Want to experience the new sidebar yourself? Sign up for GitBook and see how thoughtful interface design can make documentation feel effortless.

It’s so easy to get a sidebar wrong.

The sidebar is arguably the most visible part of any documentation platform — always there, guiding users through content hierarchies and serving as the primary navigation tool.

When we decided to completely redesign GitBook’s sidebar, it wasn’t because people were complaining — they rarely do about things like this unless they’re really broken — but because we knew it could be better.

We wanted to improve the visual consistency of the sidebar, and also make it performant, accessible, and smooth.

But we quickly realized that redesigning a sidebar came with a set of technical challenges.

Initial problems

We faced four core problems that shaped our approach:

  • Complex layout animations: Standard CSS animations couldn’t handle the coordinated movements we needed between the sidebar and content area.

  • Custom drag-and-drop requirements: Our specific business rules for content organization didn’t match what existing libraries provided.

  • API performance optimization: Loading data efficiently required rethinking our entire request strategy for organizations with varying scales.

  • Persistent state management: Maintaining user preferences across sessions while keeping animations smooth presented coordination challenges.

Here’s how we solved each challenge and what we learned about building performant, user-focused interface components.

Solving layout animations with Motion

The first major technical decision was choosing our animation library. We chose Motion over CSS animations for several technical reasons that go beyond simple preference.

Animating height changes that affect layout is notoriously difficult with CSS. While CSS has recently introduced better support for this, browser compatibility is still inconsistent while I am writing this article. Motion handles height animations performantly by design and provides physics-based animations with controllable velocity and damping. This creates more natural movement that responds appropriately to user interaction speed.

When an element needs to “push” other elements around — like our sidebar expanding and shifting the content area — CSS can’t handle this smoothly. Motion’s layout animation system coordinates these complex movements seamlessly.

But animations are all about perception. They have look great to users — and sometimes that requires some tricks. In our case, we animated the sidebar position in absolute and then artificially animated the shift of the content with another div.

{/* Animate the sidebar position */}
<motion.div
  className="z-10 flex min-h-0 w-[--sidebar-width] flex-1"
  initial={false}
  style={{ position: 'relative' }}
  animate={
        isCollapsed
            ? isOpen
                ? {
                      position: 'absolute',
                      top: '0.75rem',
                      bottom: '0.75rem',
                      translateX: 0,
                      left: '0.75rem',
                  }
                : {
                      position: 'absolute',
                      top: '0.75rem',
                      bottom: '0.75rem',
                      translateX: '-100%',
                      left: 0,
                  }
            : {
                  position: 'absolute',
                  top: 0,
                  bottom: 0,
                  translateX: 0,
                  left: 0,
                  transitionEnd: {
                      position: 'relative',
                  },
              }
    }
/>

{/* Push the content to the right */}
<motion.div
  transition={transition}
  initial={false}
  animate={isCollapsed ? { width: 0 } : { width: 'var(--sidebar-width)' }}
/>

By combining two animations we ensure it looks natural, as if the sidebar were anchored to the page. But in reality the sidebar never really pushes the content to the right.

Building custom drag and drop

The next challenge was implementing drag and drop functionality for our tree structure. The React tree component didn’t support drag and drop when we implemented this — they’ve since added it. So we built our own implementation. This turned out to be more complex than anticipated due to our specific requirements.

This kind of custom business logic is why off-the-shelf solutions often fall short for complex applications. Sometimes you need to build exactly what your users need rather than what a generic component provides.

One good thing about React Aria’s implementation is that you have complete control over the drag and drop rules you want to implement. In GitBook they are actually quite complex — for example, you can’t or drop a space inside another space but you can drop a space inside a collection.

All of these rules can be implemented in the getDropOperation :

getDropOperation(target, _types, allowedOperations) {
    // Support only item drops, not root.
    if (target.type !== 'item') {
        return 'cancel';
    }

    // Pick the best allowed operation.
    const bestAllowedOperation = allowedOperations[0];

    // If we drag on the same item, cancel the operation.
    if (draggedKeys.has(target.key)) {
        return 'cancel';
    }

    // If we want to drag just after an expanded item, cancel the operation.
    if (target.dropPosition === 'after' && expandedKeys.has(target.key)) {
        return 'cancel';
    }

    // We support only one dragged item.
    if (draggedKeys.size !== 1) {
        return 'cancel';
    }

    const targetItem = getItem(target.key, items);

    const draggedKey = Array.from(draggedKeys)[0];
    const draggedItem = getItem(draggedKey, items);

    // If we can't find the item, cancel the operation.
    if (!targetItem || !draggedItem) {
        return 'cancel';
    }

    if (draggedItem.type === 'space') {
        if (!draggedItem.space.space.permissions.admin) {
            return 'cancel';
        }

        // Disallow to drop a space on its parent.
        if (
            target.dropPosition === 'on' &&
            targetItem.type === 'collection' &&
            draggedItem.space.space.parent === targetItem.collection.collection.id
        ) {
            return 'cancel';
        }
    }

    if (draggedItem.type === 'collection') {
        if (!draggedItem.collection.collection.permissions.admin) {
            return 'cancel';
        }

        const targetAncestors = getAncestors(content, targetItem);

        if (targetAncestors === null) {
            return 'cancel';
        }

        // If we try to drop on a collection that is a child of the dragged item, cancel the operation.
        if (targetAncestors.some((ancestor) => ancestor.collection.id === draggedItem.id)) {
            return 'cancel';
        }

        // Disallow to drop a collection on its parent.
        if (
            target.dropPosition === 'on' &&
            targetItem.type === 'collection' &&
            draggedItem.collection.collection.parent === targetItem.collection.collection.id
        ) {
            return 'cancel';
        }
    }

    // Drag on an item.
    if (target.dropPosition === 'on') {
        // We can only drop on a collection.
        if (targetItem.type !== 'collection') {
            return 'cancel';
        }

        return bestAllowedOperation;
    }

    if (targetItem.type !== draggedItem.type) {
        // We can only move items of the same type.
        return 'cancel';
    }

    return bestAllowedOperation;
},

Optimizing API performance

Our third major challenge was data loading performance. Instead of making individual requests for each site and space — which could easily become dozens of requests for active organizations — we created a single dedicated API endpoint. This returns everything needed for sidebar rendering in a single call.

This optimization makes sense for GitBook because most organizations have a manageable number of sites and spaces. The data structure is relatively flat, so we can optimize the payload specifically for sidebar rendering needs. For edge cases with hundreds of sites, this might be slower than a paginated approach. But we optimized for the 90% case — organizations with one to two sites and a handful of spaces.

get:
    operationId: getOrganizationAllContent
    summary: List all content in an organization
    description: >
        Lists all spaces, collections, sites accessible by current user in the current organization.
        This endpoint is internal for now as the data structure is optimized for the sidebar.
    tags:
        - organizations
        - critical
        - internal
    security:
        - user-internal: []
    parameters:
        - $ref: '../../../parameters/path/organizationId.yaml'
    responses:
        '200':
            description: OK
            content:
                application/json:
                    schema:
                        type: object
                        properties:
                            sites:
                                type: array
                                items:
                                    $ref: './OrganizationAllSite.yaml'
                            spaces:
                                type: array
                                items:
                                    oneOf:
                                        - $ref: './OrganizationAllCollection.yaml'
                                        - $ref: './OrganizationAllSpace.yaml'
                            deletedSpaces:
                                description: List of soft-deleted spaces.
                                type: array
                                items:
                                    $ref: './OrganizationAllSpace.yaml'
                        required:
                            - sites
                            - spaces
                            - deletedSpaces

This single API endpoint allows us to display the whole sidebar in one query — and to optimize performance.

To improve things even more,we also made sure the data fetching is optimized to be done in parallel, to avoid fetching the same resource twice.

The UX improvements that emerged

A new approach to space management

Instead of a permanently visible icon-only sidebar, we adopted an approach inspired by Linear — a sidebar that appears when you hover near the left edge of the screen and disappears when you don’t need it.

This gives users the best of both worlds: full access to navigation when needed, and maximum content real estate when focused on reading or editing. When it’s visible, they can access everything without having to expand or click through additional UI elements. And they can adjust the width if they prefer it wider or narrower while visible.

Improved information hierarchy

We restructured the sidebar to better reflect our platform’s information architecture. The connection between sites and their constituent spaces is now more visually obvious. And instead of a separate selector, organization switching is now integrated into the settings panel.

We also moved settings, invites, and other actions into floating elements that don’t consume precious vertical space. We reduced spacing between elements to a single pixel, which creates visual separation without wasted space.

Animation philosophy: purposeful motion

Our philosophy is simple: animations should help users understand what’s happening, show progress, or bring joy without compromising function. For the sidebar, animation serves several key purposes. When the sidebar expands or collapses, users see the content area adjust. This helps them understand the spatial relationship and makes it clear that the same element is moving, not appearing and disappearing.

At the same time, we deliberately chose not to animate every possible interaction. For example, expanding and collapsing individual sections within the tree doesn’t use animation. It would be overwhelming and slow down power users who are quickly navigating through content.

Managing persistent state

The sidebar’s state — expanded/collapsed and which sections are open — persists across sessions using local storage. This seems simple, but coordinating animation states with persistence while maintaining smooth performance required careful state management.

// Create an "atom" that represents the expanded state
// of the space section globally in the app
const disclosureExpandedAtom = atom<boolean>({
    key: 'sidebarSpacesDisclosureExpanded',
    default: true,
    // Persist it into the local storage
    effects: [persistRecoilEffect()],
});

function SidebarSpaces() {
    const [isExpanded, setExpanded] = useRecoilState(disclosureExpandedAtom);
}

We use Recoil to manage global states in the GitBook app. Combining this with local storage allows us to persist small interactions like collapsing the spaces section in the sidebar. However, since React 19 does not support Recoil, we plan to move to Zustand soon.

The results: more than just aesthetics

The new sidebar delivers measurable improvements. We gained 30% more content space when the sidebar is collapsed. Navigation feels smoother with purposeful animations. The information architecture better reflects our platform structure. We improved performance with optimized data loading. And we enhanced accessibility with proper keyboard navigation support.

But perhaps most importantly, it creates a foundation for future improvements. The modular architecture and clean animation system make it easier to iterate and add new features without accumulating technical debt.

Technical learnings

Building this sidebar reinforced several important principles:

  • Animation should serve users, not delight developers. Every animation should have a clear purpose in helping users understand the interface.

  • Performance optimization is about understanding your users. Optimizing for the common case — few sites and spaces — rather than the edge case of hundreds of sites delivered better results for 90% of users.

  • Custom implementations aren’t always over-engineering. Sometimes building exactly what you need provides a better experience than adapting a generic solution.

  • Small details compound. Single-pixel spacing, carefully tuned easing curves, and thoughtful state persistence might seem minor individually. But together they create a noticeably better experience.

Want to experience the new sidebar yourself? Sign up for GitBook and see how thoughtful interface design can make documentation feel effortless.

It’s so easy to get a sidebar wrong.

The sidebar is arguably the most visible part of any documentation platform — always there, guiding users through content hierarchies and serving as the primary navigation tool.

When we decided to completely redesign GitBook’s sidebar, it wasn’t because people were complaining — they rarely do about things like this unless they’re really broken — but because we knew it could be better.

We wanted to improve the visual consistency of the sidebar, and also make it performant, accessible, and smooth.

But we quickly realized that redesigning a sidebar came with a set of technical challenges.

Initial problems

We faced four core problems that shaped our approach:

  • Complex layout animations: Standard CSS animations couldn’t handle the coordinated movements we needed between the sidebar and content area.

  • Custom drag-and-drop requirements: Our specific business rules for content organization didn’t match what existing libraries provided.

  • API performance optimization: Loading data efficiently required rethinking our entire request strategy for organizations with varying scales.

  • Persistent state management: Maintaining user preferences across sessions while keeping animations smooth presented coordination challenges.

Here’s how we solved each challenge and what we learned about building performant, user-focused interface components.

Solving layout animations with Motion

The first major technical decision was choosing our animation library. We chose Motion over CSS animations for several technical reasons that go beyond simple preference.

Animating height changes that affect layout is notoriously difficult with CSS. While CSS has recently introduced better support for this, browser compatibility is still inconsistent while I am writing this article. Motion handles height animations performantly by design and provides physics-based animations with controllable velocity and damping. This creates more natural movement that responds appropriately to user interaction speed.

When an element needs to “push” other elements around — like our sidebar expanding and shifting the content area — CSS can’t handle this smoothly. Motion’s layout animation system coordinates these complex movements seamlessly.

But animations are all about perception. They have look great to users — and sometimes that requires some tricks. In our case, we animated the sidebar position in absolute and then artificially animated the shift of the content with another div.

{/* Animate the sidebar position */}
<motion.div
  className="z-10 flex min-h-0 w-[--sidebar-width] flex-1"
  initial={false}
  style={{ position: 'relative' }}
  animate={
        isCollapsed
            ? isOpen
                ? {
                      position: 'absolute',
                      top: '0.75rem',
                      bottom: '0.75rem',
                      translateX: 0,
                      left: '0.75rem',
                  }
                : {
                      position: 'absolute',
                      top: '0.75rem',
                      bottom: '0.75rem',
                      translateX: '-100%',
                      left: 0,
                  }
            : {
                  position: 'absolute',
                  top: 0,
                  bottom: 0,
                  translateX: 0,
                  left: 0,
                  transitionEnd: {
                      position: 'relative',
                  },
              }
    }
/>

{/* Push the content to the right */}
<motion.div
  transition={transition}
  initial={false}
  animate={isCollapsed ? { width: 0 } : { width: 'var(--sidebar-width)' }}
/>

By combining two animations we ensure it looks natural, as if the sidebar were anchored to the page. But in reality the sidebar never really pushes the content to the right.

Building custom drag and drop

The next challenge was implementing drag and drop functionality for our tree structure. The React tree component didn’t support drag and drop when we implemented this — they’ve since added it. So we built our own implementation. This turned out to be more complex than anticipated due to our specific requirements.

This kind of custom business logic is why off-the-shelf solutions often fall short for complex applications. Sometimes you need to build exactly what your users need rather than what a generic component provides.

One good thing about React Aria’s implementation is that you have complete control over the drag and drop rules you want to implement. In GitBook they are actually quite complex — for example, you can’t or drop a space inside another space but you can drop a space inside a collection.

All of these rules can be implemented in the getDropOperation :

getDropOperation(target, _types, allowedOperations) {
    // Support only item drops, not root.
    if (target.type !== 'item') {
        return 'cancel';
    }

    // Pick the best allowed operation.
    const bestAllowedOperation = allowedOperations[0];

    // If we drag on the same item, cancel the operation.
    if (draggedKeys.has(target.key)) {
        return 'cancel';
    }

    // If we want to drag just after an expanded item, cancel the operation.
    if (target.dropPosition === 'after' && expandedKeys.has(target.key)) {
        return 'cancel';
    }

    // We support only one dragged item.
    if (draggedKeys.size !== 1) {
        return 'cancel';
    }

    const targetItem = getItem(target.key, items);

    const draggedKey = Array.from(draggedKeys)[0];
    const draggedItem = getItem(draggedKey, items);

    // If we can't find the item, cancel the operation.
    if (!targetItem || !draggedItem) {
        return 'cancel';
    }

    if (draggedItem.type === 'space') {
        if (!draggedItem.space.space.permissions.admin) {
            return 'cancel';
        }

        // Disallow to drop a space on its parent.
        if (
            target.dropPosition === 'on' &&
            targetItem.type === 'collection' &&
            draggedItem.space.space.parent === targetItem.collection.collection.id
        ) {
            return 'cancel';
        }
    }

    if (draggedItem.type === 'collection') {
        if (!draggedItem.collection.collection.permissions.admin) {
            return 'cancel';
        }

        const targetAncestors = getAncestors(content, targetItem);

        if (targetAncestors === null) {
            return 'cancel';
        }

        // If we try to drop on a collection that is a child of the dragged item, cancel the operation.
        if (targetAncestors.some((ancestor) => ancestor.collection.id === draggedItem.id)) {
            return 'cancel';
        }

        // Disallow to drop a collection on its parent.
        if (
            target.dropPosition === 'on' &&
            targetItem.type === 'collection' &&
            draggedItem.collection.collection.parent === targetItem.collection.collection.id
        ) {
            return 'cancel';
        }
    }

    // Drag on an item.
    if (target.dropPosition === 'on') {
        // We can only drop on a collection.
        if (targetItem.type !== 'collection') {
            return 'cancel';
        }

        return bestAllowedOperation;
    }

    if (targetItem.type !== draggedItem.type) {
        // We can only move items of the same type.
        return 'cancel';
    }

    return bestAllowedOperation;
},

Optimizing API performance

Our third major challenge was data loading performance. Instead of making individual requests for each site and space — which could easily become dozens of requests for active organizations — we created a single dedicated API endpoint. This returns everything needed for sidebar rendering in a single call.

This optimization makes sense for GitBook because most organizations have a manageable number of sites and spaces. The data structure is relatively flat, so we can optimize the payload specifically for sidebar rendering needs. For edge cases with hundreds of sites, this might be slower than a paginated approach. But we optimized for the 90% case — organizations with one to two sites and a handful of spaces.

get:
    operationId: getOrganizationAllContent
    summary: List all content in an organization
    description: >
        Lists all spaces, collections, sites accessible by current user in the current organization.
        This endpoint is internal for now as the data structure is optimized for the sidebar.
    tags:
        - organizations
        - critical
        - internal
    security:
        - user-internal: []
    parameters:
        - $ref: '../../../parameters/path/organizationId.yaml'
    responses:
        '200':
            description: OK
            content:
                application/json:
                    schema:
                        type: object
                        properties:
                            sites:
                                type: array
                                items:
                                    $ref: './OrganizationAllSite.yaml'
                            spaces:
                                type: array
                                items:
                                    oneOf:
                                        - $ref: './OrganizationAllCollection.yaml'
                                        - $ref: './OrganizationAllSpace.yaml'
                            deletedSpaces:
                                description: List of soft-deleted spaces.
                                type: array
                                items:
                                    $ref: './OrganizationAllSpace.yaml'
                        required:
                            - sites
                            - spaces
                            - deletedSpaces

This single API endpoint allows us to display the whole sidebar in one query — and to optimize performance.

To improve things even more,we also made sure the data fetching is optimized to be done in parallel, to avoid fetching the same resource twice.

The UX improvements that emerged

A new approach to space management

Instead of a permanently visible icon-only sidebar, we adopted an approach inspired by Linear — a sidebar that appears when you hover near the left edge of the screen and disappears when you don’t need it.

This gives users the best of both worlds: full access to navigation when needed, and maximum content real estate when focused on reading or editing. When it’s visible, they can access everything without having to expand or click through additional UI elements. And they can adjust the width if they prefer it wider or narrower while visible.

Improved information hierarchy

We restructured the sidebar to better reflect our platform’s information architecture. The connection between sites and their constituent spaces is now more visually obvious. And instead of a separate selector, organization switching is now integrated into the settings panel.

We also moved settings, invites, and other actions into floating elements that don’t consume precious vertical space. We reduced spacing between elements to a single pixel, which creates visual separation without wasted space.

Animation philosophy: purposeful motion

Our philosophy is simple: animations should help users understand what’s happening, show progress, or bring joy without compromising function. For the sidebar, animation serves several key purposes. When the sidebar expands or collapses, users see the content area adjust. This helps them understand the spatial relationship and makes it clear that the same element is moving, not appearing and disappearing.

At the same time, we deliberately chose not to animate every possible interaction. For example, expanding and collapsing individual sections within the tree doesn’t use animation. It would be overwhelming and slow down power users who are quickly navigating through content.

Managing persistent state

The sidebar’s state — expanded/collapsed and which sections are open — persists across sessions using local storage. This seems simple, but coordinating animation states with persistence while maintaining smooth performance required careful state management.

// Create an "atom" that represents the expanded state
// of the space section globally in the app
const disclosureExpandedAtom = atom<boolean>({
    key: 'sidebarSpacesDisclosureExpanded',
    default: true,
    // Persist it into the local storage
    effects: [persistRecoilEffect()],
});

function SidebarSpaces() {
    const [isExpanded, setExpanded] = useRecoilState(disclosureExpandedAtom);
}

We use Recoil to manage global states in the GitBook app. Combining this with local storage allows us to persist small interactions like collapsing the spaces section in the sidebar. However, since React 19 does not support Recoil, we plan to move to Zustand soon.

The results: more than just aesthetics

The new sidebar delivers measurable improvements. We gained 30% more content space when the sidebar is collapsed. Navigation feels smoother with purposeful animations. The information architecture better reflects our platform structure. We improved performance with optimized data loading. And we enhanced accessibility with proper keyboard navigation support.

But perhaps most importantly, it creates a foundation for future improvements. The modular architecture and clean animation system make it easier to iterate and add new features without accumulating technical debt.

Technical learnings

Building this sidebar reinforced several important principles:

  • Animation should serve users, not delight developers. Every animation should have a clear purpose in helping users understand the interface.

  • Performance optimization is about understanding your users. Optimizing for the common case — few sites and spaces — rather than the edge case of hundreds of sites delivered better results for 90% of users.

  • Custom implementations aren’t always over-engineering. Sometimes building exactly what you need provides a better experience than adapting a generic solution.

  • Small details compound. Single-pixel spacing, carefully tuned easing curves, and thoughtful state persistence might seem minor individually. But together they create a noticeably better experience.

Want to experience the new sidebar yourself? Sign up for GitBook and see how thoughtful interface design can make documentation feel effortless.

Get the GitBook newsletter

Get the latest product news, useful resources and more in your inbox. 130k+ people read it every month.

Email

Similar posts

Get started for free

Play around with GitBook and set up your docs for free. Add your team and pay when you’re ready.

Trusted by leading technical product teams

The NVIDIA logo
The Carta logo
The Ericsson logo
The Cisco logo
The Fedex logo
The Zoom logo
  • The Braze logo
  • The Bucket logo
  • The Cortex logo
  • The Count logo
  • The Digibee logo
  • The Gravitee logo
  • The Hebbia logo
  • The HockeyStack logo
  • The Ideogram logo
  • The JAM logo
  • The Make logo
  • The Material logo
  • The Multiplier logo
  • The Nightfall AI logo
  • The Onum logo
  • The Photoroom logo
  • The Pylon logo
  • The Relay.app logo
  • The Rox logo
  • The ScraperAPI logo
  • The Seam logo
  • The Sendbird logo
  • The Sola logo
  • The Synk logo
  • The Tabnine logo
  • The ZenML logo

Get started for free

Play around with GitBook and set up your docs for free. Add your team and pay when you’re ready.

Trusted by leading technical product teams

The NVIDIA logo
The Carta logo
The Ericsson logo
The Cisco logo
The Fedex logo
The Zoom logo
  • The Braze logo
  • The Bucket logo
  • The Cortex logo
  • The Count logo
  • The Digibee logo
  • The Gravitee logo
  • The Hebbia logo
  • The HockeyStack logo
  • The Ideogram logo
  • The JAM logo
  • The Make logo
  • The Material logo
  • The Multiplier logo
  • The Nightfall AI logo
  • The Onum logo
  • The Photoroom logo
  • The Pylon logo
  • The Relay.app logo
  • The Rox logo
  • The ScraperAPI logo
  • The Seam logo
  • The Sendbird logo
  • The Sola logo
  • The Synk logo
  • The Tabnine logo
  • The ZenML logo

Get started for free

Play around with GitBook and set up your docs for free. Add your team and pay when you’re ready.

Trusted by leading technical product teams

The NVIDIA logo
The Carta logo
The Ericsson logo
The Cisco logo
The Fedex logo
The Zoom logo
  • The Braze logo
  • The Bucket logo
  • The Cortex logo
  • The Count logo
  • The Digibee logo
  • The Gravitee logo
  • The Hebbia logo
  • The HockeyStack logo
  • The Ideogram logo
  • The JAM logo
  • The Make logo
  • The Material logo
  • The Multiplier logo
  • The Nightfall AI logo
  • The Onum logo
  • The Photoroom logo
  • The Pylon logo
  • The Relay.app logo
  • The Rox logo
  • The ScraperAPI logo
  • The Seam logo
  • The Sendbird logo
  • The Sola logo
  • The Synk logo
  • The Tabnine logo
  • The ZenML logo