Skip to content

Styling

Styling is an essential part of our project, ensuring a consistent and scalable design system. We use a combination of Sass for pre-processing CSS and CVA (Class Variance Authority) to manage dynamic class-based styling efficiently. This approach allows us to create modular, reusable styles while keeping our stylesheets clean and maintainable.

🖌 Sass – Powerful and Maintainable CSS

Sass is a preprocessor that extends CSS with features like variables, nested rules, mixins, and functions. It helps in writing more structured and reusable styles while keeping the code maintainable and scalable.

🎛 CVA (Class Variance Authority) – Dynamic Styling Utility

CVA (Class Variance Authority) is a utility that helps manage dynamic class composition in a structured and type-safe way. It allows us to define reusable class structures with conditional variants, making it an excellent tool for managing different UI states while keeping our styling logic clean and consistent.

CVA is particularly powerful when used with atomic components—small, reusable UI elements that form the building blocks of an application, is it can handle also a lot larger components. In our project you can find CVA is used for example for:

Example usage

ts
import { cva, type VariantProps } from "class-variance-authority";

export const buttonStyles = cva("c-btn", {
  variants: {
    intent: {
      primary: "c-btn--primary",
      secondary: "c-btn--secondary",
      // Other button variants
    },
    size: {
        small: "c-btn--small",
      base: "c-btn--base",
      big: "c-btn--big",
      // Other button sizes
    },
    isLoading: {
      true: "is-loading",
      false: "",
    },
  },
});

export type ButtonStyleProps = VariantProps<typeof buttonStyles>;
vue
<template>
  <component
    :is="buttonTag"
    :href="props.href || undefined"
    :class="buttonStyles({intent, size, isLoading}) 
    ">
    <slot v-if="!isLoading" />
    <Spinner v-else />
  </component>
</template>

📦 Scoped styles. What's the deal?

When a <style> tag has the scoped attribute, its CSS will apply to elements of the current component only. What this means is that parent component's styles will not affect child components.

However, a child component's root node will be affected by both the parent's scoped CSS and the child's scoped CSS. This is by design so that the parent can style the child root element for layout purposes.

For more details refer to Vue docs

How to style child component's children?

TIP

When you have defined design system (variants, sizes, spacing etc.) this type of styling should be very rare.

In this example we will demonstrate, how to affect styles of child component's child elements.

For demonstration we will use our rForm component:

vue
<template>
    <form class="c-form" novalidate @submit.prevent="submitHandler">
        <div v-if="$slots.header" class="c-form__header">
            <slot name="header" />
        </div>

        <slot />

        <div v-if="$slots.cta" class="c-form__cta">
            <slot name="cta" :is-submitting="form.isSubmitting" />
        </div>

        <div v-if="$slots.footer" class="c-form__footer">
            <slot name="footer" />
        </div>
  </form>
</template>
vue
<template>
  <RForm class="c-sign-in-form">
    <template #header>
      <h2 class="c-sign-in-form__title">Prihlásenie</h2>
    </template>

    /* ... */

    <template #cta>
      <RButton intent="primary">
        Prihlásiť sa
      </RButton>
    </template>

    <template #footer>
      <RLink href="/registracia" intent="primaryUnderline">
        Nemáte účet? Zaregistrujte sa
      </RLink>

      <RLink href="/" intent="primary">
        Späť na hlavnú stránku
      </RLink>
    </template>
  </RForm>
</template>

Now if we want to style rForm's root element we can do so with .c-sign-in-form because even with scoped styles we can style the root element of child component.

If we want to style elements, which are in slots, we can do so as usual because they are considered to belong to the parent.

But let's suppose we want to style header slot of rForm, which has .c-form__header class. The header is child element of rForm and since our signInForm component has scoped styles we can't directly style it.

scss
.c-sign-in-form {
    &{
        // Target root rForm's element here
    }


    // Target .c-form__header
    :deep(.c-form__header) {
        & {
            // Styles for header
        }
    }
}

In the example above we have used :deep selector with wich we can target child elements even with scoped styles.

For more details about :deep selector refer to Vue docs

Made with ♥️ by Riešenia