Style Guidelines and Tokens

Colors

We have a small set of colors that are used for theming. The following are the colors and where they should be used.

--brand

This is typically a colorful color. It should be used for actionable items. E.g. a button that can be pressed. It should not be used for text or backgrounds.

--text-1

This is the typical text color. It should be used for things like headers and primary text.

--text-2

This is the secondary text color. It is typically a little less contrasty to deemphasize it. It should be used for text that is not critical to the main action and thus doesn’t need to be as prominent.

--text-3

This is the tertiary text color. It is typically a little text like placeholders in inputs. Where the content is purposefully not punchy but provides a hint that text can be somewhere.

--surface-1

This is the primary background color. It will have the greatest contrast with the text colors and is typically used for things like backgrounds of a cards where legibility of the content is most important.

--surface-2

These is a secondary background colors. Their contrast is a little softer than surface-1 but it is still very legible. This will often be used for things like backgrounds that cards sit on top of etc.

--surface-3

These is a secondary background colors. Their contrast is a little softer than surface-2 but it is still very legible. Another option for backgrounds where there are items with surface-{1,2} backgrounds on top of them that want should pop.

--surface-4

This is a relatively low contrast background color. It should be used for contents that have large text or icons so the contrast is still acceptable. It’s useful for creating a hierarchy of information and things like headers and footers for cards etc..

--success

This is most often a shade of green. It should be used almost exclusively for success messages.

--warning

Often a shade of orange. Like success it should be used almost exclusively for warning messages.

--danger

Often a shade of red. Like success it should be used almost exclusively for error messages or other not-good things.

Sizes

The following are the design tokens related to sizes, e.g. paddings, margins, etc.

--size-xxs

This size should very rarely be used. If it is used, it should be for minor adjustments of positions, like nudging an icon up or down to make more visually centered layout.

OP mapping: calc(var(--size-xs) / 2)

--size-xs

Used for spacing things within a given small unit of design. E.g. creating a tight border around an element like a button.

OP mapping: --size-xs

--size-s

Separating distinct atomic units of a design. E.g. the margin between a series of buttons and description text above them.

OP mapping: --size-2

--size-m

Separating larger components. E.g. Padding within a card or spacing between menu items in a controls panel.

OP mapping: --size-s

--size-l

Thematic separation. E.g. separating two cards from eachother or a sidebar from another the main content of the app.

OP mapping: --size-fluid-2

--size-xl

App level separation. E.g. margins on the side of content.

OP mapping: --size-fluid-3

--size-xxl

Rarely used but can be used to create extra space between items that otherwise would be closer together. E.g. Isolating a given card to draw focus to it. Should rarely be used in internal component design.

OP mapping: --size-fluid-6

Shadows

Box shadows are a polarizing design token but seem to be have settled on a nice standard of using them to elevate certian items and bring them to the user’s attention. They should be used sparingly and with clear purpose.

no shadow

Sample card with no box-shadow for reference.

--shadow-s

This shadow can be used to minorly elevate items. Just to make them “pop” a little bit more. For instance an avatar image may use this to give it the apperance of being important but not the main thing of importance on the page.

--shadow-m

The “average” shadow. Used to draw the raise something clearly above the background and thus draw attention to it. E.g. a card filled with information. Should be used primarily with larger items or else it will overpower the item.

--shadow-l

Almost never used. Only if the effect of elevation should be really pronounced. Could be used as a hover state for something interactive that already has a --shadow-m on it.

Font sizes

Most of the variables for fonts are automatically applied when adding the text_reset snippet to a components static styles definition and as long as you use the proper semantic elements for your situation everything will work.

--font-size-m

Main/Body fonts

--font-size-s

Small callouts

--font-size-l

Emphasized text

--font-size-h1

Level-1 Headers

--font-size-h2

Level-2 Headers

--font-size-h3

Level-3 Headers

--font-size-h4

Level-4 Headers

--font-size-h5

Level-5 Headers

Font weights

Much like the font-sizes, these will almost never need to be applied manually.

--font-weight-headings

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

--font-weight-bold

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

Line heights

Line heights also are almost never applied directly to your components. However for theming someone may wish to override these to make their app denser or more spaced out.

--line-height-headings

Header text can have a tighter line-height than normal… The purpose of the spacing between lines is to help people read from one line to the next, comfortably. While a line height of at least 1.5 (150%) works well for body text, it’s unnecessary on larger sized text such as headings. The larger the font size, the smaller the line height should be, to maintain comfortable spacing.

--line-height-main

