v0.1.0GitHub

Seams

Zero-runtime CSS-in-JS for React Server Components. Build-time extraction, CSS layers for cascade control, and scoped styles — with the Stitches API you already know.

$npm install @artmsilva/seams-react
Zero runtime overhead
RSC compatible
Stitches API compatible
Atomic CSS mode

Getting Started

Seams is a zero-runtime CSS-in-JS library designed for React Server Components. It provides the same beloved API as Stitches.js but extracts all CSS at build time, producing static stylesheets with zero JavaScript overhead in production.

CSS output uses @layer for deterministic cascade ordering and @scope for component isolation, ensuring your styles compose predictably without specificity wars.

Installation

React package

The main package for React applications. Includes styled(), css(), globalCss(), and all core APIs.

terminal
npm install @artmsilva/seams-react

Vite plugin

Required for build-time CSS extraction when using Vite.

terminal
npm install -D @artmsilva/seams-vite-plugin

Next.js plugin

For Next.js projects using the App Router or Pages Router.

terminal
npm install @artmsilva/seams-next-plugin

Lit / Web Components

For Lit and Web Components. Provides Shadow DOM integration via adoptedStyleSheets.

terminal
npm install @artmsilva/seams-lit

Quick Start

Create a configuration file that defines your design tokens and exports the styling utilities. This is the single source of truth for your design system.

seams.config.ts
// seams.config.ts
import { createStitches } from "@artmsilva/seams-react";

export const { styled, css, globalCss, keyframes, createTheme, theme, getCssText } =
  createStitches({
    theme: {
      colors: {
        primary: "#0070f3",
        secondary: "#7928ca",
        text: "#111111",
        bg: "#ffffff",
      },
      space: {
        1: "4px",
        2: "8px",
        3: "16px",
        4: "32px",
      },
      radii: {
        sm: "4px",
        md: "8px",
        lg: "16px",
      },
    },
    media: {
      sm: "(min-width: 640px)",
      md: "(min-width: 768px)",
      lg: "(min-width: 1024px)",
    },
  });

Import the exported utilities to create styled components with type-safe variants.

Button.tsx
// Button.tsx
import { styled } from "./seams.config";

const Button = styled("button", {
  backgroundColor: "$primary",
  color: "white",
  padding: "$2 $3",
  borderRadius: "$md",
  border: "none",
  cursor: "pointer",
  fontSize: "16px",

  "&:hover": {
    opacity: 0.9,
  },

  variants: {
    size: {
      sm: { padding: "$1 $2", fontSize: "14px" },
      lg: { padding: "$3 $4", fontSize: "18px" },
    },
  },

  defaultVariants: {
    size: "sm",
  },
});

// Usage
<Button size="lg">Click me</Button>

createStitches

The factory function that creates your styling utilities. Call it once with your design tokens, breakpoints, and custom utilities to get a fully configured set of functions.

seams.config.ts
import { createStitches } from "@artmsilva/seams-react";

const {
  styled,       // Create styled React components
  css,          // Generate class names from style objects
  globalCss,    // Inject global styles
  keyframes,    // Define CSS animations
  createTheme,  // Create theme variants
  theme,        // The default theme object
  getCssText,   // Extract all CSS as a string (SSR)
} = createStitches({
  prefix: "my-app",             // CSS class prefix
  theme: { /* tokens */ },      // Design tokens
  media: { /* breakpoints */ }, // Responsive breakpoints
  utils: { /* utilities */ },   // Custom CSS shorthand utilities
  themeMap: { /* overrides */ }, // Map CSS properties to theme scales
  atomic: false,                // Enable atomic CSS output mode
});

Config options

prefix -- String prepended to generated class names and CSS variable names. Prevents collisions when multiple Seams instances exist.

theme -- Design tokens organized by scale: colors, space, fonts, fontSizes, radii, shadows, and more. Tokens become CSS custom properties and can be referenced with the $ prefix in styles.

media -- Named breakpoints. Use as responsive variant keys or in media query conditions.

utils -- Custom CSS shorthand properties. Each utility is a function that receives a value and returns a CSS object.

