Styling UI

Sourcegraph has many UI components. A unique constraint for these is that they need to run within different environments. Wherever a component is running, it should look native to its environment and consistent with the design of that environment. For example, our hover overlay needs to work and behave the same in the Sourcegraph webapp and in the browser extension, where it is injected into a variety of code hosts, but look native to the environment. Components need to be able to adapt styles from CSS stylesheets already loaded on the page (no matter how those were architected).

Goals

  1. Components decoupled from styling, that look consistent with the host environment.
  2. Support light and dark themes.
  3. Tooling support:
    • Autocompletion for styles when writing components
    • Autocompletion when writing styles
    • Linting for styles
    • Browser dev tools to easily inspect and iterate on styles
    • Autoprefixer
  4. Support for advanced CSS features, like state selectors, pseudo elements, flexbox, grid, media queries, CSS variables, ...

Environments

Host-agnostic UI

Components that need to run in different environments (any UI shared between our browser extension and the webapp) adopt styles from their environments through configurable CSS class names (as opposed to trying to replicate the styling with copied CSS). A component may accept multiple class names for different elements of the component. An example of this is <HoverOverlay/>: see how the different props it accepts for its child components' class names (such as buttons) are passed in the webapp and in code host integrations. They are defined for each code host referencing CSS class names that the code host defines in its own styles:

This means when one of the code hosts tweaks its design, or supports multiple themes, the UI elements contributed by our code host integrations automatically adapt with no effort on our part.

CSS classes as an approach represent the lowest common denominator for styling between all environments, by staying close to the native web platform.

You may also notice that multiple of the above code hosts define the same or very similar classes to Bootstrap, which makes it easy to map classes between our web app and code host environments.

Host-specific UI

In the environments we control ourselves (such as our webapp, the options page of the browser extension, or our marketing website), we use a customized version of Bootstrap as a CSS framework. Any code inside our webapp can and should make use of the CSS classes that Bootstrap provides as building blocks (and should generally do so instead of writing custom styles). This includes classes like cards, buttons or input groups, but also utility classes for layout and spacing. Please refer to the excellent Bootstrap documentation for everything that is available for use. To see what our customizations look like visually (in both light and dark theme), you can find a showcase in our Storybook.

Components only used in a specific host environment do not need to support customization through class names. They can however utilize environment-agnostic components by passing our Bootstrap classes as custom className values.

Our approach to styling

