Illustrative Mathematics® Learn Math for life

Contributing Guide

How to add components, documentation, and contribute to the design system

Overview

This guide covers how to contribute to our design system, whether you're adding new components, improving documentation, or enhancing existing features.

Design System Architecture

Before contributing, understand our hybrid architecture:

Architecture Overview

Adding a New Component

Precheck 1: Determine Scope

Determine if this will be specific to a single app or a candidate for reuse. Only shared components need to be implemented at the package level. All app-specific components can be created at the app level and leverage any of our styles from any CSS cascade layer.

Assuming this will be a shared component, proceed to step 2.

Precheck 2: Determine Source

Determine if a base primitive or styled component already exists that may meet the design need. We have the option to create variants for existing components.

If a new component is required, check ShadCN and Radix UI to see if a primitive component exists that would serve as a base. These primitives already have accessibility baked in and will provide a solid foundation to build on.

Primitive vs Styled components

A typical workflow is:

  • Import a primitive component from a library like ShadCN
  • Replace the included Tailwind styles with custom CSS classes using @apply
  • Create a styled version of the component based on that primitive

Primitives include functional styles. (You may need to remove some Tailwind styles provided by ShadCN to achieve this.)

Styled components add design system styles to primitives.

Note: Primitive components will often export multiple pieces for composition (as in ShadCN). Styled components often wrap those into a single component interface with defined variants and props.

Step 1: Create Component Files

Follow the directory structure closely, along with standardized naming practices. There should always be a directory above the actual component called [group-name-plural] to hold all the component variations, even if there is currently only one. An example of naming for this directory would be badges.

Make the following commands from the Turborepo root.
# Set your directory/filenaming variables
#  - copy and paste these into your terminal
#  - replace the values for the component and group name with your desired names
GROUP_NAME_PLURAL="dogs"                    # kebab-case for folders
COMPONENT_NAME_SINGULAR_PASCAL="BrownDog"   # PascalCase for TSX files
COMPONENT_NAME_SINGULAR_KEBAB="brown-dog"   # kebab-case for CSS/SCSS files

For Primitive Components:

# Create a primitive component file, index file, and pure CSS styles for the primitive component
mkdir -p ./packages/integral/src/components/primitives/$GROUP_NAME_PLURAL && 
touch ./packages/integral/src/components/primitives/$GROUP_NAME_PLURAL/$COMPONENT_NAME_SINGULAR_PASCAL.tsx && 
touch ./packages/integral/src/components/primitives/$GROUP_NAME_PLURAL/index.ts && 
mkdir -p ./packages/integral/src/styles/components/primitives/$GROUP_NAME_PLURAL && 
touch ./packages/integral/src/styles/components/primitives/$GROUP_NAME_PLURAL/$COMPONENT_NAME_SINGULAR_KEBAB.css

For Styled Components:

# Create a styled component file, index file, and SCSS styles for the styled component
mkdir -p ./packages/integral/src/components/styled/$GROUP_NAME_PLURAL && 
touch ./packages/integral/src/components/styled/$GROUP_NAME_PLURAL/$COMPONENT_NAME_SINGULAR_PASCAL.tsx && 
touch ./packages/integral/src/components/styled/$GROUP_NAME_PLURAL/index.ts && 
mkdir -p ./packages/integral/src/styles/components/styled/$GROUP_NAME_PLURAL && 
touch ./packages/integral/src/styles/components/styled/$GROUP_NAME_PLURAL/$COMPONENT_NAME_SINGULAR_KEBAB.scss

You'll also need to import the SCSS file manually in packages/integral/src/styles/components/styled/styles.scss:

@import './dogs/brown-dog.scss';

Step 2: Implement the Component

Primitive Example (direct from ShadCN):

If a primitive component includes base level styles, we need to migrate that to packages/integral/src/styles/components/primitives/[group-name-plural]/[component-name-singular]. For any Tailwind styles, we need to convert those styles into a CSS class using the @apply directive. This ensures component styles remain layered and overrideable at the styled or app level.

Here is an example of a primitive taken directly from ShadCN with base Tailwind styling applied:

  • go to Installation section of the component page
  • select Manual in tab
  • follow instructions, making sure to copy and paste the code into the primitive component file

Primitive Example (migrated to custom classes):

When adding a new primitive component, name the component using the Primitive prefix (e.g., PrimitiveBadge). This naming convention helps clarify the role of the component and distinguishes it from styled versions or app-level components.

Here is an example of converting those base Tailwind styles to custom classes defined in the styles primitives folder:

// packages/integral/src/components/primitives/badges/Badge.tsx
import * as React from 'react';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils';

