Skip to Main content

Building a New UI Component

Use this page when you need to add or extend a UI block in PreciseAlloy.Frontend and want the repo-specific path from component placement to local mocks and integration output.

This guide applies to the PreciseAlloy.Frontend package inside the repository. It is written to be useful on its own, so you can follow it without jumping between multiple documents.

What lives where

The frontend follows Atomic Design for component structure, then uses a few supporting folders for data, types, runtime helpers, and build glue.

PreciseAlloy.Frontend/
├── src/
│   ├── atoms/          # Small reusable UI primitives
│   ├── molecules/      # Small compositions of atoms
│   ├── organisms/      # Page-level sections and blocks
│   ├── templates/      # Page compositions built from organisms
│   ├── pages/          # Route entry points that render templates
│   ├── _types/         # Shared TypeScript models for atomic layers
│   ├── _data/          # Example or seeded content models for local rendering
│   ├── _api/           # Frontend API wrappers
│   ├── mocks/          # MSW mock handlers used in local development
│   ├── _helpers/       # Shared rendering helpers such as RequireCss
│   └── assets/
│       ├── styles/     # Shared SCSS layers, tokens, mixins, base styles
│       └── scripts/    # Shared runtime TypeScript and entry scripts
├── public/assets/css/  # CSS generated by the custom SCSS pipeline
└── xpack/              # Build, prerender, dev server, and integration scripts

The most important rule is to keep code at the lowest sensible layer:

  • Put a simple, reusable visual primitive in atoms.
  • Put a small composition of atoms in molecules.
  • Put a complete content block or section in organisms.
  • Put page layout composition in templates.
  • Put route entry wiring in pages.

The supporting folders are just as important:

  • src/_types is the canonical place for shared component model types.
  • src/_data is where local sample data often lives when a page or block needs seeded content.
  • src/_api is where frontend code should wrap calls to backend endpoints.
  • src/mocks is where local API behavior is mocked with MSW.
  • src/assets is where shared SCSS and shared runtime TypeScript should be added.

Atomic Design in this repository

The repository already uses a clean Atomic Design structure:

LayerFolderResponsibilityTypical output
Atomssrc/atomsButtons, links, icons, pictures, form controlsBaked into style-base.css
Moleculessrc/moleculesSmall combinations of atoms, such as section headers or price itemsBaked into style-base.css
Organismssrc/organismsFull blocks or sections such as hero, footer, partner, people, contactDedicated b-*.css files
Templatessrc/templatesPage layout compositions built from organismsDedicated p-*.css files if template SCSS exists
Pagessrc/pagesRoute-level entry points that render templates with dataNo direct CSS output pattern

Use the layer that matches the responsibility of the work, not the visual size of the markup.

When to create an atom

Create or extend an atom when the UI element is a low-level building block that could be reused in multiple places without bringing page-level meaning with it.

Examples:

  • A button variant
  • A text input or text area
  • An icon wrapper
  • A picture abstraction
  • A link style

When to create a molecule

Create or extend a molecule when the UI is still small, but its meaning comes from combining two or more atoms into a reusable pattern.

Examples:

  • A section header that groups heading and eyebrow text
  • A price item that combines icon, amount, and supporting copy
  • A list item or grouped link pattern

When to create an organism

Create an organism when the work is a real content block or section that a template can place on a page. In this repository, this is the most common layer for new feature work because it gives the block its own dedicated stylesheet and keeps template files focused on page composition.

Examples from the repo include:

  • Hero
  • Header
  • Footer
  • Partner
  • People
  • Prices
  • Contact
  • Portfolio
  • Teaser

If you are building “a new block”, the correct answer is usually “create a new organism unless the feature is obviously smaller”.

When to create a template

Create or extend a template when the work is about page composition rather than about one standalone block. Templates orchestrate organisms and define which blocks appear together for a given page type.

Templates can also have their own SCSS file, but in the current repository they are mostly composition-only. The build system supports template CSS output even though the existing templates do not currently use it.

When to create a page

