What CSS technologies are used in the Photoshop Web version?

This article is translated from CSS Findings From Photoshop Web Version
, by Ahmad, slightly edited.

A few weeks ago, Adobe released a web version of Photoshop built with web technologies such as WebAssembly, Web Components, and P3 Color.

Photoshop was the first professional design application I learned when I was 14 years old. This is one of the reasons I became a designer and eventually a front-end developer. Because of this, I thought it would be interesting to see how CSS aids the development of large-scale applications like Photoshop.

In this article, I’ll share some interesting CSS discoveries I’ve made in the web version of Photoshop.

Photoshop old version Logo

The first thing I noticed was the old logo from Photoshop (1990-1991) in the browser console.

Are you curious about how something like this is made? Here is the code:

console.info(
  "%c ?dobe %cPhotoshop Web%c ?023.22.0.0%c ?6043548b47",
  "padding-left: 36px; line-height: 36px; background-image: url('data:image/gif;base64,R0lGODlhIAAgAPEBAAAAAP///wAAAAAAACH5BAEAAAIALAAAAAAgACAAAAKkhC + py3zfopxGvIsztsCGD4La2FVAGBoBuZnox45WcqLsum5KDWdvf1n wTo + fLbgDqo7LFCJJbOY0oidt6ozVKrtib0qaCnlYcJh7rf5iK6HZaM64VeS6L + pWf89WT + 6vRAUBBVQ1gpOTJ4IYdxCnOBSJ8ZhkZNekk5ZSxpTpt6Y1eEVm00j3VALDmBXVyPEJB2r2S hoLh2ASqvU60dsr5yuBUQAAOw=='); background -size: 32px; background-repeat: no-repeat; background-position: 2px 2px", "background: #666; border-radius:0.5em 0 0 0.5em; padding:0.2em 0em 0.1em 0.5em; color: white; font-weight: bold", "background: #666; border-radius:0 0.5em 0.5em 0; padding:0.2em 0.5em 0.1em 0em; color: white;", "\ ", "background: #c3a650; border-radius:0.5em; padding:0.2em 0.5em 0.1em 0.5em; color: white;", "", "background: #15889f; border-radius: 0.5em; padding:0.2em 0.5em 0.1em 0.5em; color: white;");

body element

To make an application like Photoshop feel like a real application on a Web page, it first needs to prevent scrolling. To achieve this, the element is set with position: fixed and overflow: hidden.

body,
html {<!-- -->
  height: 100%;
}

body {<!-- -->
  font-family: adobe-clean, sans-serif;
  margin: 0;
  overflow: hidden;
  position: fixed;
  width: 100%;
}

Inside the element, there are also multiple root elements.

<psw-app>
  <psw-app-context>
    <ue-video-surface>
      <ue-drawer>
        <div id="appView">
          <psw-app-navbar></psw-app-navbar>
          <psw-document-page></psw-document-page>
        </div>
      </ue-drawer>
    </ue-video-surface>
  </psw-app-context>
</psw-app>

The innermost element is #appView which contains the navigation and documentation pages.

#appView {<!-- -->
  background-color: var(--editor-background-color);
  color: var(--spectrum-global-color-gray-800);
  display: flex;
  flex-direction: column;
}

Almost all Flexbox layouts

There are many benefits to using flexbox when building a web application. When I think about Flexbox appearing together with Photoshop, I have mixed feelings.

Photoshop is a well-known design software and is the first software for many people to enter the design field. Building components with Flexbox on the other hand becomes easier, and CSS is easier for newbies.

Instead of clearing the float with clearfix, just add display: flex and style the children as needed. Let’s explore related Flexbox usage in Photoshop.

Navigation bar

I like the naming here. They use “start“, “center” and “end” instead of using “left, center, right”,.

This logical naming is correct for applications that can work from left to right (LTR) or right to left (RTL).

Context Bar

Nested flexbox containers are necessary when building complex applications like Photoshop. In the image below, I have highlighted two containers in the context bar.

The first container is used for grab actions. The second container contains all actions and buttons.

.container {<!-- -->
  display: flex;
  flex-wrap: nowrap;
  align-items: center;
  gap: var(--spectrum-global-dimension-size-50);
}
  • The use of gap is very helpful in defining spacing. It is much better than using margin or padding.

  • The name .container is too generic, but it’s just right here since this is a web component and all the styling is encapsulated inside.

