Copy-Paste CSS Components Library for Modern Web – Open Props UI

Copy-Paste CSS Components Library for Modern Web – Open Props UI
Copy-Paste CSS Components Library for Modern Web – Open Props UI

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.

Discover more from RSS Feeds Cloud

Subscribe now to keep reading and get access to the full archive.

Continue reading