Create or extend a page when you need a route entry point. The page typically imports a template, then passes page-specific data into it. Pages are intentionally thin.

Where SCSS and TypeScript should be created

There are two different kinds of frontend code in this repository, and they belong in different places.

Component-local code

If the code belongs to one specific atom, molecule, organism, or template, keep it beside the component:

  • src/atoms/.../*.tsx
  • src/atoms/.../*.scss
  • src/molecules/.../*.tsx
  • src/molecules/.../*.scss
  • src/organisms/.../*.tsx
  • src/organisms/.../*.scss
  • src/templates/.../*.tsx
  • src/templates/.../*.scss

This is the default choice for new component work.

Shared SCSS and shared runtime TypeScript

If the code is shared, global, or supports multiple components, put it in src/assets.

Use src/assets/styles for:

  • Design tokens
  • Variables
  • Mixins
  • Base styles
  • Shared utility styles
  • Global theme or typography rules

Use src/assets/scripts for:

  • Shared browser runtime helpers
  • Entry scripts discovered by the build system through *.entry.ts
  • Site-wide JavaScript that is not naturally owned by a single React component

That distinction matters:

  • Component-specific React and component-specific SCSS belong with the component.
  • Shared SCSS and shared runtime TypeScript belong in src/assets.

Type models and sample data

Before writing a new component, decide what its model looks like.

In this repository, shared component model types usually live in src/_types. For example, organism models are defined in src/_types/organisms.d.ts, while the actual sample content often lives in matching files inside src/_data.

A common workflow is:

  1. Add or extend the relevant type in src/_types.
  2. Add seeded example data in src/_data so the page or template can render locally.
  3. Pass that data through the page and template into the organism or molecule.

This keeps component contracts explicit and makes local development easier because the component has something real to render before the backend is wired in.

How the style pipeline really works

The style system in this repository is not just “Vite compiles SCSS”. The main pipeline is a custom script in xpack/styles.ts, and that script decides how SCSS turns into the CSS files used by the site.

That matters because when you add a new SCSS file, the output behavior depends on where the file lives.

The style build entry points

The important style entry points are:

  • src/assets/styles/style-base.scss
  • xpack/styles/root.scss
  • xpack/styles/pl-states.scss
  • Every non-partial SCSS file under src/organisms
  • Every non-partial SCSS file under src/templates

The generated CSS is written to public/assets/css.

How style-base.css is generated

style-base.css is generated from src/assets/styles/style-base.scss by xpack/styles.ts.

That SCSS file pulls in the shared base stack under src/assets/styles, including the reset, font declarations, typography, base layout rules, Optimizely fixes, and theme variables.

The important part is that the output is not limited to base styles only.

When the script compiles style-base.scss, it also finds every non-partial SCSS file in:

  • src/atoms/**/*.scss
  • src/molecules/**/*.scss

It compiles those files and appends their CSS into the same bundle.

That means style-base.css is effectively:

  1. The shared base stylesheet.
  2. All atom styles.
  3. All molecule styles.

This is why atoms and molecules do not need separate CSS delivery at runtime.

How atom and molecule SCSS is baked into style-base.css

You do not manually import atom and molecule SCSS into style-base.scss one by one.

Instead, xpack/styles.ts scans the folders programmatically and appends the compiled CSS output for each non-partial SCSS file. In practice this means:

  • Add or edit SCSS under src/atoms or src/molecules.
  • Run bun dev or bun run styles.
  • The pipeline recompiles style-base.css.

This is one of the biggest differences between this repository and a more conventional Vite-only setup.

How organism SCSS becomes dedicated CSS files

Every non-partial SCSS file under src/organisms is compiled into its own dedicated CSS file in public/assets/css with a b- prefix.

Examples:

  • src/organisms/hero/index.scss becomes public/assets/css/b-hero.css
  • src/organisms/footer/index.scss becomes public/assets/css/b-footer.css
  • src/organisms/contact/contact-form.scss becomes public/assets/css/b-contact-form.css