Layer

Since the layer feature is an important part of Photoshop, it is probably one of the first few things a newbie will learn. I curiously checked the CSS implementation behind them.

Here is the HTML code for the layer component:

<psw-tree-view-item indent="0" layer-visible can-open dir="ltr" open>
  <div id="link">
    <span id="first-column"></span>
    <span id="second-column"></span>
    <span id="label"></span>
  </div>
</psw-tree-view-item>

Do you think it’s totally okay to use ID here? Since this is a web component, it doesn’t matter how many times #first-column ID appears on the page.

The #link element is the main flexbox wrapper, and the elements within #label are also flexbox wrappers.

<div class="layer-content layer-wrapper selected">
  <psw-layer-thumbnail></psw-layer-thumbnail>
  <div class="name" title="Layer name">Layer name</div>
  <div class="actions"></div>
  <overlay-trigger></overlay-trigger>
</div>

Let’s see how indentation of sublayers is done.

  • :host()Presentation layer component
  • If there is an HTML attribute indent=1, change the padding-right of the first column.

CSS :host is a pseudo-class selector that is used to select the host element of the current component. The :host selector can only be used in Shadow DOM because it selects the root element of the component, not the child elements inside the component.

:host([dir="ltr"][indent="1"]) #first-column {<!-- -->
  padding-right: var(--spectrum-global-dimension-size-200);
}

If it is indent=2, the value of padding-right is multiplied by 2 through the CSS calc() function.

:host([dir="ltr"][indent="2"]) #first-column {<!-- -->
  padding-right: calc(2 * var(--spectrum-global-dimension-size-200));
}

In the browser I tried nesting to level 6. Here is a real screenshot:

When seeing this, I checked the CSS implementation behind Figma. They used a spacer component to increase the spacing between nested layers.

Interestingly, the two major design applications use different techniques to achieve the same goal.

About CSS Grid layout

New file pop-up window

When creating a new Photoshop file, you can choose from a predefined list of sizes. To achieve this, there is a layout with multiple tabs and an active panel.

The HTML code is as follows:

<sp-tabs
  id="tabs"
  quiet=""
  selected="2"
  size="m"
  direction="horizontal"
  dir="ltr"
  focusable=""
>
  <div id="list"></div>
  <slot name="tab-panel"></slot>
</sp-tabs>

In CSS, there is a main grid with 1 column and 2 rows. The first line is auto, the second line spans the available space.

:host {<!-- -->
  display: grid;
  grid-template-columns: 100%;
}

:host(:not([direction^="vertical"])) {<!-- -->
  grid-template-rows: auto 1fr;
}

A few things here:

  • Use CSS:not() selector
  • Use the [attr^=value] selector to exclude HTML elements whose value starts with vertical.

I think this is a conditional CSS technique.

I tried changing the direction attribute to vertical.

Here is the CSS based on the property changes:

:host([direction^="vertical"]) {<!-- -->
  grid-template-columns: auto 1fr;
}

:host([direction^="vertical-right"]) #list #selection-indicator,
:host([direction^="vertical"]) #list #selection-indicator {<!-- -->
  inline-size: var(
    --mod-tabs-divider-size,
    var(--spectrum-tabs-divider-size)
  );
  inset-block-start: 0px;
  inset-inline-start: 0px;
  position: absolute;
}

To highlight which tab item is active, there is a #selection-indicator element positioned relative to the tab list.

Layer properties

I really like the CSS grid here. It works well for the problem of aligning multiple elements in a grid.

In the CSS I noticed the following code:

.content {<!-- -->
  position: relative;
  display: grid;
  grid-template-rows: [horizontal] min-content [vertical] min-content [transforms] min-content [end];
  grid-template-columns: [size-labels] min-content [size-inputs] auto [size-locks] min-content [space] min-content [position-labels] min-content [position-inputs] auto [end ];
  row-gap: var(--spectrum-global-dimension-size-150);
}

The technique used here is called named grid lines. The idea is that you name each column or grid and then define its width. Column and row widths are auto or min-content. This is a great way to make dynamic meshes.

This way every grid item should be positioned within the grid. Here are some examples:

.horizontal-size-label {<!-- -->
  grid-area: horizontal / size-labels / horizontal / size-labels;
}

.vertical-position-input {<!-- -->
  grid-area: vertical / position-inputs / vertical / position-inputs;
}