themeMap -- Controls which CSS properties map to which theme scales. Override defaults when needed (e.g., map gap to your space scale).

atomic -- When true, each CSS property-value pair gets its own globally-deduplicated class. CSS scales logarithmically with component count. See the Atomic CSS section below.

styled

Creates a React component with scoped styles and type-safe variant props. Wraps any HTML element or existing component.

styled.tsx
const Button = styled("button", {
  // Base styles
  backgroundColor: "$primary",
  color: "white",
  borderRadius: "$md",
  padding: "$2 $3",
  border: "none",
  cursor: "pointer",

  // Pseudo-selectors
  "&:hover": { opacity: 0.9 },
  "&:focus-visible": { outline: "2px solid $primary" },

  // Variants
  variants: {
    size: {
      sm: { fontSize: "14px", padding: "$1 $2" },
      md: { fontSize: "16px", padding: "$2 $3" },
      lg: { fontSize: "18px", padding: "$3 $4" },
    },
    variant: {
      solid: { backgroundColor: "$primary", color: "white" },
      outline: {
        backgroundColor: "transparent",
        border: "2px solid $primary",
        color: "$primary",
      },
    },
  },

  // Compound variants: apply when multiple variants match
  compoundVariants: [
    {
      size: "lg",
      variant: "solid",
      css: { fontWeight: 700, letterSpacing: "0.5px" },
    },
  ],

  // Default variant values
  defaultVariants: {
    size: "md",
    variant: "solid",
  },
});

// Usage with type-safe variant props
<Button size="lg" variant="outline">Click me</Button>

Variants

Variants let you define discrete visual states. Each variant is a named set of additional styles that apply when the corresponding prop is set. Variants are type-safe -- TypeScript knows which values are valid.

Compound variants

Apply styles only when specific variant combinations are active. Useful for handling interactions between multiple variant dimensions.

CSS prop

Every styled component accepts a css prop for one-off style overrides. Dynamic values in the css prop are converted to CSS variables at build time.

css

A standalone function for generating class names from style objects. Same configuration as styled() (variants, compound variants) but without creating a React component -- useful for non-component styling or integrating with other libraries.

css.ts
const cardStyles = css({
  padding: "$4",
  borderRadius: "$lg",
  backgroundColor: "$bg",
  border: "1px solid $border",

  variants: {
    elevated: {
      true: { boxShadow: "$md" },
    },
  },
});

// Returns { className, props } on render
const rendered = cardStyles({ elevated: true });

<div className={rendered.className}>Card content</div>

globalCss

Defines global CSS rules using your theme tokens. Call the returned function at your application root to inject the styles.

globalStyles.ts
const applyGlobalStyles = globalCss({
  "*, *::before, *::after": {
    boxSizing: "border-box",
    margin: 0,
    padding: 0,
  },
  html: {
    scrollBehavior: "smooth",
  },
  body: {
    fontFamily: "$body",
    backgroundColor: "$bg",
    color: "$text",
    lineHeight: 1.6,
  },
  a: {
    color: "$primary",
    textDecoration: "none",
  },
});

// Call it once at your app root
applyGlobalStyles();

keyframes

Creates CSS @keyframes animations. Returns a value you can interpolate into animation properties.

animations.ts
const fadeIn = keyframes({
  from: { opacity: 0, transform: "translateY(8px)" },
  to: { opacity: 1, transform: "translateY(0)" },
});

const spin = keyframes({
  "0%": { transform: "rotate(0deg)" },
  "100%": { transform: "rotate(360deg)" },
});

// Use in a styled component
const FadeIn = styled("div", {
  animation: `${fadeIn} 0.6s ease-out both`,
});

const Spinner = styled("div", {
  animation: `${spin} 1s linear infinite`,
  width: "24px",
  height: "24px",
  border: "2px solid $border",
  borderTopColor: "$primary",
  borderRadius: "$pill",
});

createTheme

Creates a theme variant by overriding token values. The returned object has a className property you apply to a parent element to activate that theme within its subtree.