Two details are important here:

  1. Files starting with _ are partials and do not emit standalone CSS.
  2. The common convention is to keep the main stylesheet as index.scss, but any non-partial SCSS file in the organism folder will emit a dedicated CSS file.

How template SCSS becomes dedicated CSS files

The same pipeline also supports template-level SCSS.

Every non-partial SCSS file under src/templates is compiled into its own CSS file in public/assets/css with a p- prefix.

Examples of the output naming rule:

  • src/templates/home/index.scss would become public/assets/css/p-home.css
  • src/templates/service/index.scss would become public/assets/css/p-service.css

At the time of writing, the current repository does not contain template SCSS files. The important point is that the pipeline already supports them, so you can add one when a template genuinely needs layout-specific styling.

Automatic SCSS prelude injection

Component SCSS does not need repetitive @use statements for the shared abstracts and mixins. The build script automatically injects access to:

  • src/assets/styles/00-abstracts
  • src/assets/styles/01-mixins

That means component styles can directly use the repo variables and mixins without importing them in every file.

In practical terms:

  • Use component-local SCSS files for component styles.
  • Use the shared tokens and mixins as if they were already in scope.
  • Keep partials prefixed with _ when they are support files rather than emitted stylesheets.

Runtime CSS loading with RequireCss

Organisms are expected to render their own dedicated CSS asset through RequireCss.

The common pattern looks like this:

<RequireCss path={'b-hero'} />

That tells the frontend to include the generated stylesheet from public/assets/css/b-hero.css.

For a new organism, remember both halves:

  1. Add the SCSS file in the organism folder.
  2. Render the matching RequireCss call from the organism component.

If you add template-level SCSS in the future, follow the same principle with the p- output name.

Local development with bun dev

The standard local development command for frontend work in this repository is:

bun dev

That command is defined in package.json, and it runs four processes in parallel:

  1. bun xpack/styles.ts --watch
  2. bun xpack/states.ts --watch
  3. cross-env scriptOnly=true vite build --mode development --watch
  4. bun xpack/server.ts --mode development

What each process does

1. Style watcher

The SCSS watcher recompiles:

  • style-base.css
  • Organism CSS files
  • Template CSS files, if any exist
  • The xpack support styles

This is what picks up your SCSS changes while you work.

2. States watcher

The states watcher regenerates frontend state artifacts used by the repo. Even if you are not touching those files directly, it is part of the normal local dev command and should be left running.

3. Vite build watch

This process rebuilds the frontend script output continuously in development mode. The repo does not use the default Vite dev-server-only pattern for the whole app. Instead, it watches and rebuilds assets while the custom server hosts the site.

4. Custom local server

The frontend uses xpack/server.ts and xpack/create-server.ts to start the local server.

