Global Components example

How it works

Ready-made components from the @terrahq/gc package. The package fixes the HTML, the project decides the look through SCSS option maps.

A global component is a component that comes from the @terrahq/gc package: its HTML structure and its layout rules are written once, in the package, and every Terra project reuses them. A regular c-- component is the opposite: markup and styles written by hand for one project. You recognise a global component by its class, g-- followed by a name and a two-digit number (g--btn-01, g--card-31, g--layout-04). Variations are BEM modifiers on that class, g--btn-01--second, and the project styles them with the same mixin.

SCSS decides the look, the HTML is fixed

Each component ships as two mixins. make-name-NN() writes the structure: the flex and grid rules, the spaces, the responsive behaviour. make-name-NN-modifier() takes named option maps ($background-options, $font-options, $media-options…) and writes the visual layer from them: colours, fonts, borders, hover states. The project never writes CSS for a global component by hand; it picks values, and the mixin knows which element they belong to. The HTML, on the other hand, is not negotiable: the readme of each component in node_modules/@terrahq/gc/readme gives the exact elements, and the Astro components in src/components reproduce them. Nothing is added or removed from that structure; extra content goes inside an existing element.

Configuring the package

The package reads a set of $g-* variables and a set of per-family variables ($btn-padding, $card-spaces-breakpoint, $layout-item-list-gap…). The project sets them once in src/scss/global-components/global-vars.scss, always from its own foundation vars, never from hard-coded values, and re-exports the library from the same file so that a component file needs a single import:

@use "@scss/framework/_vars/_vars.scss" as *;

@use "@terrahq/gc/variables" with (
  //* GLOBAL VARIABLES
  $g-viewport-type: $viewport-type,
  $g-measure: $measure,
  $g-container-class: f--container,
  $g-row-class: f--row,
  $g-row-remove-gutter-class: f--row--remove-gutter,
  $g-transition-duration: 0.3s,
  $g-transition-timing-function: $ease-standard-a,
  
  //* btn
  $btn-padding: $measure*1.5 $measure * 3,
  $btn-icon-gap: $measure,
  $btn-full-width-breakpoint: $tablets,

  // … one block per family: pill, icon, form fields, card, CTA, layout, hero…
);

@forward "@terrahq/gc/library";

Enabling a component

One file per component in src/scss/global-components/[family]/. It imports global-vars.scss, calls the structure mixin and then the modifier with the project's options. Foundation classes go into the maps where they exist ("className": f--font-e, "background-class": f--background-d), so the component stays on the design system:

// src/scss/global-components/btn/_g--btn-01.scss
@use "@scss/global-components/global-vars.scss" as *;

.g--btn-01 {
  @include make-btn-01();            // the structure, from the package
  @include make-btn-01-modifier(     // the look, from the project
    $font-options: (
      "className": f--font-e f--color-c,
    ),
  );
}

The file is then registered in src/scss/style.scss, under the global components block, one @use per component:

// src/scss/style.scss
//* global components
@use "@scss/global-components/btn/_g--btn-01.scss";
@use "@scss/global-components/card/_g--card-31.scss";

Three rules to remember

  • Responsive is another modifier call. Inside a media query, call make-name-NN-modifier() again with only the options that change. Card 31 below does it at five breakpoints.
  • A modifier must override hover. When &--second changes the colours, it sets $hover-options too, or it inherits the hover of the base and the two no longer match.
  • Anything else goes through $custom-options. "parent" targets the root, a full selector targets a child, and "class" borrows the styles of an existing class. Never a nested block outside the mixin call.

Btn 01

A button with text and padding around it. The project's primary and secondary buttons.

Button g--btn-01g--btn-01--second

g--btn-01 is the simplest global component: one element, a button or a link, with padding from $btn-padding and a full width below $btn-full-width-breakpoint, both set in global-vars.scss. The project gives the base a dark background ($background-options), the font and colour through a foundation class ($font-options) and a rounded hover ($hover-options). &--second re-calls the modifier with another background and a hover that also animates background-size from 0 to 100%.

@use "@scss/global-components/global-vars.scss" as *;
@use "@scss/framework/foundation/foundation.scss" as *;
@use "@scss/framework/utilities/utilities.scss" as *;
@use "@scss/framework/_vars/_vars.scss" as *;
@use "sass:map";