themes.ts
const darkTheme = createTheme("dark", {
  colors: {
    primary: "#3291ff",
    bg: "#111111",
    text: "#ededed",
    border: "#333333",
  },
  shadows: {
    md: "0 4px 6px rgba(0, 0, 0, 0.4)",
  },
});

// Apply the theme by adding the className to a parent element
function App() {
  const [isDark, setIsDark] = useState(false);

  return (
    <div className={isDark ? darkTheme.className : undefined}>
      <MyComponent />
    </div>
  );
}

getCssText

Returns all generated CSS as a string. Use this for server-side rendering to inline the critical CSS into your HTML document head, avoiding a flash of unstyled content.

ssr.ts
// In your SSR handler or layout component
import { getCssText } from "./seams.config";

// Use getCssText() to get all generated CSS as a string,
// then inject it into a <style> tag in your document <head>.
// This avoids a flash of unstyled content on first load.

const cssString = getCssText();
// => "@layer seams.themed { :root { --colors-primary: ... } } ..."

Composition

Pass an existing styled component as the first argument to styled() or css() to create an extended version. The new component inherits the base's element type, styles, and variant definitions.

composition.tsx
// Base component
const Box = styled("div", {
  padding: "$2",
  color: "$text",
});

// Extended component inherits Box's element type, base styles, and variants
const Card = styled(Box, {
  borderRadius: "$md",
  boxShadow: "$md",
  backgroundColor: "$bg",
});

// Multi-level: Card inherits from Box, FeaturedCard from Card
const FeaturedCard = styled(Card, {
  border: "2px solid $primary",
});

// Works with plain React components too (wraps without style inheritance)
const StyledLink = styled(MyReactRouterLink, {
  color: "$primary",
  textDecoration: "none",
});

How it works

Internally, each css() call produces a set of composers (style + variant definitions). When you compose, the base component's composers are merged into the new component's set. This means styles stack additively -- the new component gets all base styles plus its own. Multi-level chaining (A B C) works naturally.

Plain React components (functions without Seams internals) can also be passed. They become the element type but don't contribute any style inheritance, since they have no composers.

Atomic CSS

Atomic mode changes how CSS is generated. Instead of one class per component with multiple properties, each property-value pair gets its own hash-based class. Identical declarations across different components share the same class, so CSS deduplicates globally.

seams.config.ts
// Enable atomic mode in your config
const { styled, css, getCssText } = createStitches({
  atomic: true,
  theme: { /* tokens */ },
});

// API is identical -- only the CSS output changes
const Button = styled("button", {
  backgroundColor: "$primary",
  color: "white",
  padding: "$2 $3",
});

const Badge = styled("span", {
  color: "white",       // Shares atomic class with Button!
  padding: "$1 $2",
  borderRadius: "$pill",
});

Output comparison

Here's what changes under the hood. The color: white declaration appears once in atomic mode even though two components use it.

output.css
/* Standard mode: one class per component */
.c-Button { background-color: var(--colors-primary); color: white; padding: 8px 16px; }
.c-Badge  { color: white; padding: 4px 8px; border-radius: 9999px; }

/* Atomic mode: one class per declaration, shared globally */
.s-abc { background-color: var(--colors-primary) }
.s-def { color: white }                /* shared by Button AND Badge */
.s-ghi { padding: 8px 16px }
.s-jkl { padding: 4px 8px }
.s-mno { border-radius: 9999px }

Why atomic?

In standard mode, CSS grows linearly with the number of components. In atomic mode, it grows logarithmically -- new components reuse existing atomic classes instead of generating new rule blocks. For large design systems this can significantly reduce bundle size.

How specificity works

When a variant overrides a base property, both atomic classes are in the className. The variant wins because Seams' existing @layer ordering ensures variant layers (seams.onevar) always beat the base layer (seams.styled), regardless of class order in the DOM.

Selector targeting

Each component keeps an identifier class (c-Button) with no CSS rules. This preserves the ability to target components in other selectors via ${Button}. The rendered className includes both the identifier and all atomic classes.

Theming

