Checkbox Select All Rails Components

Bulk selection with shift-click, indeterminate states, keyboard navigation, nested selections, and paginated "select all pages" support. Perfect for task lists, data tables, and permission systems.

Installation

1. Stimulus Controller Setup

Start by adding the following controller to your project:

import { Controller } from "@hotwired/stimulus";

// Connects to data-controller="checkbox-select-all"
export default class extends Controller {
  static targets = [
    "selectAll", // The "select all" checkbox that toggles all other checkboxes
    "checkbox", // Individual checkboxes to be selected/deselected
    "child", // Child checkboxes within a parent group (for nested hierarchies)
    "parent", // Container element for parent-child checkbox groups
    "actionBar", // Element shown/hidden based on selection state (e.g., bulk actions toolbar)
    "count", // Element displaying the count of selected items
    "total", // Element displaying the calculated total from selected amounts
    "amount", // Elements containing numeric values to sum when their checkbox is selected
    "pageSelectionInfo", // Shows "X of Y row(s) selected." (visible when NOT in all-pages mode)
    "selectAllPagesPrompt", // Shows "Select all Y rows" button (when all on page selected but not all pages)
    "allPagesSelectedInfo", // Shows "All Y row(s) selected." (when all pages are selected)
    "allPagesInput", // Hidden input to pass all-pages selection state to server
  ];
  static values = {
    toggleKey: { type: String, default: "" }, // Keyboard shortcut to toggle focused checkbox (recommended: "x")
    baseAmount: { type: Number, default: 0 }, // Base amount for total calculation
    totalItems: { type: Number, default: 0 }, // Total items across all pages (for "select all pages" feature)
  };

  connect() {
    this.lastCheckedIndex = null;
    this.lastCheckedState = null;
    this.lastShiftEnd = null; // Track the endpoint of the last shift-selection
    this.allPagesSelected = false; // Virtual "all pages" selection state
    this.shiftHeld = false; // Track if shift key is currently held
    this.updateSelectAllState();
    this.updateActionBarVisibility();
    this.updateTotal();

    // Bind keyboard navigation
    this.handleKeydown = this.handleKeydown.bind(this);
    this.element.addEventListener("keydown", this.handleKeydown);

    // Bind global shift key listeners for anchor indicator
    this.handleGlobalKeydown = this.handleGlobalKeydown.bind(this);
    this.handleGlobalKeyup = this.handleGlobalKeyup.bind(this);
    this.handleWindowBlur = this.handleWindowBlur.bind(this);
    document.addEventListener("keydown", this.handleGlobalKeydown);
    document.addEventListener("keyup", this.handleGlobalKeyup);
    window.addEventListener("blur", this.handleWindowBlur);
  }

  disconnect() {
    this.element.removeEventListener("keydown", this.handleKeydown);
    document.removeEventListener("keydown", this.handleGlobalKeydown);
    document.removeEventListener("keyup", this.handleGlobalKeyup);
    window.removeEventListener("blur", this.handleWindowBlur);
    this.clearAnchorIndicator();
  }

  // Handle global keydown to show anchor indicator when shift is pressed
  handleGlobalKeydown(event) {
    if (event.key === "Shift" && !this.shiftHeld) {
      this.shiftHeld = true;
      this.updateAnchorIndicator();
    }
  }

  // Handle global keyup to hide anchor indicator when shift is released
  handleGlobalKeyup(event) {
    if (event.key === "Shift") {
      this.shiftHeld = false;
      this.clearAnchorIndicator();
    }
  }

  // Handle window blur to clear shift state (in case shift is released while window is not focused)
  handleWindowBlur() {
    this.shiftHeld = false;
    this.clearAnchorIndicator();
  }

  // Show dashed outline on the anchor checkbox when shift is held
  updateAnchorIndicator() {
    this.clearAnchorIndicator();

    if (this.shiftHeld && this.lastCheckedIndex !== null) {
      const anchorCheckbox = this.checkboxTargets[this.lastCheckedIndex];
      if (anchorCheckbox) {
        anchorCheckbox.classList.add("checkbox-anchor");
      }
    }
  }

  // Remove anchor indicator from all checkboxes
  clearAnchorIndicator() {
    this.checkboxTargets.forEach((cb) => {
      cb.classList.remove("checkbox-anchor");
    });
  }

  // Check if we have more items than visible on current page
  get hasMultiplePages() {
    return this.hasTotalItemsValue && this.totalItemsValue > this.checkboxTargets.length;
  }

  // Check if all checkboxes on current page are selected
  get allOnPageSelected() {
    const enabledCheckboxes = this.checkboxTargets.filter((cb) => !cb.disabled);
    return enabledCheckboxes.length > 0 && enabledCheckboxes.every((cb) => cb.checked);
  }

  // Toggle all checkboxes when the select-all checkbox is clicked
  toggleAll(event) {
    const isChecked = event.target.checked;

    // Reset "all pages" selection when manually toggling
    this.allPagesSelected = false;

    // Select ALL checkboxes including nested ones
    this.checkboxTargets.forEach((checkbox) => {
      if (!checkbox.disabled) {
        checkbox.checked = isChecked;
        checkbox.indeterminate = false;
      }
    });

    // Also update any parent checkboxes that aren't in the checkbox targets
    this.element
      .querySelectorAll('[data-checkbox-select-all-target="parent"] input[type="checkbox"]')
      .forEach((parentCheckbox) => {
        if (!parentCheckbox.disabled) {
          parentCheckbox.checked = isChecked;
          parentCheckbox.indeterminate = false;
        }
      });

    this.lastCheckedIndex = null;
    this.lastCheckedState = null;
    this.lastShiftEnd = null;
    this.clearAnchorIndicator();
    this.updateSelectAllState();
    this.updateActionBarVisibility();
    this.updateTotal();
  }

  // Select all items across ALL pages (virtual selection)
  // This doesn't load any data, just sets a flag that can be passed to the server
  selectAllPages() {
    // First, ensure all checkboxes on current page are selected
    this.checkboxTargets.forEach((checkbox) => {
      if (!checkbox.disabled) {
        checkbox.checked = true;
        checkbox.indeterminate = false;
      }
    });

    if (this.hasSelectAllTarget) {
      this.selectAllTarget.checked = true;
      this.selectAllTarget.indeterminate = false;
    }

    // Set virtual "all pages" selection
    this.allPagesSelected = true;

    this.updateActionBarVisibility();
    this.updateTotal();
  }

  // Clear "all pages" selection and uncheck everything
  clearAllPages() {
    this.allPagesSelected = false;
    this.clearAll();
  }

  // Clear all selections (current page only, resets all-pages state)
  clearAll() {
    // Reset "all pages" selection
    this.allPagesSelected = false;

    this.checkboxTargets.forEach((checkbox) => {
      if (!checkbox.disabled) {
        checkbox.checked = false;
        checkbox.indeterminate = false;
      }
    });

    // Clear parent checkboxes too
    this.element
      .querySelectorAll('[data-checkbox-select-all-target="parent"] input[type="checkbox"]')
      .forEach((parentCheckbox) => {
        if (!parentCheckbox.disabled) {
          parentCheckbox.checked = false;
          parentCheckbox.indeterminate = false;
        }
      });

    if (this.hasSelectAllTarget) {
      this.selectAllTarget.checked = false;
      this.selectAllTarget.indeterminate = false;
    }

    this.lastCheckedIndex = null;
    this.lastCheckedState = null;
    this.lastShiftEnd = null;
    this.clearAnchorIndicator();
    this.updateActionBarVisibility();
    this.updateTotal();
  }

  // Handle individual checkbox clicks
  toggle(event) {
    const checkbox = event.target;
    const currentIndex = this.checkboxTargets.indexOf(checkbox);

    // If the clicked element isn't one of our targets, ignore
    if (currentIndex === -1) return;

    // Reset "all pages" selection when manually toggling individual items
    this.allPagesSelected = false;

    // Handle shift-click for batch selection
    if (event.shiftKey && this.lastCheckedIndex !== null) {
      const anchor = this.lastCheckedIndex;
      const targetState = this.lastCheckedState;

      const newStart = Math.min(anchor, currentIndex);
      const newEnd = Math.max(anchor, currentIndex);

      // If we had a previous shift selection, handle items outside the new range
      if (this.lastShiftEnd !== null) {
        const prevStart = Math.min(anchor, this.lastShiftEnd);
        const prevEnd = Math.max(anchor, this.lastShiftEnd);

        // Deselect items that were in the previous range but not in the new range
        this.checkboxTargets.forEach((cb, index) => {
          if (!cb.disabled) {
            const wasInPrevRange = index >= prevStart && index <= prevEnd;
            const isInNewRange = index >= newStart && index <= newEnd;

            if (wasInPrevRange && !isInNewRange) {
              // This item was selected by previous shift-click but is outside new range
              cb.checked = !targetState;
              cb.indeterminate = false;
            }
          }
        });
      }

      // Apply the target state to all items in the new range
      this.checkboxTargets.forEach((cb, index) => {
        if (!cb.disabled && index >= newStart && index <= newEnd) {
          cb.checked = targetState;
          cb.indeterminate = false;
        }
      });

      // Remember this shift end for future shift-clicks
      this.lastShiftEnd = currentIndex;

      // We do NOT update lastCheckedIndex on shift-click, preserving the original anchor
    } else {
      // Normal click - this becomes the new anchor
      this.lastCheckedIndex = currentIndex;
      this.lastCheckedState = checkbox.checked;
      this.lastShiftEnd = null; // Reset shift selection tracking
      this.updateAnchorIndicator(); // Update visual indicator for new anchor
    }

    this.updateAllParentStates();
    this.updateSelectAllState();
    this.updateActionBarVisibility();
    this.updateTotal();
  }

  // Update the select-all checkbox state based on individual checkboxes
  updateSelectAllState() {
    if (!this.hasSelectAllTarget) return;

    // Get all leaf checkboxes (checkboxes that don't have children)
    // These are checkboxes that are NOT parent checkboxes
    const leafCheckboxes = this.checkboxTargets.filter((cb) => {
      if (cb.disabled) return false;
      // Check if this checkbox is a parent (has a parent container with children)
      const parentContainer = cb.closest('[data-checkbox-select-all-target="parent"]');
      if (!parentContainer) return true; // Not in a parent container, so it's a leaf

      // If it's in a parent container, check if it's the parent checkbox itself
      const parentCheckbox = parentContainer.querySelector('input[type="checkbox"]');
      return cb !== parentCheckbox; // Only include if it's not the parent checkbox
    });

    const checkedCount = leafCheckboxes.filter((cb) => cb.checked).length;

    if (checkedCount === 0) {
      this.selectAllTarget.checked = false;
      this.selectAllTarget.indeterminate = false;
    } else if (checkedCount === leafCheckboxes.length) {
      this.selectAllTarget.checked = true;
      this.selectAllTarget.indeterminate = false;
    } else {
      this.selectAllTarget.checked = false;
      this.selectAllTarget.indeterminate = true;
    }
  }

  // Update visibility of action bar based on selection state
  updateActionBarVisibility() {
    if (!this.hasActionBarTarget) return;

    const checkedCount = this.checkboxTargets.filter((cb) => cb.checked && !cb.disabled).length;
    const hasSelection = checkedCount > 0 || this.allPagesSelected;

    // Show/hide action bar
    this.actionBarTargets.forEach((actionBar) => {
      actionBar.hidden = !hasSelection;
    });

    // Update "select all pages" UI elements
    this.updateSelectAllPagesUI(checkedCount);

    // Update hidden input for form submission
    this.updateAllPagesInput();

    this.updateCount();
  }

  // Update the "select all pages" UI elements
  updateSelectAllPagesUI(checkedCount) {
    const showSelectAllPrompt = this.allOnPageSelected && this.hasMultiplePages && !this.allPagesSelected;
    const showAllPagesSelected = this.allPagesSelected;
    const showPageSelection = !showAllPagesSelected;

    // "X of Y row(s) selected." - shown when NOT in all-pages mode
    this.pageSelectionInfoTargets.forEach((el) => {
      el.classList.toggle("hidden", !showPageSelection);
    });

    // "Select all Y rows" prompt - shown when all on page selected but not all pages
    this.selectAllPagesPromptTargets.forEach((el) => {
      el.classList.toggle("hidden", !showSelectAllPrompt);
    });

    // "All Y row(s) selected." - shown when all pages selected
    this.allPagesSelectedInfoTargets.forEach((el) => {
      el.classList.toggle("hidden", !showAllPagesSelected);
    });
  }

  // Update hidden input for form submission
  // This allows the server to know if "all pages" is selected
  updateAllPagesInput() {
    this.allPagesInputTargets.forEach((input) => {
      input.value = this.allPagesSelected ? "true" : "false";
      input.disabled = !this.allPagesSelected;
    });
  }

  // Update the count display
  updateCount() {
    if (!this.hasCountTarget) return;

    // When all pages selected, show total count; otherwise show checked count
    const displayCount = this.allPagesSelected
      ? this.totalItemsValue
      : this.checkboxTargets.filter((cb) => cb.checked && !cb.disabled).length;

    this.countTargets.forEach((count) => {
      count.textContent = `${displayCount}`;
    });
  }

