Pinned, Draggable Browser-Style Tabs — Reorder and Pin Like a Real Browser

Pinned, Draggable Browser-Style Tabs · Navigation · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

Native HTML5 Drag and Drop API reordering — no external sortable library
Live drop-position indicator shows exactly which side of the hovered tab a dragged tab will land on
Pinning is a genuine positional guarantee — pinned tabs are always re-sorted leftmost, not just styled differently
Pin/unpin available via both right-click (matching real browser tab context menus) and a dedicated pin icon
Pinned tabs collapse to an icon-only width and lose their close button, matching real pinned-tab behavior
Roving tabindex keyboard navigation — Arrow Left/Right move between tabs following the ARIA tabs pattern
Close button removes a tab directly from the DOM with a dedicated accessible label per tab

About this UI Snippet

Browser-Style Tabs — Drag Reordering and Position-Guaranteeing Pins

Screenshot of the Pinned, Draggable Browser-Style Tabs snippet rendered live

Browser tabs are one of the most-used UI patterns on the planet, and users bring very specific expectations from them: tabs can be dragged into any order, and pinning a tab doesn't just style it differently — it *guarantees* it stays leftmost, ahead of every unpinned tab, no matter what. This snippet reproduces both behaviors correctly, along with the keyboard navigation and native draggable API wiring that make it feel like a real tab strip rather than a static row of buttons.

Drag-and-drop uses the native HTML Drag and Drop API, not a library

Every tab has draggable="true", and the reordering logic is built entirely from dragstart, dragover, drop, and dragend events — no external sortable library. dragover must call e.preventDefault() on every fire (this is a common gotcha: without it, the drop event never fires at all, since the browser's default behavior for a dragover is to disallow dropping). The live "which side would this land on" indicator is computed on every dragover by comparing the pointer's clientX to the midpoint of the tab currently being hovered — left half means "drop before this tab," right half means "drop after," giving continuous visual feedback rather than only showing where a drop will land after the fact.

Pinning is a positional guarantee, not just a style

togglePin() doesn't just toggle a CSS class — after flipping the class, it re-sorts every tab in the bar into two groups, pinned first, then unpinned, and re-appends them all in that order. This means pinning a tab that's currently at the far right instantly moves it to sit immediately after the last pinned tab, and unpinning a tab moves it back into the unpinned group — matching exactly how pinning works in Chrome, Firefox, and every other tab-based browser, where "pinned" is fundamentally a positional property, not merely a visual one.

Right-click and a dedicated pin icon both work

The pin toggle is wired to two separate triggers: a contextmenu handler (so right-clicking anywhere on a tab pins/unpins it, matching real browser tab context menus) and a click on the small pin icon itself for discoverability without needing to know the right-click shortcut. Both call the same togglePin() function, so there's exactly one implementation of the pin behavior regardless of which trigger fired it.

Keyboard navigation follows the ARIA tabs pattern

Only the currently selected tab has tabindex="0"; every other tab has tabindex="-1", so a single Tab keypress moves focus into the tab strip once rather than tabbing through every individual tab. Once focus is inside, Arrow Left/Right move focus (and selection) between adjacent tabs — the same roving-tabindex convention used by native OS tab bars and the ARIA Authoring Practices tabs pattern, keeping the whole strip a single stop in the page's overall tab order.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Ask an AI assistant to explain why calling preventDefault() inside the dragover handler is required for the drop event to fire at all, and to trace through exactly what togglePin() does step by step when pinning a tab that currently sits at the far right of an unpinned group. It's also worth asking for a version that persists tab order and pin state to localStorage, or one that adds a proper role="tablist"/role="tabpanel" pairing so each tab actually shows different panel content when selected.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build a draggable, pinnable browser-style tab bar in HTML, CSS, and vanilla JavaScript using the native HTML5 Drag and Drop API — no external sortable library.

Requirements:
- A row of at least four tabs, each with a label and a close button, one of them marked as pinned initially (shown as an icon-only, narrower tab with no close button).
- Implement drag-to-reorder using dragstart, dragover, drop, and dragend events. While dragging, show a live visual indicator on whichever tab is currently hovered, distinguishing whether the dragged tab would land before or after it based on which half of that tab the pointer is over.
- Implement pin/unpin toggling triggered by both a right-click (contextmenu event, with the default browser context menu suppressed) on any tab and a dedicated small pin icon/button, both calling the same shared toggle function.
- Pinning must be a genuine positional guarantee: whenever a tab's pinned state changes, immediately re-sort the entire tab bar so all pinned tabs form a contiguous group at the leftmost position, ahead of every unpinned tab — not merely a visual style applied in place.
- Implement roving-tabindex keyboard navigation: only the currently selected tab should have tabindex="0", every other tab tabindex="-1", and pressing Arrow Left/Right while a tab has focus should move both focus and selection to the adjacent tab.
- Clicking a tab's close button should remove just that tab from the DOM without affecting the others.

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

Step by step

How to Use

  1. 1
    Click a tab to select itThe selected tab gets a raised white background; aria-selected updates for assistive technology.
  2. 2
    Drag a tab to reorderA blue indicator line shows exactly which side of the hovered tab the dragged tab will land on before you release.
  3. 3
    Right-click a tab, or click its pin iconToggles pinning — a pinned tab shrinks to an icon-only width and jumps to sit immediately after the other pinned tabs.
  4. 4
    Try to drag a pinned tab past an unpinned oneReordering still works within drag-and-drop, but re-pinning any tab always re-sorts pinned tabs back to the leftmost group.
  5. 5
    Use Arrow Left/Right with a tab focusedMoves focus (and selection) to the adjacent tab, following the standard ARIA tabs keyboard pattern.

Real-world uses

Common Use Cases

EDITOR
Code and document editors
Multi-file editing tools (IDE-style web apps, note-taking apps) benefit from familiar drag-reorder and pin-to-keep-open behavior.
BROWSER
In-app browser or preview tabs
Apps embedding multiple preview panes or in-app browser tabs benefit from matching users' existing browser tab mental model.
DASHBOARD
Multi-view dashboards
Dashboards with several switchable views benefit from letting users reorder and pin their most-used views to the front.
PRODUCTIVITY
Workspace and project switchers
Any tool managing multiple open "documents" or "workspaces" benefits from this exact interaction model.
Related: Sidebar Mini
See the Sidebar Mini for a related navigation pattern worth pairing with this one.

Got questions?

Frequently Asked Questions

The browser's default behavior during a dragover is to disallow dropping entirely — without calling preventDefault() on every dragover event, the drop event will simply never fire, no matter how the drag ends.

Pinning re-sorts the entire tab bar so all pinned tabs sit as a contiguous group at the leftmost position, ahead of every unpinned tab — this happens immediately on pin/unpin, not just as a passive style change, matching real browser tab bar behavior.

Click the small pin icon shown on the currently-pinned example tab (or add a pin icon to any tab) — both the right-click context menu handler and the pin-icon click call the same togglePin() function.

Matching real browser behavior — a pinned tab represents something the user wants to always keep open, so removing the close affordance (both visually, since the tab collapses to icon width, and functionally) reinforces that intent.

Only the selected tab has tabindex="0"; all others have tabindex="-1", so Tab moves focus into the strip once. Once a tab has focus, Arrow Left/Right move focus (and selection) between adjacent tabs — the standard roving-tabindex pattern for ARIA tablists.

Yes — dropping on the right half of the last tab inserts the dragged tab immediately after it (using nextSibling as the insertion reference), correctly placing it at the very end of the strip.