Styling

Carats uses scoped SASS for styling, promoting semantic HTML with minimal CSS classes.

base.sass

The base.sass file in src/client contains global design tokens and style resets. All other SASS files import it with @use and as * so the tokens are available without a namespace prefix.

Creating base.sass

// Colors
$color-primary: #000000
$color-secondary: #ffffff
$color-background: #f5f5f5
$color-text: #333333
$color-text-light: #666666

// Spacing
$spacing-xs: 4px
$spacing-sm: 8px
$spacing-md: 16px
$spacing-lg: 24px
$spacing-xl: 32px

// Typography
$font-family-base: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif
$font-size-base: 16px
$font-size-sm: 14px
$font-size-lg: 18px
$font-size-xl: 24px
$font-weight-normal: 400
$font-weight-bold: 700

// Breakpoints
$breakpoint-sm: 576px
$breakpoint-md: 768px
$breakpoint-lg: 992px
$breakpoint-xl: 1200px

// Border radius
$border-radius: 4px
$border-radius-lg: 8px

// Box shadows
$box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1)
$box-shadow-lg: 0 4px 8px rgba(0, 0, 0, 0.15)

// Transitions
$transition-fast: 150ms ease
$transition-normal: 250ms ease

Design Tokens

Define colors, spacing, typography, breakpoints and other design tokens at the top level of base.sass. Do not nest them.

Correct Usage

@use '../base.sass' as *

.my-component
    color: $color-text
    padding: $spacing-md
    font-size: $font-size-base

Incorrect Usage

// Tokens must stay at the top level of base.sass
.my-component
    $color-text: #333
    .inner
        color: $color-text

A token defined inside a rule only exists inside that rule, so any other stylesheet that needs it has to redefine it. This is how a palette quietly drifts apart across a project.

Semantic HTML

Prefer semantic HTML elements with minimal class usage. Let the component structure and the design tokens handle the styling, and nest selectors in SASS instead of adding utility classes to the markup.

Example Component

import './card.sass';

export default function Card(props: { title: string; children?: JSX.Element }) {
    return (
        <article class="card">
            <header>
                <h2>{props.title}</h2>
            </header>
            <section>
                {props.children}
            </section>
            <footer>
                <button>Action</button>
            </footer>
        </article>
    );
}

Corresponding SASS

@use '../base.sass' as *

.card
    background: $color-secondary
    border-radius: $border-radius-lg
    box-shadow: $box-shadow
    padding: $spacing-md
    margin-bottom: $spacing-md

    header
        margin-bottom: $spacing-sm

        h2
            font-size: $font-size-lg
            font-weight: $font-weight-bold

    section
        margin-bottom: $spacing-md

    footer
        display: flex
        justify-content: flex-end

        button
            background: $color-primary
            color: $color-secondary
            padding: $spacing-sm $spacing-md
            border: none
            border-radius: $border-radius
            cursor: pointer
            transition: background $transition-fast

            &:hover
                opacity: 0.9

Responsive Styling

@use '../base.sass' as *

.container
    max-width: $breakpoint-xl
    margin: 0 auto
    padding: $spacing-md

    @media (max-width: $breakpoint-md)
        padding: $spacing-sm

.grid
    display: grid
    grid-template-columns: repeat(3, 1fr)
    gap: $spacing-md

    @media (max-width: $breakpoint-lg)
        grid-template-columns: repeat(2, 1fr)

    @media (max-width: $breakpoint-sm)
        grid-template-columns: 1fr

Best Practices

Follow these best practices for maintainable styling:

  1. Define tokens at the top level. Never nest design tokens inside mixins or functions.
  2. Prefer semantic HTML. Let the structure define the styling instead of adding classes.
  3. Import base.sass correctly. Always use @use 'relative/path/to/base.sass' as *.
  4. Keep specificity low. Avoid deeply nested selectors.
  5. Use variables for consistency. Never hardcode colors, spacing or sizes.