
Open Props UI is a PURE CSS component library that uses next-generation CSS features (like container queries, CSS layers, and OKLCH color spaces) to create highly customizable components you can copy and paste directly into your project.
The library takes a refreshing approach to component distribution. Instead of importing a black-box library or NPM package, you take the provided HTML and CSS and adapt it.
Features:
- Copy-paste architecture – No npm dependencies for components, just copy the code you need
- Modern CSS foundation – Built with CSS Grid, container queries, CSS layers, and OKLCH colors
- Open Props integration – Uses the Open Props design token system for consistency
- Accessibility-first design – Follows WCAG guidelines with proper focus management and semantic markup
- Framework agnostic – Works with any framework or vanilla HTML
How to use it:
1. Install the necessary Open Props package.
# NPM $ npm install opbeta@npm:open-props@VERSION -S # PNPM $ pnpm install opbeta@npm:open-props@VERSION -S
2. Create a main.css
file to define the structure and import the necessary files.
@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"; @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/tab-buttons.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 "./inputs/range.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";
3. You’ll also need to grab the normalize.css
, utils.css
, and theme.css
files from the Open Props UI repository.
/* normalize.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; } }
/* Utils */ @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; } } } } }
/* 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) ); } }
4. Browse the component registry, then copy both HTML and CSS for the components you need. All available components:
- Button
- Button group
- Icon Button
- Tab buttons
- Toggle button group
- Checkbox
- Radio
- Range
- Select
- Switch
- Textarea
- Text field
- Accordion
- Avatar
- Badge
- Card
- Chip
- Definition list
- Divider
- List
- Table
- Alert
- Dialog
- Progress
- Snackbar
- Spinner
- Rich text
- Typography
FAQs:
Q: What browsers support these components?
A: Most components work in modern browsers (Chrome 90+, Firefox 88+, Safari 14+), but some experimental features require Chrome with feature flags enabled.
Q: Can I use this in production applications?
A: Use caution with experimental features in production. The base components using stable CSS features are production-ready, but always test thoroughly in your target browsers.
Q: How do I customize component styles?
A: Since you own the CSS code, you can modify anything directly. The theme system provides CSS custom properties for common customizations, or you can edit the component CSS files directly.
Q: How do I handle browser fallbacks?
A: The library includes some fallbacks through @supports
queries, but you need to add additional fallback styles for your specific browser support requirements. Consider progressive enhancement strategies for critical features.
The post Copy-Paste CSS Components Library for Modern Web – Open Props UI appeared first on CSS Script.
Discover more from RSS Feeds Cloud
Subscribe to get the latest posts sent to your email.