const primitiveBadgeVariants = cva('primitive-badge', {
  variants: {
    variant: {
      default: 'primitive-badge-default',
      secondary: 'primitive-badge-secondary',
      destructive: 'primitive-badge-destructive',
    },
  },
  defaultVariants: {
    variant: 'default',
  },
});

export interface PrimitiveBadgeProps
  extends React.HTMLAttributes<HTMLDivElement>,
    VariantProps<typeof primitiveBadgeVariants> {}

export function PrimitiveBadge({
  className,
  variant,
  ...props
}: PrimitiveBadgeProps) {
  return (
    <div
      className={cn(primitiveBadgeVariants({ variant }), className)}
      {...props}
    />
  );
}
/* packages/integral/src/styles/components/primitives/badges/badge.css */
.primitive-badge {
  @apply inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden;
}

.primitive-badge-default {
  @apply border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90;
}

.primitive-badge-secondary {
  @apply border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90;
}

.primitive-badge-destructive {
  @apply border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60;
}

Note: Not all styles provided by ShadCN will need to be included in the primitive component. The goal is for the primitive component to work functionally, but not hold any of the visual style decisions. The Integral style decisions should reside in the styled component.

If none of the primitive Tailwind classes are preserved, add a comment to the .css file noting this was intentional. There may come a time when that is no longer the case, and keeping the file will communicate intentionality clearer.

Note: ShadCN expects certain CSS variables to be defined in the base config, so some of those may need to be overridden in the styles layer with our design tokens.

Styled Component Example:

Component naming here should match the primitive component, minus the Primitive prefix (e.g., SampleButton):

import { PrimitiveSampleButton } from '@im/integral/components/primitives';
import type { ComponentProps } from 'react';

type SampleButtonProps = ComponentProps<typeof PrimitiveSampleButton> & {
  theme?: 'brand' | 'accent';
};

const SampleButton = ({
  theme = 'brand',
  className = '',
  ...props
}: SampleButtonProps) => {
  const themeClasses = {
    brand: 'integral-sample-button',
    accent: 'integral-sample-button--accent',
  };

  return (
    <PrimitiveSampleButton
      className={`${themeClasses[theme]} ${className}`}
      {...props}
    />
  );
};

export default SampleButton;
Class Naming Convention for Styled Components

Styled component classes should follow the pattern:

.integral-[component-name]

For example:

.integral-button { ... }
.integral-button--accent { ... }

This keeps styles easily identifiable as coming from the design system and avoids collisions with app-level or utility classes.

Important! Organize the component into a subfolder and reflect that in the styles path. In this case, we are placing the SampleButton component under the buttons subfolder. Each subfolder should contain one SCSS file per component (e.g., sample-button.scss, link-button.scss), and these files should be imported into the main styles.scss file manually.

// packages/integral/src/styles/components/styled/buttons/sample-button.scss

.integral-sample-button {
  background: linear-gradient(
    135deg,
    $color-brand-primary-900 0%,
    darken($color-brand-primary-500, 10%) 100%
  );
  color: $color-text-ondark-default;
  border: none;

  &:hover {
    background: linear-gradient(
      135deg,
      darken($color-brand-primary-500, 5%) 0%,
      darken($color-brand-primary-500, 15%) 100%
    );
    transform: translateY(-1px);
    box-shadow: 0 4px 12px rgba($color-brand-primary-500, 0.3);
  }
}

.integral-sample-button--accent {
  background: linear-gradient(
    135deg,
    $color-text-interactive-ondark-default 0%,
    darken($color-text-interactive-ondark-default, 10%) 100%
  );
  color: $color-text-ondark-default;
  border: none;

  &:hover {
    background: linear-gradient(
      135deg,
      darken($color-text-interactive-ondark-default, 5%) 0%,
      darken($color-text-interactive-ondark-default, 15%) 100%
    );
    transform: translateY(-1px);
    box-shadow: 0 4px 12px rgba($color-text-interactive-ondark-default, 0.3);
  }
}

Step 3: Export the Component

Update the appropriate index file:

// For primitives
// packages/integral/src/components/primitives/badges/index.ts
export { PrimitiveBadge } from './Badge';

// packages/integral/src/components/primitives/index.ts
export * from './badges';

// For styled components
// packages/integral/src/components/styled/buttons/index.ts
export { default as SampleButton } from './SampleButton';
export { default as LinkButton } from './LinkButton';

// packages/integral/src/components/styled/index.ts
export * from './accordions';

Each subfolder should include an index.ts file to re-export its components for easier import at the package level.

Step 4: Build and Test

# Build the design system
turbo @im/integral#build

# Run dev mode to test in apps (after importing and using it)
turbo dev

Note: We may introduce a linting step above, but we need to establish biome.js first