  // Update the total by summing amounts from checked items
  updateTotal() {
    if (!this.hasTotalTarget) return;

    // Start with the base amount (e.g., subscription fee)
    let total = this.baseAmountValue;

    // For each checked checkbox, find its associated amount element
    this.checkboxTargets.forEach((checkbox) => {
      if (checkbox.checked && !checkbox.disabled) {
        // Find the amount target in the same container as the checkbox
        const container = checkbox.closest('[data-controller*="checkbox-select-all"]') || this.element;
        const allAmounts = Array.from(container.querySelectorAll('[data-checkbox-select-all-target*="amount"]'));

        // Find the amount that's in the same parent as this checkbox
        const checkboxParent = checkbox.closest("label, div, tr, li");
        if (checkboxParent) {
          const amount = allAmounts.find((amt) => checkboxParent.contains(amt));
          if (amount) {
            // Parse the number from the amount's text content
            // Remove currency symbols, commas, /mo, /year, etc.
            const text = amount.textContent.trim();
            const numberMatch = text.match(/[\d,]+\.?\d*/);
            if (numberMatch) {
              const value = parseFloat(numberMatch[0].replace(/,/g, ""));
              if (!isNaN(value)) {
                total += value;
              }
            }
          }
        }
      }
    });

    // Update all total targets with the calculated sum
    this.totalTargets.forEach((totalElement) => {
      // Get the original format from the element (to preserve currency symbols, etc.)
      const originalText = totalElement.textContent;
      const currencyMatch = originalText.match(/^[^\d]*/);
      const suffixMatch = originalText.match(/[^\d,.]+$/);

      const prefix = currencyMatch ? currencyMatch[0] : "$";
      const suffix = suffixMatch ? suffixMatch[0] : "";

      // Format with 2 decimal places
      const formattedTotal = total.toFixed(2);

      totalElement.textContent = `${prefix}${formattedTotal}${suffix}`;
    });
  }

  // Update state of all parent groups
  updateAllParentStates() {
    this.element.querySelectorAll('[data-checkbox-select-all-target="parent"]').forEach((container) => {
      this.updateChildrenState(container);
    });
  }

  // Update children checkboxes when parent is toggled
  toggleChildren(event) {
    const parentCheckbox = event.target;
    const isChecked = parentCheckbox.checked;

    const container = parentCheckbox.closest('[data-checkbox-select-all-target="parent"]');
    if (!container) return;

    // Select ALL child checkboxes at ALL nesting levels within this parent
    const allChildCheckboxes = container.querySelectorAll('input[type="checkbox"]');
    allChildCheckboxes.forEach((checkbox) => {
      // Skip the parent checkbox itself
      if (checkbox !== parentCheckbox && !checkbox.disabled) {
        checkbox.checked = isChecked;
        checkbox.indeterminate = false;
      }
    });

    // Ensure parent stays in the correct state (checked or unchecked, not indeterminate)
    parentCheckbox.indeterminate = false;

    this.updateAllParentStates();
    this.updateSelectAllState();
    this.updateActionBarVisibility();
    this.updateTotal();
  }

  // Update parent state based on children
  updateChildrenState(container) {
    const parentCheckboxInput = container.querySelector('input[type="checkbox"]');
    if (!parentCheckboxInput) return;

    // Get all checkboxes in the container
    const allCheckboxes = Array.from(container.querySelectorAll('input[type="checkbox"]'));

    // Filter to get only children (exclude the parent itself)
    const childCheckboxes = allCheckboxes.filter((cb) => cb !== parentCheckboxInput && !cb.disabled);

    const checkedChildren = childCheckboxes.filter((cb) => cb.checked).length;

    if (checkedChildren === 0) {
      parentCheckboxInput.checked = false;
      parentCheckboxInput.indeterminate = false;
    } else if (checkedChildren === childCheckboxes.length) {
      parentCheckboxInput.checked = true;
      parentCheckboxInput.indeterminate = false;
    } else {
      parentCheckboxInput.checked = false;
      parentCheckboxInput.indeterminate = true;
    }
  }

  // Keyboard navigation handler
  handleKeydown(event) {
    const activeElement = document.activeElement;

    // Check if the active element is the select-all checkbox
    const isSelectAll = this.hasSelectAllTarget && activeElement === this.selectAllTarget;

    // Check if the active element is one of our checkboxes
    const currentIndex = this.checkboxTargets.indexOf(activeElement);

    // If it's neither, ignore
    if (!isSelectAll && currentIndex === -1) return;

    // Handle arrow key navigation (ArrowDown and ArrowRight move forward, ArrowUp and ArrowLeft move backward)
    if (event.key === "ArrowDown" || event.key === "ArrowRight") {
      event.preventDefault();
      if (isSelectAll) {
        // From select-all, go to first checkbox
        this.focusCheckboxAtIndex(0);
      } else if (currentIndex < this.checkboxTargets.length - 1) {
        // Go to next checkbox
        this.focusNextCheckbox(currentIndex);
      } else {
        // At the last checkbox, loop back to select-all or first checkbox
        if (this.hasSelectAllTarget) {
          this.selectAllTarget.focus();
        } else {
          this.focusCheckboxAtIndex(0);
        }
      }
    } else if (event.key === "ArrowUp" || event.key === "ArrowLeft") {
      event.preventDefault();
      if (isSelectAll) {
        // From select-all, loop to last checkbox
        this.focusCheckboxAtIndex(this.checkboxTargets.length - 1);
      } else if (currentIndex > 0) {
        // Go to previous checkbox
        this.focusPreviousCheckbox(currentIndex);
      } else {
        // At the first checkbox, go to select-all or loop to last
        if (this.hasSelectAllTarget) {
          this.selectAllTarget.focus();
        } else {
          this.focusCheckboxAtIndex(this.checkboxTargets.length - 1);
        }
      }
    }
    // Handle toggle key (if configured) - case insensitive
    else if (this.hasToggleKeyValue && event.key.toLowerCase() === this.toggleKeyValue.toLowerCase()) {
      event.preventDefault();

      // Only handle toggle for regular checkboxes (not select-all)
      if (!isSelectAll) {
        // Shift + toggle key: batch toggle like shift-click
        if (event.shiftKey && this.lastCheckedIndex !== null) {
          const anchor = this.lastCheckedIndex;
          const targetState = this.lastCheckedState;

          const newStart = Math.min(anchor, currentIndex);
          const newEnd = Math.max(anchor, currentIndex);

          // If we had a previous shift selection, handle items outside the new range
          if (this.lastShiftEnd !== null) {
            const prevStart = Math.min(anchor, this.lastShiftEnd);
            const prevEnd = Math.max(anchor, this.lastShiftEnd);

            // Deselect items that were in the previous range but not in the new range
            this.checkboxTargets.forEach((cb, index) => {
              if (!cb.disabled) {
                const wasInPrevRange = index >= prevStart && index <= prevEnd;
                const isInNewRange = index >= newStart && index <= newEnd;

                if (wasInPrevRange && !isInNewRange) {
                  cb.checked = !targetState;
                  cb.indeterminate = false;
                }
              }
            });
          }

          // Apply the target state to all items in the new range
          this.checkboxTargets.forEach((cb, index) => {
            if (!cb.disabled && index >= newStart && index <= newEnd) {
              cb.checked = targetState;
              cb.indeterminate = false;
            }
          });

          // Remember this shift end
          this.lastShiftEnd = currentIndex;

          this.updateAllParentStates();
          this.updateSelectAllState();
          this.updateActionBarVisibility();
          this.updateTotal();
        } else {
          // Normal toggle: just click the focused checkbox
          activeElement.click();
        }
      } else {
        // For select-all, just click it
        activeElement.click();
      }
    }
  }

  // Focus a checkbox at a specific index
  focusCheckboxAtIndex(index) {
    if (index >= 0 && index < this.checkboxTargets.length) {
      const checkbox = this.checkboxTargets[index];
      if (!checkbox.disabled) {
        checkbox.focus();
        this.scrollToCheckbox(checkbox);
      } else {
        // If disabled, try the next one
        if (index < this.checkboxTargets.length - 1) {
          this.focusCheckboxAtIndex(index + 1);
        }
      }
    }
  }

  // Focus the next enabled checkbox
  focusNextCheckbox(currentIndex) {
    const nextIndex = currentIndex + 1;
    if (nextIndex < this.checkboxTargets.length) {
      const nextCheckbox = this.checkboxTargets[nextIndex];
      if (!nextCheckbox.disabled) {
        nextCheckbox.focus();
        this.scrollToCheckbox(nextCheckbox);
      } else {
        // Skip disabled checkboxes
        this.focusNextCheckbox(nextIndex);
      }
    }
  }

  // Focus the previous enabled checkbox
  focusPreviousCheckbox(currentIndex) {
    const prevIndex = currentIndex - 1;
    if (prevIndex >= 0) {
      const prevCheckbox = this.checkboxTargets[prevIndex];
      if (!prevCheckbox.disabled) {
        prevCheckbox.focus();
        this.scrollToCheckbox(prevCheckbox);
      } else {
        // Skip disabled checkboxes
        this.focusPreviousCheckbox(prevIndex);
      }
    }
  }

  // Scroll checkbox into view with padding
  scrollToCheckbox(checkbox) {
    // Find the row element (for table layouts)
    const row = checkbox.closest("tr");
    const element = row || checkbox;

    // Find the scrollable container
    const content = element.closest(".overflow-y-auto, .overflow-auto");
    if (!content) return;

    const contentRect = content.getBoundingClientRect();
    const elementRect = element.getBoundingClientRect();

    // Find sticky elements that reduce visible scroll area
    const stickyHeader = content.querySelector(".sticky.top-0, thead .sticky");
    const stickyFooter = content.querySelector(".sticky.bottom-0");

    const stickyHeaderHeight = stickyHeader ? stickyHeader.offsetHeight : 0;
    const stickyFooterHeight = stickyFooter && !stickyFooter.hidden ? stickyFooter.offsetHeight : 0;

    // Calculate the element's position relative to the scrollable content
    const elementRelativeTop = elementRect.top - contentRect.top;
    const elementRelativeBottom = elementRelativeTop + elementRect.height;
    const contentScrollTop = content.scrollTop;
    const contentHeight = contentRect.height;

    // Define padding: scroll when the element is within this distance from the edge
    const scrollPadding = elementRect.height * 1;

    // Adjust visible area by excluding sticky elements
    const visibleTop = stickyHeaderHeight + scrollPadding;
    const visibleBottom = contentHeight - stickyFooterHeight - scrollPadding;

    // Check if we need to scroll down (element is too far down)
    if (elementRelativeBottom > visibleBottom) {
      content.scrollTop = contentScrollTop + (elementRelativeBottom - visibleBottom);
    }
    // Check if we need to scroll up (element is too far up)
    else if (elementRelativeTop < visibleTop) {
      content.scrollTop = contentScrollTop + (elementRelativeTop - visibleTop);
    }
  }
}

2. Custom CSS

Here are the custom CSS classes that we used on Rails Blocks to style the forms & checkboxes. You can copy and paste these into your own CSS file to style & personalize your forms.

/* Forms */

label,
.label {
  @apply text-sm/6 font-medium text-neutral-700;
  @apply dark:text-neutral-100;
}

.form-input[disabled] {
  @apply cursor-not-allowed bg-neutral-200;
}

/* Custom search input clear button styling */
input[type="search"]::-webkit-search-cancel-button {
  -webkit-appearance: none;
  appearance: none;
  height: 16px;
  width: 16px;
  cursor: pointer;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cg fill='%236b7280'%3E%3Cpath d='m2.25,10.5c-.192,0-.384-.073-.53-.22-.293-.293-.293-.768,0-1.061L9.22,1.72c.293-.293.768-.293,1.061,0s.293.768,0,1.061l-7.5,7.5c-.146.146-.338.22-.53.22Z' stroke-width='0'%3E%3C/path%3E%3Cpath d='m9.75,10.5c-.192,0-.384-.073-.53-.22L1.72,2.78c-.293-.293-.293-.768,0-1.061s.768-.293,1.061,0l7.5,7.5c.293.293.293.768,0,1.061-.146.146-.338.22-.53.22Z' stroke-width='0'%3E%3C/path%3E%3C/g%3E%3C/svg%3E");
  background-size: 12px 12px;
  background-repeat: no-repeat;
  background-position: center;
}

input[type="search"]::-webkit-search-cancel-button:hover {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cg fill='%23374151'%3E%3Cpath d='m2.25,10.5c-.192,0-.384-.073-.53-.22-.293-.293-.293-.768,0-1.061L9.22,1.72c.293-.293.768-.293,1.061,0s.293.768,0,1.061l-7.5,7.5c-.146.146-.338.22-.53.22Z' stroke-width='0'%3E%3C/path%3E%3Cpath d='m9.75,10.5c-.192,0-.384-.073-.53-.22L1.72,2.78c-.293-.293-.293-.768,0-1.061s.768-.293,1.061,0l7.5,7.5c.293.293.293.768,0,1.061-.146.146-.338.22-.53.22Z' stroke-width='0'%3E%3C/path%3E%3C/g%3E%3C/svg%3E");
}

