Japanese woodblock-inspired waterfalls flowing through a misty mountain landscape

kni-cascade

kni-cascade version published on npm View kni-cascade on GitHub
Scaling demo

kni-cascade is a shared CSS foundation for building responsive websites. It gives every project a familiar folder structure, so developers know where settings, base styles, components, and page styles belong.

At its core is a scaling system that preserves the relationships between typography, spacing, and dimensions as the viewport changes. It helps carry a design across screen sizes with less repetitive adjustment and fewer media queries.

Alongside that system are the foundations projects repeatedly need: typography, buttons, forms, layout helpers, and visual effects. Use what helps, leave out what doesn’t, and build on top of it.

kni-cascade is a starting point you own. Customize the defaults, extend what’s useful, and change what the project needs. It should help you build your design, never limit what you can do.

Getting started

KNI’s WordPress, Eleventy, and Next.js boilerplates seed your project’s scss directory with the latest version of kni-cascade during setup. From there, those files belong to your project—customize them freely. Existing projects don’t automatically receive future kni-cascade updates.

Configuration

Before building pages, set the project’s typography, scaling, and component defaults to match the Figma design. The settings below show where to make those changes.

Typography
Match $type-scale in scss/00-sass/_variables.scss to the desktop and mobile typography variables in Figma.
Scaling
Check the mobile and desktop reference dimensions in scss/01-base/_scaling.scss. They default to 390px and 1440px wide.
Components
Set shared appearance and sizing once, then extend each component as needed. For buttons, use $btn-design and $btn-sizes.
Only what you need
Comment out unused forwards in the components and utilities entry files, then rebuild to omit their CSS.

Folder structure

Each layer builds on the one before it. Keep shared decisions near the top and project-specific styling in the layer that owns it.

FolderWhat belongs here
00-sassSettings, variables, functions, and mixins. This layer produces no CSS on its own.
01-baseGlobal resets, typography, scaling, layout, and utilities.
02-componentsReusable interface elements, such as buttons, with shared appearance and states.
03-modulesPage sections composed from those foundations and components.
04-pagesStyles specific to a page or template.

Scaling

Scaling is at the foundation of kni-cascade. A shared scale keeps typography, spacing, and dimensions working together as the viewport changes, preserving the proportions of your design without sizing every element independently. The demo shows how width, height, and limits shape that scale.

Explore the scaling demo


postcss-pxv

View postcss-pxv on GitHub postcss-pxv version published on npm

kni-cascade comes bundled with pxv, our custom viewport unit. Write the size from your design, and kni-cascade scales it using the viewport and the active mobile or desktop settings.

You write

.card {
  padding: 24pxv;
}

PostCSS outputs

.card {
  padding: calc(24 * var(--pxv-unit));
}

Browser computes

One shared unit keeps every pxv value in proportion. The browser calculates the final size as the viewport changes. Here’s what that padding becomes at a few desktop widths:

Computed padding using the default 1440px desktop reference width, with enough viewport height for width to determine the scale
Viewport widthComputed padding
1280px21.33px
1440px (desktop basis)24px
1600px26.67px

Values are rounded to two decimal places. Standard px values stay fixed.

Breakpoints

One simple media query

One simple media query switches the site-basis settings that power scaling in kni-cascade. The site starts with its mobile basis settings, including a reference width of 390px. At $tm (768px by default), the query activates the desktop basis settings, including a reference width of 1440px. Typography switches between its mobile and desktop sizes at the same threshold by default.

That media query switches the design basis; pxv handles the sizes in between. Spacing, dimensions, and other pxv values scale together as the viewport changes, while typography follows its configured scaling and readability limits. Instead of repeatedly adjusting individual values across tablet, laptop, and desktop widths, you define the mobile and desktop compositions and let the shared scaling system carry them through each range.

Use @include sm and @include lg for structural changes at that same switch. With the defaults, sm applies below 768px and lg applies from 768px upward. For example, stack a feature on mobile and place its image and text side by side on desktop:

