Open Props UI is just CSS. Therefore you're able to go and copy and paste any component right now without any installs. You don't even need Open Props! It won't necessarily look and feel as intended, but if that suits your project then skip this page.
Guide
Getting started
You can choose between:
- Manual setup (more fun, recommended)
- NPM installation (a lot less fun)
- CDN (not fun either but good for quick prototyping)
Manual 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.
# pnpm
pnpm add opbeta@npm:open-props@2.0.0-beta.5 -S
# npm
npm i opbeta@npm:open-props@2.0.0-beta.5 -S
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.
This is the folder structure that comes out of the box. Feel free to change it to your needs.
├─ main.css
├─ opui.css
├─ open-props.css
├─ theme.css
├─ core
│ └─ normalize.css
│ └─ utils.css
│ └─ components.css
├─ components
│ └─ button.css
│ └─ ...
main.css
is the home of all your CSSopui.css
consists of OPUI imports, onlyopen-props.css
consists of Open Props imports, onlytheme.css
the default theme provided with OPUI
/*
This main.css file represents whatever file you choose to use as an entry point for OPUI
Ideally it's the root/entry CSS file in your project.
*/
/* Open Props imports */
@import "./open-props.css";
/* OPUI Core */
@import "./opui.css";
/* Theme (example) */
/* @import "./theme.css"; */
@layer openprops, normalize, utils, theme, components.root, components.extended;
/* Normalize */
@import "./core/normalize.css";
/* Utils */
@import "./core/utils.css";
/* Components */
@import "./core/components.css";
/*
* Open Props
* Import as many props as you need here.
* https://unpkg.com/browse/open-props@2.0.0-beta.5/css
*/
@import "open-props/css/media-queries.css";
@import "open-props/index.css" layer(openprops);
@import "open-props/css/sizes/media.css" layer(openprops);
@import "open-props/css/font/lineheight.css" layer(openprops);
@import "open-props/css/color/hues.oklch.css" layer(openprops);
/*
OPUI default theme
*/
@layer theme {
.light {
--color-scheme: light;
}
.dark {
--color-scheme: dark;
}
:where(html) {
color-scheme: var(--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)
);
}
}
src/core
@layer normalize {
*,
::before,
::after {
box-sizing: inherit;
}
* {
scrollbar-width: thin;
}
:where(html) {
--_page-bg-color: var(--surface-default);
accent-color: var(--primary);
background-color: var(--_page-bg-color);
block-size: 100%;
box-sizing: border-box;
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;
}
}
@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;
}
}
}
}
}
/*
* Components are divided into two categories - if they are stand-alone (root) or if they are built on top of others (extended).
*/
/*** Root components (no dependencies) */
@import "../components/button.css";
@import "../components/icon-button.css";
@import "../components/tab-buttons.css";
@import "../components/toggle-button-group.css";
@import "../components/avatar.css";
@import "../components/badge.css";
@import "../components/card.css";
@import "../components/chip.css";
@import "../components/definition-list.css";
@import "../components/divider.css";
@import "../components/link.css";
@import "../components/table.css";
@import "../components/progress.css";
@import "../components/spinner.css";
@import "../components/checkbox-radio.css";
@import "../components/switch.css";
@import "../components/range.css";
@import "../components/typography.css";
/*** Extended components (has dependencies) */
@import "../components/button-group.css";
@import "../components/accordion.css";
@import "../components/list.css";
@import "../components/alert.css";
@import "../components/dialog.css";
@import "../components/snackbar.css";
@import "../components/field-group.css";
@import "../components/field.css";
@import "../components/select.css";
@import "../components/text-field.css";
@import "../components/textarea.css";
@import "../components/rich-text.css";
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!
NPM installation
# pnpm
pnpm add opui-css -S
# npm
npm i opui-css -S
Then check your node_modules
folder for the opui-css
package and pick and choose what files you want to use!
Bundled files
The dist
folder includes bundled files with:
dist/op.css
all the neededOpen Props
importsdist/ui.css
the entireOPUI
librarydist/op+ui.css
both files above combined