.dark input[type="search"]::-webkit-search-cancel-button {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cg fill='%23d1d5db'%3E%3Cpath d='m2.25,10.5c-.192,0-.384-.073-.53-.22-.293-.293-.293-.768,0-1.061L9.22,1.72c.293-.293.768-.293,1.061,0s.293.768,0,1.061l-7.5,7.5c-.146.146-.338.22-.53.22Z' stroke-width='0'%3E%3C/path%3E%3Cpath d='m9.75,10.5c-.192,0-.384-.073-.53-.22L1.72,2.78c-.293-.293-.293-.768,0-1.061s.768-.293,1.061,0l7.5,7.5c.293.293.293.768,0,1.061-.146.146-.338.22-.53.22Z' stroke-width='0'%3E%3C/path%3E%3C/g%3E%3C/svg%3E");
}

.dark input[type="search"]::-webkit-search-cancel-button:hover {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cg fill='%23f3f4f6'%3E%3Cpath d='m2.25,10.5c-.192,0-.384-.073-.53-.22-.293-.293-.293-.768,0-1.061L9.22,1.72c.293-.293.768-.293,1.061,0s.293.768,0,1.061l-7.5,7.5c-.146.146-.338.22-.53.22Z' stroke-width='0'%3E%3C/path%3E%3Cpath d='m9.75,10.5c-.192,0-.384-.073-.53-.22L1.72,2.78c-.293-.293-.293-.768,0-1.061s.768-.293,1.061,0l7.5,7.5c.293.293.293.768,0,1.061-.146.146-.338.22-.53.22Z' stroke-width='0'%3E%3C/path%3E%3C/g%3E%3C/svg%3E");
}

/* non-input elements (like the Stripe card form) can be styled to look like an input */
div.form-control {
  -webkit-appearance: none;
  -moz-appearance: none;
  appearance: none;
  background-color: #fff;
  border-width: 1px;
  padding-top: 0.5rem;
  padding-right: 0.75rem;
  padding-bottom: 0.5rem;
  padding-left: 0.75rem;
  font-size: 1rem;
  line-height: 1.5rem;
}

.form-control {
  @apply block w-full rounded-lg bg-white border-0 px-3 py-2 text-base/6 text-neutral-900 shadow-xs ring-1 ring-neutral-300 outline-hidden ring-inset placeholder:text-neutral-500 focus:ring-2 focus:ring-neutral-600 dark:bg-neutral-700 dark:text-white dark:placeholder-neutral-300 dark:ring-neutral-600 dark:focus:ring-neutral-500;
}

@media (min-width: 640px) {
  .form-control {
    font-size: 0.875rem; /* text-sm equivalent (14px) for larger screens */
  }
}

.form-control[disabled] {
  @apply cursor-not-allowed bg-neutral-100 dark:bg-neutral-600;
}

.form-control.error {
  @apply border-red-400 ring-red-300 focus:ring-red-500 dark:border-red-600 dark:ring-red-500;
}

select:not([multiple]) {
  @apply w-full appearance-none rounded-lg border-0 bg-white px-3 py-2 text-base/6 text-neutral-900 shadow-xs ring-1 ring-neutral-300 outline-hidden ring-inset focus:ring-2 focus:ring-neutral-600;

  /* Custom dropdown arrow */
  background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%23737373' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
  background-position: right 0.75rem center;
  background-repeat: no-repeat;
  background-size: 1.25em 1.25em;
  padding-right: 2.5rem;
}

@media (min-width: 640px) {
  select:not([multiple]) {
    font-size: 0.875rem; /* text-sm equivalent (14px) for larger screens */
  }
}

/* Dark mode styling for single select */
.dark {
  select:not([multiple]) {
    @apply dark:bg-neutral-700 dark:text-white dark:ring-neutral-600 dark:focus:ring-neutral-500;
    background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%23A1A1AA' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
  }
}

select:not([multiple])[disabled] {
  @apply cursor-not-allowed bg-neutral-100 opacity-75 ring-neutral-200 dark:bg-neutral-600 dark:ring-neutral-500;
}

select[multiple] {
  @apply w-full rounded-lg rounded-r-none border-0 bg-white px-3 py-2.5 text-base/6 text-neutral-900 shadow-xs outline-1 -outline-offset-1 outline-neutral-300 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-neutral-600 dark:outline-neutral-600;
  min-height: 120px;
}

select[multiple] option {
  @apply rounded-md;
}

@media (min-width: 640px) {
  select[multiple] {
    font-size: 0.875rem; /* text-sm equivalent (14px) for larger screens */
  }
}

/* Dark mode styling for multiple select */
.dark {
  select[multiple] {
    @apply dark:bg-neutral-700 dark:text-white dark:ring-neutral-600 dark:focus:ring-neutral-500;
  }
}

select[multiple][disabled] {
  @apply cursor-not-allowed bg-neutral-100 opacity-75 ring-neutral-200 dark:bg-neutral-600 dark:ring-neutral-500;
}

option {
  @apply bg-white px-3 py-2 text-sm text-neutral-900 dark:bg-neutral-700 dark:text-neutral-100;
}

option:checked {
  @apply bg-neutral-100 dark:bg-neutral-600;
}

option:hover {
  @apply bg-neutral-50 dark:bg-neutral-600;
}

.caret {
  @apply pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-neutral-800;
}

[type="checkbox"] {
  @apply size-4 cursor-pointer appearance-none rounded-sm border border-neutral-300 bg-white checked:border-neutral-700 checked:bg-neutral-700 focus:outline-2 focus:outline-offset-2 focus:outline-neutral-600 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 disabled:border-neutral-300 disabled:bg-neutral-100 disabled:checked:bg-neutral-100 dark:border-white/20 dark:bg-neutral-800 dark:checked:border-white/20 dark:checked:bg-neutral-900 dark:focus:outline-neutral-200 dark:focus-visible:outline-neutral-200 dark:disabled:border-neutral-500 dark:disabled:bg-neutral-400 dark:disabled:checked:bg-neutral-500 forced-colors:appearance-auto;
}

[type="checkbox"]:checked {
  @apply text-white dark:text-neutral-800;
  background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e");
  background-size: 100% 100%;
  background-position: center;
  background-repeat: no-repeat;
}

[type="checkbox"]:indeterminate {
  @apply border-neutral-400 bg-neutral-500 dark:border-white/20 dark:bg-neutral-700;
  background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3e%3cg fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' %3e%3cline x1='10.75' y1='6' x2='1.25' y2='6'%3e%3c/line%3e%3c/g%3e%3c/svg%3e");
  background-size: 75% 75%;
  background-position: center;
  background-repeat: no-repeat;
}

[type="checkbox"]:disabled {
  @apply cursor-not-allowed border-neutral-300 bg-neutral-300 text-neutral-400 opacity-75 hover:text-neutral-300 dark:border-neutral-600 dark:bg-neutral-700 dark:text-neutral-300 dark:hover:text-neutral-500;
}

[type="checkbox"]:disabled:checked {
  @apply border-neutral-300 dark:border-neutral-600 dark:bg-neutral-600;
}

/* Anchor indicator for shift-click range selection */
[type="checkbox"].checkbox-anchor {
  outline: 2px dashed currentColor;
  outline-offset: 2px;
  @apply outline-neutral-600 dark:outline-neutral-200;
}

[type="radio"] {
  @apply size-4 cursor-pointer appearance-none rounded-full border border-neutral-300 bg-white checked:border-neutral-700 checked:bg-neutral-700 focus:outline-2 focus:outline-offset-2 focus:outline-neutral-600 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 disabled:border-neutral-300 disabled:bg-neutral-100 disabled:checked:bg-neutral-100 dark:border-white/20 dark:bg-neutral-800 dark:checked:border-white/20 dark:checked:bg-neutral-900 dark:focus:outline-neutral-200 dark:focus-visible:outline-neutral-200 dark:disabled:border-neutral-500 dark:disabled:bg-neutral-400 dark:disabled:checked:bg-neutral-500 forced-colors:appearance-auto;
}

[type="radio"]:checked {
  @apply text-white dark:text-neutral-800;
  background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3ccircle cx='8' cy='8' r='3'/%3e%3c/svg%3e");
  background-size: 100% 100%;
  background-position: center;
  background-repeat: no-repeat;
}

[type="radio"]:disabled {
  @apply cursor-not-allowed border-neutral-300 bg-neutral-300 text-neutral-400 opacity-75 hover:text-neutral-300 dark:border-neutral-600 dark:bg-neutral-700 dark:text-neutral-300 dark:hover:text-neutral-500;
}

[type="radio"]:disabled:checked {
  @apply border-neutral-300 dark:border-neutral-600 dark:bg-neutral-600;
}

/* Datalist styling */
input[list] {
  -webkit-appearance: none;
  -moz-appearance: none;
  appearance: none;
}

/* Replace default datalist arrow in WebKit browsers */
input[list].replace-default-datalist-arrow::-webkit-calendar-picker-indicator {
  display: none !important;
  -webkit-appearance: none !important;
}

input[list].replace-default-datalist-arrow {
  padding-right: 2.5rem;
  background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%23737373' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
  background-position: right 0.75rem center;
  background-repeat: no-repeat;
  background-size: 1.25em 1.25em;
}

/* Dark mode datalist arrow */
.dark {
  input[list].replace-default-datalist-arrow {
    background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%23A1A1AA' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
  }
}

Examples

Basic Checkbox Select All

A simple select-all checkbox with shift-click batch selection.

<div class="w-full max-w-md mx-auto" data-controller="checkbox-select-all" data-checkbox-select-all-toggle-key-value="x">
  <div class="bg-white dark:bg-neutral-900 rounded-xl border border-black/10 dark:border-white/10 shadow-xs overflow-hidden">
    <!-- Header with Select All -->
    <div class="px-4 py-3 border-b border-black/10 dark:border-white/10">
      <div class="flex items-center gap-x-3">
        <input
          type="checkbox"
          id="select-all-tasks"
          tabindex="0"
          data-checkbox-select-all-target="selectAll"
          data-action="click->checkbox-select-all#toggleAll">
        <label for="select-all-tasks" class="inline-block font-medium text-sm cursor-pointer select-none">
          Select All Tasks
        </label>
      </div>
    </div>

    <!-- Task List -->
    <div class="divide-y divide-black/10 dark:divide-white/10">
      <!-- Task 1 -->
      <div class="px-4 py-3 hover:bg-neutral-50 dark:hover:bg-neutral-900/50 has-[:checked]:bg-neutral-50 dark:has-[:checked]:bg-neutral-800/50">
        <div class="flex items-center gap-x-3">
          <input
            type="checkbox"
            id="task-1"
            name="tasks[]"
            value="1"
            tabindex="0"
            data-checkbox-select-all-target="checkbox"
            data-action="click->checkbox-select-all#toggle">
          <label for="task-1" class="inline-block text-sm cursor-pointer select-none">
            Update landing page design
          </label>
        </div>
      </div>

      <!-- Task 2 -->
      <div class="px-4 py-3 hover:bg-neutral-50 dark:hover:bg-neutral-900/50 has-[:checked]:bg-neutral-50 dark:has-[:checked]:bg-neutral-800/50">
        <div class="flex items-center gap-x-3">
          <input
            type="checkbox"
            id="task-2"
            name="tasks[]"
            value="2"
            tabindex="0"
            data-checkbox-select-all-target="checkbox"
            data-action="click->checkbox-select-all#toggle">
          <label for="task-2" class="inline-block text-sm cursor-pointer select-none">
            Review pull requests
          </label>
        </div>
      </div>

      <!-- Task 3 -->
      <div class="px-4 py-3 hover:bg-neutral-50 dark:hover:bg-neutral-900/50 has-[:checked]:bg-neutral-50 dark:has-[:checked]:bg-neutral-800/50">
        <div class="flex items-center gap-x-3">
          <input
            type="checkbox"
            id="task-3"
            name="tasks[]"
            value="3"
            tabindex="0"
            data-checkbox-select-all-target="checkbox"
            data-action="click->checkbox-select-all#toggle">
          <label for="task-3" class="inline-block text-sm cursor-pointer select-none">
            Write documentation
          </label>
        </div>
      </div>

      <!-- Task 4 -->
      <div class="px-4 py-3 hover:bg-neutral-50 dark:hover:bg-neutral-900/50 has-[:checked]:bg-neutral-50 dark:has-[:checked]:bg-neutral-800/50">
        <div class="flex items-center gap-x-3">
          <input
            type="checkbox"
            id="task-4"
            name="tasks[]"
            value="4"
            tabindex="0"
            data-checkbox-select-all-target="checkbox"
            data-action="click->checkbox-select-all#toggle">
          <label for="task-4" class="inline-block text-sm cursor-pointer select-none">
            Fix responsive issues
          </label>
        </div>
      </div>

      <!-- Task 5 -->
      <div class="px-4 py-3 hover:bg-neutral-50 dark:hover:bg-neutral-900/50 has-[:checked]:bg-neutral-50 dark:has-[:checked]:bg-neutral-800/50">
        <div class="flex items-center gap-x-3">
          <input
            type="checkbox"
            id="task-5"
            name="tasks[]"
            value="5"
            tabindex="0"
            data-checkbox-select-all-target="checkbox"
            data-action="click->checkbox-select-all#toggle">
          <label for="task-5" class="inline-block text-sm cursor-pointer select-none">
            Deploy to production
          </label>
        </div>
      </div>

      <!-- Task 6 -->
      <div class="px-4 py-3 hover:bg-neutral-50 dark:hover:bg-neutral-900/50 has-[:checked]:bg-neutral-50 dark:has-[:checked]:bg-neutral-800/50">
        <div class="flex items-center gap-x-3">
          <input
            type="checkbox"
            id="task-6"
            name="tasks[]"
            value="6"
            tabindex="0"
            data-checkbox-select-all-target="checkbox"
            data-action="click->checkbox-select-all#toggle">
          <label for="task-6" class="inline-block text-sm cursor-pointer select-none">
            Update dependencies
          </label>
        </div>
      </div>
    </div>
  </div>