.feature {
  display: grid;
  gap: 24pxv;

  // Mobile: stack the image and text.
  @include sm {
    grid-template-columns: 1fr;
  }

  // Desktop: show two equal columns.
  @include lg {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

The layout changes once, while the 24pxv gap scales within each range. Use the same mixins to change navigation or rearrange content, and add other media queries when the content needs them.

Primary scaling ranges
ModeWidthBehavior
MobileBelow 768pxMobile reference dimensions and type sizes.
Desktop768px and aboveDesktop reference dimensions and type sizes.
Additional breakpoint references

These Sass values provide convenient names for reference widths. They are not a checklist of media queries to add: the scaling system handles sizing between widths, and most layouts only need sm and lg. Reach for an additional named value only when the content needs a specific layout adjustment.

Defaults in scss/00-sass/_variables.scss
Sass variableNameWidth
$msMobile small320px
$mmMobile medium375px
$mlMobile large500px
$tsTablet small600px
$tmTablet medium768px
$tlTablet large1024px
$dxsDesktop extra-small (alias of $tl)1024px
$dsDesktop small1280px
$dmDesktop medium1440px
$dlDesktop large1600px
$dxlDesktop extra-large1800px

Advanced scaling settings

These defaults should work for most projects without adjustment. If you’re feeling ambitious, here’s what’s happening under the hood—and how to fine-tune the scaling for your design.

Edit the mobile and desktop CSS custom properties in scss/01-base/_scaling.scss; the primary media query activates the appropriate set.

Reference dimensions
--mobile-basis-w and --desktop-basis-w match the design widths: 390 and 1440 by default. Their height counterparts, --mobile-basis-h and --desktop-basis-h, default to 667 and 750. Reference height affects scaling only when squish is above zero. Enter these dimensions as unitless numbers.
Desktop squish
--desktop-squish defaults to 1. Normally, pxv scales from viewport width. When a desktop window is wide but short, width alone can make the composition too large for the available height. Squish lets the calculation switch to a viewport-height-based scale when that produces a smaller result. This helps a hero's typography, spacing, and imagery stay in proportion in a short laptop window.
How the switch works
At 1, the calculation uses whichever scale is smaller: viewport width relative to the reference width, or viewport height relative to the reference height. With the default desktop basis of 1440 × 750, a 1440 × 600 window produces a height-based scale of 0.8, so 100pxv becomes 80px. At 0, height has no influence and that same value stays 100px. Values between zero and one soften the height influence. Mobile defaults to width-only scaling.
Floor
--desktop-floor defaults to 500. Below that viewport height, the height calculation continues using 500px, preventing short windows from shrinking the composition indefinitely. This does not set a minimum page height. The mobile floor defaults to 0 and has no effect while mobile squish is zero.
Scaling limits
The active minimum and maximum clamp the scale to equivalent viewport widths. Mobile uses 0 through $tm - 1; desktop starts at $tm and caps at $desktop-max, which defaults to 1800. Set $desktop-max in scss/00-sass/_variables.scss to change that cap. Height influence can keep the scale below it. These limits constrain sizing without changing the layout breakpoint.

For example, reduce --desktop-squish if a wide, short window makes the composition feel too small. Raise --desktop-floor if you want the height-based shrinking to stop sooner. Use @include max for layout rules at and above $desktop-max.

Typography

The Sass type-scale() mixin powers every heading and body size from one shared $type-scale map. Each token defines a mobile and desktop size; updating those values updates every class or component that uses that token.

Start here: update $type-scale in scss/00-sass/_variables.scss to match your project's Figma typography variables. kni-cascade handles mobile/desktop sizes and viewport scaling for every matching class or mixin. These are the default reference sizes, before scaling and font-size clamps.

Default type scale — desktop and mobile reference sizes
TokenDesktopMobile
heading-xxxl6438
heading-xxl6034
heading-xl4830
heading-l3826
heading-m3222
heading-s2620
heading-xs2018
heading-xxs1816
body-l1816
body-m1614
body-s1412
body-xs1211
body-xxs1110
body-xxxs1010

Headers

heading--xxxl

A shared foundation for every design

Desktop: 64px · Mobile: 38px

heading--xxl

Fluid grids jump quickly when pixel boundaries fade

Desktop: 60px · Mobile: 34px

heading--xl

Bold layers of type mix sharp quartz and new glyphs

Desktop: 48px · Mobile: 30px

heading--l

Jazzy mockups bring quirky UX flow and crisp detail

Desktop: 38px · Mobile: 26px

heading--m

Quick designers juggle hazy fonts, warm color, and depth

Desktop: 32px · Mobile: 22px

heading--s
Twelve bright panels form a just-in-time CSS layout

Desktop: 26px · Mobile: 20px

heading--xs
Dynamic boxes flex through jumpy scroll and radiant hue

Desktop: 20px · Mobile: 18px

heading--xxs
Small headings keep the hierarchy clear

Desktop: 18px · Mobile: 16px

Body

body--l

Alex, Brianna, Charlie, Danielle, Elijah, Fiona, Gabriel, Harper, Isaac, Jasmine, Kieran, Lillian, Mateo, Nadia, Oliver, Penelope, Quinn, Rafael, Sophia, Tristan, Ursula, Victor, Willow, Xavier, Yasmine, and Zachary gathered in the quirky old library, flipping through dusty books while exchanging jokes.

Desktop: 18px · Mobile: 16px

body--l longform

From the highest shelf, an ancient volume tumbled, making Jasmine yelp and Quinn laugh. “Unexpected surprises keep things exciting!” Xavier quipped, adjusting his glasses, amazed by the bizarre discovery.

Desktop: 18px · Mobile: 16px

body--m

The efficient officer shuffled swiftly through the artificial lights, affixing a final note to the affable florist’s door. He studied an old encyclopædia and an œuvre on mediæval craftsmanship while enjoying a hors d’œuvre on the café’s terrace in the straße

Desktop: 16px · Mobile: 14px

body--m longform

AVoid wavy kerning! Typographers often fix TA, WA, To, and VA pairs while refining ‘tight’ and ‘loose’ spaces. “It’s tricky!” Xavier joked, adjusting 7.9° of alignment.

Desktop: 16px · Mobile: 14px

body--s

Great ideas rarely arrive fully formed. They start as a question, a sketch on the nearest scrap of paper, or a conversation that runs a little longer than expected. We make room for that messy beginning, then bring enough care and craft to turn a promising thought into something people want to use.

Desktop: 14px · Mobile: 12px

body--xs

Somewhere between the first draft and the final detail, a good project finds its rhythm. The pieces start talking to each other. The unnecessary falls away. What remains feels clear, useful, and just a little unexpected—the kind of work that makes you stop scrolling and lean in.

Desktop: 12px · Mobile: 11px

body--xxs

A small detail can change the whole experience: a label that answers the right question, a transition that gives you your bearings, or a sentence that sounds like a person wrote it. None of these moments needs to shout. Together, they make something worth coming back to.

Desktop: 11px · Mobile: 10px

body--xxxs

Mapping Cellular Networks for Microbiome Contributions to Neurodegeneration. The gut microbiome is increasingly implicated in neurodegenerative diseases, with links to amyloid pathology, microglial activation, neurodegeneration, motor impairments and cognitive decline in animal models. However, molecular and cellular mechanisms underlying how the gut microbiota influences core symptoms of neurodegenerative disease remain undefined.

Desktop: 10px · Mobile: 10px

Caps

Caps inherit their sizes from the corresponding body tokens in the shared type-scale() mixin. Combine caps with a body class such as body--m; update the body values in $type-scale to change both.

body--l caps

Featured story

Desktop: 18px · Mobile: 16px

body--m caps

Research & insights

Desktop: 16px · Mobile: 14px

body--s caps

Our approach

Desktop: 14px · Mobile: 12px

body--xs caps

Behind the work

Desktop: 12px · Mobile: 11px

body--xxs caps

Latest updates

Desktop: 11px · Mobile: 10px

body--xxs caps

Explore the collection

Desktop: 11px · Mobile: 10px

Text helpers

Alignment

Apply alignment to a text element or its container.

Default (left)

Good typography makes room for every word.

.text-center

Good typography makes room for every word.

.text-right

Good typography makes room for every word.

Wrapping

Compare the same headline at equal widths. Balance aims for similar line lengths; pretty improves line breaks, including short last lines. Results depend on the font, available width, and browser support.

Normal

Thoughtful typography brings clarity to every screen and every story.

.wrap-pretty

Thoughtful typography brings clarity to every screen and every story.

.wrap-balance

Thoughtful typography brings clarity to every screen and every story.

Longform reading

Use .longform to increase line-height to 1.5 for longer passages.

Default — 1.35

Good typography makes a long story easier to follow. A little more space between lines helps readers keep their place as they move from one thought to the next. Compare how the same words feel in each example.

.longform — 1.5

Good typography makes a long story easier to follow. A little more space between lines helps readers keep their place as they move from one thought to the next. Compare how the same words feel in each example.

Font weight

Font-weight classes run from .w--100 through .w--900 in increments of 100. Load matching font weights for accurate results.

.w--100 — Thin text

.w--200 — Extra-light text

.w--300 — Light text

.w--400 — Regular text

.w--500 — Medium text

.w--600 — Semibold text

.w--700 — Bold text

.w--800 — Extra-bold text

.w--900 — Black text

Components

Components provide reusable starting points for complete interface elements, with consistent sizing, appearance, and interaction states. Customize them for your project and build on top of them.

Only include what you need. In your project's copy of scss/02-components/_index.scss, comment out unused components and rebuild to remove their CSS from the output:

// Comment out this line if your project does not use kni-cascade buttons.
@forward 'button';
@forward 'forms';
@forward 'tables';

[btn]

A shared starting point for consistent button sizing across projects. Make the appearance your own with project colors, spacing, hover effects, and animation. Extend [btn], or take just the sizing and build your own.

Make it yours

Set your project’s colors, corners, and spacing in scss/00-sass/_variables.scss. $btn-design controls appearance; $btn-sizes keeps responsive label sizes and padding together. Start with these defaults and adapt them to your design.

// Allow labels to grow beyond their configured mobile/desktop font sizes.
$btn-scale-up: false !default;

$btn-design: (
  radius: 999pxv,              // Corner radius; 999pxv creates a pill
  weight: 600,                 // Label font weight
  nudge: 0.01em,               // Optical vertical label adjustment
  gap: 0.5em,                  // Space between label and icon
  speed: 0.2s,                 // Color transition duration
  background: #001a3d,         // Default background and outline color
  text: #fff,                  // Default label color
  hover-background: #123456,   // Hover and pressed background
  hover-text: #fff,            // Hover and pressed label color
  focus: #0061c7               // Keyboard focus ring
) !default;

Adjust horizontal and vertical padding for each size separately. Height follows the label, line-height, and padding; no fixed or minimum button height is applied.

// padding-x = left/right; padding-y = top/bottom. Both follow the label size.
$btn-sizes: (
  xs: (desktop: 11, mobile: 10, padding-x: 1.5em, padding-y: 0.5em),
  s: (desktop: 12, mobile: 11, padding-x: 1.5em, padding-y: 0.625em),
  m: (desktop: 14, mobile: 12, padding-x: 1.5em, padding-y: 0.75em),
  l: (desktop: 16, mobile: 14, padding-x: 1.5em, padding-y: 0.75em)
) !default;

Usage

Bare btn defaults to medium. Add size and treatment tokens as needed.

<button type="button" btn="s">Small action</button>

Sizes

Use xs, s, m, or l. Each size controls typography and padding together. Labels use the desktop and mobile values in $btn-sizes, using the global --font-min-clamp minimum (12px in this demo). Button tokens start one step below their corresponding body tokens. The global minimum can make the smallest labels render at the same size; their padding still differs. By default, labels scale down but stop growing at their active mobile or desktop reference size. Set $btn-scale-up: true in settings to allow upward scaling.

btn="xs"

btn="s"

btn

btn="l"

Treatments

Set your button colors once. outline uses its background color for the stroke and label. inverted swaps background and text colors for dark surfaces, with a subtle hover tint derived from the same palette. Combine both treatments in either order; neither needs a separate settings map.

btn

btn="outline"

btn="inverted"

btn="inverted outline"

Links and full width

Use a real button for an action and an a with an href for navigation. Set type="button" for buttons that should not submit a form.

Explore scaling

btn="outline"

btn="l block"

<button type="button" btn="l block">Full-width action</button>

Focus and disabled states

Tab to the examples to check the focus ring. Native disabled buttons cannot be activated and keep their disabled appearance on hover or press. aria-disabled="true" provides the same styling but does not prevent an action: projects must disable the underlying behavior too. This disabled link has no href.

btn

btn disabled

Unavailable link

btn="outline" aria-disabled="true"

Custom hover, same foundation

The default hover and pressed states change colors only. There is no built-in filter, movement, or decorative pseudo-element to undo. This project class adds its own animated fill while keeping btn="m" dimensions, focus, and disabled behavior. Reduced-motion preferences disable its movement.

btn="m"

btn="m" disabled

View custom hover code

Add this project class to btn="m". The foundation supplies sizing, focus, and disabled states; this class adds the purple fill animation.

<button type="button" btn="m" class="btn-demo-reveal">
  <span>Explore the details</span>
</button>
.btn-demo-reveal {
  --btn-bg: #172332;
  --btn-hover-bg: #172332;
  --btn-hover-border: #172332;

  position: relative;
  isolation: isolate;
  overflow: hidden;

  &::before {
    content: '';
    position: absolute;
    inset: 0;
    z-index: -1;
    background: #7c3aed;
    transform: translateX(-101%);
    transition: transform 350ms var(--easing);
  }


  &:where(:not(:disabled, [aria-disabled='true'])):focus-visible {
    &::before {
      transform: translateX(0);
    }

  }

  @media (hover: hover) {
    &:where(:not(:disabled, [aria-disabled='true'])):hover {
      &::before {
        transform: translateX(0);
      }

    }
  }

  @media (prefers-reduced-motion: reduce) {
    &::before {
      transform: none;
      transition: none;
    }
  }
}

Build on the foundation

Add a project class to override properties such as --btn-bg, --btn-hover-bg, --btn-radius, and --btn-weight. Or use the sizing mixin alone for a completely custom component.

btn

To omit button CSS entirely, comment out @forward 'button' in scss/02-components/_index.scss.

Additional color themes are optional. Add an entry to $btn-themes only when the project needs another palette; the default empty map generates no extra theme selectors.

For a completely custom button, @include btn-size(m) supplies just font size and padding. Your component owns its appearance and interaction states.

@use 'kni-cascade/scss/00-sass' as *;

.custom-button {
  @include btn-size(m);
  // Add your project appearance and interaction styles.
}

Tables

Three starting points for native HTML tables. Add one class to choose a treatment; font and text color inherit from the surrounding content.

Minimal

Horizontal separators keep rows easy to follow.

Project schedule
PhaseServiceIncludesDuration
01Brand strategyDiscovery, positioning, and messaging2 weeks
02Visual identityTypography, color, and art direction3 weeks
03WebsiteDesign, development, and launch6 weeks
<table class="table-min">...</table>

Bordered

A full cell grid with a lightly shaded header.

Project schedule
PhaseServiceIncludesDuration
01Brand strategyDiscovery, positioning, and messaging2 weeks
02Visual identityTypography, color, and art direction3 weeks
03WebsiteDesign, development, and launch6 weeks
<table class="table-bordered">...</table>

Zebra

Alternating body rows help you scan across columns.

Project schedule
PhaseServiceIncludesDuration
01Brand strategyDiscovery, positioning, and messaging2 weeks
02Visual identityTypography, color, and art direction3 weeks
03WebsiteDesign, development, and launch6 weeks
<table class="table-zebra">...</table>

Make it yours

Edit $table-design in scss/00-sass/_variables.scss for project defaults. Override inherited CSS variables on a section or table for a local treatment. Padding uses em so it follows the table’s font size.

.pricing {
  --table-padding-y: 1em;
  --table-scroll-background: #fff;
  --table-border: #bac8d3;
  --table-stripe-background: #f0f5f8;
}

On smaller screens

Wrap wide tables in .table-scroll to keep columns intact and allow horizontal scrolling. The examples above use the same wrapper. A focusable, labeled region lets keyboard users scroll it too. The CSS-only right-edge fade is enabled by default with mobile-overflow-scrim: true in $table-design. Set it to false to disable it. Wrap the scroller in .table-scroll-frame to apply the fade on mobile. It stays visible at the scroll end; omit the frame for tables that fit without scrolling. The fade defaults to 5% black; override --table-scroll-background to change its color. --table-scroll-fade-width controls its width.

<div class="table-scroll" role="region"
     aria-label="Project schedule" tabindex="0">
  <table class="table-min">...</table>
</div>

Include @forward 'tables'; in scss/02-components/_index.scss when the project needs these styles. Unclassed tables keep their existing appearance.

Forms

Sensible defaults for native form elements. Include @forward 'forms' in scss/02-components/_index.scss to use them, or comment it out to omit them. No styling attribute or JavaScript replacement is needed.

Make it yours

Edit $form-design in scss/00-sass/_variables.scss for project-wide appearance. Font family, weight, and text color inherit from the surrounding content. Controls use at least 16px text with font-relative padding; borders stay 1px. Layout and spacing between fields belong to your project.

// scss/00-sass/_variables.scss
$form-design: (
  radius: 4pxv,
  padding-x: 0.875em,
  padding-y: 0.4375em,
  background: #fff,
  border: #c5cbd1,
  muted: #68717e,
  accent: #001a3d,
  focus: #0061c7,
  error: #b42318
) !default;

Everyday controls

Inputs and selects share sizing. Textareas resize vertically. Checkboxes and radios use thin borders and a subtle 0.2-second selection animation, while keeping native keyboard and selection behavior. These examples are previews and do not send data.

Use your work email.

Preferred contact
Stay in touch
View form markup
<form class="contact-form">
  <label for="email">Email</label>
  <input id="email" name="email" type="email"
         autocomplete="email" aria-describedby="email-help">
  <p id="email-help" class="form-help">Use your work email.</p>
  <button type="submit" btn>Get in touch</button>
</form>

States

Use native disabled and readonly attributes. Mark an error with aria-invalid="true", explain it in text, and connect that message using aria-describedby. Validation timing and submission behavior remain your application's responsibility.

Enter a complete email address, such as you@company.com.

Disabled group

Try these controls directly. Range, color, file, and date pickers retain their browser behavior. Their exact appearance can vary by browser and operating system.

Drag the thumb or use the arrow keys.

Local preview only. Selected files are not uploaded.

Desktop: use Command or Control to select more than one.

60%
60 out of 100

Choice-control edge cases

The mixed state is set through the native indeterminate property. Click to select or clear it.

Please agree to the terms to continue.

Choose a contact method (required)

Choose one contact method.

Required fields and browser validation

Try submitting this preview empty, then fill it in. Native validation handles required fields and email format. Real projects supply submission and server-side validation; nothing here is sent.

One form, a different look

Override inherited --form-* variables on a form or wrapper. Low-specificity selectors make ordinary CSS overrides easy. The select's dropdown stays native; only its closed box is styled.

.contact-form {
  color: #172332;
  --form-bg: #fff9f7;
  --form-border: #963a30;
  --form-radius: 0;
  --form-accent: #963a30;
  --form-focus: #963a30;
}

Color, date, range, and file inputs keep their specialized native UI; this component does not replace browser pickers. Use --form-select-arrow to supply a different arrow image if needed.

Utilities

kni-cascade ships with helpful utility classes for layout, alignment, and visibility, plus visual effects such as shadows, scrims, and animated inset borders. Use them to handle common styling tasks while keeping your project's design and component structure your own.

Apply layout helpers through classes, such as <div class="flex j-sb a-c">...</div>. Add visual effects to individual elements with attributes, or use their Sass mixins to build the same treatment into a component class. Both approaches share the same implementation; choose whichever fits the way you're building.

Only include what you need. In your project's copy of scss/01-base/utilities/_index.scss, comment out unused utilities and rebuild to remove their CSS from the output:

@forward 'dev';
@forward 'display';
@forward 'flex';
@forward 'spacing';
// @forward 'scrim';
// @forward 'shadow';
// @forward 'inset-border';

Visual effects use custom HTML attributes alongside your component classes. CSS attribute selectors match these attributes and their space-separated options, keeping each effect's settings together in your markup.

This card gets a shadow with elevation 20 and intensity .1:

<div class="news-card" shadow="20 .1">Card content</div>

This image gets a scrim that fades toward the top, with .5 opacity and a 75% fade distance:

<figure class="hero-image" scrim="to-t .5 75%">
  <img src="photo.jpg" alt="Mountain landscape">
</figure>

Your component class still controls the rest of its design. If every instance needs the same effect, use its Sass mixin in the component class instead. Mixins remain available through scss/00-sass even when their attribute utilities are omitted.

.flex

Use class="flex" to create a flex container, then add modifier classes such as class="flex j-sb a-c column-mobile wrap".

Justification follows the main axis; alignment follows the cross axis. These axes swap when using a column. Modifiers ending in -mobile apply below the shared tablet breakpoint; -desktop modifiers apply at and above it.

Justification

class="flex j-start"

OneTwoThree

class="flex j-c"

OneTwoThree

class="flex j-end"

OneTwoThree

class="flex j-sb"

OneTwoThree

class="flex j-sa"

OneTwoThree

class="flex j-se"

OneTwoThree

Alignment

class="flex a-start"

OneTwoThree

class="flex a-c"

OneTwoThree

class="flex a-end"

OneTwoThree

class="flex a-baseline"

OneTwoThree

class="flex center"

OneTwoThree

Direction, wrapping, and inline display

class="flex column"

OneTwoThree

class="flex row-reverse"

OneTwoThree

class="flex wrap"

OneTwoThree

class="flex inline"

OneTwoThree

Responsive options — resize the window

Responsive modifier classes: wrap-mobile inline-mobile column-mobile column-mobile-reverse column-desktop row-reverse-desktop a-c-desktop a-start-desktop j-c-desktop j-sb-desktop j-end-desktop.

class="flex wrap-mobile"

OneTwoThree

class="flex inline-mobile"

OneTwoThree

class="flex column-mobile"

OneTwoThree

class="flex column-mobile-reverse"

OneTwoThree

class="flex column-desktop"

OneTwoThree

class="flex row-reverse-desktop"

OneTwoThree

class="flex a-c-desktop"

OneTwoThree

class="flex a-start-desktop"

OneTwoThree

class="flex j-c-desktop"

OneTwoThree

class="flex j-sb-desktop"

OneTwoThree

class="flex j-end-desktop"

OneTwoThree

Item options — apply to the child

grow fills available space, grow-0 disables growth, and no-shrink prevents shrinking. Use these modifiers with class="flex" on the child; it also becomes a flex container.

Fixedgrow
Can shrinkno-shrink

a-self-start a-self-c a-self-end position individual children.

StartCenterEnd

Combine modifiers

class="flex j-sb a-c column-mobile"

OneTwoThree

[shadow]

Shadows add depth and help separate an element from its background. Use them on cards, menus, or floating controls to make the visual hierarchy clearer. Each preset combines multiple shadow layers for a soft falloff; elevation controls how lifted it feels, while intensity controls how dark it appears.

Layering a tight shadow with broader, softer shadows creates a more natural sense of depth than a single uniform blur. The presets balance those layers for you and provide a consistent set of shadows to reuse across a site, without tuning blur, spread, offset, and opacity for every component. They use standard CSS box-shadow underneath. Use a preset for a shared elevation level, or write your own box-shadow when a component needs a custom treatment.

Explore the shadow demo


Combine elevation, intensity, and an optional color. Options can appear in any order.

<div shadow="20 .1 blue">A low blue shadow</div>
Elevation
10, 20, 30, 40, or 50, from a tight shadow to a broad, lifted shadow. These are preset names, not pixel measurements.
Intensity
.1 through .9, from lighter to darker. This sets the base opacity; individual shadow layers also apply weights, so it is not the final combined opacity.
Color
black by default. Add blue for the blue preset, or set style="--shadow-color: 120, 40, 90;" for a custom RGB color.
Hover only
use shadow-hover="30 .1". It also activates on keyboard focus for buttons and links.
Resting and hover
combine shadow="20 .1" with shadow-hover="50 .2". Hover settings override only the options supplied; omitted options retain the resting values.

Use shadow for a persistent shadow. Elevation presets run from 10 (tight) to 50 (broad). Bare shadow defaults to elevation 30 with intensity 0.08.

Elevation — same .1 intensity

Elevation 10shadow="10 .1"
Elevation 20shadow="20 .1"
Elevation 30shadow="30 .1"
Elevation 40shadow="40 .1"
Elevation 50shadow="50 .1"

Intensity — same elevation 20

Tokens .1 through .9 set the base alpha directly; each shadow layer also applies its own weight.

Intensity .1shadow="20 .1"
Intensity .3shadow="20 .3"
Intensity .5shadow="20 .5"

Defaults and color

Defaultshadow
Blue presetshadow="20 .2 blue"
Custom color--shadow-color: 120, 40, 90

Hover or keyboard focus

Hover these buttons or reach them with Tab. Their shadows fade in without changing layout. Reduced-motion settings disable the transition.

Resting shadow → hover shadow

These buttons already have a shadow. Hover or Tab to them to change elevation, intensity, and optionally color; move away to restore the resting settings.

Build shadows into a component

Use attributes for individual elements, or include the same styling in a component class. Import scss/00-sass for mixins without generating utility CSS. Omitted hover arguments retain the resting settings.

@use '../scss/00-sass' as *;

.mixin-shadow-card {
  @include shadow(20, 0.1);
  @include shadow-hover(30, 0.2);
}

.mixin-shadow-hover {
  @include shadow-hover(20, 0.2, (0, 20, 190));
}

[scrim]

A scrim is a gradient overlay that darkens part of an image and fades to transparent. Use it behind text on photos, hero images, or cards to improve readability while keeping the image visible. Choose a direction that puts the darkest area behind your text, and check contrast against the actual image.

Scrims use an eased, multi-stop gradient for a more gradual fade into the image than a standard two-stop gradient. Learn more in Easing Linear Gradients on CSS-Tricks.

Explore the scrim demo


Change "to-t" to try dif directions.

scrim="to-t .9 75%"

default value is to-t-tr

scrim

to-br

scrim="to-br"

x

scrim="to-bl 50%" style="--scrim-color:0,20,72"

Build a scrim into a component

When every image in a component needs the same treatment, include scrim() on its class instead of repeating the attribute. Arguments are direction, opacity, fade distance, and an optional RGB channel list. The effect reserves ::before.

Waterfalls in a misty mountain landscape

One class, shared treatment

class="mixin-scrim-image" — no scrim attribute

@use '../scss/00-sass' as *;

.mixin-scrim-image {
  @include scrim(to-t, 0.6, 75%, (0, 20, 72));
}

[inset-border]

An inset border grows inward on hover or keyboard focus, over images and other content without changing layout. Use the attribute on individual elements or the mixin on a component class. The effect is invisible at rest.

@include inset-border(5pxv, #a5106f);
inset-border disabled
<a class="news-card" inset-border href="/story">Read the story</a>

Set color, thickness, and duration on your component class. Defaults are currentColor, 5pxv, and 350ms. The overlay inherits the element's corner radius.

.news-card {
  border-radius: 12pxv;
  --inset-border-color: #2a649c;
  --inset-border-width: 5pxv;
  --inset-border-speed: 350ms;
}

Build it into a component

@use '../scss/00-sass' as *;

.news-card {
  @include inset-border(5pxv, #2a649c, 350ms);
  border-radius: 12pxv;
}

The effect reserves ::after and sets position: relative. Set your own positioning after the mixin or override the attribute rule if the host needs absolute, fixed, or sticky positioning. Use a block or inline-block host. The overlay ignores pointer events, preserves the host's own shadow, and disables its transition for reduced motion. Keep an accessible focus indicator; this decorative effect does not replace it.

To omit the attribute CSS, comment out @forward 'inset-border'; in scss/01-base/utilities/_index.scss. The mixin remains available through 00-sass.

Layout & visibility

.container
Centers content with mobile gutters and a desktop maximum width set by --container-size.
.desktop-only
Visible at and above the tablet breakpoint.
.mobile-only
Visible below the tablet breakpoint.
<div class="container">Centered content</div>
<p class="desktop-only">Desktop only</p>
<p class="mobile-only">Mobile only</p>

You are seeing the desktop-only example.

You are seeing the mobile-only example.

Hidden examples use display: none, so do not put essential information exclusively in one variant. The helpers switch at the shared tablet breakpoint (768px by default).

Images & positioning

Image fitting

Give the frame a height or aspect ratio, then choose how the image fills it.

Cover

Fill the frame; crop the edges.

A gold circle on a teal square, cropped to fill a wide frame
Contain

Show the whole image; leave space as needed.

The same gold circle and complete teal square contained in a wide frame
<figure class="fit-photo cover" style="aspect-ratio: 16 / 9;">
  <img src="photo.jpg" alt="Describe the image">
</figure>

Use .fit-photo.contain instead of .fit-photo.cover to preserve the entire image. Both work with direct child images and videos.

Positioning

.relative
Establishes a positioned parent.
.absolute
Positions an element relative to its positioned ancestor.
.abs-full
Fills a positioned parent with an absolutely positioned element.

Clipping

A decorative layer clipped to its wrapper, with a fixed, viewport-height inner layer.

<div class="clip-container">
  <div class="clip-container__inner">Decorative content</div>
</div>

The wrapper disables pointer events. Keep interactive content outside it.

Colors

Color

.color--white
White text.
.color--black
Black text.
.background--white
White background.
.background--black
Black background.
background--black color--white
background--white color--black