Is it possible to customize the scrollbar

I would like to change the width and color for the scrollbar and have tried it with the settings as I did with the CEP panels. But unfortunately this does not work. Is there another possibility

::-webkit-scrollbar {
  width: 8px;
}

::-webkit-scrollbar-track {
-webkit-box-shadow: none;
    background-color: #262626;
}

::-webkit-scrollbar-thumb {
    border-radius: 0;
    background-color: #363636;
    border: 1px solid #262626;
    margin: 1px;
    -webkit-box-shadow: none;
}

Does anyone have an idea

No, unfortunately it’s not possible to style scrollbars in UXP.

Maybe you could try to make entirely your own. But it will take some effort.

If you do make your own and are in a sharing mood, I think the community would love to see what you come up with!

But as @simonhenke notes, there’s no CSS support for modifying the native scroll bars. (These are drawn entirely by the host [Ps or XD]).

I personally don’t think that building an own solution would be a great idea (in UXP), because…

  1. You can’t hide scrollbars. This means, you can’t make use of the native scrolling behavior. That’s how most of the custom scrollbar libraries for the web work, they just hide the original scrollbars and draw their own ones. Not possible in this case due to the z-index/always visible bug.

  2. onScroll or onMouseWheel events don’t work. This means even if you’d build your own wrapper component that hides excess content via overflow:hidden and write functions to move its content’s position, you couldn’t bind it to the scroll event. And nobody wants to scroll by dragging a scrollbar, that’s just bad UX :sweat_smile:

Actually, Textarea on windows does not have any scrollbar at all. So it would be an improvement. Now you can scroll only using keyboards and page up/down or selecting and dragging the text.

Can’t you just make it large and put it in a smaller container which is then scrollable?
Still not a great solution though, as you’d have to give it a fixed height.

I don’t know. Can I? It bothers me the most in the Alchemist plugin.

Maybe, but the results might be weird.

I’ve set up a small test component (react) to experiment with a custom scrollbar, it can definitely be built…

If anyone wants to use the code, i’ll attach it here, but I won’t keep on working on it. There’s a lot of things that need to be handled (horizontal scrollbar, hide scrollbars when content fits, clicking on the track, …) and the code probably has some flaws (had to use setTimeout to get the actual height of the children elements).
The better choice would probably be to use an existing library and adjust it to fit to UXP if necessary.

Scroller.zip (1.3 KB)

Could you use onLoad event instead of timeout?

Not sure about that in a React Context.
Usually, reading the height in componentDidMount or componentDidUpdate should be fine as it happens after the render, but it didn’t in this case. Maybe the props.children takes longer to draw, I don’t know.

It’s worth noting that this is one of those big differences between UXP and browsers – UXP’s DOM is not really synchronous. It looks like that most of the time, but layout is one of those places where it doesn’t.

UXP doesn’t calculate element sizes until a layout pass occurs, and those passes occur only when frames are generated. So if you add an element to the DOM, it could be upwards of 16ms before you see size information.

UXP tries to be helpful here: it will send a resize event your way when it’s safe to look at an element’s size, but of course you can use setTimeout or even requestAnimationFrame, but resize is the guaranteed method that works.

Of course, libraries made for the browser don’t do this, but they would have to be tweaked using timeouts or resize to work properly in UXP.

Ah that’s interesting and it does make more sense now why some solutions from Github or StackOverflow weren’t working, where they just added setTimeout(...) or setTimeout(...,1) to wait for the actual size. In my case I needed to wait about ~80ms.

That resize event you mentioned sounds perfect, will it be fired on the document or where else? (document.addEventListener('resize', () => ...) for eample?)

resize does not bubble, so you’d want to register the event listener wherever it makes the most sense; for a scroller like this, I’d put it on the scroll container.

even better, thanks!

Hi everyone, I’m a beginner programing still learning I create this shadcn style scrollbar, but the only problem is the mouse wheel support I don’t find a way to make it work I attach the code if someone wants to explore into the code.

:: | Scrollbar Code | ::