</div>

Nested Checkbox Select All

Parent checkboxes with indeterminate states that automatically reflect child selections.

<div class="w-full max-w-2xl mx-auto" data-controller="checkbox-select-all" data-checkbox-select-all-toggle-key-value="x">
  <div class="bg-white dark:bg-neutral-900 rounded-xl border border-black/10 dark:border-white/10 shadow-xs overflow-hidden">
    <!-- Header with Select All -->
    <div class="px-6 py-4 border-b border-black/10 dark:border-white/10">
      <div class="flex items-center justify-between">
        <div class="flex items-center gap-x-3">
          <input
            type="checkbox"
            id="select-all-permissions"
            tabindex="0"
            data-checkbox-select-all-target="selectAll"
            data-action="click->checkbox-select-all#toggleAll">
          <label for="select-all-permissions" class="inline-block font-semibold text-sm cursor-pointer select-none">
            Select All Permissions
          </label>
        </div>
      </div>
    </div>

    <!-- Nested Permission Groups -->
    <div class="divide-y divide-black/10 dark:divide-white/10">
      <!-- Content Management Group -->
      <div class="px-6 py-4 space-y-2" data-checkbox-select-all-target="parent">
        <div class="flex items-start gap-x-3 -mx-3 px-3 py-2 rounded-lg border border-transparent has-[:checked]:bg-neutral-100 dark:has-[:checked]:bg-neutral-800 has-[:checked]:border-black/5 dark:has-[:checked]:border-white/10">
          <input
            type="checkbox"
            id="content-parent"
            class="mt-0.5"
            tabindex="0"
            data-checkbox-select-all-target="checkbox"
            data-action="click->checkbox-select-all#toggleChildren click->checkbox-select-all#toggle">
          <label for="content-parent" class="inline-block cursor-pointer select-none flex-1">
            <div class="flex items-center gap-x-1.5 text-sm font-semibold text-neutral-900 dark:text-neutral-100">
              <svg xmlns="http://www.w3.org/2000/svg" class="size-4" width="18" height="18" viewBox="0 0 18 18"><g fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" stroke="currentColor"><path d="M2.75,15.25s3.599-.568,4.546-1.515c.947-.947,7.327-7.327,7.327-7.327,.837-.837,.837-2.194,0-3.03-.837-.837-2.194-.837-3.03,0,0,0-6.38,6.38-7.327,7.327s-1.515,4.546-1.515,4.546h0Z"></path><path d="M5.493,3.492l-.946-.315-.316-.947c-.102-.306-.609-.306-.711,0l-.316,.947-.946,.315c-.153,.051-.257,.194-.257,.356s.104,.305,.257,.356l.946,.315,.316,.947c.051,.153,.194,.256,.355,.256s.305-.104,.355-.256l.316-.947,.946-.315c.153-.051,.257-.194,.257-.356s-.104-.305-.257-.356Z" fill="currentColor" data-stroke="none" stroke="none"></path><path d="M16.658,12.99l-1.263-.421-.421-1.263c-.137-.408-.812-.408-.949,0l-.421,1.263-1.263,.421c-.204,.068-.342,.259-.342,.474s.138,.406,.342,.474l1.263,.421,.421,1.263c.068,.204,.26,.342,.475,.342s.406-.138,.475-.342l.421-1.263,1.263-.421c.204-.068,.342-.259,.342-.474s-.138-.406-.342-.474Z" fill="currentColor" data-stroke="none" stroke="none"></path><circle cx="7.75" cy="1.75" r=".75" fill="currentColor" data-stroke="none" stroke="none"></circle></g></svg>
              Content Management
            </div>
            <div class="text-xs text-neutral-500 dark:text-neutral-400 mt-0.5">
              Manage posts, pages, and media
            </div>
          </label>
        </div>

        <!-- Nested Children -->
        <div class="ml-[7px] pl-5 space-y-2 border-l-2 border-black/10 dark:border-white/10 has-[:checked]:border-neutral-400 dark:has-[:checked]:border-neutral-500">
          <div class="-ml-2 pl-2 -mr-3 pr-3 py-1.5 rounded-md border border-transparent flex items-center gap-x-3 has-[:checked]:bg-neutral-100 dark:has-[:checked]:bg-neutral-800 has-[:checked]:border-black/5 dark:has-[:checked]:border-white/10">
            <input
              type="checkbox"
              id="content-create"
              name="permissions[]"
              value="content.create"
              tabindex="0"
              data-checkbox-select-all-target="checkbox child"
              data-action="click->checkbox-select-all#toggle">
            <label for="content-create" class="inline-block text-sm cursor-pointer select-none has-[:checked]:text-neutral-900 dark:has-[:checked]:text-neutral-50 has-[:checked]:font-medium">
              Create content
            </label>
          </div>

          <div class="-ml-2 pl-2 -mr-3 pr-3 py-1.5 rounded-md border border-transparent flex items-center gap-x-3 has-[:checked]:bg-neutral-100 dark:has-[:checked]:bg-neutral-800 has-[:checked]:border-black/5 dark:has-[:checked]:border-white/10">
            <input
              type="checkbox"
              id="content-edit"
              name="permissions[]"
              value="content.edit"
              tabindex="0"
              data-checkbox-select-all-target="checkbox child"
              data-action="click->checkbox-select-all#toggle">
            <label for="content-edit" class="inline-block text-sm cursor-pointer select-none has-[:checked]:text-neutral-900 dark:has-[:checked]:text-neutral-50 has-[:checked]:font-medium">
              Edit content
            </label>
          </div>

          <div class="-ml-2 pl-2 -mr-3 pr-3 py-1.5 rounded-md border border-transparent flex items-center gap-x-3 has-[:checked]:bg-neutral-100 dark:has-[:checked]:bg-neutral-800 has-[:checked]:border-black/5 dark:has-[:checked]:border-white/10">
            <input
              type="checkbox"
              id="content-delete"
              name="permissions[]"
              value="content.delete"
              tabindex="0"
              data-checkbox-select-all-target="checkbox child"
              data-action="click->checkbox-select-all#toggle">
            <label for="content-delete" class="inline-block text-sm cursor-pointer select-none has-[:checked]:text-neutral-900 dark:has-[:checked]:text-neutral-50 has-[:checked]:font-medium">
              Delete content
            </label>
          </div>

          <!-- Publish Content with Nested Media Library -->
          <div class="mt-2" data-checkbox-select-all-target="parent">
            <div class="-ml-2 pl-2 -mr-3 pr-3 py-1.5 rounded-md border border-transparent flex items-center gap-x-3 has-[:checked]:bg-neutral-100 dark:has-[:checked]:bg-neutral-800 has-[:checked]:border-black/5 dark:has-[:checked]:border-white/10">
              <input
                type="checkbox"
                id="content-publish"
                tabindex="0"
                data-checkbox-select-all-target="checkbox child"
                data-action="click->checkbox-select-all#toggleChildren click->checkbox-select-all#toggle">
              <label for="content-publish" class="inline-block text-sm cursor-pointer select-none has-[:checked]:text-neutral-900 dark:has-[:checked]:text-neutral-50 has-[:checked]:font-medium">
                Publish content
              </label>
            </div>

            <!-- Nested options under Publish Content -->
            <div class="mt-2 ml-[7px] pl-5 space-y-2 border-l-2 border-black/10 dark:border-white/10 has-[:checked]:border-neutral-400 dark:has-[:checked]:border-neutral-500">
              <div class="-ml-2 pl-2 -mr-3 pr-3 py-1.5 rounded-md border border-transparent flex items-center gap-x-3 has-[:checked]:bg-neutral-100 dark:has-[:checked]:bg-neutral-800 has-[:checked]:border-black/5 dark:has-[:checked]:border-white/10">
                <input
                  type="checkbox"
                  id="publish-schedule"
                  name="permissions[]"
                  value="publish.schedule"
                  tabindex="0"
                  data-checkbox-select-all-target="checkbox child"
                  data-action="click->checkbox-select-all#toggle">
                <label for="publish-schedule" class="inline-block text-sm cursor-pointer select-none has-[:checked]:text-neutral-900 dark:has-[:checked]:text-neutral-50 has-[:checked]:font-medium">
                  Schedule publications
                </label>
              </div>

              <div class="-ml-2 pl-2 -mr-3 pr-3 py-1.5 rounded-md border border-transparent flex items-center gap-x-3 has-[:checked]:bg-neutral-100 dark:has-[:checked]:bg-neutral-800 has-[:checked]:border-black/5 dark:has-[:checked]:border-white/10">
                <input
                  type="checkbox"
                  id="publish-preview"
                  name="permissions[]"
                  value="publish.preview"
                  tabindex="0"
                  data-checkbox-select-all-target="checkbox child"
                  data-action="click->checkbox-select-all#toggle">
                <label for="publish-preview" class="inline-block text-sm cursor-pointer select-none has-[:checked]:text-neutral-900 dark:has-[:checked]:text-neutral-50 has-[:checked]:font-medium">
                  Preview before publish
                </label>
              </div>

              <!-- Nested Media Group (Third Level) -->
              <div class="mt-2" data-checkbox-select-all-target="parent">
                <div class="-ml-2 pl-2 -mr-3 pr-3 py-1.5 rounded-md border border-transparent flex items-center gap-x-3 has-[:checked]:bg-neutral-100 dark:has-[:checked]:bg-neutral-800 has-[:checked]:border-black/5 dark:has-[:checked]:border-white/10">
                  <input
                    type="checkbox"
                    id="media-parent"
                    tabindex="0"
                    data-checkbox-select-all-target="checkbox child"
                    data-action="click->checkbox-select-all#toggleChildren click->checkbox-select-all#toggle">
                  <label for="media-parent" class="inline-block text-sm font-medium cursor-pointer select-none">
                    Media Library
                  </label>
                </div>

                <div class="ml-[7px] pl-5 space-y-2 mt-2 border-l-2 border-black/10 dark:border-white/10 has-[:checked]:border-neutral-400 dark:has-[:checked]:border-neutral-500">
                  <div class="-ml-2 pl-2 -mr-3 pr-3 py-1.5 rounded-md border border-transparent flex items-center gap-x-3 has-[:checked]:bg-neutral-100 dark:has-[:checked]:bg-neutral-800 has-[:checked]:border-black/5 dark:has-[:checked]:border-white/10">
                    <input
                      type="checkbox"
                      id="media-upload"
                      name="permissions[]"
                      value="media.upload"
                      tabindex="0"
                      data-checkbox-select-all-target="checkbox child"
                      data-action="click->checkbox-select-all#toggle">
                    <label for="media-upload" class="inline-block text-sm cursor-pointer select-none has-[:checked]:text-neutral-900 dark:has-[:checked]:text-neutral-50 has-[:checked]:font-medium">
                      Upload files
                    </label>
                  </div>

                  <div class="-ml-2 pl-2 -mr-3 pr-3 py-1.5 rounded-md border border-transparent flex items-center gap-x-3 has-[:checked]:bg-neutral-100 dark:has-[:checked]:bg-neutral-800 has-[:checked]:border-black/5 dark:has-[:checked]:border-white/10">
                    <input
                      type="checkbox"
                      id="media-delete"
                      name="permissions[]"
                      value="media.delete"
                      tabindex="0"
                      data-checkbox-select-all-target="checkbox child"
                      data-action="click->checkbox-select-all#toggle">
                    <label for="media-delete" class="inline-block text-sm cursor-pointer select-none has-[:checked]:text-neutral-900 dark:has-[:checked]:text-neutral-50 has-[:checked]:font-medium">
                      Delete files
                    </label>
                  </div>
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>

      <!-- User Management Group -->
      <div class="px-6 py-4 space-y-2" data-checkbox-select-all-target="parent">
        <div class="flex items-start gap-x-3 -mx-3 px-3 py-2 rounded-lg border border-transparent has-[:checked]:bg-neutral-100 dark:has-[:checked]:bg-neutral-800 has-[:checked]:border-black/5 dark:has-[:checked]:border-white/10">
          <input
            type="checkbox"
            id="users-parent"
            class="mt-0.5"
            tabindex="0"
            data-checkbox-select-all-target="checkbox"
            data-action="click->checkbox-select-all#toggleChildren click->checkbox-select-all#toggle">
          <label for="users-parent" class="inline-block cursor-pointer select-none flex-1">
            <div class="flex items-center gap-x-1.5 text-sm font-semibold text-neutral-900 dark:text-neutral-100">
              <svg xmlns="http://www.w3.org/2000/svg" class="size-4" width="18" height="18" viewBox="0 0 18 18"><g fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" stroke="currentColor"><circle cx="9" cy="4.5" r="2.75"></circle><path d="M13.762,15.516c.86-.271,1.312-1.221,.947-2.045-.97-2.191-3.159-3.721-5.709-3.721s-4.739,1.53-5.709,3.721c-.365,.825,.087,1.774,.947,2.045,1.225,.386,2.846,.734,4.762,.734s3.537-.348,4.762-.734Z"></path></g></svg>
              User Management
            </div>
            <div class="text-xs text-neutral-500 dark:text-neutral-400 mt-0.5">
              Manage users and roles
            </div>
          </label>
        </div>

        <!-- Nested Children -->
        <div class="ml-[7px] pl-5 space-y-2 border-l-2 border-black/10 dark:border-white/10 has-[:checked]:border-neutral-400 dark:has-[:checked]:border-neutral-500">
          <div class="-ml-2 pl-2 -mr-3 pr-3 py-1.5 rounded-md border border-transparent flex items-center gap-x-3 has-[:checked]:bg-neutral-100 dark:has-[:checked]:bg-neutral-800 has-[:checked]:border-black/5 dark:has-[:checked]:border-white/10">
            <input
              type="checkbox"
              id="users-view"
              name="permissions[]"
              value="users.view"
              tabindex="0"
              data-checkbox-select-all-target="checkbox child"
              data-action="click->checkbox-select-all#toggle">
            <label for="users-view" class="inline-block text-sm cursor-pointer select-none has-[:checked]:text-neutral-900 dark:has-[:checked]:text-neutral-50 has-[:checked]:font-medium">
              View users
            </label>
          </div>

          <div class="-ml-2 pl-2 -mr-3 pr-3 py-1.5 rounded-md border border-transparent flex items-center gap-x-3 has-[:checked]:bg-neutral-100 dark:has-[:checked]:bg-neutral-800 has-[:checked]:border-black/5 dark:has-[:checked]:border-white/10">
            <input
              type="checkbox"
              id="users-create"
              name="permissions[]"
              value="users.create"
              tabindex="0"
              data-checkbox-select-all-target="checkbox child"
              data-action="click->checkbox-select-all#toggle">
            <label for="users-create" class="inline-block text-sm cursor-pointer select-none has-[:checked]:text-neutral-900 dark:has-[:checked]:text-neutral-50 has-[:checked]:font-medium">
              Create users
            </label>
          </div>

          <div class="-ml-2 pl-2 -mr-3 pr-3 py-1.5 rounded-md border border-transparent flex items-center gap-x-3 has-[:checked]:bg-neutral-100 dark:has-[:checked]:bg-neutral-800 has-[:checked]:border-black/5 dark:has-[:checked]:border-white/10">
            <input
              type="checkbox"
              id="users-edit"
              name="permissions[]"
              value="users.edit"
              tabindex="0"
              data-checkbox-select-all-target="checkbox child"
              data-action="click->checkbox-select-all#toggle">
            <label for="users-edit" class="inline-block text-sm cursor-pointer select-none has-[:checked]:text-neutral-900 dark:has-[:checked]:text-neutral-50 has-[:checked]:font-medium">
              Edit users
            </label>
          </div>

          <div class="-ml-2 pl-2 -mr-3 pr-3 py-1.5 rounded-md border border-transparent flex items-center gap-x-3 has-[:checked]:bg-neutral-100 dark:has-[:checked]:bg-neutral-800 has-[:checked]:border-black/5 dark:has-[:checked]:border-white/10">
            <input
              type="checkbox"
              id="users-delete"
              name="permissions[]"
              value="users.delete"
              tabindex="0"
              data-checkbox-select-all-target="checkbox child"
              data-action="click->checkbox-select-all#toggle">
            <label for="users-delete" class="inline-block text-sm cursor-pointer select-none has-[:checked]:text-neutral-900 dark:has-[:checked]:text-neutral-50 has-[:checked]:font-medium">
              Delete users
            </label>
          </div>
        </div>
      </div>

      <!-- Settings Group -->
      <div class="px-6 py-4 space-y-2" data-checkbox-select-all-target="parent">
        <div class="flex items-start gap-x-3 -mx-3 px-3 py-2 rounded-lg border border-transparent has-[:checked]:bg-neutral-100 dark:has-[:checked]:bg-neutral-800 has-[:checked]:border-black/5 dark:has-[:checked]:border-white/10">
          <input
            type="checkbox"
            id="settings-parent"
            class="mt-0.5"
            tabindex="0"
            data-checkbox-select-all-target="checkbox"
            data-action="click->checkbox-select-all#toggleChildren click->checkbox-select-all#toggle">
          <label for="settings-parent" class="inline-block cursor-pointer select-none flex-1">
            <div class="flex items-center gap-x-1.5 text-sm font-semibold text-neutral-900 dark:text-neutral-100">
              <svg xmlns="http://www.w3.org/2000/svg" class="size-4" width="18" height="18" viewBox="0 0 18 18"><g fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" stroke="currentColor"><path d="M9 11.2495C10.2426 11.2495 11.25 10.2422 11.25 8.99951C11.25 7.75687 10.2426 6.74951 9 6.74951C7.75736 6.74951 6.75 7.75687 6.75 8.99951C6.75 10.2422 7.75736 11.2495 9 11.2495Z"></path> <path d="M15.175 7.27802L14.246 6.95001C14.144 6.68901 14.027 6.42999 13.883 6.17999C13.739 5.92999 13.573 5.69999 13.398 5.48099L13.578 4.513C13.703 3.842 13.391 3.164 12.8 2.823L12.449 2.62C11.857 2.278 11.115 2.34699 10.596 2.79099L9.851 3.42801C9.291 3.34201 8.718 3.34201 8.148 3.42801L7.403 2.79001C6.884 2.34601 6.141 2.27699 5.55 2.61899L5.199 2.82199C4.607 3.16299 4.296 3.84099 4.421 4.51199L4.601 5.47699C4.241 5.92599 3.955 6.42299 3.749 6.95099L2.825 7.27701C2.181 7.50401 1.75 8.11299 1.75 8.79599V9.20099C1.75 9.88399 2.181 10.493 2.825 10.72L3.754 11.048C3.856 11.309 3.972 11.567 4.117 11.817C4.262 12.067 4.427 12.297 4.602 12.517L4.421 13.485C4.296 14.156 4.608 14.834 5.199 15.175L5.55 15.378C6.142 15.72 6.884 15.651 7.403 15.207L8.148 14.569C8.707 14.655 9.28 14.655 9.849 14.569L10.595 15.208C11.114 15.652 11.857 15.721 12.448 15.379L12.799 15.176C13.391 14.834 13.702 14.157 13.577 13.486L13.397 12.52C13.756 12.071 14.043 11.575 14.248 11.047L15.173 10.721C15.817 10.494 16.248 9.885 16.248 9.202V8.797C16.248 8.114 15.817 7.50502 15.173 7.27802H15.175Z"></path></g></svg>
              Settings
            </div>
            <div class="text-xs text-neutral-500 dark:text-neutral-400 mt-0.5">
              Manage system settings
            </div>
          </label>
        </div>

        <!-- Nested Children -->
        <div class="ml-[7px] pl-5 space-y-2 border-l-2 border-black/10 dark:border-white/10 has-[:checked]:border-neutral-400 dark:has-[:checked]:border-neutral-500">
          <div class="-ml-2 pl-2 -mr-3 pr-3 py-1.5 rounded-md border border-transparent flex items-center gap-x-3 has-[:checked]:bg-neutral-100 dark:has-[:checked]:bg-neutral-800 has-[:checked]:border-black/5 dark:has-[:checked]:border-white/10">
            <input
              type="checkbox"
              id="settings-view"
              name="permissions[]"
              value="settings.view"
              tabindex="0"
              data-checkbox-select-all-target="checkbox child"
              data-action="click->checkbox-select-all#toggle">
            <label for="settings-view" class="inline-block text-sm cursor-pointer select-none has-[:checked]:text-neutral-900 dark:has-[:checked]:text-neutral-50 has-[:checked]:font-medium">
              View settings
            </label>
          </div>

          <div class="-ml-2 pl-2 -mr-3 pr-3 py-1.5 rounded-md border border-transparent flex items-center gap-x-3 has-[:checked]:bg-neutral-100 dark:has-[:checked]:bg-neutral-800 has-[:checked]:border-black/5 dark:has-[:checked]:border-white/10">
            <input
              type="checkbox"
              id="settings-edit"
              name="permissions[]"
              value="settings.edit"
              tabindex="0"
              data-checkbox-select-all-target="checkbox child"
              data-action="click->checkbox-select-all#toggle">
            <label for="settings-edit" class="inline-block text-sm cursor-pointer select-none has-[:checked]:text-neutral-900 dark:has-[:checked]:text-neutral-50 has-[:checked]:font-medium">
              Edit settings
            </label>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>