That server:

  • Serves assets from public/assets/*
  • Serves built assets from the Vite output
  • Uses Vite middleware in development
  • Handles rendering through the repo's custom server setup

The port comes from VITE_PORT if set, otherwise it defaults to 5000.

Practical local workflow

A normal frontend loop looks like this:

  1. Start bun dev.
  2. Open the local site.
  3. Change React, SCSS, sample data, or mocks.
  4. Refresh and verify the block.
  5. Repeat until the structure, styles, and mocked behavior are stable.

If the component depends on an API response, keep the mock in place while building the UI. That lets the frontend move independently before the real backend integration is ready.

How to mock an API in this repository

API mocking is handled with MSW, not with ad hoc fetch stubs scattered through components.

The moving parts are:

  • src/mocks/browser.ts starts the MSW worker in the browser.
  • src/mocks/consts.ts holds the shared handlers array.
  • src/mocks/handlers.ts imports each feature mock folder so the handlers register themselves.
  • src/mocks/<feature>/index.ts pushes feature-specific handlers into that array.
  • xpack/scripts/mock-api.entry.ts starts the worker.
  • xpack/hooks/options.ts ensures the mock-api entry is excluded from production builds.

The mock flow

The local mock flow works like this:

  1. A frontend component calls an API wrapper in src/_api.
  2. The wrapper uses window.appApi, which is implemented in src/assets/scripts/main/api.ts.
  3. In local development, the MSW service worker intercepts the matching request.
  4. The mock handler returns a fake response.
  5. The UI behaves as if it had talked to a live backend.

When to add a mock

Add a mock when:

  • A new block depends on an API that is not ready yet.
  • You need stable frontend development data.
  • You want predictable success and failure cases.
  • You need to demonstrate the block independently of backend availability.

Step-by-step API mock pattern

For a new endpoint, the standard repo pattern is:

  1. Add an API wrapper in src/_api.
  2. Add a mock folder in src/mocks/<feature>.
  3. Push a handler from that folder into the shared handlers array.
  4. Import that mock folder from src/mocks/handlers.ts.
  5. Add the endpoint URL to the relevant environment file.
  6. Keep component code calling the wrapper, not the raw fetch implementation.

Example pattern

The existing contact form shows the complete pattern:

  • src/organisms/contact/contact-form.tsx calls submitContactForm.
  • src/_api/contact-form.ts wraps the endpoint.
  • src/assets/scripts/main/api.ts provides window.appApi.postAsync.
  • src/mocks/contact-form/index.ts intercepts /api/contact-form.
  • src/_data/contact.ts provides example success and failure payloads.

That is the model to copy for new frontend API work.

Build outputs and backend handoff

Frontend work in this repository is not finished when the page looks right locally. The final output has to be built in a form the backend team can consume.

Important commands

Use these commands for the corresponding stages:

CommandPurpose
bun run stylesCompile the custom SCSS outputs into public/assets/css
bun run buildBuild the static frontend output and the server bundle
bun run generateBuild, prerender pages, and add hashes to the prerender flow
bun inteBuild the frontend handoff package for backend integration

What bun run build produces

bun run build runs:

  1. bun run build:static
  2. bun run build:server

The main build output is:

  • dist/static for frontend assets and pages intended for delivery
  • dist/server for the server-side rendering bundle

What bun inte does

bun inte is the key handoff command for backend integration.

It runs the style build, the frontend build, the prerender step, and the integration script. The integration script in xpack/integration.ts then copies frontend output into the configured backend integration paths.

The important handoff behavior is:

  • CSS, fonts, images, JavaScript, and vendor assets are copied to the backend asset directory defined by VITE_INTE_ASSET_DIR.
  • Pattern or page outputs are copied to the pattern directory defined by VITE_INTE_PATTERN_DIR when that path is configured.
  • A hashes.json file is generated for asset hashing support.

In other words, the backend team does not consume your raw source files. They consume the generated frontend assets and pattern output created by the integration flow.

What the backend team typically needs

For backend integration, the useful outputs are:

  • The built CSS files
  • The built JavaScript files
  • Images, fonts, and vendor assets
  • The hash manifest
  • The generated page or pattern snapshots when they are part of the workflow

If you are handing work to the backend team, make sure the block has been built through the integration flow rather than just tested through bun dev.

Sample workflow: create a new block end to end

The most common new feature in this repo is a new organism-level block. That is the best sample because it covers structure, SCSS, optional JavaScript behavior, API integration, and backend handoff.

This example creates a fictional organism called consultation, built from existing atoms and molecules.

Goal of the example

The block will:

  • Render a section header
  • Render name and email fields using existing atoms
  • Render a CTA button using the existing button atom
  • Submit to a mocked API while the backend is not ready
  • Carry its own SCSS file and load a dedicated CSS asset

Step 1: Decide the correct layer

This feature is a full content block that can appear on a page template, so it belongs in src/organisms/consultation.

If the work were only a new button variant, it would belong in atoms instead. If it were only a small header-plus-copy pattern, it might belong in molecules. This one is a real block, so organism is the correct layer.

Step 2: Add or extend the model types

Add a new model to src/_types/organisms.d.ts.

Example:

type ConsultationModel = BaseAtomicModel & {
  header?: SectionHeaderModel;
  name?: TextInputModel;
  email?: TextInputModel;
  submitButton: ButtonModel;
  successMessage?: string;
};

This keeps the component contract explicit and consistent with the rest of the repo.

Step 3: Add local sample data

Create src/_data/consultation.ts with local sample content.

Example:

const consultation: ConsultationModel = {
  globalModifier: ['section-margin-top'],
  header: { heading: 'Book a consultation' },
  name: {
    label: 'Name',
    name: 'name',
    id: 'consultation-name',
    required: true,
    requiredMessage: 'Name is required.',
  },
  email: {
    label: 'Email',
    name: 'email',
    id: 'consultation-email',
    required: true,
    requiredMessage: 'Email is required.',
    type: 'email',
  },
  submitButton: {
    text: 'Request a callback',
  },
};

export { consultation };

Now the block can be rendered locally before the backend exists.

Step 4: Build the organism from existing atomic components

Create src/organisms/consultation/index.tsx.

Reuse the existing building blocks instead of recreating them:

  • @molecules/section-header
  • @atoms/forms/text-input
  • @atoms/buttons
  • @helpers/RequireCss

Example shape:

import Button from '@atoms/buttons';
import TextInput, { FormValuesModel } from '@atoms/forms/text-input';
import RequireCss from '@helpers/RequireCss';
import SectionHeader from '@molecules/section-header';
import { FormProvider, useForm } from 'react-hook-form';
import { submitConsultation } from '@_api/consultation';

const Consultation = (model: ConsultationModel) => {
  const methods = useForm<FormValuesModel>();
  const { handleSubmit } = methods;

  const processSubmit = async (data: FormValuesModel) => {
    await submitConsultation({
      name: model.name?.name ? data[model.name.name] : undefined,
      email: model.email?.name ? data[model.email.name] : undefined,
    });
  };

  return (
    <section className="zzz-o-consultation">
      <div className="zzz-container">
        {model.header && <SectionHeader {...model.header} />}

        <FormProvider {...methods}>
          <form onSubmit={handleSubmit(processSubmit)}>
            {model.name && <TextInput {...model.name} />}
            {model.email && <TextInput {...model.email} />}
            <Button type="submit" {...model.submitButton} />
          </form>
        </FormProvider>
      </div>

      <RequireCss path={'b-consultation'} />
    </section>
  );
};

export default Consultation;

There are three important conventions here:

  1. Reuse atoms and molecules rather than rebuilding them.
  2. Use the zzz-o- class prefix for organism markup.
  3. Load the dedicated organism stylesheet through RequireCss.

Step 5: Add organism SCSS

Create src/organisms/consultation/index.scss.

You do not need to import the shared abstracts or mixins manually because the style build injects them.

Example:

.zzz-o-consultation {
  padding: px2rem(48px) 0;
  background: var(--color-surface, $white);

  &__form {
    display: grid;
    gap: px2rem(16px);
    max-width: px2rem(520px);
  }

  @include desktop {
    padding: px2rem(72px) 0;
  }
}

When the styles compile, this file will emit public/assets/css/b-consultation.css.

Step 6: Add shared runtime TypeScript only if it is really shared

Most block behavior should stay inside the React component. Only add files in src/assets/scripts when the logic is shared, entry-based, or needs to run outside the normal React component boundary.

Use src/assets/scripts for cases like:

  • A shared browser helper reused across multiple blocks
  • A custom entry script discovered via *.entry.ts
  • Site-level behavior that should not live inside one organism component

For this sample block, you may not need anything in src/assets/scripts at all. That is normal.

Step 7: Add the API wrapper

Create src/_api/consultation.ts.

Follow the existing wrapper pattern so the component does not own raw fetch logic.

Example:

interface ConsultationRequestModel {
  name?: string;
  email?: string;
}

interface ConsultationResponseModel {
  success?: boolean;
  message?: string;
}

const consultationApiUrl = import.meta.env.VITE_APP_API_CONSULTATION_URL;

export const submitConsultation = (body: ConsultationRequestModel) => {
  return window.appApi.postAsync<ConsultationResponseModel>(consultationApiUrl, undefined, body);
};

This keeps endpoint details in one place and aligns with the rest of the repository.

Step 8: Add the mock handler

Create src/mocks/consultation/index.ts.

Example:

import { handlers } from '@mocks/handlers';
import { HttpResponse, http } from 'msw';

handlers.push(
  http.post('/api/consultation', async ({ request }) => {
    const body = (await request.json()) as { email?: string };

    if (body.email && body.email.includes('failed')) {
      return HttpResponse.json({
        success: false,
        message: 'Consultation request failed.',
      });
    }

    return HttpResponse.json({
      success: true,
      message: 'Consultation request received.',
    });
  })
);

Then import that folder from src/mocks/handlers.ts:

import './consultation';

Now local development can exercise both success and failure cases without waiting for the backend.

Step 9: Add the environment variable

Add the endpoint URL to the appropriate environment file, for example:

VITE_APP_API_CONSULTATION_URL=/api/consultation

Keep the frontend wrapper and the MSW mock aligned on the same path.

Step 10: Wire the block into a template and page

Once the organism exists, add it to the correct template and pass data from the page layer.

Typical flow:

  1. Import the new organism into a template such as src/templates/home/index.tsx.
  2. Pass the model into that template from a page such as src/pages/home.tsx.
  3. Pull the sample data from src/_data/consultation.ts until the real data source is integrated.

That keeps the same page flow the rest of the repo already uses.

Step 11: Run local development

Run:

bun dev

Then verify:

  1. The organism renders in the correct template.
  2. The stylesheet is generated and loaded.
  3. The form submits through the wrapper.
  4. The MSW mock intercepts the call.
  5. Success and failure cases both behave correctly.

Step 12: Prepare backend handoff

When the block is ready for backend integration, run:

bun inte

That generates the frontend output in the format the backend team expects. At that point the handoff is no longer “here are my source files”; it is “here are the built assets and generated outputs ready for integration”.

Use this checklist when building or reviewing a new component:

  1. The component is in the correct Atomic Design layer.
  2. The TypeScript model is declared or updated in src/_types.
  3. Local development data exists in src/_data when useful.
  4. Component-local SCSS sits beside the component.
  5. Shared SCSS and shared runtime TypeScript, if any, live in src/assets.
  6. Organisms render their own dedicated CSS through RequireCss.
  7. API calls go through src/_api, not raw ad hoc fetch logic inside unrelated files.
  8. Mock handlers exist in src/mocks for local development when backend APIs are not ready.
  9. The component is verified through bun dev.
  10. The frontend handoff is prepared through the correct build flow for backend integration.

Common mistakes to avoid

  • Do not put a full page block into atoms or molecules just because the markup is short.
  • Do not add shared SCSS tokens directly into a component folder when they really belong in src/assets/styles.
  • Do not bypass the API wrapper layer with raw endpoint calls scattered across components.
  • Do not forget to register a new mock folder in src/mocks/handlers.ts.
  • Do not assume templates cannot have their own styles; they can, but use that only when the styling is genuinely template-level.
  • Do not assume Vite alone is responsible for CSS generation; the real style pipeline is in xpack/styles.ts.
  • Do not hand backend developers unbuilt frontend source files when the integration flow expects generated assets.

Final guidance

If you are unsure where a new feature belongs, start by asking one question: is this a reusable primitive, a small composition, a block, a page layout, or a route entry?

That one decision usually determines the correct folder, styling pattern, and integration approach.

In day-to-day work, most new UI feature work in this repository should follow this path:

  1. Create or extend an organism.
  2. Reuse existing atoms and molecules.
  3. Add component-local SCSS beside the organism.
  4. Add shared assets only when they are genuinely shared.
  5. Mock APIs locally through MSW.
  6. Develop through bun dev.
  7. Deliver the finished frontend output through bun inte.

If you follow that shape, your component will fit the way this repository already builds, styles, tests, and hands work to the backend team.