.g--btn-01 {
    @include make-btn-01();
    @include make-btn-01-modifier(
        $background-options: (
            "background-color": map.get($color-options, d),
        ),
        $hover-options:(
            "border-radius": 30px,
        ),
        $font-options: (
            "className": f--font-e f--color-c,
        ),
    );

    &--second{
        @include make-btn-01-modifier(
            $background-options: (
                "background-color": map.get($color-options, e),
                        ),
            $hover-options:
                (
                    "border-radius": 30px,
                    "background-size": (
                        0,
                        100%,
                    ),
                ),
        );
    }

}

The markup

The package gives the same class to a button and to an a. Here the two live examples use ButtonLink from @terrahq/astro-core, which renders either.

<!-- A button or a link, the class is the same -->
<button class="g--btn-01">Read the docs</button>
<a href="#" class="g--btn-01 g--btn-01--second">Read the docs</a>

<!-- In Astro, through ButtonLink from @terrahq/astro-core -->
<ButtonLink payload={{ option: "button", customClass: "g--btn-01", label: "Read the docs" }} />
Live example
g--btn-01g--btn-01--second

Card 31

Media on one side, two text elements on the other. The responsive example: the modifier is called again at five breakpoints.

Card g--card-31$media-options

g--card-31 places a media element (image, video, embed or Lottie) next to a primary text and a list group. The project sets the two-column breakpoint in $card-options, a background and a radius, a foundation font for each text, and in $media-options asks the image to come first and to be hidden on small screens. Then, inside media queries, it calls make-card-31-modifier() again: the image shows up from $tabletm, moves after the text from $desktop, and the background and radius change along the way. Each call only carries what changes.

@use "@scss/global-components/global-vars.scss" as *;
@use "@scss/framework/foundation/foundation.scss" as *;
@use "@scss/framework/_vars/_vars.scss" as *;
@use "sass:map";

.g--card-31 {
    @include make-card-31();
    @include make-card-31-modifier(
        $card-options: (
            "breakpoint": $tabletl,
        ),
        $background-options: (
            "background-color": rgba(map.get($color-options, a), 1),
        ),
        $border-options: (
            "border-radius": $border-radius-b,
        ),
        $item-primary-options: (
            "className": f--font-b f--color-b,
        ),
        $list-group-item-options: (
            "className": f--font-d f--color-b,
        ),
        $media-options: (
            "first": yes,
            "hide": yes,
        ),
    );

    @media all and ($viewport-type: $tablets) {
        @include make-card-31-modifier(
            $background-options: (
                "background-color": map.get($color-options, f),
            ),
        );
    }

    @media all and ($viewport-type: $tabletm) {
        @include make-card-31-modifier(
            $media-options: (
                "hide": no,
            ),
        );
    }

    @media all and ($viewport-type: $laptop) {
        @include make-card-31-modifier(
            $background-options: (
                "background-color": map.get($color-options, a),
            ),
        );
    }

    @media all and ($viewport-type: $desktop) {
        @include make-card-31-modifier(
            $media-options: (
                "first": no,
            ),
        );
    }

    @media all and ($viewport-type: $wide) {
        @include make-card-31-modifier(
            $background-options: (
                "background-color": map.get($color-options, e),
            ),
            $border-options: (
                "border-radius": 0,
            ),
        );
    }
}

The markup

Card31.astro reproduces the image option of the package readme. The image is lazy-loaded by g--lazy-01, so src is the placeholder and data-src the real file; the alt text comes from the payload.

<div class={["g--card-31", customClass ].filter(Boolean).join(" ")}>
    <div class="g--card-31__wrapper">
        <p class="g--card-31__wrapper__item-primary">{ title }</p>
        <div class="g--card-31__wrapper__list-group">
            <p class="g--card-31__wrapper__list-group__item">{ subtitle }</p>
        </div>
    </div>
    <figure class="g--card-31__media-wrapper">
        <img
            width=600
            height=700
            src="/img/terra-placeholder.webp"
            data-src={ imgSrc }
            alt={ imgAlt }
            class="g--card-31__media-wrapper__media g--lazy-01"
        />
    </figure>
</div>
<Card31 payload={{
  title: 'Handlers are built when they come into view',
  subtitle: 'Boostify watches the page and only imports a handler when its element is close to the viewport.',
  imgSrc: 'https://placeholder.terrahq.com/img-1by1.webp',
  imgAlt: 'Punky handler lifecycle',
}} />
Live example
g--card-31$media-options

Handlers are built when they come into view

Boostify watches the page and only imports a handler when its element is close to the viewport. Libraries like GSAP come from the Manager, loaded once and cached, so the first paint never waits for code the page does not need yet.

Punky handler lifecycle