Table with Checkbox Select All

Data table with bulk selection and an action bar that appears when items are selected.

Name Email Role Status
Alex Johnson alex@example.com Admin Active
Sarah Miller sarah@example.com Editor Active
Michael Chen michael@example.com Viewer Pending
Emily Davis emily@example.com Editor Active
James Wilson james@example.com Viewer Inactive
Lisa Anderson lisa@example.com Admin Active
Robert Brown robert@example.com Viewer Active
Patricia Taylor patricia@example.com Editor Pending
David Martinez david@example.com Admin Active
Jennifer Garcia jennifer@example.com Viewer Active
Christopher Lee christopher@example.com Editor Inactive
Nancy White nancy@example.com Admin Active
Daniel Harris daniel@example.com Viewer Pending
Karen Clark karen@example.com Editor Active
Paul Rodriguez paul@example.com Admin Active
<div class="w-full max-w-4xl mx-auto" data-controller="checkbox-select-all" data-checkbox-select-all-toggle-key-value="x">
  <div class="bg-white dark:bg-neutral-900 rounded-xl border border-black/10 dark:border-white/10 shadow-xs overflow-hidden">
    <!-- Table -->
    <div class="overflow-x-auto max-h-[500px] overflow-y-auto small-scrollbar">
      <table class="w-full">
        <thead class="sticky top-0 z-10 bg-neutral-100/75 dark:bg-neutral-800/75 backdrop-blur-sm backdrop-filter">
          <tr class="*:py-3 *:text-left *:[box-shadow:inset_0_-1px_0_0_rgb(229_229_229)] dark:*:[box-shadow:inset_0_-1px_0_0_rgb(46_46_46)]">
            <th scope="col" class="pl-3 pr-1.5">
              <div class="flex items-center">
                <input
                  type="checkbox"
                  id="table-select-all"
                  tabindex="0"
                  data-checkbox-select-all-target="selectAll"
                  data-action="click->checkbox-select-all#toggleAll">
              </div>
            </th>
            <th scope="col" class="pl-1.5 pr-6 text-sm font-semibold text-neutral-900 dark:text-neutral-50">
              Name
            </th>
            <th scope="col" class="px-6 text-sm font-semibold text-neutral-900 dark:text-neutral-50">
              Email
            </th>
            <th scope="col" class="px-6 text-sm font-semibold text-neutral-900 dark:text-neutral-50">
              Role
            </th>
            <th scope="col" class="px-6 text-sm font-semibold text-neutral-900 dark:text-neutral-50">
              Status
            </th>
          </tr>
        </thead>
        <tbody class="*:even:bg-neutral-50 dark:*:even:bg-neutral-800/50 *:has-[:checked]:bg-neutral-100 dark:*:has-[:checked]:bg-neutral-700/50 *:*:py-4">
          <tr class="group">
            <td class="relative pl-3 pr-1.5">
              <div class="absolute inset-y-0 left-0 hidden w-0.5 bg-neutral-700 dark:bg-neutral-300 group-has-[:checked]:block"></div>
              <div class="flex items-center">
                <input
                  type="checkbox"
                  id="user-1"
                  name="users[]"
                  value="1"
                  tabindex="0"
                  data-checkbox-select-all-target="checkbox"
                  data-action="click->checkbox-select-all#toggle">
              </div>
            </td>
            <td class="pl-1.5 pr-6 text-sm font-medium text-neutral-900 dark:text-neutral-100">
              Alex Johnson
            </td>
            <td class="px-6 text-sm text-neutral-500 dark:text-neutral-400">
              alex@example.com
            </td>
            <td class="px-6 text-sm text-neutral-500 dark:text-neutral-400">
              Admin
            </td>
            <td class="px-6">
              <span class="inline-flex items-center gap-1 rounded-full bg-green-50 px-2 py-1 text-xs font-medium text-green-700 outline outline-green-600/20 dark:bg-green-400/10 dark:text-green-400 dark:outline-green-400/25">
                Active
              </span>
            </td>
          </tr>

          <tr class="group">
            <td class="relative pl-3 pr-1.5">
              <div class="absolute inset-y-0 left-0 hidden w-0.5 bg-neutral-700 dark:bg-neutral-300 group-has-[:checked]:block"></div>
              <div class="flex items-center">
                <input
                  type="checkbox"
                  id="user-2"
                name="users[]"
                value="2"
                tabindex="0"
                data-checkbox-select-all-target="checkbox"
                data-action="click->checkbox-select-all#toggle">
              </div>
            </td>
            <td class="pl-1.5 pr-6 text-sm font-medium text-neutral-900 dark:text-neutral-100">
              Sarah Miller
            </td>
            <td class="px-6 text-sm text-neutral-500 dark:text-neutral-400">
              sarah@example.com
            </td>
            <td class="px-6 text-sm text-neutral-500 dark:text-neutral-400">
              Editor
            </td>
            <td class="px-6">
              <span class="inline-flex items-center gap-1 rounded-full bg-green-50 px-2 py-1 text-xs font-medium text-green-700 outline outline-green-600/20 dark:bg-green-400/10 dark:text-green-400 dark:outline-green-400/25">
                Active
              </span>
            </td>
          </tr>

          <tr class="group">
            <td class="relative pl-3 pr-1.5">
              <div class="absolute inset-y-0 left-0 hidden w-0.5 bg-neutral-700 dark:bg-neutral-300 group-has-[:checked]:block"></div>
              <div class="flex items-center">
                <input
                  type="checkbox"
                  id="user-3"
                name="users[]"
                value="3"
                tabindex="0"
                data-checkbox-select-all-target="checkbox"
                data-action="click->checkbox-select-all#toggle">
              </div>
            </td>
            <td class="pl-1.5 pr-6 text-sm font-medium text-neutral-900 dark:text-neutral-100">
              Michael Chen
            </td>
            <td class="px-6 text-sm text-neutral-500 dark:text-neutral-400">
              michael@example.com
            </td>
            <td class="px-6 text-sm text-neutral-500 dark:text-neutral-400">
              Viewer
            </td>
            <td class="px-6">
              <span class="inline-flex items-center gap-1 rounded-full bg-yellow-50 px-2 py-1 text-xs font-medium text-yellow-700 outline outline-yellow-700/20 dark:bg-yellow-400/10 dark:text-yellow-500 dark:outline-yellow-400/25">
                Pending
              </span>
            </td>
          </tr>

          <tr class="group">
            <td class="relative pl-3 pr-1.5">
              <div class="absolute inset-y-0 left-0 hidden w-0.5 bg-neutral-700 dark:bg-neutral-300 group-has-[:checked]:block"></div>
              <div class="flex items-center">
                <input
                  type="checkbox"
                  id="user-4"
                name="users[]"
                value="4"
                tabindex="0"
                data-checkbox-select-all-target="checkbox"
                data-action="click->checkbox-select-all#toggle">
              </div>
            </td>
            <td class="pl-1.5 pr-6 text-sm font-medium text-neutral-900 dark:text-neutral-100">
              Emily Davis
            </td>
            <td class="px-6 text-sm text-neutral-500 dark:text-neutral-400">
              emily@example.com
            </td>
            <td class="px-6 text-sm text-neutral-500 dark:text-neutral-400">
              Editor
            </td>
            <td class="px-6">
              <span class="inline-flex items-center gap-1 rounded-full bg-green-50 px-2 py-1 text-xs font-medium text-green-700 outline outline-green-600/20 dark:bg-green-400/10 dark:text-green-400 dark:outline-green-400/25">
                Active
              </span>
            </td>
          </tr>

          <tr class="group">
            <td class="relative pl-3 pr-1.5">
              <div class="absolute inset-y-0 left-0 hidden w-0.5 bg-neutral-700 dark:bg-neutral-300 group-has-[:checked]:block"></div>
              <div class="flex items-center">
                <input
                  type="checkbox"
                  id="user-5"
                name="users[]"
                value="5"
                tabindex="0"
                data-checkbox-select-all-target="checkbox"
                data-action="click->checkbox-select-all#toggle">
              </div>
            </td>
            <td class="pl-1.5 pr-6 text-sm font-medium text-neutral-900 dark:text-neutral-100">
              James Wilson
            </td>
            <td class="px-6 text-sm text-neutral-500 dark:text-neutral-400">
              james@example.com
            </td>
            <td class="px-6 text-sm text-neutral-500 dark:text-neutral-400">
              Viewer
            </td>
            <td class="px-6">
              <span class="inline-flex items-center gap-1 rounded-full bg-red-50 px-2 py-1 text-xs font-medium text-red-700 outline outline-red-600/20 dark:bg-red-400/10 dark:text-red-400 dark:outline-red-400/25">
                Inactive
              </span>
            </td>
          </tr>

          <tr class="group">
            <td class="relative pl-3 pr-1.5">
              <div class="absolute inset-y-0 left-0 hidden w-0.5 bg-neutral-700 dark:bg-neutral-300 group-has-[:checked]:block"></div>
              <div class="flex items-center">
                <input
                  type="checkbox"
                  id="user-6"
                name="users[]"
                value="6"
                tabindex="0"
                data-checkbox-select-all-target="checkbox"
                data-action="click->checkbox-select-all#toggle">
              </div>
            </td>
            <td class="pl-1.5 pr-6 text-sm font-medium text-neutral-900 dark:text-neutral-100">
              Lisa Anderson
            </td>
            <td class="px-6 text-sm text-neutral-500 dark:text-neutral-400">
              lisa@example.com
            </td>
            <td class="px-6 text-sm text-neutral-500 dark:text-neutral-400">
              Admin
            </td>
            <td class="px-6">
              <span class="inline-flex items-center gap-1 rounded-full bg-green-50 px-2 py-1 text-xs font-medium text-green-700 outline outline-green-600/20 dark:bg-green-400/10 dark:text-green-400 dark:outline-green-400/25">
                Active
              </span>
            </td>
          </tr>

          <tr class="group">
            <td class="relative pl-3 pr-1.5">
              <div class="absolute inset-y-0 left-0 hidden w-0.5 bg-neutral-700 dark:bg-neutral-300 group-has-[:checked]:block"></div>
              <div class="flex items-center">
                <input
                  type="checkbox"
                  id="user-7"
                name="users[]"
                value="7"
                tabindex="0"
                data-checkbox-select-all-target="checkbox"
                data-action="click->checkbox-select-all#toggle">
              </div>
            </td>
            <td class="pl-1.5 pr-6 text-sm font-medium text-neutral-900 dark:text-neutral-100">
              Robert Brown
            </td>
            <td class="px-6 text-sm text-neutral-500 dark:text-neutral-400">
              robert@example.com
            </td>
            <td class="px-6 text-sm text-neutral-500 dark:text-neutral-400">
              Viewer
            </td>
            <td class="px-6">
              <span class="inline-flex items-center gap-1 rounded-full bg-green-50 px-2 py-1 text-xs font-medium text-green-700 outline outline-green-600/20 dark:bg-green-400/10 dark:text-green-400 dark:outline-green-400/25">
                Active
              </span>
            </td>
          </tr>

          <tr class="group">
            <td class="relative pl-3 pr-1.5">
              <div class="absolute inset-y-0 left-0 hidden w-0.5 bg-neutral-700 dark:bg-neutral-300 group-has-[:checked]:block"></div>
              <div class="flex items-center">
                <input
                  type="checkbox"
                  id="user-8"
                name="users[]"
                value="8"
                tabindex="0"
                data-checkbox-select-all-target="checkbox"
                data-action="click->checkbox-select-all#toggle">
              </div>
            </td>
            <td class="pl-1.5 pr-6 text-sm font-medium text-neutral-900 dark:text-neutral-100">
              Patricia Taylor
            </td>
            <td class="px-6 text-sm text-neutral-500 dark:text-neutral-400">
              patricia@example.com
            </td>
            <td class="px-6 text-sm text-neutral-500 dark:text-neutral-400">
              Editor
            </td>
            <td class="px-6">
              <span class="inline-flex items-center gap-1 rounded-full bg-yellow-50 px-2 py-1 text-xs font-medium text-yellow-700 outline outline-yellow-700/20 dark:bg-yellow-400/10 dark:text-yellow-500 dark:outline-yellow-400/25">
                Pending
              </span>
            </td>
          </tr>

          <tr class="group">
            <td class="relative pl-3 pr-1.5">
              <div class="absolute inset-y-0 left-0 hidden w-0.5 bg-neutral-700 dark:bg-neutral-300 group-has-[:checked]:block"></div>
              <div class="flex items-center">
                <input
                  type="checkbox"
                  id="user-9"
                name="users[]"
                value="9"
                tabindex="0"
                data-checkbox-select-all-target="checkbox"
                data-action="click->checkbox-select-all#toggle">
              </div>
            </td>
            <td class="pl-1.5 pr-6 text-sm font-medium text-neutral-900 dark:text-neutral-100">
              David Martinez
            </td>
            <td class="px-6 text-sm text-neutral-500 dark:text-neutral-400">
              david@example.com
            </td>
            <td class="px-6 text-sm text-neutral-500 dark:text-neutral-400">
              Admin
            </td>
            <td class="px-6">
              <span class="inline-flex items-center gap-1 rounded-full bg-green-50 px-2 py-1 text-xs font-medium text-green-700 outline outline-green-600/20 dark:bg-green-400/10 dark:text-green-400 dark:outline-green-400/25">
                Active
              </span>
            </td>
          </tr>

          <tr class="group">
            <td class="relative pl-3 pr-1.5">
              <div class="absolute inset-y-0 left-0 hidden w-0.5 bg-neutral-700 dark:bg-neutral-300 group-has-[:checked]:block"></div>
              <div class="flex items-center">
                <input
                  type="checkbox"
                  id="user-10"
                name="users[]"
                value="10"
                tabindex="0"
                data-checkbox-select-all-target="checkbox"
                data-action="click->checkbox-select-all#toggle">
              </div>
            </td>
            <td class="pl-1.5 pr-6 text-sm font-medium text-neutral-900 dark:text-neutral-100">
              Jennifer Garcia
            </td>
            <td class="px-6 text-sm text-neutral-500 dark:text-neutral-400">
              jennifer@example.com
            </td>
            <td class="px-6 text-sm text-neutral-500 dark:text-neutral-400">
              Viewer
            </td>
            <td class="px-6">
              <span class="inline-flex items-center gap-1 rounded-full bg-green-50 px-2 py-1 text-xs font-medium text-green-700 outline outline-green-600/20 dark:bg-green-400/10 dark:text-green-400 dark:outline-green-400/25">
                Active
              </span>
            </td>
          </tr>

          <tr class="group">
            <td class="relative pl-3 pr-1.5">
              <div class="absolute inset-y-0 left-0 hidden w-0.5 bg-neutral-700 dark:bg-neutral-300 group-has-[:checked]:block"></div>
              <div class="flex items-center">
                <input
                  type="checkbox"
                  id="user-11"
                name="users[]"
                value="11"
                tabindex="0"
                data-checkbox-select-all-target="checkbox"
                data-action="click->checkbox-select-all#toggle">
              </div>
            </td>
            <td class="pl-1.5 pr-6 text-sm font-medium text-neutral-900 dark:text-neutral-100">
              Christopher Lee
            </td>
            <td class="px-6 text-sm text-neutral-500 dark:text-neutral-400">
              christopher@example.com
            </td>
            <td class="px-6 text-sm text-neutral-500 dark:text-neutral-400">
              Editor
            </td>
            <td class="px-6">
              <span class="inline-flex items-center gap-1 rounded-full bg-red-50 px-2 py-1 text-xs font-medium text-red-700 outline outline-red-600/20 dark:bg-red-400/10 dark:text-red-400 dark:outline-red-400/25">
                Inactive
              </span>
            </td>
          </tr>

          <tr class="group">
            <td class="relative pl-3 pr-1.5">
              <div class="absolute inset-y-0 left-0 hidden w-0.5 bg-neutral-700 dark:bg-neutral-300 group-has-[:checked]:block"></div>
              <div class="flex items-center">
                <input
                  type="checkbox"
                  id="user-12"
                name="users[]"
                value="12"
                tabindex="0"
                data-checkbox-select-all-target="checkbox"
                data-action="click->checkbox-select-all#toggle">
              </div>
            </td>
            <td class="pl-1.5 pr-6 text-sm font-medium text-neutral-900 dark:text-neutral-100">
              Nancy White
            </td>
            <td class="px-6 text-sm text-neutral-500 dark:text-neutral-400">
              nancy@example.com
            </td>
            <td class="px-6 text-sm text-neutral-500 dark:text-neutral-400">
              Admin
            </td>
            <td class="px-6">
              <span class="inline-flex items-center gap-1 rounded-full bg-green-50 px-2 py-1 text-xs font-medium text-green-700 outline outline-green-600/20 dark:bg-green-400/10 dark:text-green-400 dark:outline-green-400/25">
                Active
              </span>
            </td>
          </tr>

          <tr class="group">
            <td class="relative pl-3 pr-1.5">
              <div class="absolute inset-y-0 left-0 hidden w-0.5 bg-neutral-700 dark:bg-neutral-300 group-has-[:checked]:block"></div>
              <div class="flex items-center">
                <input
                  type="checkbox"
                  id="user-13"
                name="users[]"
                value="13"
                tabindex="0"
                data-checkbox-select-all-target="checkbox"
                data-action="click->checkbox-select-all#toggle">
              </div>
            </td>
            <td class="pl-1.5 pr-6 text-sm font-medium text-neutral-900 dark:text-neutral-100">
              Daniel Harris
            </td>
            <td class="px-6 text-sm text-neutral-500 dark:text-neutral-400">
              daniel@example.com
            </td>
            <td class="px-6 text-sm text-neutral-500 dark:text-neutral-400">
              Viewer
            </td>
            <td class="px-6">
              <span class="inline-flex items-center gap-1 rounded-full bg-yellow-50 px-2 py-1 text-xs font-medium text-yellow-700 outline outline-yellow-700/20 dark:bg-yellow-400/10 dark:text-yellow-500 dark:outline-yellow-400/25">
                Pending
              </span>
            </td>
          </tr>

          <tr class="group">
            <td class="relative pl-3 pr-1.5">
              <div class="absolute inset-y-0 left-0 hidden w-0.5 bg-neutral-700 dark:bg-neutral-300 group-has-[:checked]:block"></div>
              <div class="flex items-center">
                <input
                  type="checkbox"
                  id="user-14"
                name="users[]"
                value="14"
                tabindex="0"
                data-checkbox-select-all-target="checkbox"
                data-action="click->checkbox-select-all#toggle">
              </div>
            </td>
            <td class="pl-1.5 pr-6 text-sm font-medium text-neutral-900 dark:text-neutral-100">
              Karen Clark
            </td>
            <td class="px-6 text-sm text-neutral-500 dark:text-neutral-400">
              karen@example.com
            </td>
            <td class="px-6 text-sm text-neutral-500 dark:text-neutral-400">
              Editor
            </td>
            <td class="px-6">
              <span class="inline-flex items-center gap-1 rounded-full bg-green-50 px-2 py-1 text-xs font-medium text-green-700 outline outline-green-600/20 dark:bg-green-400/10 dark:text-green-400 dark:outline-green-400/25">
                Active
              </span>
            </td>
          </tr>

          <tr class="group">
            <td class="relative pl-3 pr-1.5">
              <div class="absolute inset-y-0 left-0 hidden w-0.5 bg-neutral-700 dark:bg-neutral-300 group-has-[:checked]:block"></div>
              <div class="flex items-center">
                <input
                  type="checkbox"
                  id="user-15"
                name="users[]"
                value="15"
                tabindex="0"
                data-checkbox-select-all-target="checkbox"
                data-action="click->checkbox-select-all#toggle">
              </div>
            </td>
            <td class="pl-1.5 pr-6 text-sm font-medium text-neutral-900 dark:text-neutral-100">
              Paul Rodriguez
            </td>
            <td class="px-6 text-sm text-neutral-500 dark:text-neutral-400">
              paul@example.com
            </td>
            <td class="px-6 text-sm text-neutral-500 dark:text-neutral-400">
              Admin
            </td>
            <td class="px-6">
              <span class="inline-flex items-center gap-1 rounded-full bg-green-50 px-2 py-1 text-xs font-medium text-green-700 outline outline-green-600/20 dark:bg-green-400/10 dark:text-green-400 dark:outline-green-400/25">
                Active
              </span>
            </td>
          </tr>
        </tbody>
      </table>

      <!-- Action Bar (shown when items are selected) -->
      <div class="sticky bottom-0 inset-x-0 z-10 px-6 py-3 bg-neutral-50/75 dark:bg-neutral-900/75 backdrop-blur-sm backdrop-filter border-t border-black/10 dark:border-white/10" data-checkbox-select-all-target="actionBar" hidden>
        <div class="flex justify-between items-center gap-4">
          <div class="flex gap-1 text-sm font-medium text-neutral-700 dark:text-neutral-300">
            <span data-checkbox-select-all-target="count">0</span>
            <span>selected</span>
          </div>
          <div class="flex items-center gap-2">
            <button
              type="button"
              class="flex items-center justify-center gap-1.5 rounded-lg border border-black/10 bg-white/90 px-2 sm:px-3 py-2 text-xs font-medium whitespace-nowrap text-neutral-800 shadow-xs transition-all duration-100 ease-in-out select-none hover:bg-neutral-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 disabled:cursor-not-allowed disabled:opacity-50 dark:border-white/10 dark:bg-neutral-700/50 dark:text-neutral-50 dark:hover:bg-neutral-700/75 dark:focus-visible:outline-neutral-200"
              data-action="click->checkbox-select-all#clearAll">
              <svg xmlns="http://www.w3.org/2000/svg" class="size-3.5 sm:size-4" width="18" height="18" viewBox="0 0 18 18"><g fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" stroke="currentColor"><path d="M14 4L4 14"></path> <path d="M4 4L14 14"></path></g></svg>
              <span class="hidden sm:block">Clear</span>
            </button>
          <button type="button" class="flex items-center justify-center gap-1.5 rounded-lg border border-black/10 bg-white/90 px-2 sm:px-3 py-2 text-xs font-medium whitespace-nowrap text-neutral-800 shadow-xs transition-all duration-100 ease-in-out select-none hover:bg-neutral-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 disabled:cursor-not-allowed disabled:opacity-50 dark:border-white/10 dark:bg-neutral-700/50 dark:text-neutral-50 dark:hover:bg-neutral-700/75 dark:focus-visible:outline-neutral-200">
            <svg xmlns="http://www.w3.org/2000/svg" class="size-3.5 sm:size-4" width="18" height="18" viewBox="0 0 18 18"><g fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" stroke="currentColor"><path d="M9 6.5C12.452 6.5 15.25 5.493 15.25 4.25C15.25 3.007 12.452 2 9 2C5.548 2 2.75 3.007 2.75 4.25C2.75 5.493 5.548 6.5 9 6.5Z"></path> <path d="M15.25 9V4.25"></path> <path d="M2.75 4.25V13.75C2.75 14.993 5.548 16 9 16C9.3532 16 9.6994 15.9893 10.0365 15.969"></path> <path d="M2.75 9C2.75 10.243 5.548 11.25 9 11.25C12.452 11.25 15.25 10.243 15.25 9"></path> <path d="M17.25 16.25V12.75H13.75"></path> <path d="M17 13L12.75 17.25"></path></g></svg>
            <span class="hidden sm:block">Export Selected</span>
          </button>
          <button type="button" class="flex items-center justify-center gap-1.5 rounded-lg border border-red-300/30 bg-red-600 px-2 sm:px-3 py-2 text-xs font-medium whitespace-nowrap text-white shadow-sm transition-all duration-100 ease-in-out select-none hover:bg-red-500 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 disabled:cursor-not-allowed disabled:opacity-50 dark:focus-visible:outline-neutral-200">
            <svg xmlns="http://www.w3.org/2000/svg" class="size-3.5 sm:size-4" width="18" height="18" viewBox="0 0 18 18"><g fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" stroke="currentColor"><path d="M2.75 4.75H15.25"></path> <path d="M6.75 4.75V2.75C6.75 2.2 7.198 1.75 7.75 1.75H10.25C10.802 1.75 11.25 2.2 11.25 2.75V4.75"></path> <path d="M7.375 8.75L7.59219 13.25"></path> <path d="M10.625 8.75L10.4078 13.25"></path> <path d="M13.6977 7.75L13.35 14.35C13.294 15.4201 12.416 16.25 11.353 16.25H6.64804C5.58404 16.25 4.70703 15.42 4.65103 14.35L4.30334 7.75"></path></g></svg>
            <span class="hidden sm:block">Delete Selected</span>
          </button>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>