Main text should be slightly more spaced out… The purpose of the spacing between lines is to help people read from one line to the next, comfortably. While a line height of at least 1.5 (150%) works well for body text, it’s unnecessary on larger sized text such as headings. The larger the font size, the smaller the line height should be, to maintain comfortable spacing.

Font-Families

--font-sans

For our font stacks we just directly match open-props stacks. These use modern system font stacks which look good, are familiar, and are fast.

--font-serif

For our font stacks we just directly match open-props stacks. These use modern system font stacks which look good, are familiar, and are fast.

--font-mono

For our font stacks we just directly match open-props stacks. These use modern system font stacks which look good, are familiar, and are fast.

Animation Speeds

Animations should be used very sparingly in components as it almost always results in the app feeling slower. However, sometimes animation can help the user understand what is happening such as a sidebar collapsing away.
By using consitant animation speeds we can make sure animations are cohesive. The default animation speeds are all multiples of eachother so they will appear thematically similar.
These are automatically set to zero if prefers-reduced-motion is set to reduce.

--speed-fast

Hover for animation

--speed-normal

Hover for animation

--speed-slow

Hover for animation

Border widths

Borders/outlines should be used in moderation as it’s typically better to use spacing to group related content.

--border-thin

Thin borders can be used to provide a delicate visual clue that the contents of an item are related when background color or other visual indicators are not available.

--border-normal

Normal borders are appropriate for things such as dividers between sections. Although spacing should be prefered to reduce visual clutter if possible.

--border-thick

Thick borders are primarily a stylistic thing. Rarely are they neccesary or optimal for a design.

--border-optional

Used to provide an optional border to items that by default may not have them. The user can choose to set this to some value themselves if they favor a border-heavy design. Defaults to 0, aka invisble.

Radii

Radii typically are used to round edges of boxes. They can be used to create softer more organic elements.

--radius-s

The small radius option should be used for interior elements within a larger container like a card.

--radius-m

Medium radii should be used for general purpose containers such as cards. Tweaking this token can make a big difference in how the app appears.

--radius-l

Large radii should be reserved for more decorative situations and infrequently be used.

--radius-pill

Useful for buttons or other items where a large click/selection space is needed but the content is not neccesarily large. Need to be careful with content being cut off.

Example Component

To demonstrate how these tokens work together we can build up a “card” component that sits on a background.

Background color

We can start by building our background. We’ll use the token surface-3 for its color.

<div class="background">
  I'm a background
</div>
.background {
  background-color: var(--surface-3);
}

I’m a background

Adding card element

Next we can place a card onto that background with a plot and two sections of info in it.

<div class="background">
 <div class="my-card">
   <h3>A cool plot</h3>
  <div class="header-img"></div>
  <section>
    A: Here's an important description the user should see about our plot.
  </section>
  <section class="secondary">
    B: This is some secondarily important content that is useful to show but should not be as important as section A.
  </section>
 </div>
</div>

We’ll give the card a background color of surface-1 to make it “pop,” we’ll also round the corners using radius-m and give it a shadow to further emphasize it as important.

.my-card {
  /* Light background and box-shadow
     to pop off background */
  background-color: var(--surface-1);
  box-shadow: var(--shadow-m);

  /* Primary text color */
  color: var(--text-1);

  /* Round corners */
  border-radius: var(--radius-m);
}

A cool plot

A: Here’s an important description the user should see about our plot.
B: This is some secondarily important content that is useful to show but should not be as important as section A.

Padding card contents

We have a card but the content of that card is completely pushed up against the edge, so we should give it some padding using the size-m token.

.my-card {
  /* Light background and box-shadow
     to pop off background */
  background-color: var(--surface-1);
  box-shadow: var(--shadow-m);

  /* Primary text color */
  color: var(--text-1);

  /* Round corners */
  border-radius: var(--radius-m);

  /* Give content some breathing room */
  padding: var(--size-m);
}

A cool plot

A: Here’s an important description the user should see about our plot.
B: This is some secondarily important content that is useful to show but should not be as important as section A.

Using smaller sizes to separate internal content

Now the outline looks good but we need to make it clear that the info about section A and section B are distinct. To do this we will use size-s to create separation of distincts sections within the card. We’ll do this by using flexbox and setting the gap property.

.my-card {
  /* Light background and box-shadow
     to pop off background */
  background-color: var(--surface-1);
  box-shadow: var(--shadow-m);

  /* Primary text color */
  color: var(--text-1);

  /* Round corners */
  border-radius: var(--radius-m);

  /* Give content some breathing room */
  padding: var(--size-m);

  /* Give visual separation to each
     sub-section of the card */
  display: flex;
  flex-direction: column;
  gap: var(--size-s);
}

