Skip to content
NativeCSS
Docs contents

Build Your Own Components

For a piece that doesn't exist in ncss at all yet (not just a variant of an existing one - for that, Tune individual components is enough). Four decisions make a custom component feel built-in instead of bolted-on.

1. Your own prefix, not ncss-

The ncss- prefix is reserved for the library itself - a later ncss version could introduce a new class called ncss-stat-card at any time, colliding with an identically named class of your own. Use a short, project-specific prefix instead (e.g. mp- for "My Project"):

<div class="mp-stat-card">...</div>

Modifier classes in the same style as ncss itself: mp-stat-card--compact (double dash for a variant, like .ncss-btn--primary) - not required, but recognizable to anyone already working with ncss.

2. Use ncss tokens instead of raw values

Color, spacing, radius, font, shadow, motion - pull these from var(--ncss-*) instead of inventing your own values. A custom component then follows theme.css automatically (see Build Your Own Theme), without ever needing to be touched again:

.mp-stat-card {
  padding: var(--ncss-space-md);
  border-radius: var(--ncss-radius-lg);
  background-color: var(--ncss-color-surface);
  box-shadow: var(--ncss-shadow-sm);
  color: var(--ncss-color-text);
}

Full token list (colors, spacing, radii, shadows, motion, breakpoint convention): Design Tokens.

3. Hook into the existing components layer

ncss.css already declares the cascade layer order page-wide (tokens < reset < base < helpers < components < ..., see Architecture) - a custom component simply hooks into that ALREADY EXISTING layer name via @layer components { ... }, regardless of which file or <link> order that happens in (layer order is global, not per stylesheet):

/* mp-stat-card.css - load after ncss.css */
@layer components {
  .mp-stat-card {
    padding: var(--ncss-space-md);
    border-radius: var(--ncss-radius-lg);
    background-color: var(--ncss-color-surface);
    box-shadow: var(--ncss-shadow-sm);
  }
}

This makes the custom component behave EXACTLY like a built-in one: general ncss utilities from helpers/ (.ncss-border, .ncss-radius-*, .ncss-shadow-*, see Component Reference) combine with it freely, because helpers comes BEFORE components in the layer order - a later layer rule always wins over an earlier one, regardless of selector specificity. The reverse does NOT hold: a helpers utility cannot override a same-named property from components, even if the utility class appears after the component class in the markup (layer order beats source order).

Unlayered only for one-off overrides, not for whole components

The theme.css trick (unlayered CSS, always wins) suits individual properties of an EXISTING component (see Step 7). Declaring a brand-new, custom component unlayered works too, but prevents anyone from later combining/adjusting it with an ncss helpers utility - unlayered wins against EVERYTHING, including utilities that are specifically meant to override something.

4. Container queries instead of viewport breakpoints

ncss components respond to the width of their OWN container, not the viewport - follow the same convention for a custom component so it behaves consistently wherever it lands, whether in a narrow sidebar or a wide main column:

@layer components {
  .mp-stat-card {
    container-type: inline-size;
    container-name: mp-stat-card;
    padding: var(--ncss-space-md);
  }

  @container mp-stat-card (min-width: 20rem) {
    .mp-stat-card {
      display: flex;
      align-items: center;
      gap: var(--ncss-space-md);
    }
  }
}

Give each component its own container-name (not a generic name shared with some foreign container) - otherwise a nested foreign component could accidentally match your container query. Details: Layout.

Full example

All four points together - a small stat card (value + label) that switches from stacked to side-by-side above a given width:

<div class="mp-stat-card">
  <span class="mp-stat-card-value">128</span>
  <span class="mp-stat-card-label">New Orders</span>
</div>
@layer components {
  .mp-stat-card {
    container-type: inline-size;
    container-name: mp-stat-card;
    display: grid;
    gap: var(--ncss-space-3xs);
    padding: var(--ncss-space-md);
    border-radius: var(--ncss-radius-lg);
    background-color: var(--ncss-color-surface);
    box-shadow: var(--ncss-shadow-sm);
  }
  .mp-stat-card-value {
    font-size: var(--ncss-font-size-xl);
    font-weight: 700;
    color: var(--ncss-color-brand-on-soft);
  }
  .mp-stat-card-label {
    font-size: var(--ncss-font-size-sm);
    color: var(--ncss-color-text-muted);
  }

  @container mp-stat-card (min-width: 16rem) {
    .mp-stat-card {
      grid-template-columns: auto 1fr;
      align-items: baseline;
      gap: var(--ncss-space-sm);
    }
  }
}

Freely combinable with ncss utilities, because components comes after helpers in the layer order: class="mp-stat-card ncss-border ncss-shadow-hover-md" adds a border + hover shadow without touching a single line of your own file.

File organization

Organize custom component files the same way ncss does - one file per component, e.g. assets/components/stat-card.css, loaded AFTER ncss.css/theme.css in the HTML (a pure readability convention, not technically required, since layer order is already fixed page-wide by ncss.css's @layer line, independent of <link> order):

<link rel="stylesheet" href="ncss.css">
<link rel="stylesheet" href="theme.css">
<link rel="stylesheet" href="components/stat-card.css">

General enough for other projects? Pull requests welcome at github.com/skerbis/nativecss - using the ncss- prefix instead of your own project prefix in that case.