Addon Selection with Select All

Addon selection with select all checkbox and automatic total calculation.

Pro Plan Subscription

Your base monthly subscription

$99/mo
Total monthly cost $99.00/mo
<div class="w-full max-w-xl mx-auto space-y-4" data-controller="checkbox-select-all" data-checkbox-select-all-toggle-key-value="x" data-checkbox-select-all-base-amount-value="99">
  <!-- Base Subscription -->
  <div class="bg-white dark:bg-neutral-900 rounded-xl border border-black/10 dark:border-white/10 shadow-xs overflow-hidden">
    <div class="px-6 py-4">
      <div class="flex items-center justify-between select-none">
        <div>
          <h3 class="text-sm font-semibold text-neutral-900 dark:text-neutral-100">Pro Plan Subscription</h3>
          <p class="text-xs text-neutral-500 dark:text-neutral-400">Your base monthly subscription</p>
        </div>
        <span class="text-sm font-medium text-neutral-900 dark:text-neutral-100">$99/mo</span>
      </div>
    </div>
  </div>

  <!-- Add-ons Card -->
  <div class="bg-white dark:bg-neutral-900 rounded-xl border border-black/10 dark:border-white/10 shadow-xs overflow-hidden">
    <!-- Header with Select All -->
    <div class="px-6 py-4 border-b border-black/10 dark:border-white/10">
      <div class="flex items-center justify-between select-none">
        <div class="flex items-center gap-x-3">
          <input
            type="checkbox"
            id="select-all-addons"
            tabindex="0"
            data-checkbox-select-all-target="selectAll"
            data-action="click->checkbox-select-all#toggleAll">
          <label for="select-all-addons" class="inline-block font-semibold text-sm cursor-pointer select-none">
            Select All Add-ons
          </label>
        </div>
        <div data-checkbox-select-all-target="actionBar" hidden class="flex items-center gap-3">
          <div class="flex gap-1 text-sm font-medium text-neutral-700 dark:text-neutral-300">
            <span data-checkbox-select-all-target="count">0</span>
            <span>selected</span>
          </div>
          <button type="button"
                  data-action="click->checkbox-select-all#clearAll"
                  class="text-sm font-medium text-neutral-600 hover:text-neutral-900 dark:text-neutral-400 dark:hover:text-neutral-100">
            Clear
          </button>
        </div>
      </div>
    </div>

    <!-- Checkbox Cards -->
    <div class="p-6 space-y-3">
      <!-- Card 1 -->
      <label for="addon-analytics" class="relative py-3 px-4 flex items-center font-medium bg-white text-neutral-800 rounded-xl cursor-pointer ring-1 ring-neutral-200 has-[:checked]:ring-2 has-[:checked]:ring-neutral-400 dark:bg-neutral-700/50 dark:text-neutral-200 dark:ring-neutral-700 dark:has-[:checked]:ring-neutral-400 has-[:checked]:bg-neutral-100 has-[:checked]:text-neutral-900 dark:has-[:checked]:bg-neutral-600/60 dark:has-[:checked]:text-white transition-all">
        <input
          type="checkbox"
          id="addon-analytics"
          name="addons[]"
          value="analytics"
          class="absolute left-4"
          tabindex="0"
          data-checkbox-select-all-target="checkbox"
          data-action="click->checkbox-select-all#toggle">
        <div class="flex-1 ml-8">
          <div class="flex items-center justify-between select-none">
            <div>
              <h3 class="text-sm font-semibold text-neutral-900 dark:text-neutral-100">Advanced Analytics</h3>
              <p class="text-xs text-neutral-500 dark:text-neutral-400">Track user behavior and conversions</p>
            </div>
            <span class="text-sm font-medium text-neutral-900 dark:text-neutral-100"><span data-checkbox-select-all-target="amount">$29</span>/mo</span>
          </div>
        </div>
      </label>

      <!-- Card 2 -->
      <label for="addon-support" class="relative py-3 px-4 flex items-center font-medium bg-white text-neutral-800 rounded-xl cursor-pointer ring-1 ring-neutral-200 has-[:checked]:ring-2 has-[:checked]:ring-neutral-400 dark:bg-neutral-700/50 dark:text-neutral-200 dark:ring-neutral-700 dark:has-[:checked]:ring-neutral-400 has-[:checked]:bg-neutral-100 has-[:checked]:text-neutral-900 dark:has-[:checked]:bg-neutral-600/60 dark:has-[:checked]:text-white transition-all">
        <input
          type="checkbox"
          id="addon-support"
          name="addons[]"
          value="support"
          class="absolute left-4"
          tabindex="0"
          data-checkbox-select-all-target="checkbox"
          data-action="click->checkbox-select-all#toggle">
        <div class="flex-1 ml-8">
          <div class="flex items-center justify-between select-none">
            <div>
              <h3 class="text-sm font-semibold text-neutral-900 dark:text-neutral-100">Priority Support</h3>
              <p class="text-xs text-neutral-500 dark:text-neutral-400">24/7 support with 1-hour response time</p>
            </div>
            <span class="text-sm font-medium text-neutral-900 dark:text-neutral-100"><span data-checkbox-select-all-target="amount">$49</span>/mo</span>
          </div>
        </div>
      </label>

      <!-- Card 3 -->
      <label for="addon-storage" class="relative py-3 px-4 flex items-center font-medium bg-white text-neutral-800 rounded-xl cursor-pointer ring-1 ring-neutral-200 has-[:checked]:ring-2 has-[:checked]:ring-neutral-400 dark:bg-neutral-700/50 dark:text-neutral-200 dark:ring-neutral-700 dark:has-[:checked]:ring-neutral-400 has-[:checked]:bg-neutral-100 has-[:checked]:text-neutral-900 dark:has-[:checked]:bg-neutral-600/60 dark:has-[:checked]:text-white transition-all">
        <input
          type="checkbox"
          id="addon-storage"
          name="addons[]"
          value="storage"
          class="absolute left-4"
          tabindex="0"
          data-checkbox-select-all-target="checkbox"
          data-action="click->checkbox-select-all#toggle">
        <div class="flex-1 ml-8">
          <div class="flex items-center justify-between select-none">
            <div>
              <h3 class="text-sm font-semibold text-neutral-900 dark:text-neutral-100">Extra Storage</h3>
              <p class="text-xs text-neutral-500 dark:text-neutral-400">Additional 500GB cloud storage</p>
            </div>
            <span class="text-sm font-medium text-neutral-900 dark:text-neutral-100"><span data-checkbox-select-all-target="amount">$19</span>/mo</span>
          </div>
        </div>
      </label>

      <!-- Card 4 -->
      <label for="addon-api" class="relative py-3 px-4 flex items-center font-medium bg-white text-neutral-800 rounded-xl cursor-pointer ring-1 ring-neutral-200 has-[:checked]:ring-2 has-[:checked]:ring-neutral-400 dark:bg-neutral-700/50 dark:text-neutral-200 dark:ring-neutral-700 dark:has-[:checked]:ring-neutral-400 has-[:checked]:bg-neutral-100 has-[:checked]:text-neutral-900 dark:has-[:checked]:bg-neutral-600/60 dark:has-[:checked]:text-white transition-all">
        <input
          type="checkbox"
          id="addon-api"
          name="addons[]"
          value="api"
          class="absolute left-4"
          tabindex="0"
          data-checkbox-select-all-target="checkbox"
          data-action="click->checkbox-select-all#toggle">
        <div class="flex-1 ml-8">
          <div class="flex items-center justify-between select-none">
            <div>
              <h3 class="text-sm font-semibold text-neutral-900 dark:text-neutral-100">API Access</h3>
              <p class="text-xs text-neutral-500 dark:text-neutral-400">Full REST API with unlimited requests</p>
            </div>
            <span class="text-sm font-medium text-neutral-900 dark:text-neutral-100"><span data-checkbox-select-all-target="amount">$39</span>/mo</span>
          </div>
        </div>
      </label>

      <!-- Card 5 -->
      <label for="addon-whitelabel" class="relative py-3 px-4 flex items-center font-medium bg-white text-neutral-800 rounded-xl cursor-pointer ring-1 ring-neutral-200 has-[:checked]:ring-2 has-[:checked]:ring-neutral-400 dark:bg-neutral-700/50 dark:text-neutral-200 dark:ring-neutral-700 dark:has-[:checked]:ring-neutral-400 has-[:checked]:bg-neutral-100 has-[:checked]:text-neutral-900 dark:has-[:checked]:bg-neutral-600/60 dark:has-[:checked]:text-white transition-all">
        <input
          type="checkbox"
          id="addon-whitelabel"
          name="addons[]"
          value="whitelabel"
          class="absolute left-4"
          tabindex="0"
          data-checkbox-select-all-target="checkbox"
          data-action="click->checkbox-select-all#toggle">
        <div class="flex-1 ml-8">
          <div class="flex items-center justify-between select-none">
            <div>
              <h3 class="text-sm font-semibold text-neutral-900 dark:text-neutral-100">White Label</h3>
              <p class="text-xs text-neutral-500 dark:text-neutral-400">Remove branding and add your own logo</p>
            </div>
            <span class="text-sm font-medium text-neutral-900 dark:text-neutral-100"><span data-checkbox-select-all-target="amount">$99</span>/mo</span>
          </div>
        </div>
      </label>

      <!-- Card 6 -->
      <label for="addon-sso" class="relative py-3 px-4 flex items-center font-medium bg-white text-neutral-800 rounded-xl cursor-pointer ring-1 ring-neutral-200 has-[:checked]:ring-2 has-[:checked]:ring-neutral-400 dark:bg-neutral-700/50 dark:text-neutral-200 dark:ring-neutral-700 dark:has-[:checked]:ring-neutral-400 has-[:checked]:bg-neutral-100 has-[:checked]:text-neutral-900 dark:has-[:checked]:bg-neutral-600/60 dark:has-[:checked]:text-white transition-all">
        <input
          type="checkbox"
          id="addon-sso"
          name="addons[]"
          value="sso"
          class="absolute left-4"
          tabindex="0"
          data-checkbox-select-all-target="checkbox"
          data-action="click->checkbox-select-all#toggle">
        <div class="flex-1 ml-8">
          <div class="flex items-center justify-between select-none">
            <div>
              <h3 class="text-sm font-semibold text-neutral-900 dark:text-neutral-100">SSO Integration</h3>
              <p class="text-xs text-neutral-500 dark:text-neutral-400">SAML & OAuth single sign-on</p>
            </div>
            <span class="text-sm font-medium text-neutral-900 dark:text-neutral-100"><span data-checkbox-select-all-target="amount">$79</span>/mo</span>
          </div>
        </div>
      </label>
    </div>
  </div>

  <!-- Total -->
  <div class="bg-white dark:bg-neutral-900 rounded-xl border border-black/10 dark:border-white/10 shadow-xs overflow-hidden">
    <div class="px-6 py-4">
      <div class="flex items-center justify-between select-none">
        <span class="text-sm font-semibold text-neutral-900 dark:text-neutral-100">Total monthly cost</span>
        <span class="text-lg font-semibold text-neutral-900 dark:text-neutral-100"><span data-checkbox-select-all-target="total">$99.00</span>/mo</span>
      </div>
    </div>
  </div>

  <!-- Update Button -->
  <button type="button" class="w-full flex items-center justify-center gap-1.5 rounded-lg border border-neutral-400/30 bg-neutral-800 px-3.5 py-2.5 text-sm font-medium whitespace-nowrap text-white shadow-sm transition-all duration-100 ease-in-out select-none hover:bg-neutral-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-white dark:text-neutral-800 dark:hover:bg-neutral-100 dark:focus-visible:outline-neutral-200">
    Update Plan
  </button>