A cool plot

A: Here’s an important description the user should see about our plot.
B: This is some secondarily important content that is useful to show but should not be as important as section A.

Using different text colors to provide visual hierarchy

This is looking better but we may want to give a better indication to the user that the secondary section is not the most important thing in the card. To do that we can give it text-2 as it’s color so it’s not as visually prevelant upon first scanning the card.

.my-card {
 ...
}

.my-card .secondary {
  /* Slightly de-emphasize the secondary text */
  color: var(--text-2);
}

A cool plot

A: Here’s an important description the user should see about our plot.
B: This is some secondarily important content that is useful to show but should not be as important as section A.

The difference is very subtle, but if you defocus your eyes while looking at the card you will notice that there is a definite difference in visual emphasis between the two text sections. When quickly scanning an app this will help guide the user to the most important parts first.

Building a theme

Because of how css custom properties are inhereted using the css cascade, you can theme a whole app by defining the variables you wish to change and targeting a root-level selector such as html or :root. (If you use the theme chooser on this page that’s what is being done.)

However, because you’re just using css you can apply a theme to a specific part of your app without over-riding the rest of the app…

Defining a theme

We can setup a basic theme that updates a few tokens to make a “greenery” theme that is a green dark-mode, makes less use of rounded corners, and has more spaced out content…

.greenery {
  /* Build theme based on the open-props
     jungle color palette */
  --brand: var(--jungle-6);
  --text-1: var(--jungle-0);
  --text-2: var(--jungle-1);
  --text-3: var(--jungle-2);
  --surface-1: var(--jungle-12);
  --surface-2: var(--jungle-11);
  --surface-3: var(--jungle-10);
  --surface-4: var(--jungle-9);

  /* Make corners much less rounded */
  --radius-s: 2px;
  --radius-m: 5px;
  --radius-l: 10px;

  /* Make things more spaced out */
  --size-s: 30px;
  --size-m: 50px;
}

Setting theme on a part of the app

Notice we defined our theme under the selector .greenery. This means that we can apply it to our previous card component demo simply by adding the class greenery to the container (or the card).

<div class="background greenery">
 <div class="my-card">
  <h3>A cool plot</h3>
  <div class="header-img"></div>
  <section>
    A: Here's an important description the user should see about our plot.
  </section>
  <section class="secondary">
    B: This is some secondarily important content that is useful to show but should not be as important as section A.
  </section>
 </div>
</div>

A cool plot

A: Here’s an important description the user should see about our plot.
B: This is some secondarily important content that is useful to show but should not be as important as section A.

Setting theme on whole app

While this theme may not be to your taste, all you need to do to make your own is tweak the tokens above. You can make it apply to the whole app by simply making the selector at the root level…

/* Setup app-wide theme */
:root {
  /* Build theme based on the open-props
     jungle color palette */
  --brand: var(--jungle-6);
  ...
}

or by adding the class greenery to your page component.

...
app_ui = sc.page(
  ...,
  class_='greenery'
)

Adapting theme to other component design systems

If you’re building a component that uses some other design system you may want to make that component respect the current design-token driven theme. This can be done by writing an adapter using css to make sure the correct tokens are applied to their equivalent places in the other system.

Shoelace’s design system

We use a project for many of our components called “shoelace.” Shoelace has it’s own design system. All we need to do to make shoelace components respect our theme is, in a css file, use our design tokens. Here’s a snippet of what that looks like:

html {
  /* Neutral */
  --sl-color-neutral-50: var(--suface-3);
  --sl-color-neutral-100: var(--surface-4);
  --sl-color-neutral-200: var(--suface-4);
  --sl-color-neutral-300: var(--text-3);
  --sl-color-neutral-400: var(--text-3);
  --sl-color-neutral-500: var(--text-3);
  --sl-color-neutral-600: var(--text-2);
  --sl-color-neutral-700: var(--text-2);
  --sl-color-neutral-800: var(--text-1);

  /* Border radii */
  --sl-border-radius-small: var(calc(--radius-s/2));
  --sl-border-radius-medium: var(--radius-s);
  --sl-border-radius-large: var(--radius-m);
  --sl-border-radius-x-large: var(--radius-l);
  --sl-border-radius-circle: 50%;
  --sl-border-radius-pill: var(--radius-pill);
}

This same pattern can be used for any system that uses non-inline css. Just figure out where the colors are used and update those declarations to use the appropriate token above.