async function createDialog() {
  // Add body cursor style to prevent Photoshop interaction
  document.body.style.cursor = "default";

  const dlg = document.createElement("dialog");
  dlg.style.display = "flex";
  dlg.style.width = "450px";
  dlg.style.height = "600px";
  dlg.style.backgroundColor = "#0b0b0b";

  function createCustomScrollArea(items = []) {
    // Create isolation wrapper
    const isolationWrapper = document.createElement("div");
    isolationWrapper.style.position = "relative";
    isolationWrapper.style.width = "260px";
    isolationWrapper.style.height = "200px";
    isolationWrapper.style.contain = "layout style paint size";
    isolationWrapper.style.isolation = "isolate";
    isolationWrapper.style.zIndex = "1000";
    isolationWrapper.style.transform = "translateZ(0)";
    isolationWrapper.style.willChange = "transform";
    isolationWrapper.style.pointerEvents = "auto";
    isolationWrapper.style.overflow = "hidden";

    const container = document.createElement("div");
    container.style.position = "relative";
    container.style.width = "100%";
    container.style.height = "100%";
    container.style.border = "1px solid #ccc";
    container.style.borderRadius = "8px";
    container.style.background = "#fff";
    container.style.overflow = "hidden";
    container.style.padding = "8px";
    container.tabIndex = 0;
    container.style.outline = "none";

    const scrollTrack = document.createElement("div");
    scrollTrack.style.position = "relative";
    scrollTrack.style.height = "100%";
    scrollTrack.style.overflow = "hidden";

    const scrollContent = document.createElement("div");
    scrollContent.style.position = "absolute";
    scrollContent.style.top = "0";
    scrollContent.style.left = "0";
    scrollContent.style.right = "0";
    scrollContent.style.transition = "top 0.1s ease";

    items.forEach((text) => {
      const item = document.createElement("div");
      item.textContent = text;
      item.style.padding = "6px 8px";
      item.style.fontSize = "13px";
      item.style.borderBottom = "1px solid #eee";
      scrollContent.appendChild(item);
    });

    scrollTrack.appendChild(scrollContent);
    container.appendChild(scrollTrack);

    const scrollBar = document.createElement("div");
    scrollBar.style.position = "absolute";
    scrollBar.style.top = "8px";
    scrollBar.style.right = "4px";
    scrollBar.style.width = "6px";
    scrollBar.style.height = "100%";
    scrollBar.style.background = "transparent";

    const scrollThumb = document.createElement("div");
    scrollThumb.style.position = "absolute";
    scrollThumb.style.top = "0";
    scrollThumb.style.left = "0";
    scrollThumb.style.width = "100%";
    scrollThumb.style.height = "40px";
    scrollThumb.style.background = "#999";
    scrollThumb.style.borderRadius = "4px";
    scrollThumb.style.opacity = "0";
    scrollThumb.style.transition = "opacity 0.2s ease";
    scrollThumb.style.cursor = "grab";

    scrollBar.appendChild(scrollThumb);
    container.appendChild(scrollBar);

    // Mouse over/leave handlers with debug logging
    container.addEventListener("mouseenter", () => {
      console.log("Container mouseenter - showing scrollbar");
      scrollThumb.style.opacity = "1";
    });

    container.addEventListener("mouseleave", () => {
      console.log("Container mouseleave - hiding scrollbar");
      if (!isDragging) {
        scrollThumb.style.opacity = "0";
      }
    });

    let currentScrollTop = 0;

    function updateScrollPosition(newScrollTop) {
      const contentHeight = scrollContent.scrollHeight;
      const visibleHeight = scrollTrack.clientHeight;
      const maxScroll = Math.max(0, contentHeight - visibleHeight);

      currentScrollTop = Math.max(0, Math.min(newScrollTop, maxScroll));

      scrollContent.style.top = `-${currentScrollTop}px`;

      if (maxScroll > 0) {
        const scrollRatio = currentScrollTop / maxScroll;
        const maxThumbTop =
          container.clientHeight - scrollThumb.clientHeight - 16;
        const thumbTop = scrollRatio * maxThumbTop;
        scrollThumb.style.top = `${thumbTop}px`;
      }
    }

    // Drag functionality with debug logging
    let isDragging = false;
    let startY = 0;
    let startTop = 0;

    scrollThumb.addEventListener("mousedown", (e) => {
      console.log("Thumb mousedown - starting drag");
      isDragging = true;
      startY = e.clientY;
      startTop = Number.parseInt(scrollThumb.style.top || "0");
      scrollThumb.style.cursor = "grabbing";
      e.preventDefault();
    });

    document.addEventListener("mousemove", (e) => {
      if (!isDragging) return;
      console.log("Dragging thumb, clientY:", e.clientY, "startY:", startY);

      const deltaY = e.clientY - startY;
      let newTop = startTop + deltaY;
      const maxThumbTop =
        container.clientHeight - scrollThumb.clientHeight - 16;
      newTop = Math.max(0, Math.min(newTop, maxThumbTop));

      console.log("Setting thumb top to:", newTop, "maxThumbTop:", maxThumbTop);
      scrollThumb.style.top = `${newTop}px`;

      // Visual debug - change thumb color temporarily
      scrollThumb.style.background = "#ff0000";
      setTimeout(() => {
        scrollThumb.style.background = "#999";
      }, 200);

      if (maxThumbTop > 0) {
        const scrollRatio = newTop / maxThumbTop;
        const contentHeight = scrollContent.scrollHeight;
        const visibleHeight = scrollTrack.clientHeight;
        const maxScroll = Math.max(0, contentHeight - visibleHeight);
        currentScrollTop = scrollRatio * maxScroll;
        console.log("Setting content scroll to:", currentScrollTop);
        scrollContent.style.top = `-${currentScrollTop}px`;
      }
    });

    document.addEventListener("mouseup", () => {
      if (isDragging) {
        console.log("Thumb mouseup - ending drag");
        isDragging = false;
        scrollThumb.style.cursor = "grab";
      }
    });

    // Store wheel handler function for later binding
    let wheelHandler = null;
    let isHovered = false;

    container.addEventListener("mouseenter", () => {
      isHovered = true;
    });

    container.addEventListener("mouseleave", () => {
      isHovered = false;
    });

    // Wheel event handler with aggressive blocking
    wheelHandler = function(e) {
      if (!isHovered) return;

      console.log("Wheel event caught:", e.type, "deltaY:", e.deltaY);

      // Aggressive event blocking for UXP
      e.preventDefault();
      e.stopPropagation();
      e.stopImmediatePropagation();
      e.cancelBubble = true;
      e.returnValue = false;

      // Block on the event object itself
      if (e.stopEvent) e.stopEvent();
      if (e.halt) e.halt();

      let delta = e.deltaY || e.wheelDelta || e.detail || 0;

      // Normalize wheel delta
      if (e.deltaMode === 1) {
        delta *= 16;
      } else if (e.deltaMode === 2) {
        delta *= scrollTrack.clientHeight;
      }

      const scrollStep = Math.min(Math.abs(delta), 40);
      const scrollDirection = delta > 0 ? 1 : -1;
      const newScrollTop = currentScrollTop + scrollStep * scrollDirection;

      updateScrollPosition(newScrollTop);

      return false;
    };

    // Store reference to container for later event binding
    container._wheelHandler = wheelHandler;

    // Keyboard alternative
    container.addEventListener("keydown", (e) => {
      let scrollDelta = 0;
      switch(e.key) {
        case "ArrowUp": scrollDelta = -30; break;
        case "ArrowDown": scrollDelta = 30; break;
        case "PageUp": scrollDelta = -100; break;
        case "PageDown": scrollDelta = 100; break;
      }

      if (scrollDelta !== 0) {
        e.preventDefault();
        updateScrollPosition(currentScrollTop + scrollDelta);
      }
    });

    // Initialize scroll position and check geometry
    setTimeout(() => {
      console.log("Initializing scroll area...");
      const contentHeight = scrollContent.scrollHeight;
      const visibleHeight = scrollTrack.clientHeight;
      console.log("Content height:", contentHeight, "Visible height:", visibleHeight);

      // Set proper thumb height based on content ratio
      if (contentHeight > visibleHeight) {
        const scrollRatio = visibleHeight / contentHeight;
        const thumbHeight = Math.max(20, scrollRatio * scrollBar.clientHeight);
        scrollThumb.style.height = `${thumbHeight}px`;
        console.log("Set thumb height to:", thumbHeight);
      }

      updateScrollPosition(0);
      console.log("Scroll area initialized");
    }, 100);

    isolationWrapper.appendChild(container);
    return isolationWrapper;
  }

  const versions = [
    "v1.2.0-beta.32",
    "v1.2.0-beta.31",
    "v1.2.0-beta.30",
    "v1.2.0-beta.29",
    "v1.2.0-beta.28",
    "v1.2.0-beta.27",
    "v1.2.0-beta.26",
    "v1.2.0-beta.25",
    "v1.2.0-beta.24",
    "v1.2.0-beta.23",
    "v1.2.0-beta.22",
    "v1.2.0-beta.21",
    "v1.2.0-beta.20",
    "v1.2.0-beta.19",
    "v1.2.0-beta.18",
    "v1.2.0-beta.17",
    "v1.2.0-beta.16",
    "v1.2.0-beta.15"
  ];

const panel = document.createElement("div");
  panel.style.display = "flex";
  panel.style.padding = "20px";
  panel.style.width = "350px";
  panel.style.height = "480px";
  panel.style.borderRadius = "12px";
  panel.style.backgroundColor = "#3d3d3f";
panel.appendChild(createCustomScrollArea(versions));

  dlg.appendChild(panel);
  document.body.appendChild(dlg);
  await dlg.showModal();

  // Bind wheel events AFTER dialog is shown and rendered
  setTimeout(() => {
    const scrollArea = panel.querySelector('div[tabindex="0"]');
    const isolationWrapper = panel.querySelector('div > div');

    if (scrollArea && scrollArea._wheelHandler && isolationWrapper) {
      console.log("Binding wheel events after dialog load...");

      // Bind with capture to intercept before other handlers
      scrollArea.addEventListener("wheel", scrollArea._wheelHandler, { passive: false, capture: true });
      scrollArea.addEventListener("mousewheel", scrollArea._wheelHandler, { passive: false, capture: true });
      scrollArea.addEventListener("DOMMouseScroll", scrollArea._wheelHandler, { passive: false, capture: true });

      // Block all wheel events on the isolation wrapper
      const wrapperBlocker = function(e) {
        console.log("Isolation wrapper blocking wheel event");
        e.preventDefault();
        e.stopPropagation();
        e.stopImmediatePropagation();
        e.cancelBubble = true;
        e.returnValue = false;
        return false;
      };

      isolationWrapper.addEventListener("wheel", wrapperBlocker, { passive: false, capture: true });
      isolationWrapper.addEventListener("mousewheel", wrapperBlocker, { passive: false, capture: true });
      isolationWrapper.addEventListener("DOMMouseScroll", wrapperBlocker, { passive: false, capture: true });

      console.log("Wheel events bound successfully with isolation wrapper");
    }
  }, 500);
}

await createDialog();