Card 09

Three text elements over an optional background image, with a link as the last one.

Card g--card-09"back-items": yes

g--card-09 is a text card with a background: a title, a subtitle and a list group where the last text becomes a link. The project separates the background into its own element ("back-items": yes in $card-options) so it can be lazy-loaded with g--lazy-01, aligns the content at the bottom with a minimum height of 500px, darkens the picture with a background-overlay and rounds the corners. The link borrows its look from g--link-01 through "className", which is why the file also imports that component. The project file keeps every other option of the package commented out as a reference; the snippet shows only the active lines.

@use "@scss/global-components/global-vars.scss" as *;
@use "@scss/framework/foundation/foundation.scss" as *;
@use "@scss/framework/_vars/_vars.scss" as *;
@use "sass:map";
@use "@scss/global-components/link/_g--link-01.scss";

.g--card-09 {
    @include make-card-09();
    @include make-card-09-modifier(
        $card-options: (
            "vertical-alignment": bottom,
            "min-height": 500px,
            "back-items": yes
        ),
        $background-options: (
            "background-position": center,
            "background-overlay": (
                "background-color": rgba(map.get($color-options, a), .5),
            ),
        ),
        $border-options: (
            "border-radius": $border-radius-b,
        ),
        $item-primary-options: (
            "className": f--font-d f--color-d,
        ),
        $item-secondary-options: (
            "className": f--font-e f--color-b,
        ),
        $list-group-item-options: (
            "className": f--font-f g--link-01 g--link-01--second,
        ),
    );
}

The markup

With back-items on, the picture is a __bg-items div with the image as data-src, and the texts live in __ft-items.

<div class={["g--card-09", customClass ].filter(Boolean).join(" ")}>
    <div class="g--card-09__bg-items g--lazy-01" data-src={ imgSrc }></div>
    <div class="g--card-09__ft-items">
        <h3 class="g--card-09__ft-items__item-primary">{ title }</h3>
        <p class="g--card-09__ft-items__item-secondary">{ subtitle }</p>
        <div class="g--card-09__ft-items__list-group">
            <a href={ linkHref } target="_blank" rel="noopener noreferrer" class="g--card-09__ft-items__list-group__item">{ linkText }</a>
        </div>
    </div>
</div>
Live example
g--card-09"back-items": yes

Boot sequence

How Punky starts, from the Manager to the first handler.

Handlers

The two-file pattern every interactive component follows.

Foundation

Fonts, colors, spacing and grid, all from one set of maps.

Card 38

Two texts and an interactive element over a background. Here it is the item of the horizontal accordion.

Card g--card-38$custom-options

g--card-38 is a background card with a title and a list group, built for cards that react to something: the texts sit in a __ft-items block that can be shown or hidden. In this project it is the item of the horizontal accordion. The base call sets the bottom alignment, the separate background ("back-items": yes) with a gradient overlay, the radius, the fonts, and "aligned-at-bottom" on the list. $custom-options does the rest: "parent" gives the card the height of the accordion item, and from $tabletl three selectors size the background to the expanded width of the accordion and fade __ft-items in and out with the --is-visible modifier the handler toggles. The widths and easings come from the project's _vars.scss, next to the ones of c--accordion-a.

@use "@scss/global-components/global-vars.scss" as *;
@use "@scss/framework/foundation/foundation.scss" as *;
@use "@scss/framework/_vars/_vars.scss" as *;
@use "sass:map";
@use "@scss/global-components/link/_g--link-01.scss";

.g--card-38 {
    @include make-card-38();
    @include make-card-38-modifier(
        $card-options: (
            "vertical-alignment": bottom,
            "back-items": yes,
        ),
        $background-options: (
            "background-repeat": no-repeat,
            "background-size": cover,
            "background-position": center,
            "background-overlay": (
                "background-image": linear-gradient(to top, map.get($color-options, a) 0%, transparent 50%),
            ),
        ),
        $border-options: (
            "border-radius": $border-radius-a,
        ),
        $item-primary-options: (
            "className": f--font-d f--color-b,
        ),
        $list-group-item-options: (
            "className": f--font-f f--color-b,
            "font-weight": 700,
            "text-transform": uppercase,
        ),
        $list-group-item-list-options: (
            "aligned-at-bottom": "yes",
        ),
        $custom-options: (
            "parent": (
                "height": 100%,
            ),
            ".g--card-38__ft-items__list-group:last-child": (
                "margin-bottom": 0,
            ),
        ),
    );

    @media all and ($viewport-type: $tabletl) {
        @include make-card-38-modifier(
            $custom-options: (
                ".g--card-38__bg-items": (
                    "left": 50%,
                    "transform": translateX(-50%),
                    "width": $accordion-a-expanded-width,
                ),
                ".g--card-38__ft-items": (
                    "flex-shrink": 0,
                    "width": $accordion-a-expanded-width,
                    "opacity": 0,
                    "visibility": hidden,
                    "transition": (opacity 0.25s $accordion-a-ease, visibility 0s $accordion-a-ease 0.25s),
                ),
                ".g--card-38__ft-items--is-visible": (
                    "opacity": 1,
                    "visibility": visible,
                    "transition": (opacity 1s $accordion-a-ease, visibility 0s $accordion-a-ease),
                ),
            ),
        );
    }
}