.horizontal-position-input {<!-- -->
  grid-area: horizontal / position-inputs / horizontal /
    position-inputs;
}

Another detail that caught my attention is the use of position: absolute in grid items. The lock button is placed in the center of the grid, but it needs to be slightly offset at the left and top positions.

.lock-button {<!-- -->
  grid-area: horizontal / size-locks / horizontal / size-locks;
  position: absolute;
  left: 8px;
  top: 22px;
}

Drop-Shadow input box

Here is an example of many CSS grids being used for input field layout.

:host([editable]) {<!-- -->
  display: grid;
  grid-template-areas:
    "label ."
    "slider number";
  grid-template-columns: 1fr auto;
}

:host([editable]) #label-container {<!-- -->
  grid-area: label / label / label / label;
}

:host([editable]) #label-container + div {<!-- -->
  grid-area: slider / slider / slider / slider;
}

:host([editable]) sp-number-field {<!-- -->
  grid-area: number / number / number / number;
}

When inspecting in a browser, you can see grid line names or grid area names.

Corresponding grid line name:

You can view the layout in two different ways, useful for debugging or understanding the layout you are trying to build/fix.
CSS grid should be used more often in our web applications, but definitely not like the example below.

Menu grid

I think using CSS grid layout is a bit over the top here, so here’s my understanding.

sp-menu-item {<!-- -->
  display: grid;
  grid-template-areas:
    ". chevronAreaCollapsible . iconArea sectionHeadingArea . . ."
    "selectedArea chevronAreaCollapsible checkmarkArea iconArea labelArea valueArea actionsArea chevronAreaDrillIn"
    ". . . . descriptionArea . . ."
    ". . . . submenuArea . . .";
  grid-template-columns: auto auto auto auto 1fr auto auto auto;
  grid-template-rows: 1fr auto auto auto;
}

This is a grid with 8 columns * 4 rows. From the time I’ve spent trying to understand why they do this, it seems that only one row of the grid is active at a time, with other rows collapsing due to empty content or missing HTML elements.

Interestingly, the CSS above is my simplified version. The original version looked like this, with the team using the grid-template shorthand.

Below are the relevant menu items I can find in the application.

This CSS grid is designed for this widget and I think using CSS grid here is an over design.

Below is an example using a grid.

.checkmark {<!-- -->
  align-self: start;
  grid-area: checkmarkArea / checkmarkArea / checkmarkArea /
    checkmarkArea;
}

#label {<!-- -->
  grid-area: labelArea / labelArea / labelArea / labelArea;
}

::slotted([slot="value"]) {<!-- -->
  grid-area: valueArea / valueArea / valueArea / valueArea;
}

Note that the gray portion of the CSS grid is inactive. They are collapsed because they have no content. For this specific example, the author could also do this:

.checkmark {<!-- -->
  align-self: start;
  grid-area: checkmarkArea;
}

#label {<!-- -->
  grid-area: labelArea;
}

::slotted([slot="value"]) {<!-- -->
  grid-area: valueArea;
}

There is no need to define the start and end of each column and row when they are the same value.

Extensive use of CSS variables

I really like how CSS variables can be used to change the UI. I will highlight several examples of this.

Change the size of layer thumbnails

If you are familiar with Photoshop, you can control the thumbnail size and make it smaller. This is useful when you have many layers and want to view more layers in less space.

First of all, there is an HTML attribute large-thumbs on the main container of the layer panel.

<psw-layers-panel large-thumbs></psw-layers-panel>

In CSS, there is :host([large-thumbs]) to assign specific CSS variables.

:host([large-thumbs]) {<!-- -->
  --psw-custom-layer-thumbnail-size: var(
    --spectrum-global-dimension-size-800
  );
  --psw-custom-layer-thumbnail-border-size: var(
    --spectrum-global-dimension-size-50
  );
}

For each layer, there is an element named psw-layer-thumbnail. This is where CSS variables will be applied. It will inherit it from the main container.

<psw-layers-panel-item>
  <psw-tree-view-item>
    <psw-layer-thumbnail class="thumb"></psw-layer-thumbnail>
  </psw-tree-view-item>
</psw-layers-panel-item>

Here CSS variables are assigned to thumbnails.