Seams uses CSS custom properties for theming. Your design tokens are converted to CSS variables, and theme variants override those variables through class scoping -- no JavaScript runtime overhead for theme switching.

Token system

Tokens are defined by scale (colors, space, fonts, etc.) and referenced in styles with a $ prefix. Seams automatically maps CSS properties to the correct scale based on the built-in theme map.

tokens.ts
// Define tokens in your config
const { styled, createTheme, theme } = createStitches({
  theme: {
    colors: {
      primary: "#0070f3",
      text: "#111111",
      bg: "#ffffff",
    },
    space: {
      1: "4px",
      2: "8px",
      3: "16px",
    },
  },
});

// Reference tokens with $ prefix
const Box = styled("div", {
  color: "$text",            // -> var(--colors-text)
  backgroundColor: "$bg",   // -> var(--colors-bg)
  padding: "$3",             // -> var(--space-3)
});

// Cross-scale references
const Card = styled("div", {
  color: "$colors$primary",  // Explicit scale reference
});

Creating themes

Use createTheme() to define theme variants. You only need to specify the tokens that change -- everything else inherits from the default theme.

themes.ts
// The default theme is created from your config
// Create additional themes by overriding tokens

const darkTheme = createTheme("dark", {
  colors: {
    primary: "#3291ff",
    text: "#ededed",
    bg: "#111111",
  },
});

// Only override the tokens you need to change.
// Unspecified tokens inherit from the default theme.

const highContrastTheme = createTheme("high-contrast", {
  colors: {
    primary: "#0000FF",
    text: "#000000",
    bg: "#FFFFFF",
  },
});

Applying themes

Apply a theme by setting its className on a parent element. All descendant components will pick up the overridden tokens through CSS variable inheritance.

App.tsx
function App() {
  const [currentTheme, setCurrentTheme] = useState<string | undefined>();

  return (
    // Apply theme class to a parent element
    <div className={currentTheme}>
      <Header />
      <Main />

      <button onClick={() => setCurrentTheme(undefined)}>
        Default
      </button>
      <button onClick={() => setCurrentTheme(darkTheme.className)}>
        Dark
      </button>
      <button onClick={() => setCurrentTheme(highContrastTheme.className)}>
        High Contrast
      </button>
    </div>
  );
}

How it works

Under the hood, the default theme generates CSS custom properties on :root. Each additional theme generates a scoped class that overrides those variables. Since components reference the variables (not hard-coded values), theme switching is instantaneous and requires no re-renders.

generated CSS
/* Default theme generates CSS custom properties on :root */
:root {
  --colors-primary: #0070f3;
  --colors-text: #111111;
  --colors-bg: #ffffff;
}

/* createTheme generates a class that overrides variables */
.dark {
  --colors-primary: #3291ff;
  --colors-text: #ededed;
  --colors-bg: #111111;
}

/* Component styles reference the variables */
.c-Box {
  color: var(--colors-text);
  background-color: var(--colors-bg);
}

Build Plugins

Seams extracts CSS at build time using framework-specific plugins. The plugins analyze your source code, extract all Seams usage, and emit static CSS files -- leaving zero CSS-in-JS runtime in your production bundle.

Vite

The Vite plugin processes your source files during development and build, collecting CSS into a virtual module that Vite handles natively.

vite.config.ts
// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import seams from "@artmsilva/seams-vite-plugin";

export default defineConfig({
  plugins: [
    react(),
    seams({
      // Enable @scope for component isolation
      useScope: true,
      // Enable @layer for cascade ordering
      useLayers: true,
    }),
  ],
});

Next.js

The Next.js plugin wraps your configuration and adds a webpack loader for Seams processing. Works with both App Router and Pages Router.

next.config.js
// next.config.js
const { withSeams } = require("@artmsilva/seams-next-plugin");

/** @type {import('next').NextConfig} */
const nextConfig = {
  // Your Next.js config here
};

module.exports = withSeams(nextConfig);

For server-side rendering, use getCssText() to inline the critical CSS into your document head.