</div>

Configuration

The checkbox select all component is powered by a Stimulus controller that provides shift-click batch selection, keyboard navigation, nested selections with indeterminate states, and "select all pages" support for paginated data.

Controller Setup

Basic checkbox select all structure with required data attributes:

<div data-controller="checkbox-select-all">
  <!-- Select All Checkbox -->
  <input type="checkbox"
         data-checkbox-select-all-target="selectAll"
         data-action="change->checkbox-select-all#toggleAll">

  <!-- Individual Checkboxes -->
  <input type="checkbox"
         data-checkbox-select-all-target="checkbox"
         data-action="change->checkbox-select-all#toggle">
  <input type="checkbox"
         data-checkbox-select-all-target="checkbox"
         data-action="change->checkbox-select-all#toggle">
</div>

Configuration Values

Prop Description Type Default
toggleKey
Keyboard shortcut to toggle focused checkbox (e.g., "x" for Gmail-like behavior) string ""
baseAmount
Base amount to add to the total calculation (e.g., subscription fee) number 0
totalItems
Total items across all pages for the "select all pages" feature number 0

Targets

Target Description Required
selectAll
Optional master checkbox that toggles all other checkboxes when clicked Optional
checkbox
Individual checkboxes that can be selected Required
parent
Container for nested checkbox groups with parent/child relationships Optional
child
Child checkboxes within a parent group Optional
actionBar
Element that shows/hides based on selection state Optional
count
Element that displays the count of selected items Optional
total
Element that displays the calculated total amount Optional
amount
Elements containing numeric values to sum when checked Optional
pageSelectionInfo
Element showing "X of Y row(s) selected" (visible when not all pages selected) Optional
selectAllPagesPrompt
Element showing "Select all Y rows" button (visible when all on page selected but not all pages) Optional
allPagesSelectedInfo
Element showing "All Y row(s) selected" (visible when all pages selected) Optional
allPagesInput
Hidden input to pass all-pages selection state to server for form submission Optional

