Skip to content

Guide

Getting started

This guide will set you up with:

  • Open Props
  • a normalize
  • some util classes
  • base theme setup

1. Install Open Props

Open Props v2 hasn't dropped yet, which is why this project relies on the OPv2 beta. The difference right now between the OPv1 and the OPv2 beta aren't huge (the way OPUI consumes it), so you should be fine with either version.

sh
# pnpm
pnpm i open-props@2.0.0-beta.5 --save-alias opbeta

# npm
npm i open-props@2.0.0-beta.5 --save-alias opbeta

# yarn
yarn add open-props@2.0.0-beta.5 --alias opbeta

2. Base setup

The setup process will differ a bit if you use a framework, but the core principles still apply. You should have no problems getting it to work though 👍 Otherwise, let me know.

Setup files
css
@layer openprops, normalize, utils, theme, components.base, components.has-deps;

/*
* Open Props
* Import as many props as you need here.
* https://unpkg.com/browse/open-props@2.0.0-beta.5/css
*/
@import "opbeta/css/media-queries.css" layer(openprops);
@import "opbeta/index.css" layer(openprops);
@import "opbeta/css/sizes/media.css" layer(openprops);
@import "opbeta/css/font/lineheight.css" layer(openprops);
@import "opbeta/css/color/hues.oklch.css" layer(openprops);

/* Normalize */
@import "./normalize.css";

/* Utils */
@import "./utils.css";

/* Theme */
@import "./theme.css";

/*
* Components
* Components are divided into two categories - if they are stand-alone (base) or if they have dependencies (has-deps).
*/
/*** Base components (no dependencies)  */
@import "./actions/button.css";
@import "./actions/icon-button.css";
@import "./actions/toggle-button-group.css";
@import "./data-display/avatar.css";
@import "./data-display/badge.css";
@import "./data-display/card.css";
@import "./data-display/chip.css";
@import "./data-display/definition-list.css";
@import "./data-display/divider.css";
@import "./data-display/link.css";
@import "./data-display/table.css";
@import "./feedback/progress.css";
@import "./feedback/spinner.css";
@import "./inputs/checkbox-radio.css";
@import "./inputs/switch.css";
@import "./text/typography.css";