Adding Documentation

Step 1: Create MDX File

mkdir -p ./apps/design-system-docs/content/docs/components/$GROUP_NAME_PLURAL && 
touch ./apps/design-system-docs/content/docs/components/$GROUP_NAME_PLURAL/$COMPONENT_NAME_SINGULAR_KEBAB.mdx && 
touch ./apps/design-system-docs/content/docs/components/$GROUP_NAME_PLURAL/meta.json

Step 2: Write Documentation

Use this template:

---
title: Component Name
description: Brief description of the component's purpose
---

import { StyledComponentName } from '@im/integral/components/styled';
import { PrimitivesComponentName } from '@im/integral/components/primitives';

## Overview

Explain what the component does and when to use it.

## Examples

### Basic Usage

<ComponentPreview>
  <ComponentPreviewRender>
    content
  </ComponentPreviewRender>
  <ComponentPreviewCode>
```tsx
import { ComponentName } from '@im/integral/components/styled';

export function ComponentNameDemo() {
  return (content)
}
```
  </ComponentPreviewCode>
</ComponentPreview>

### Variants

<ComponentPreview>
  <ComponentPreviewRender>
    <div className="space-y-4">
      <StyledComponentName variant="default">Default</StyledComponentName>
      <StyledComponentName variant="secondary">Secondary</StyledComponentName>
    </div>
  </ComponentPreviewRender>
  <ComponentPreviewCode>
```tsx
<StyledComponentName variant="default">Default</StyledComponentName>
<StyledComponentName variant="secondary">Secondary</StyledComponentName>
```
  </ComponentPreviewCode>
</ComponentPreview>

## Props

Note: Use the code below to add a props table with types.

<Wrapper>
  <TypeTable
    type={{
      variable: {
        description: 'theme',
        type: 'brand | accent',
        required: true,
      },
    }}
  />
</Wrapper>

## Import

```tsx
import { StyledComponentName } from '@im/integral/components/styled';
```

## Design Details (Optional)

Explain any important design or implementation decisions.

Step 3: Update Navigation

Add to apps/design-system-docs/content/docs/components/[group-name-plural]/meta.json:

{
  "title": "Styled Components",
  "pages": [
    "button",
    "gradient-text" // Add new component alphabetically
  ]
}

Add to apps/design-system-docs/content/docs/components/[group-name-plural]/[component-name-singular]/meta.json:

{
  "title": "Component Name Plural",
  "pages": ["component-name-singular"]
}

Development Workflow

Testing Changes

  1. Make changes in the design-system package
  2. Changes hot-reload in apps automatically
  3. Test in any apps that may use this
  4. Verify CSS cascade behavior

Best Practices

Component Guidelines

Do's

  • Use design tokens
  • Follow existing naming conventions
  • Export the component prop types as well as the component
  • Create documentation with examples

Don'ts

  • Don't use SCSS in primitive component styles.
  • Don't leave Tailwind in primitives (move to custom class with @apply)
  • Don't inline or !important styles in packages

CSS/SCSS Guidelines

Primitives (CSS only):

/* ✅ Good */
.primitive-sample-button-primary {
  @apply bg-gray-900 text-white hover:bg-gray-800;
  @apply focus:outline-none focus:ring-2 focus:ring-gray-900;
}

/* ❌ Bad - No SCSS features in primitives */
.primitive-sample-button-primary {
  $color: #000;
  background: $color;

  &:hover {
    background: lighten($color, 10%);
  }
}

Styled Components (SCSS):

/* ✅ Good - Leverage SCSS features */
.card-brand {
  $base-padding: var(--spacing-4);

  padding: $base-padding;

  @include breakpoint(md) {
    padding: calc($base-padding * 1.5);
  }

  &--featured {
    border: 2px solid var(--color-primary);
  }
}

Importing Styled SCSS Files

All styled component SCSS files should be imported individually into the central entrypoint at packages/integral/src/styles/components/styled/styled.scss:

@import '../../../../../integral-tokens/dist/css/tokens.css';
@import '../../../../../integral-tokens/dist/scss/_tokens.scss';

@import './buttons/link-button.scss';
@import './buttons/sample-button.scss';
@import './skeletons/skeleton.scss';
@import './text/gradient-text.scss';

Note: Maintain alphabetical order by folder and then filename for all @import statements. This helps keep diffs clean and avoids merge conflicts.

Documentation Standards

  • Always include live examples with <ComponentPreview> or <Wrapper>
  • Type props with TypeScript and export
  • Show multiple variants and states
  • Include import instructions
  • Explain when to use the component

Submitting Changes

Open a Pull Request using the Design System Pull Request template.

Changes to package components and styles should be handled with care. More details on this process is incoming.