The markup

Card38.astro takes the attributes of __ft-items as a payload, because the accordion needs an id, an aria-hidden and the --is-visible class on it. The js--accordion-item-content hook is what the horizontal accordion handler looks for.

<div class="g--card-38" {...attributes}>
    <div class="g--card-38__bg-items g--lazy-01" data-src={imgSrc}>
        <div class="g--card-38__bg-items__artwork"></div>
    </div>
    <div class:list={["g--card-38__ft-items js--accordion-item-content", ftItemsClass]} {...ftItemsAttributes}>
        {title && (
            <h3 class="g--card-38__ft-items__item-primary">{title}</h3>
        )}
        {subtitle && (
            <div class="g--card-38__ft-items__list-group">
                <span class="g--card-38__ft-items__list-group__item">{subtitle}</span>
            </div>
        )}
    </div>
</div>
<!-- Card 38 is one item of the horizontal accordion: the accordion sets the width, the card fills it -->
<div class="c--accordion-a js--horizontal-accordion" data-autoplay="false" data-autoplay-delay="4000" data-hover-delay="80">
  <div class="c--accordion-a__item js--horizontal-accordion-item c--accordion-a__item--is-active" tabindex="0">
    <Card38 payload={{
      ftItemsAttributes: { class: 'g--card-38__ft-items--is-visible', id: 'gcContent-1', 'aria-hidden': 'false' },
      title: 'One SCSS file per component, registered in style.scss',
      subtitle: 'Structure',
      imgSrc: '…',
    }} />
  </div>
  …
</div>
Live example
g--card-38$custom-options

One SCSS file per component, registered in style.scss

Structure

Layout 04

Two columns: three text elements on one side, a media item on the other. A full-width section with its own background.

Layout g--layout-04"background-class"

g--layout-04 is a section, not a card: it builds its own container and row with the classes from global-vars.scss, and its column widths come from $layout-two-col-media-width-class and $layout-two-col-content-width-class. The project centres text and media on each other ("align-media-text"), paints the section with a foundation background class, gives the title and the paragraph a font each, and turns the list item into a button by borrowing g--btn-01 g--btn-01--second through "className". Layouts ship with no vertical padding, so the project adds it with "parent" > "class" in $custom-options, as utility classes.

@use "@scss/global-components/global-vars.scss" as *;
@use "@scss/framework/foundation/foundation.scss" as *;
@use "@scss/framework/_vars/_vars.scss" as *;
@use "@scss/global-components/btn/_g--btn-01.scss";
@use "sass:map";

.g--layout-04 {
    @include make-layout-04();
    @include make-layout-04-modifier(
        $layout-options: (
            "align-media-text": center,
        ),
        $background-options: (
            "background-class": f--background-d,
        ),
        $item-primary-options: (
            "className": f--font-a f--color-e,
        ),
        $item-secondary-options: (
            "className": f--font-d f--color-f,
        ),
        $list-group-item-options: (
            "className": g--btn-01 g--btn-01--second,
        ),
        $custom-options: (
            "parent": (
                "class": u--pt-5 u--pt-tabletm-8 u--pb-5 u--pb-tabletm-8,
            )
        ),
    );
}

The markup

A primary text, a secondary text, a list group with the button, and a figure. ButtonLink and Asset from @terrahq/astro-core render the last two with the classes the mixin expects.