General guidelines

  • Colocate styles with the corresponding component. Stylesheet file should be named like the .tsx component file.
  • Prefer classes over descendant/child selectors. It decouples styles from the DOM structure of the component, ensures encapsulation and avoids CSS specificity issues.
  • Create utility classes for styles that should be shared horizontally between components.
  • Avoid hardcoding colors, use CSS variables if they are available / the color makes sense to share.
  • If possible, prefer CSS variables to SCSS variables.
  • Try to minimize the usage of advanced SCSS features. They can lead to bugs and complicate styles.
    • Encouraged features are nesting and imports (which is the intersection of Less', SCSS' and PostCSS' feature set).
  • Think about mobile at least so much that no feature breaks when the browser window is resized.
  • Prefer flexbox over absolute positioning.
  • Avoid styling the children of your components. This couples your component to the implementation of the child.
  • Order your rules so that layout rules (that describe how the component is laid out to its parents) come first, then rules that describe the layout of its children, and finally visual details.

Structuring style sheets

A component may need styles that are common to all environments, like internal layout. We write those styles in SCSS stylesheets that are imported into the host environment. In some cases these can be overridden by passing another class name for that element.

CSS Modules

CSS modules is the preferred way to avoid name conflicts in CSS classes. To use this approach, colocate a SCSS stylesheet with the React component and use the .module.scss. suffix in a file name.

Example:

  • PageSelector.tsx component would have a PageSelector.module.scss file next to it.
  • Use yarn watch-generate to generate a Typescript type declaration file: PageSelector.module.scss.d.ts in the same folder.
  • After that, it's possible to type-safely use class names from the CSS module.
import styles from './PageSelector.module.scss'

<button className={styles.pageSelectorButton} />

To use mixins/functions provided by Bootstrap in CSS modules use explicit imports to the required module.

@import 'bootstrap/scss/functions';
@import 'bootstrap/scss/mixins/caret';

It's not safe to import all global Bootstrap helpers and variables into the CSS module because we redefine many Bootstrap variables on our side. If mixin relies on Bootstrap variables, create a separate SCSS file for the target mixin, ensuring that it uses correct SCSS variables. This file should not contain any real CSS rules so that it can be included in multiple CSS modules without additional overhead.

@import 'branded/src/global-styles/breakpoints';

Do not use BEM convention in CSS modules. Use short descriptive classes specific only for the corresponding component because CSS modules provide scoping out of the box. It outputs shorter classes that are more readable in the component markup.

BEM convention

The older approach is the BEM convention (Block - Element - Modifier). The block name is always the React component name, elements and modifiers are used as specified in BEM. A block must not be referenced in any other React component than the one with the matching name.

Example:

.some-component {
    // ... styles ...

    &__element {
        // ... styles ...

        &--modifier {
            // ... styles ...
        }
    }
}
  • Block: A React component name in kebab-case. This class is always assigned to the root DOM element of the component.
  • Element: A sub-element of the component. This should be a name that describes the semantic of this element within the component.
  • Modifier: A modifier of the element, e.g. --loading or --closed. This is only rarely needed.

Please note that there is no hierarchy in elements, as that would couple the styling to the DOM structure. Element names should be unambiguous within their component/block, or be split into a separate component/block.

Typography

Avoid ever overriding font family, text sizes or text colors. These are set globally by the host environment for semantic HTML elements, e.g. <h1>, <a>, <code> or <small>.

Theming

Theming is done through toggling top-level CSS classes theme-light and theme-dark. Any style can be made different on either theme by scoping it to one of those two classes. Where possible, we use CSS variables, but unfortunately they don't work with compile-time color manipulation (darken() etc) and runtime color manipulation is not yet implemented in CSS (coming in CSS Color Level 4).

Example:

.some-component {
    // ... styles ...

    :global(.theme-dark) & {
        // ... styles ...
    }
    
    :global(.theme-light) & {
        // ... styles ...
    }
}

Colors

The brand color palette is OpenColor. In addition to these, we define a blueish grayscale palette for backgrounds, text and borders. These colors are all available as CSS and SCSS variables.

However, directly referencing these may not work well in both light and dark themes, and may not match code host themes (if the component is shared). The best approach is to not reference colors at all and use building blocks that have borders, text colors etc defined. This saves code and makes it easy to maintain design consistency even if we want to change colors in the future. When that is not possible (for example UI contributed by extensions), prefer to reference CSS variables with semantic colors like var(--danger), var(--success), var(--border-color), var(--body-bg) etc. The values of these variables are changed globally when the theme changes. Be aware that this means our stylesheets for each host environment need to define these variables too.

Spacing

We use rem units in all component styling and strive to use 0.25rem steps. This ensures our spacing generally aligns with an 8pt grid, but also gracefully scales in environments that have a different base rem size. In our webapp, it is recommended to make use of Bootstrap's margin and padding utilities, which are configured to align with the 8pt grid.

Layout

We use modern CSS for our layouting needs. You can find a small playground in our Storybook. The dev tools of modern browsers provide a lot of useful tooling to work with CSS layouts.

Layouts should always be responsive to make sure Sourcegraph is usable with different screen resolutions and window sizes, e.g. when resizing the browser window and using Sourcegraph side-by-side with an editor.

CSS Flexbox is used for one-dimensional layouts (single rows or columns, with optional wrapping). In the webapp, you can use utility classes for simple flexbox layouts and responsive layouts. This is the most common layout method.

For complex two-dimensional layouts, CSS Grid can be used.