:host {<!-- -->
  --layer-thumbnail-size: var(
    --psw-custom-layer-thumbnail-size,
    var(--spectrum-global-dimension-size-400)
  );
  --layer-badge-size: var(--spectrum-global-dimension-size-200);
  position: relative;
  width: var(--layer-thumbnail-size);
  min-width: var(--layer-thumbnail-size);
  height: var(--layer-thumbnail-size);
}

Loading progress bar

Managing the size of a component is done using the size property, a CSS variable that changes based on the size.

:host([size="m"]) {<!-- -->
  --spectrum-progressbar-size-default: var(
    --spectrum-progressbar-size-2400
  );
  --spectrum-progressbar-font-size: var(--spectrum-font-size-75);
  --spectrum-progressbar-thickness: var(
    --spectrum-progress-bar-thickness-large
  );
  --spectrum-progressbar-spacing-top-to-text: var(
    --spectrum-component-top-to-text-75
  );
}

Image control

If the HTML attribute quite is present, the UI is simpler.

This can also be achieved through CSS variables.

:host([quiet]) {<!-- -->
  --spectrum-actionbutton-background-color-default: var(
    --system-spectrum-actionbutton-quiet-background-color-default
  );
  --spectrum-actionbutton-background-color-hover: var(
    --system-spectrum-actionbutton-quiet-background-color-hover
  );
  /* And a lot more styles that I removed for the purpose of keeping the article clean. */
}

Radio button

In this example, the team uses CSS variables to change the size of a radio button based on the size HTML attribute.

<sp-radio size="m" checked="" role="radio"></sp-radio>
:host([size="m"]) {<!-- -->
  --spectrum-radio-height: var(--spectrum-component-height-100);
  --spectrum-radio-button-control-size: var(
    --spectrum-radio-button-control-size-medium
  );
  /* And a lot more styles that I removed for the purpose of keeping the article clean. */
}

Lock page when menu is active

When the main menu is active, there is a “holder” element that fills the entire screen and is located below the menu.

#actual[aria-hidden] + #holder {<!-- -->
  display: flex;
}

#holder {<!-- -->
  display: none;
  align-items: center;
  justify-content: center;
  flex-flow: column;
  width: 100%;
  height: 100%;
  position: absolute;
  top: 0;
  left: 0;
}

This element is used to prevent users from clicking or hovering over other parts of the page, appearing to mimic a desktop application.

Mixed Mode Menu

I discovered the use of the CSS viewport unit here. The maximum height of a blended mode menu is 55vh.

sp-menu {<!-- -->
  max-height: 55vh;
  --mod-menu-item-min-height: auto;
}

::slotted(*) {<!-- -->
  overscroll-behavior: contain;
}

overscroll-behavior: contain is also useful. This is a great feature to avoid scrolling the body content.

Annotation component

Users can pin notes or drawings anywhere on the canvas. I inspected the component to see how it was built.

I like dynamic positioning and color CSS variables

To place each comment at the location chosen by the user, the team used CSS variables provided via JS.

<div
  data-html2canvas-ignore="true"
  class="Pin__component ccx-annotation"
  style="
    --offset-x: 570.359375px;
    --offset-y: 74.23046875px;
    --ccx-comments-pin-color: #16878C;
  "
></div>
.Pin__component {<!-- -->
  --pin-diameter: 24px;
  left: calc(var(--offset-x) - var(--pin-diameter) / 2);
  top: calc(var(--offset-y) - var(--pin-diameter) / 2);
  position: absolute;
  height: var(--pin-diameter);
  width: var(--pin-diameter);
  border-radius: var(--pin-diameter);
  border: 1px solid white;
  background: var(--ccx-comments-pin-color);
}

Use SVG for engineering drawing annotation

When you make the image smaller, the SVG strokes don’t resize and look thick.

As far as I know, this can be solved by adding vector-effect: non-scaling-stroke. But I haven’t tried it.

Use Object-Fit: Contain for layer thumbnails

In the Layers panel, use object-fit: contain for thumbnails to avoid distortion.

Finally

The article ends here, introducing some CSS techniques used in the web version of Photoshop. Compared with common domestic CSS technology, there are many differences, many of which are worth learning and learning from. Of course this is only part of it, if you are interested, you can check out their source code to delve deeper.

If you find this article useful, remember to give it a like and support it. You may use it someday if you save it~

Focus on front-end development, share front-end related technical information, public account: Nancheng Big Front-end (ID: nanchengfe)