<section class={["g--layout-04", customClass ].filter(Boolean).join(" ")}>
    <div class="g--layout-04__wrapper">
        <div class="g--layout-04__wrapper__content">
            <h2 class="g--layout-04__wrapper__content__item-primary">{ title }</h2>
            <p class="g--layout-04__wrapper__content__item-secondary">{ subtitle }</p>
            <div class="g--layout-04__wrapper__content__list-group">
                <ButtonLink payload={{ href: "#", option: "target_self", customClass: "g--layout-04__wrapper__content__list-group__item", label: linkLabel }} />
            </div>
        </div>
        <figure class="g--layout-04__wrapper__media-wrapper">
            <Asset payload={{ type: "Image", url: imgSrc, placeholderUrl: "/img/terra-placeholder.webp", alt: imgAlt, sizes: "(max-width: 768px) 100vw, 600px", customClass: "g--layout-04__wrapper__media-wrapper__media", lazy: true, lazyClass: "g--lazy-01", attributes: { width: 600, height: 700 } }} />
        </figure>
    </div>
</section>
Live example
g--layout-04"background-class"

One framework, every Terra project

Punky gives a project its foundation, its component rules and its handlers from day one. Global components add the pieces every site needs, already laid out, so the work starts at the look and not at the structure.

A punky project in the browser

Layout 01

Two text elements that do not take the full width on desktop. The simplest layout of the package.

Layout g--layout-01"align": center

g--layout-01 is a title and a text, centred in a column whose width is $layout-one-col-width-class from global-vars.scss (f--col-8 here). The project sets almost nothing: "align": center in $layout-options, a font and a bottom space for the title through foundation classes, and a font for the text. No background and no padding, so the section is placed where it is needed and spaced from outside, as the live example does with two utility classes. Like Card 09, the file keeps the unused options commented; the snippet shows the active lines.

@use "@scss/global-components/global-vars.scss" as *;
@use "@scss/framework/foundation/foundation.scss" as *;

.g--layout-01 {
    @include make-layout-01();
    @include make-layout-01-modifier(
        $layout-options: (
            "align": center,
        ),
        $item-primary-options: (
            "className": f--font-e f--sp-bottom-a,
        ),
        $list-group-item-options: (
            "className": f--font-d,
        ),
    );
}

The markup

<section class:list={["g--layout-01", customClass].filter(Boolean)}>
    <div class="g--layout-01__wrapper">
        <div class="g--layout-01__wrapper__content">
            <h2 class="g--layout-01__wrapper__content__item-primary">{ title }</h2>
            <div class="g--layout-01__wrapper__content__list-group">
                <p class="g--layout-01__wrapper__content__list-group__item">{ subtitle }</p>
            </div>
        </div>
    </div>
</section>
Live example
g--layout-01"align": center

Built to be forgotten

The best framework is the one nobody notices. Punky stays out of the way: the foundation sets the tokens, the handlers wait until they are needed, and the global components bring the structure so that every hour goes into what makes the project different.

Options reference

The option maps each project file passes to its modifier, and the keys it sets.

Reference
ComponentOption mapKeys the project sets
g--btn-01$background-optionsbackground-color
$hover-optionsborder-radius; background-size on --second
$font-optionsclassName
g--card-31$card-optionsbreakpoint
$background-optionsbackground-color, per breakpoint
$border-optionsborder-radius, per breakpoint
$item-primary-optionsclassName
$list-group-item-optionsclassName
$media-optionsfirst, hide, per breakpoint
g--card-09$card-optionsvertical-alignment, min-height, back-items
$background-optionsbackground-position, background-overlay → background-color
$border-optionsborder-radius
$item-primary-optionsclassName
$item-secondary-optionsclassName
$list-group-item-optionsclassName
g--card-38$card-optionsvertical-alignment, back-items
$background-optionsbackground-repeat, background-size, background-position, background-overlay → background-image
$border-optionsborder-radius
$item-primary-optionsclassName
$list-group-item-optionsclassName, font-weight, text-transform
$list-group-item-list-optionsaligned-at-bottom
$custom-optionsparent; .g--card-38__bg-items, .g--card-38__ft-items and its --is-visible state from $tabletl
g--layout-04$layout-optionsalign-media-text
$background-optionsbackground-class
$item-primary-optionsclassName
$item-secondary-optionsclassName
$list-group-item-optionsclassName
$custom-optionsparent → class
g--layout-01$layout-optionsalign
$item-primary-optionsclassName
$list-group-item-optionsclassName

These are the keys this project uses, not everything the maps accept. Each component also takes $shadow-options, $hover-options, the *-hover-options of each text, and for media components $media-hover-options. The full list with every key is in the package: node_modules/@terrahq/gc/readme/[family]/[name].md for the component, readme/properties/ for each map, readme/vars/ for the variables of global-vars.scss, and the live catalogue at gc-terra.netlify.app.