@use "./variables.module" as *; @use "./theme"; :root { --zIndex-canvas: 1; --zIndex-interactiveCanvas: 2; --zIndex-svgLayer: 3; --zIndex-wysiwyg: 3; --zIndex-canvasButtons: 3; --zIndex-layerUI: 4; --zIndex-viewportStatusFrame: 5; --zIndex-eyeDropperBackdrop: 5; --zIndex-eyeDropperPreview: 6; --zIndex-hyperlinkContainer: 7; --zIndex-cursorHint: 8; --zIndex-ui-bottom: 60; --zIndex-ui-context-menu: 90; --zIndex-ui-styles-popup: 100; --zIndex-ui-top: 100; --zIndex-ui-main-menu: 110; --zIndex-ui-library: 120; --zIndex-modal: 1000; --zIndex-popup: 1001; --zIndex-toast: 999999; --sab: env(safe-area-inset-bottom); --sal: env(safe-area-inset-left); --sar: env(safe-area-inset-right); --sat: env(safe-area-inset-top); } body.excalidraw-cursor-resize, body.excalidraw-cursor-resize a:hover, body.excalidraw-cursor-resize * { cursor: ew-resize; } .excalidraw { --ui-font: Assistant, system-ui, BlinkMacSystemFont, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif; --viewport-status-frame-border-width: 0px; &.excalidraw--viewport-status-border { --viewport-status-frame-border-width: 4px; } font-family: var(--ui-font); position: relative; overflow: hidden; color: var(--text-primary-color); display: flex; top: 0; bottom: 0; left: 0; right: 0; height: 100%; width: 100%; button { // browser default. Looks kinda good on low-dpi. font-size: 0.8333rem; } button, label { @include buttonNoHighlight; } button { cursor: pointer; user-select: none; } &:focus { outline: none; } // serves 2 purposes: // 1. prevent selecting text outside the component when double-clicking or // dragging inside it (e.g. on canvas) // 2. prevent selecting UI, both from the inside, and from outside the // component (e.g. if you select text in a sidebar) user-select: none; .excalidraw-link, a { font-weight: 500; text-decoration: none; color: var(--link-color); user-select: none; cursor: pointer; &:hover { text-decoration: underline; } &:active { text-decoration: none; } } canvas { touch-action: none; // following props improve blurriness at certain devicePixelRatios. // AFAIK it doesn't affect export (in fact, export seems sharp either way). image-rendering: pixelated; // chromium // NOTE: must be declared *after* the above image-rendering: -moz-crisp-edges; // FF z-index: var(--zIndex-canvas); &.interactive { z-index: var(--zIndex-interactiveCanvas); } // Remove the main canvas from document flow to avoid resizeObserver // feedback loop (see https://github.com/excalidraw/excalidraw/pull/3379) } &__canvas-wrapper, &__canvas.static { pointer-events: none; } &__canvas { position: absolute; } &__embeddable { width: 100%; height: 100%; border: 0; } &__embeddable-container { position: absolute; z-index: 2; transform-origin: top left; pointer-events: none; } .FixedSideContainer { padding-top: var(--sat, 0); padding-right: var(--sar, 0); padding-bottom: var(--sab, 0); padding-left: var(--sal, 0); } .panelRow { display: flex; justify-content: space-between; } .selected-shape-actions { display: flex; flex-direction: column; row-gap: 0.75rem; h3, legend, .control-label { margin: 0; margin-bottom: 0.25rem; font-size: 0.75rem; color: var(--text-primary-color); font-weight: 400; display: block; } .control-label input { display: block; width: 100%; } legend { padding: 0; } .iconSelectList { flex-wrap: wrap; position: relative; } .buttonList { flex-wrap: wrap; display: flex; column-gap: 0.5rem; row-gap: 0.5rem; label { font-size: 0.75rem; } input[type="radio"], input[type="button"] { opacity: 0; position: absolute; pointer-events: none; } .iconRow { margin-top: 8px; } } fieldset { margin: 0; padding: 0; border: none; } } .divider { width: 1px; background-color: $color-gray-2; margin: 1px; } .welcome-screen-menu-item:focus-visible, .dropdown-menu-item:focus-visible, button:focus-visible, .buttonList label:focus-within, input:focus-visible { outline: transparent; box-shadow: 0 0 0 1px var(--color-brand-hover); } // radix doesn't allow differntiating between hover and keyboard active // states (it's forcing :focus on both). // // proper handling would be to disable :focus-visible by default, and enable // on keyboard arrows (it'd then have to be disabled again, e.g. on keydown // or container focus) // // alas, that is left for another day [data-radix-collection-item]:focus-visible { box-shadow: none !important; } .buttonList { .ToolIcon__icon { all: unset !important; display: flex !important; } button { background-color: transparent; } label, button, .zIndexButton { @include outlineButtonIconStyles; } } // an icon button rendered outside a .buttonList context (e.g. the font // picker trigger) button.standalone { @include outlineButtonIconStyles; .ToolIcon__icon { all: unset !important; display: flex !important; } & > * { // disallow pointer events on children, so we always have event.target // on the button itself pointer-events: none; } } .App-top-bar { z-index: var(--zIndex-ui-top); display: flex; flex-direction: column; } // mobile-only: pinned near the very top of the viewport (just below the // viewport-status-frame border), independent of the top icon row's // height, rather than competing for space with the bottom toolbar. .viewport-status-frame__badge-row { position: absolute; top: calc(var(--viewport-status-frame-border-width) + 0.5rem); left: 50%; transform: translateX(-50%); // explicit (not auto) so it participates correctly against the // interactive canvas's own explicit z-index, regardless of value. z-index: var(--zIndex-viewportStatusFrame); pointer-events: none; } .App-welcome-screen { z-index: var(--zIndex-layerUI); } .App-bottom-bar { position: absolute; // account for margins width: calc(100% - 28px); max-width: 450px; bottom: 0; left: 50%; transform: translateX(-50%); --bar-padding: calc(4 * var(--space-factor)); z-index: var(--zIndex-ui-bottom); display: flex; flex-direction: column; pointer-events: none; justify-content: center; > .Island { box-sizing: border-box; max-height: 100%; padding: 4px; display: flex; flex-direction: column; pointer-events: var(--ui-pointerEvents); } } .App-toolbar { display: flex; justify-content: center; .eraser { &.ToolIcon:hover { --icon-fill-color: #fff; --keybinding-color: #fff; } &.active { background-color: var(--color-primary); } } } .excalidraw-ui-top-left, .excalidraw-ui-top-right { display: flex; align-items: center; gap: 0.5rem; } .App-toolbar-content { display: flex; flex-direction: column; pointer-events: none; & > * { pointer-events: var(--ui-pointerEvents); } } .App-mobile-menu { width: 100%; overflow-x: visible; overflow-y: auto; box-sizing: border-box; margin-bottom: var(--bar-padding); .selected-shape-actions { padding: 8px 8px 0 8px; } } .App-menu { display: grid; color: var(--icon-fill-color); } .shapes-section { display: flex; justify-content: center; pointer-events: none !important; & > * { pointer-events: var(--ui-pointerEvents); } } .App-menu_top { grid-template-columns: 1fr 2fr 1fr; grid-gap: 1rem; align-items: flex-start; cursor: default; pointer-events: none !important; & > * { pointer-events: var(--ui-pointerEvents); } @media (min-width: 1536px) { grid-template-columns: 1fr 1fr 1fr; grid-gap: 3rem; } } .App-menu_top > .App-menu_top__left { grid-column: 1; justify-self: start; // App-menu_top__left is a single-column grid, so its track width is // driven by the widest row — including the host-provided // renderTopLeftUI content, which can be arbitrarily wide. Its // (stretched) grid-item wrappers must stay pointer-events:none so that // slack isn't a dead zone over the canvas; only the actual // (content-sized, non-stretched) interactive pieces opt back in. // Specificity (not source order) is why this beats `.App-menu_top > *`. pointer-events: none; .selected-shape-actions-container, .ToolIcon__penMode { pointer-events: var(--ui-pointerEvents); } // .excalidraw-ui-top-left is itself `display: flex` (block-level), so // it *also* stretches to fill this track's width — its own box needs // the same none-then-opt-in treatment, one level deeper, so its real // (content-sized, unstretched) children stay clickable without // resizing/repositioning the row itself. .excalidraw-ui-top-left { pointer-events: none; > * { pointer-events: var(--ui-pointerEvents); } } } .App-menu_top > .shapes-section { grid-column: 2; } .selected-shape-actions-container { width: fit-content; &--compact { min-width: 48px; } } .App-menu_top > .layer-ui__wrapper__top-right { grid-column: 3; justify-self: end; } .App-menu_bottom { position: absolute; bottom: 1rem; display: flex; justify-content: space-between; align-items: flex-start; cursor: default; pointer-events: none !important; box-sizing: border-box; padding: 0 1rem; &--transition-left { section { width: 185px; } } section { display: flex; } } .App-menu_bottom > *:first-child { justify-self: flex-start; } .App-menu_bottom > *:last-child { justify-self: flex-end; } .App-menu_left { grid-template-rows: 1fr auto 1fr; height: 100%; } .App-menu_right { grid-template-rows: 1fr; height: 100%; } .App-menu__left { overflow-y: auto; padding: 0.75rem; width: 12.5rem; box-sizing: border-box; position: absolute; } .dropdown-select { height: 1.5rem; padding: 0; padding-inline-start: 0.5rem; padding-inline-end: 1.5rem; color: var(--icon-fill-color); background-color: var(--button-gray-1); border-radius: var(--space-factor); border: 1px solid var(--button-gray-2); font-size: 0.8rem; font-family: inherit; outline: none; appearance: none; background-image: var(--dropdown-icon); background-repeat: no-repeat; background-position: right 0.7rem top 50%, 0 0; :root[dir="rtl"] & { background-position: left 0.7rem top 50%, 0 0; } background-size: 0.65em auto, 100%; &:focus { box-shadow: 0 0 0 2px var(--focus-highlight-color); } &:hover { background-color: var(--button-gray-2); } &:active { background-color: var(--button-gray-2); } &__language { height: 2rem; background-color: var(--island-bg-color); border-color: var(--default-border-color) !important; cursor: pointer; &:hover { background-color: var(--island-bg-color); } } } .scroll-back-to-content { border-radius: var(--border-radius-lg); background-color: var(--island-bg-color); color: var(--icon-fill-color); border: 1px solid var(--default-border-color); padding: 10px 20px; position: absolute; left: 50%; bottom: 30px; transform: translateX(-50%); box-sizing: border-box; max-width: calc( 100% - var(--viewport-status-frame-border-width) - var(--viewport-status-frame-border-width) ); pointer-events: var(--ui-pointerEvents); font-family: inherit; &:hover { background-color: var(--button-hover-bg); } &:active { border: 1px solid var(--button-active-border); } } // always rendered as a flex column of [scroll-back-to-content, badge] — // this positions the stack itself; children are centered/spaced via flex. .floating-status-stack { position: absolute; left: var(--viewport-status-frame-border-width); right: var(--viewport-status-frame-border-width); bottom: calc(0.5rem + var(--viewport-status-frame-border-width)); display: flex; flex-direction: column; align-items: center; gap: 0.5rem; pointer-events: none; .scroll-back-to-content { position: static; left: auto; bottom: auto; transform: none; max-width: 100%; pointer-events: var(--ui-pointerEvents); } } // on mobile, when the default bottom toolbar renders, the stack is a // normal (in-flow) row above MobileShapeActions/the toolbar Island inside // .App-bottom-bar instead of being independently viewport-positioned — // self-sizing regardless of whether MobileShapeActions is showing, with // no magic-number height math needed to clear the toolbar. .App-bottom-bar > .floating-status-stack { position: static; left: auto; right: auto; bottom: auto; margin-bottom: 0.5rem; } .help-icon { @include outlineButtonStyles; @include filledButtonOnCanvas; width: var(--lg-button-size); height: var(--lg-button-size); svg { width: var(--lg-icon-size); height: var(--lg-icon-size); } } .reset-zoom-button { font-family: var(--ui-font); } .finalize-button { display: grid; grid-auto-flow: column; gap: 0.4em; margin-top: auto; margin-bottom: auto; margin-inline-start: 0.6em; } .undo-redo-buttons, .eraser-buttons { display: grid; grid-auto-flow: column; margin-top: auto; margin-bottom: auto; margin-inline-start: 0.6em; } @include isMobile { aside { display: none; } .floating-status-stack { bottom: calc(30px + var(--sab, 0)); z-index: var(--zIndex-ui-bottom); } } .rtl-mirror { :root[dir="rtl"] & { transform: scaleX(-1); } } .zen-mode-visibility { visibility: visible; opacity: 1; height: auto; width: auto; transition: opacity 0.5s; &.zen-mode-visibility--hidden { visibility: hidden; opacity: 0; height: 0; width: 0; transition: opacity 0.5s; } } .disable-pointerEvents { pointer-events: none !important; } &.excalidraw--view-mode { .App-menu { display: flex; justify-content: space-between; } } &.excalidraw--non-interactive { user-select: none; canvas { // let the page handle scrolling over the editor, but not the browser // zoom gestures (pinch, double-tap) touch-action: pan-x pan-y; // no !important — inline cursor styles must win so that // `interaction: { enabled: { links: true } }` can show a pointer over // links cursor: default; } &.excalidraw--allow-browser-zoom canvas { touch-action: auto; } // with navigation allowed, touch input drives the canvas pan/zoom again &.excalidraw--navigation canvas { touch-action: none; } // with the active tool allowed (`interaction.enabled.tools`), touch // input drives the tool again — e.g. laser strokes must not scroll the // page &.excalidraw--tools canvas { touch-action: none; } // override inline pointer-events set when embeddable is active // (also covers custom `renderEmbeddable` content) — unless embeds are // an allowed interaction, in which case the inline (activation-driven) // pointer-events apply, same as in view mode &:not(.excalidraw--embeds) .excalidraw__embeddable-container__inner { pointer-events: none !important; } } input.is-redacted { // we don't use type=password because browsers (chrome?) prompt // you to save it which is annoying -webkit-text-security: disc; } input[type="text"], textarea:not(.excalidraw-wysiwyg) { color: var(--text-primary-color); border: 1.5px solid var(--input-border-color); padding: 0.75rem; white-space: nowrap; border-radius: var(--space-factor); background-color: var(--input-bg-color); &:not(:focus) { &:hover { border-color: var(--color-brand-hover); } } &:focus { outline: none; border-color: var(--color-brand-hover); } } @media print { .App-bottom-bar, .FixedSideContainer, .layer-ui__wrapper { display: none; } } // use custom, minimalistic scrollbar // (doesn't work in Firefox) ::-webkit-scrollbar { width: 4px; height: 3px; } select::-webkit-scrollbar { width: 10px; } ::-webkit-scrollbar-thumb { background: var(--scrollbar-thumb); border-radius: 10px; } ::-webkit-scrollbar-thumb:hover { background: var(--scrollbar-thumb-hover); } ::-webkit-scrollbar-thumb:active { background: var(--scrollbar-thumb); } .mobile-misc-tools-container { position: absolute; top: calc(5rem - var(--editor-container-padding)); right: calc(var(--editor-container-padding) * -1); display: flex; flex-direction: column; border: 1px solid var(--sidebar-border-color); border-top-left-radius: var(--border-radius-lg); border-bottom-left-radius: var(--border-radius-lg); border-right: 0; overflow: hidden; background-color: var(--island-bg-color); .ToolIcon__icon { width: 2rem; height: 2rem; border-radius: 0; } .default-sidebar-trigger { border: 0; } } .App-toolbar--mobile { overflow: visible; max-width: 98vw; .ToolIcon__keybinding { display: none; } } .UserList-Wrapper { margin: 0; padding: 0; border: none; text-align: left; legend { display: block; font-size: 0.75rem; font-weight: 400; margin: 0 0 0.25rem; padding: 0; } } .main-menu { z-index: var(--zIndex-ui-main-menu); } .main-menu-trigger { @include filledButtonOnCanvas; } .App-mobile-menu, .App-menu__left { --button-border: transparent; --button-bg: var(--color-surface-mid); } @at-root .excalidraw.theme--dark#{&} { .App-mobile-menu, .App-menu__left { --button-hover-bg: #363541; --button-bg: var(--color-surface-high); } } .App-menu__left { .buttonList { padding: 0.25rem 0; } } .excalidraw__paragraph { margin: 1rem 0; } .Modal__content { .excalidraw__paragraph:first-child { margin-top: 0; } .excalidraw__paragraph + .excalidraw__paragraph { margin-top: 0rem; } } .link-button { display: flex; text-decoration: none !important; } } .ErrorSplash.excalidraw { min-height: 100vh; padding: 20px 0; overflow: auto; display: flex; align-items: center; justify-content: center; user-select: text; .ErrorSplash-messageContainer { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 40px; background-color: $color-red-1; border: 3px solid $color-red-9; } .ErrorSplash-paragraph { margin: 15px 0; max-width: 600px; &.align-center { text-align: center; } } .bigger, .bigger button { font-size: 1.1em; } .smaller, .smaller button { font-size: 0.9em; } .ErrorSplash-details { display: flex; flex-direction: column; align-items: flex-start; textarea { width: 100%; margin: 10px 0; font-family: "Cascadia"; font-size: 0.8em; } } } .excalidraw__embeddable-container { .excalidraw__embeddable-container__inner { position: relative; overflow: hidden; display: flex; align-items: center; justify-content: center; border-radius: var(--embeddable-radius); } .excalidraw__embeddable__generating { position: absolute; z-index: 1; bottom: 1rem; right: 1rem; display: flex; align-items: center; gap: 0.5rem; padding: 0.375rem 0.75rem; border-radius: 12px; background: rgba(0, 0, 0, 0.5); color: #fff; font-size: 0.875rem; font-family: "Assistant"; pointer-events: none; .Spinner { --spinner-color: #fff; flex: 0 0 auto; height: auto; margin: 0; } } .excalidraw__embeddable__outer { width: 100%; height: 100%; } .excalidraw__embeddable__content { width: 100%; height: 100%; transform-origin: top left; &, & > * { border-radius: var(--embeddable-radius); } } .excalidraw__embeddable-hint { position: absolute; z-index: 1; background: rgba(0, 0, 0, 0.5); padding: 1rem 1.6rem; border-radius: 12px; color: #fff; font-weight: 700; letter-spacing: 0.6px; font-family: "Assistant"; } }