app/layout.tsx
// app/layout.tsx (App Router)
import { getCssText } from "../seams.config";

// Inject critical CSS into the document head for SSR.
// Use getCssText() to retrieve the generated CSS string
// and render it inside a <style> tag in your layout.

export default function RootLayout({ children }) {
  return (
    <html>
      <head>
        <style id="seams">{getCssText()}</style>
      </head>
      <body>{children}</body>
    </html>
  );
}

Plugin options

Both plugins accept the same core options for controlling CSS output.

options
seams({
  // CSS @scope for component-level isolation (default: false)
  useScope: true,

  // CSS @layer for deterministic cascade ordering (default: false)
  useLayers: true,

  // File extensions to process (default: [".tsx", ".ts", ".jsx", ".js"])
  extensions: [".tsx", ".ts"],

  // Directories to include, relative to project root
  include: ["src", "app", "components"],

  // Directories to exclude
  exclude: ["node_modules"],

  // Minify output CSS in production (auto-detected from mode)
  minify: true,

  // Custom layer prefix
  layerPrefix: "seams",
})

CSS layer order

When useLayers is enabled, Seams organizes CSS into layers with a deterministic cascade order. This eliminates specificity issues and ensures styles are applied predictably regardless of import order.

layer order
@layer seams.themed,    /* Theme CSS variables */
       seams.global,    /* globalCss() styles */
       seams.styled,    /* Base component styles */
       seams.onevar,    /* Single variant styles */
       seams.resonevar, /* Responsive variant styles */
       seams.allvar,    /* Compound variant styles */
       seams.inline;    /* css prop styles */

Lit Integration

The @artmsilva/seams-lit package brings Seams to Lit and Web Components. It uses the adoptedStyleSheets API to inject Seams-generated CSS directly into each component's Shadow DOM, keeping styles encapsulated without <style> tag duplication.

SeamsController

A Lit ReactiveController that keeps a shadow root's adopted stylesheets in sync with dynamically generated Seams CSS. It monitors the Seams sheet for new rules and updates the adopted CSSStyleSheet via replaceSync. This is the recommended approach for components that generate styles dynamically based on properties or state.

