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.
If you only need the short version, the normal workflow is:
- Decide which Atomic Design layer your new UI belongs to.
- Create the component in
src/atoms,src/molecules,src/organisms,src/templates, orsrc/pages. - Add or reuse the matching model type in
src/_types. - Keep component-local SCSS beside the component, and keep shared SCSS or shared runtime TypeScript in
src/assets. - If the component calls an API, add a wrapper in
src/_apiand a mock insrc/mocks. - Run
bun devand verify the component locally. - When the frontend is ready for backend consumption, build and hand off the output with
bun inte.
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 scriptsThe 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/_typesis the canonical place for shared component model types.src/_datais where local sample data often lives when a page or block needs seeded content.src/_apiis where frontend code should wrap calls to backend endpoints.src/mocksis where local API behavior is mocked with MSW.src/assetsis where shared SCSS and shared runtime TypeScript should be added.
Atomic Design in this repository
The repository already uses a clean Atomic Design structure:
| Layer | Folder | Responsibility | Typical output |
|---|---|---|---|
| Atoms | src/atoms | Buttons, links, icons, pictures, form controls | Baked into style-base.css |
| Molecules | src/molecules | Small combinations of atoms, such as section headers or price items | Baked into style-base.css |
| Organisms | src/organisms | Full blocks or sections such as hero, footer, partner, people, contact | Dedicated b-*.css files |
| Templates | src/templates | Page layout compositions built from organisms | Dedicated p-*.css files if template SCSS exists |
| Pages | src/pages | Route-level entry points that render templates with data | No 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/.../*.tsxsrc/atoms/.../*.scsssrc/molecules/.../*.tsxsrc/molecules/.../*.scsssrc/organisms/.../*.tsxsrc/organisms/.../*.scsssrc/templates/.../*.tsxsrc/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:
- Add or extend the relevant type in
src/_types. - Add seeded example data in
src/_dataso the page or template can render locally. - 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.scssxpack/styles/root.scssxpack/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/**/*.scsssrc/molecules/**/*.scss
It compiles those files and appends their CSS into the same bundle.
That means style-base.css is effectively:
- The shared base stylesheet.
- All atom styles.
- 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/atomsorsrc/molecules. - Run
bun devorbun 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.scssbecomespublic/assets/css/b-hero.csssrc/organisms/footer/index.scssbecomespublic/assets/css/b-footer.csssrc/organisms/contact/contact-form.scssbecomespublic/assets/css/b-contact-form.css
Two details are important here:
- Files starting with
_are partials and do not emit standalone CSS. - 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.scsswould becomepublic/assets/css/p-home.csssrc/templates/service/index.scsswould becomepublic/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-abstractssrc/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:
- Add the SCSS file in the organism folder.
- Render the matching
RequireCsscall 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 devThat command is defined in package.json, and it runs four processes in parallel:
bun xpack/styles.ts --watchbun xpack/states.ts --watchcross-env scriptOnly=true vite build --mode development --watchbun 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:
- Start
bun dev. - Open the local site.
- Change React, SCSS, sample data, or mocks.
- Refresh and verify the block.
- 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.tsstarts the MSW worker in the browser.src/mocks/consts.tsholds the sharedhandlersarray.src/mocks/handlers.tsimports each feature mock folder so the handlers register themselves.src/mocks/<feature>/index.tspushes feature-specific handlers into that array.xpack/scripts/mock-api.entry.tsstarts the worker.xpack/hooks/options.tsensures themock-apientry is excluded from production builds.
The mock flow
The local mock flow works like this:
- A frontend component calls an API wrapper in
src/_api. - The wrapper uses
window.appApi, which is implemented insrc/assets/scripts/main/api.ts. - In local development, the MSW service worker intercepts the matching request.
- The mock handler returns a fake response.
- 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:
- Add an API wrapper in
src/_api. - Add a mock folder in
src/mocks/<feature>. - Push a handler from that folder into the shared handlers array.
- Import that mock folder from
src/mocks/handlers.ts. - Add the endpoint URL to the relevant environment file.
- Keep component code calling the wrapper, not the raw
fetchimplementation.
Example pattern
The existing contact form shows the complete pattern:
src/organisms/contact/contact-form.tsxcallssubmitContactForm.src/_api/contact-form.tswraps the endpoint.src/assets/scripts/main/api.tsprovideswindow.appApi.postAsync.src/mocks/contact-form/index.tsintercepts/api/contact-form.src/_data/contact.tsprovides 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:
| Command | Purpose |
|---|---|
bun run styles | Compile the custom SCSS outputs into public/assets/css |
bun run build | Build the static frontend output and the server bundle |
bun run generate | Build, prerender pages, and add hashes to the prerender flow |
bun inte | Build the frontend handoff package for backend integration |
What bun run build produces
bun run build runs:
bun run build:staticbun run build:server
The main build output is:
dist/staticfor frontend assets and pages intended for deliverydist/serverfor 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_DIRwhen that path is configured. - A
hashes.jsonfile 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:
- Reuse atoms and molecules rather than rebuilding them.
- Use the
zzz-o-class prefix for organism markup. - 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/consultationKeep 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:
- Import the new organism into a template such as
src/templates/home/index.tsx. - Pass the model into that template from a page such as
src/pages/home.tsx. - Pull the sample data from
src/_data/consultation.tsuntil 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 devThen verify:
- The organism renders in the correct template.
- The stylesheet is generated and loaded.
- The form submits through the wrapper.
- The MSW mock intercepts the call.
- Success and failure cases both behave correctly.
Step 12: Prepare backend handoff
When the block is ready for backend integration, run:
bun inteThat 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”.
Recommended authoring checklist
Use this checklist when building or reviewing a new component:
- The component is in the correct Atomic Design layer.
- The TypeScript model is declared or updated in
src/_types. - Local development data exists in
src/_datawhen useful. - Component-local SCSS sits beside the component.
- Shared SCSS and shared runtime TypeScript, if any, live in
src/assets. - Organisms render their own dedicated CSS through
RequireCss. - API calls go through
src/_api, not raw ad hoc fetch logic inside unrelated files. - Mock handlers exist in
src/mocksfor local development when backend APIs are not ready. - The component is verified through
bun dev. - 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
atomsormoleculesjust 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:
- Create or extend an organism.
- Reuse existing atoms and molecules.
- Add component-local SCSS beside the organism.
- Add shared assets only when they are genuinely shared.
- Mock APIs locally through MSW.
- Develop through
bun dev. - 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.