DEV Community

Cover image for CSS only components
Marat Sabitov
Marat Sabitov

Posted on

CSS only components

Modern CSS has become so powerful that the basic layout can be styled without using third-party tools. And simple user interface templates can be described using pure CSS. Let's look at an example in which we will try to create a simple CSS component, organizing it in the manner of React.

Initial styles

Let's imagine that we are describing a button group component that defines both its own and nested button styles, including for :hover and :active states:

.btn {
  width: calc(100% * var(--offset) / 2);
  border-radius: calc(1rem * (var(--offset) - 1));
  background-color: var(--neutral-color);
}

.btn-group {
  display: flex;
  gap: 0.25rem;
  margin: 1rem 0;

  & > * {
    flex: 1 1 auto;
    padding: 0.5rem;
    background: oklch(from currentColor 0.8 calc(c / 4) h / 0.2);
    border: none;
    cursor: pointer;
    transition: all 100ms ease-in 0s;
    transition-property: background translate box-shadow;

    &:hover {
      background: oklch(from currentColor 0.8 calc(c / 4) h / 0.6);
      box-shadow: 0 0.2rem 0 currentColor;
    }
    &:active {
      translate: 0 0.2rem;
      box-shadow: 0 0 0 currentColor;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

For now, it's just CSS that can't be customized. At the same time React component can accept props and have a state. Let's try to implement similar mechanics.

Thinking in React way

The component's props allow you to customize its behavior from the outside. Let's say I want to be able to adjust the color of the button, the size of the shadow, and the duration of the transition.

Component states are something that can change according to internal logic and affect the final rendering. If you look at the initial example, you can highlight such states as the value of the alpha channel of the button color, the size of the shadow, and the value of the button translate property.

Then we will get the following code:

.btn-group-component {
  /* props */
  --color-prop: currentColor;
  --shadow-size-prop: 0.2rem;
  --duration-prop: 100ms;

  /* state */
  --alpha-state: 0.2;
  --shadow-size-state: 0;
  --translate-state: 0;

  /* render */
  display: flex;
  gap: 0.25rem;
  margin: 1rem 0;

  & > * {
    flex: 1 1 auto;
    padding: 0.5rem;
    background: oklch(from var(--color-prop) 0.8 c h / var(--alpha-state));
    box-shadow: 0 var(--shadow-size-state) 0 var(--color-prop);
    translate: 0 var(--translate-state);
    border: none;
    cursor: pointer;
    transition: all var(--duration-prop) ease-in 0s;
    transition-property: background translate box-shadow;

    &:hover {
      --alpha-state: 0.6;
      --shadow-size-state: var(--shadow-size-prop);
    }

    &:active {
      --translate-state: var(--shadow-size-prop);
      --shadow-size-state: 0;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

So we have defined special CSS variables for props and states, but we forgot that each React component has its own scope and name. Let's fix this:

@scope (btn-group) {
  /* props */
  --color-prop: currentColor;
  --shadow-size-prop: 0.2rem;
  --duration-prop: 100ms;

  /* state */
  --alpha-state: 0.2;
  --shadow-size-state: 0;
  --translate-state: 0;

  /* styles */
  :scope {
    display: flex;
    gap: 0.25rem;
    margin: 1rem 0;

    & > * {
      flex: 1 1 auto;
      padding: 0.5rem;
      background: oklch(from var(--color-prop) 0.8 c h / var(--alpha-state));
      box-shadow: 0 var(--shadow-size-state) 0 var(--color-prop);
      translate: 0 var(--translate-state);
      border: none;
      cursor: pointer;
      transition: all var(--duration-prop) ease-in 0s;
      transition-property: background translate box-shadow;

      &:hover {
        --alpha-state: 0.6;
        --shadow-size-state: var(--shadow-size-prop);
      }

      &:active {
        --translate-state: var(--shadow-size-prop);
        --shadow-size-state: 0;
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

@scope helps to use contextual proximity to resolve conflicts between styles. Additionally, it visually highlights the isolation of styles within their context. However, I would like to pass React style properties as separate HTML attributes rather than a single string within the style attribute. Fortunately, this future is just around the corner.

attr() will change all

Imagine what will happen if the attr() function can be used in arbitrary CSS properties - then HTML attributes can be used as real component props:

@scope (btn-group) {
  /* props */
  --color-prop: attr(color type(<color>), currentColor);
  --shadow-size-prop: attr(shadow-size type(<length>), 0.2rem);
  --duration-prop: attr(duration type(<time>), 100ms);

  /* state */
  --alpha-state: 0.2;
  --shadow-size-state: 0;
  --translate-state: 0;

  /* styles */
  :scope {
    display: flex;
    gap: 0.25rem;
    margin: 1rem 0;

    & > * {
      flex: 1 1 auto;
      padding: 0.5rem;
      background: oklch(from var(--color-prop) 0.8 c h / var(--alpha-state));
      box-shadow: 0 var(--shadow-size-state) 0 var(--color-prop);
      translate: 0 var(--translate-state);
      border: none;
      cursor: pointer;
      transition: all var(--duration-prop) ease-in 0s;
      transition-property: background translate box-shadow;

      &:hover {
        --alpha-state: 0.6;
        --shadow-size-state: var(--shadow-size-prop);
      }

      &:active {
        --translate-state: var(--shadow-size-prop);
        --shadow-size-state: 0;
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Just one feature can change the way you approach web styling. Just try it in Chrome to feel the power.

Your can explore all code examples with StackBlitz:

Final thoughts

I think that the CSS-only approach, which has been promoted so long and hard with Bootstrap, Tailwind, Bulma, and many other frameworks, is finally taking shape. But now, CSS can be more than just a collection of utilities or classes; it can be a complete set of components. And I'm looking forward to having the attr() function for all properties becoming widely available.

Enjoy your Frontend Development!

Top comments (0)