/*** Has dependencies */
@import "./actions/button-group.css";
@import "./data-display/accordion.css";
@import "./data-display/list.css";
@import "./feedback/alert.css";
@import "./feedback/dialog.css";
@import "./feedback/snackbar.css";
@import "./inputs/field-group.css";
@import "./inputs/field.css";
@import "./inputs/select.css";
@import "./inputs/text-field.css";
@import "./inputs/textarea.css";
@import "./text/rich-text.css";
css
@layer normalize {
  *,
  :before,
  ::after {
    box-sizing: border-box;
  }

  * {
    scrollbar-width: thin;
  }

  :where(html) {
    --_page-bg-color: var(--surface-default);

    accent-color: var(--primary);
    background-color: var(--_page-bg-color);
    block-size: 100%;
    caret-color: var(--primary);
    color: var(--text-color-2);
    font-family: var(--font-sans);
    interpolate-size: allow-keywords;
    line-height: var(--font-lineheight-4);

    /* https://kilianvalkhof.com/2022/css-html/your-css-reset-needs-text-size-adjust-probably/ */
    -moz-text-size-adjust: none;
    -webkit-text-size-adjust: none;
    text-size-adjust: none;

    @media (--motionOK) {
      scroll-behavior: smooth;
    }
  }

  :where(body) {
    -moz-osx-font-smoothing: grayscale;
    -webkit-font-smoothing: antialiased;
    container-type: inline-size;
    font-size: 16px;
    font-synthesis: style;
    font-weight: 400;
    inline-size: 100%;
    margin: 0;
    min-block-size: 100%;
    min-inline-size: 320px;
    position: relative;
    text-rendering: optimizeLegibility;
  }

  /* TODO */
  :where(:not(dialog, popover)) {
    margin: 0;
  }

  :where(:not(fieldset, progress, meter)) {
    background-origin: border-box;
    background-repeat: no-repeat;
    border-style: solid;
    border-width: 0;
  }

  :where(fieldset) {
    border: var(--field-border-width) solid var(--field-border-color);
    border-radius: var(--field-border-radius);
    padding: var(--size-3);
    display: grid;
    gap: var(--size-3);
  }

  :where(input, button, textarea),
  :where(input[type="file"])::-webkit-file-upload-button {
    color: inherit;
    font-size: inherit;
    font: inherit;
    letter-spacing: inherit;
  }

  :where(input):-webkit-autofill,
  :where(input):-webkit-autofill:hover,
  :where(input):-webkit-autofill:focus,
  :where(textarea):-webkit-autofill,
  :where(textarea):-webkit-autofill:hover,
  :where(textarea):-webkit-autofill:focus,
  :where(select):-webkit-autofill,
  :where(select):-webkit-autofill:hover,
  :where(select):-webkit-autofill:focus,
  :where(input):autofill,
  :where(input):autofill:hover,
  :where(input):autofill:focus,
  :where(textarea):autofill,
  :where(textarea):autofill:hover,
  :where(textarea):autofill:focus,
  :where(select):autofill,
  :where(select):autofill:hover,
  :where(select):autofill:focus {
    -webkit-text-fill-color: var(--text-color-2);
    -webkit-box-shadow: 0 0 0px 1e5px var(--well-1) inset;
    transition: background-color 5000s ease-in-out 0s;
  }

  ::placeholder {
    color: var(--text-color-2);
  }

  ::-moz-placeholder {
    opacity: 1;
  }

  :focus-visible {
    /* Inverts the --_page-bg-color */
    --_focus-visible-color: rgb(
      from var(--_page-bg-color) calc(255 - r) calc(255 - g) calc(255 - b)
    );

    border-radius: var(--border-radius, 0px);
    outline: 2px solid var(--_focus-visible-color);
    outline-offset: 2px;
  }

  @media (--motionOK) {
    :where(:focus-visible) {
      transition: outline-offset 145ms var(--ease-2);
    }
    :where(:not(:active):focus-visible) {
      transition-duration: 0.15s;
    }
  }

  :where(:not(:active):focus-visible) {
    outline-offset: var(--outline-offset, 0px);
  }

  :where(
    a[href],
    area,
    button,
    input:not(
        [type="text"],
        [type="email"],
        [type="number"],
        [type="password"],
        [type=""],
        [type="tel"],
        [type="url"]
      ),
    label[for],
    select,
    summary
  ) {
    cursor: pointer;
  }

  :where(
    a[href],
    area,
    button,
    [role="button"],
    input,
    label[for],
    select,
    summary,
    textarea,
    [tabindex]:not([tabindex*="-"])
  ) {
    -webkit-tap-highlight-color: transparent;
    touch-action: manipulation;
  }

  :where(img, svg, video, canvas, audio, iframe, embed, object) {
    display: block;
  }

  :where(img, svg, video) {
    block-size: auto;
    max-inline-size: 100%;
  }

  :where(svg:not([width])) {
    inline-size: var(--size-7);
  }

  :where(dt:not(:first-of-type)) {
    margin-block-start: var(--size-5);
  }

  :where(figure) {
    display: grid;
    gap: var(--size-2);
    place-items: center;
  }

  :target {
    scroll-margin-block-start: 2rem;
  }
}
css
@layer utils {
  /*
Screen-reader only
When you visibly want to hide an element but make it accessible for screen readers.
*/
  .sr-only {
    block-size: 1px;
    clip-path: inset(50%);
    inline-size: 1px;
    overflow: hidden;
    position: absolute;
    white-space: nowrap;
  }

  /* Hover and active effect for checkbox, radio and icon buttons */
  :where(.checkbox input, .radio input, .icon-button) {
    --isLTR: 1;
    --isRTL: -1;

    position: relative;
    transform-style: preserve-3d;

    &:dir(rtl) {
      --isLTR: -1;
      --isRTL: 1;
    }

    &:where(:not([disabled])) {
      &:hover:before {
        --thumb-scale: 1;
      }

      &:active:before {
        --thumb-scale: 1.1;
      }

      &:before {
        --thumb-scale: 0.01;
        --highlight-size: 150%;

        background-color: oklch(0.6 0 0 / 0.2);
        block-size: var(--highlight-size);
        clip-path: circle(50%);
        content: "";
        inline-size: var(--highlight-size);
        inset-block-start: 50%;
        inset-inline-start: 50%;
        position: absolute;
        transform-origin: center center;
        transform: translateX(calc(var(--isRTL) * 50%)) translateY(-50%)
          translateZ(-1px) scale(var(--thumb-scale));
        will-change: transform;

        @media (prefers-reduced-motion: no-preference) {
          transition: transform 0.2s ease;
        }
      }
    }
  }
}
css
@layer theme {
  html {
    color-scheme: var(--color-scheme);
  }

  .light {
    --color-scheme: light;
  }
  .dark {
    --color-scheme: dark;
  }

  :where(html) {
    color-scheme: light dark;

    --palette-hue: var(--oklch-teal);
    --palette-hue-rotate-by: 5;
    --palette-chroma: 0.89;

    /* Primary */
    --primary: var(--color-8);
    --primary-light: oklch(from var(--primary) calc(l * 1.25) c h);
    --primary-dark: oklch(from var(--primary) calc(l * 0.75) c h);
    --primary-contrast: var(--gray-1);

    /* Text */
    --text-color-1: light-dark(var(--gray-15), var(--gray-1));
    --text-color-1-contrast: light-dark(var(--gray-2), var(--gray-15));
    --text-color-2: light-dark(var(--gray-13), var(--gray-4));
    --text-color-2-contrast: light-dark(var(--gray-4), var(--gray-13));

    /* Surface */
    --surface-default: light-dark(var(--gray-1), var(--gray-13));
    --surface-filled: light-dark(var(--gray-3), var(--gray-15));
    --surface-tonal: light-dark(var(--gray-3), var(--gray-12));
    --surface-elevated: light-dark(var(--gray-1), var(--gray-12));

    /* Shadows */
    --shadow-color: light-dark(220 3% 15%, 220 40% 2%);
    --shadow-strength: light-dark(1%, 10%);
    --inner-shadow-highlight: light-dark(
      inset 0 -0.5px 0 0 #fff,
      inset 0 0.5px 0 0 #0001,
      inset 0 -0.5px 0 0 #fff1,
      inset 0 0.5px 0 0 #0007
    );

    /* Typography */
    --font-size-h1: var(--font-size-fluid-3, 3.5rem);
    --font-size-h2: var(--font-size-fluid-2, 2rem);
    --font-size-h3: var(--font-size-fluid-1, 1.5rem);
    --font-size-h4: var(--font-size-3, 1.25rem);
    --font-size-h5: var(--font-size-2, 1.1rem);
    --font-size-h6: var(--font-size-fluid-0, 1rem);
    --font-size-lg: var(--font-size-3, 1.25rem);
    --font-size-md: var(--font-size-fluid-0, 1rem);
    --font-size-sm: 0.875rem;
    --font-size-xs: var(--font-size-0, 0.75rem);

    /* Borders */
    --border-color: light-dark(var(--gray-4), var(--gray-12));
    --border-radius: var(--size-1);
    --border-width: 1px;

    /* Input Field */
    --field-border-color: var(--border-color);
    --field-border-radius: var(--size-1);
    --field-border-width: 1px;
    --field-size: 2.3lh;
    --field-size-small: 1.9lh;

    /* Button */
    --button-border-radius: var(--radius-round);
    /* Ripple effect */
    @media (prefers-reduced-motion: no-preference) {
      --button-ripple-size: 100%;
      --button-ripple-duration: 0.5s;
    }
  }

  /* Highlight colors */
  :where(.red, .error, del) {
    --palette-hue: var(--oklch-red, 25);
    --palette-chroma: 1;
    --palette-hue-rotate-by: 1;
  }
  :where(.blue, .ok, abbr, dfn) {
    --palette-hue: var(--oklch-blue, 210);
    --palette-chroma: 1;
    --palette-hue-rotate-by: 1;
  }
  :where(.green, .good, ins) {
    --palette-hue: var(--oklch-green, 145);
    --palette-chroma: 1;
    --palette-hue-rotate-by: 1;
  }
  :where(.orange, .warning) {
    --palette-hue: var(--oklch-orange, 75);
    --palette-chroma: 1;
    --palette-hue-rotate-by: 1;
  }

  :where(html) {
    --red: oklch(from var(--color-9) l 0.2 25);
    --blue: oklch(from var(--color-9) l 0.2 210);
    --green: oklch(from var(--color-9) l 0.2 145);
    --orange: oklch(from var(--color-7) l 0.2 75);
  }

  /* Gray palette */
  :where(html) {
    --gray-chroma: 0.01;
    --gray-lightness: 255;

    --gray-1: oklch(
      from var(--color-1) l var(--gray-chroma) var(--gray-lightness)
    );
    --gray-2: oklch(
      from var(--color-2) l var(--gray-chroma) var(--gray-lightness)
    );
    --gray-3: oklch(
      from var(--color-3) l var(--gray-chroma) var(--gray-lightness)
    );
    --gray-4: oklch(
      from var(--color-4) l var(--gray-chroma) var(--gray-lightness)
    );
    --gray-5: oklch(
      from var(--color-5) l var(--gray-chroma) var(--gray-lightness)
    );
    --gray-6: oklch(
      from var(--color-6) l var(--gray-chroma) var(--gray-lightness)
    );
    --gray-7: oklch(
      from var(--color-7) l var(--gray-chroma) var(--gray-lightness)
    );
    --gray-8: oklch(
      from var(--color-8) l var(--gray-chroma) var(--gray-lightness)
    );
    --gray-9: oklch(
      from var(--color-9) l var(--gray-chroma) var(--gray-lightness)
    );
    --gray-10: oklch(
      from var(--color-10) l var(--gray-chroma) var(--gray-lightness)
    );
    --gray-11: oklch(
      from var(--color-11) l var(--gray-chroma) var(--gray-lightness)
    );
    --gray-12: oklch(
      from var(--color-12) l var(--gray-chroma) var(--gray-lightness)
    );
    --gray-13: oklch(
      from var(--color-13) l var(--gray-chroma) var(--gray-lightness)
    );
    --gray-14: oklch(
      from var(--color-14) l var(--gray-chroma) var(--gray-lightness)
    );
    --gray-15: oklch(
      from var(--color-15) l var(--gray-chroma) var(--gray-lightness)
    );
    --gray-16: oklch(
      from var(--color-16) l var(--gray-chroma) var(--gray-lightness)
    );
  }
}

Folder structure

This is folder structure that comes out of the box. Feel free to change it to your needs.

├─ main.css
├─ normalize.css
├─ theme.css
├─ utils.css
├─ actions
│  └─ ...
├─ data-display
│  └─ ...
├─ feedback
│  └─ ...
├─ inputs
│  └─ ...
└─ text
   └─ ...

3. Copy & paste

Browse all the components.

Copy and paste the HTML and CSS (see the "Installation" section on each component page) and you're good to go!