my-button.ts
// my-button.ts
import { LitElement, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import { SeamsController } from "@artmsilva/seams-lit";
import { stitches, css } from "../seams.config";

const buttonStyles = css({
  backgroundColor: "$primary",
  color: "white",
  padding: "$2 $3",
  borderRadius: "$md",
  border: "none",
  cursor: "pointer",

  variants: {
    size: {
      sm: { padding: "$1 $2", fontSize: "14px" },
      lg: { padding: "$3 $4", fontSize: "18px" },
    },
  },
});

@customElement("my-button")
export class MyButton extends LitElement {
  private seams = new SeamsController(this, stitches);

  @property() size: "sm" | "lg" = "sm";

  render() {
    const styles = buttonStyles({ size: this.size });
    return html`<button class=${styles.className}><slot></slot></button>`;
  }
}

SeamsElement base class

A convenience base class that extends LitElement and sets up a SeamsController automatically. Subclasses only need to set the static seamsInstance property. Use this when you want the simplest possible setup with no boilerplate.

my-card.ts
// my-card.ts
import { html } from "lit";
import { customElement, property } from "lit/decorators.js";
import { SeamsElement } from "@artmsilva/seams-lit";
import { stitches, css } from "../seams.config";

const cardStyles = css({
  padding: "$4",
  borderRadius: "$lg",
  backgroundColor: "$bg",
  boxShadow: "0 2px 8px rgba(0, 0, 0, 0.1)",
});

@customElement("my-card")
export class MyCard extends SeamsElement {
  static seamsInstance = stitches;

  @property() heading = "";

  render() {
    const styles = cardStyles();
    return html`
      <div class=${styles.className}>
        <h2>${this.heading}</h2>
        <slot></slot>
      </div>
    `;
  }
}

seamsStyles()

Creates a static CSSStyleSheet snapshot from all Seams CSS collected at call time. Use this with Lit's static styles property when your component's styles are fully known at definition time and do not change dynamically. The returned sheet does not update when new rules are added.

my-badge.ts
// my-badge.ts
import { LitElement, html, css as litCss } from "lit";
import { customElement } from "lit/decorators.js";
import { seamsStyles } from "@artmsilva/seams-lit";
import { stitches, css } from "../seams.config";

const badgeStyles = css({
  display: "inline-block",
  padding: "$1 $2",
  borderRadius: "$sm",
  backgroundColor: "$secondary",
  color: "white",
  fontSize: "$xs",
});

// Call css() at module level so rules are registered
const badge = badgeStyles();

@customElement("my-badge")
export class MyBadge extends LitElement {
  // seamsStyles() creates a static snapshot of all Seams CSS
  static styles = [
    seamsStyles(stitches),
    litCss`:host { display: inline-block; }`,
  ];

  render() {
    return html`<span class=${badge.className}><slot></slot></span>`;
  }
}

adoptSeams()

A one-shot utility that creates a CSSStyleSheet from current Seams CSS and appends it to a shadow root. Useful for non-Lit web components or cases where you need manual control over when styles are adopted. For automatic syncing, prefer SeamsController instead.

my-tooltip.ts
// my-tooltip.ts
import { LitElement, html } from "lit";
import { customElement } from "lit/decorators.js";
import { adoptSeams } from "@artmsilva/seams-lit";
import { stitches, css } from "../seams.config";

const tooltipStyles = css({
  padding: "$1 $2",
  backgroundColor: "$text",
  color: "$bg",
  borderRadius: "$sm",
  fontSize: "$xs",
});

@customElement("my-tooltip")
export class MyTooltip extends LitElement {
  connectedCallback() {
    super.connectedCallback();
    // One-shot adoption of current Seams CSS
    adoptSeams(this.shadowRoot!, stitches);
  }

  render() {
    const styles = tooltipStyles();
    return html`<span class=${styles.className}><slot></slot></span>`;
  }
}

Which approach to use?

Use SeamsController or SeamsElement when styles depend on reactive properties, since they automatically sync new CSS rules into the shadow root. Use seamsStyles() for static components where all style variants are known at module evaluation time. Use adoptSeams() for vanilla web components outside Lit or for one-off style injection.

React Server Components

This very documentation site is the proof. It is built with Waku and uses Seams for every single style you see. The layout, sidebar, all content sections, and code blocks are React Server Components -- they ship zero JavaScript to the browser for styling.

Only the interactive parts -- the theme toggle, copy buttons, mobile navigation, and the example demos below -- are client components. Everything else is rendered on the server with Seams' styled(), css(), and globalCss() functions, and the CSS is collected via getCssText() into a single <style> tag in the document head.

Server components on this page

These components use Seams for styling but ship no JavaScript to the browser. The CSS is extracted at render time and included in the initial HTML.

ServerLayout
ServerSidebar
ServerHeader (shell)
ServerHero
ServerGettingStarted
ServerApiReference
ServerTheming
ServerPlugins
ServerLitIntegration
ServerRscDemo (this section)
ServerSection
ServerCodeBlock

Client components on this page

These components need browser interactivity (state, event handlers, DOM APIs) and are marked with 'use client'. Seams handles their styles via runtime DOM injection -- the same API, just a different delivery mechanism.

ClientThemeToggle
ClientCopyButton
ClientMobileNav
ClientExamples

CSS output

All server-side CSS is collected into a single string via getCssText() and injected as a <style> tag in the document head. This is the total CSS footprint for the server-rendered portion of this page:

4.8 KBServer CSS output
12Server components
4Client components

Examples

These interactive demos are built with Seams -- the same library powering all the styles on this documentation site.

Button with variant toggles

Toggle size and color variants to see how Seams handles variant composition. The compound variant (large + brand) applies bold uppercase styling.

Size
Color
Result

Keyframe animation

Click replay to trigger the fade-slide animation. This uses keyframes() from Seams.

This card animates in with a fade + slide transition defined entirely in Seams.

Starter templates

Explore the example projects in the repository for complete starter setups.