Actions

Action Description Usage
toggleAll
Toggles all checkboxes on/off change->checkbox-select-all#toggleAll
toggle
Handles individual checkbox clicks and shift-click batch selection change->checkbox-select-all#toggle
clearAll
Clears all selections at once click->checkbox-select-all#clearAll
toggleChildren
Toggles all child checkboxes when parent is clicked change->checkbox-select-all#toggleChildren
selectAllPages
Virtually selects all items across all pages without loading all data click->checkbox-select-all#selectAllPages
clearAllPages
Clears the "all pages" selection state and unchecks all checkboxes click->checkbox-select-all#clearAllPages

Shift-Click Batch Selection

Hold Shift while clicking to select all items between clicks. Works like Gmail: Shift+click sets the range to match the clicked state (all checked or all unchecked). When holding Shift, an anchor indicator highlights the starting point of the selection range.

Keyboard Navigation

  • Arrow Keys: Use or to navigate between checkboxes
  • Space: Toggle the focused checkbox
  • Custom Toggle Key: Configure a custom key (e.g., X) for toggling checkboxes
  • Shift + Toggle Key: Batch toggle from last clicked to current (keyboard-only batch selection)

Indeterminate States

Parent checkboxes automatically show indeterminate state when some (but not all) children are selected. Select-all checkbox shows indeterminate state when some (but not all) items are selected.

Dynamic Total Calculation

Automatically calculates totals from selected items. Add amount targets to elements with numeric values and a total target where the sum should display. Supports currency symbols and custom formatting.

Select All Pages

Enable bulk selection across paginated data with the "select all pages" feature. This allows users to virtually select all items across all pages without loading all data—the server receives a flag indicating full selection.

How It Works
Virtual Selection: Sets a flag indicating all items are selected without loading all data
Server Communication: Use the allPagesInput target to pass the selection state to your server
UI Updates: The count target automatically shows total items when all pages are selected
<!-- Enable with totalItems value -->
<div data-controller="checkbox-select-all"
     data-checkbox-select-all-total-items-value="150">

  <!-- Selection info: "5 of 150 row(s) selected" -->
  <span data-checkbox-select-all-target="pageSelectionInfo">
    <span data-checkbox-select-all-target="count">0</span> of 150 row(s) selected.
  </span>

  <!-- Prompt: "Select all 150 rows" (shown when all on page selected) -->
  <button data-checkbox-select-all-target="selectAllPagesPrompt"
          data-action="click->checkbox-select-all#selectAllPages"
          class="hidden">
    Select all 150 rows
  </button>

  <!-- Confirmation: "All 150 row(s) selected" -->
  <span data-checkbox-select-all-target="allPagesSelectedInfo" class="hidden">
    All 150 row(s) selected.
    <button data-action="click->checkbox-select-all#clearAllPages">Clear selection</button>
  </span>

  <!-- Hidden input for form submission -->
  <input type="hidden"
         name="select_all_pages"
         data-checkbox-select-all-target="allPagesInput"
         value="false"
         disabled>
</div>

Form Submission

To pass selected checkbox data to the server, give each checkbox a name and value attribute. Rails will automatically collect checked values into an array.

1. Add name/value to checkboxes:
<input type="checkbox"
       name="item_ids[]"
       value="<%= item.id %>"
       data-checkbox-select-all-target="checkbox"
       data-action="change->checkbox-select-all#toggle">
2. Add hidden input for "select all pages":
<input type="hidden"
       name="select_all_pages"
       data-checkbox-select-all-target="allPagesInput"
       value="false"
       disabled>
3. Pass filter params for "select all pages":
<!-- Pass current filters so server knows what to select -->
<input type="hidden" name="status" value="<%= params[:status] %>">
<input type="hidden" name="category" value="<%= params[:category] %>">

Server-Side Handling

Handle both individual selections and "select all pages" in your Rails controller. When select_all_pages is "true", apply your current filters without pagination to get all matching records:

1. Check the selection mode:
params[:select_all_pages] == "true"  # All pages selected
params[:item_ids]                     # Array of selected IDs
2. Query the right records:
# For "select all pages": use filters without pagination
items = Item.where(status: params[:status], category: params[:category])

# For individual selection: use the IDs
items = Item.where(id: params[:item_ids])
3. Perform the bulk action:
items.update_all(archived: true)
# or items.destroy_all, items.each(&:process!), etc.
Important: Preserve Filter State
When "select all pages" is active, you need to pass the current filter parameters to your bulk action so the server knows which items to include. Add hidden fields for each active filter:
<input type="hidden" name="status" value="<%= params[:status] %>">

Anchor Indicator

When holding Shift, a visual indicator (dashed outline) appears on the anchor checkbox showing where the next shift-click range will start from. This helps users understand the shift-click batch selection behavior.

CSS Styling
The controller adds the checkbox-anchor class to the anchor checkbox when Shift is held. Style it in your CSS:
.checkbox-anchor {
  outline: 2px dashed currentColor;
  outline-offset: 2px;
}

Table of contents

Get notified when new components come out