- Baseplate
- Baseplate Overview
- Baseplate Features
- Backlog Features
- Baseplate Launch Checklist
- Baseplate Roadmap
- Baseplate Team Makeup
- Business Applications
- Business Model
- Competitors and Other Options
- Developer Guide
- Team Tools
- Feature Scope
- Global Look and Feel
- Meeting and Methods
- Role Based Access Control
- Terms and Concepts
- Third Party Tools and Integrations
Baseplate User Interface Paradigms
Baseplate’s frontend stack leverages React 19 with TypeScript and modern UI libraries - MUI Joy v5, Phosphor Icons, React Hook Form, Zod, TanStack React Query and Storybook. We chose this platform as it balanced scope (not too many libraries) with coding best practice (build it right, once). This guide outlines how to build user interfaces (UI) in that environment, UI development conventions and best practices for Baseplate developers. You can read through this document to understand the paradigms OR just add it into your IDE prompt and have a reasonably good chance of getting a high quality user interface built out.
Our overarching focus in user interface development is on rapid prototyping and MVP-quality UIs. We want to build these with consistency, clarity, and speed in mind. In practical terms that means we aim to maximize development velocity and our willing to sacrifice refinement of the user experience for quickly getting a product into the market.
Coding Agent Prompts
We use Cursor at 1 to 100 but you can use any agentic IDE or copilot to help write code for Baseplate apps. We recommend using a coding agent from primary code generation for all Baseplate apps. When they are generating user interfaces we recommend the following additional concerns:
- Validate All Components are available in MUI. Don't take it for granted that the MUI component is available - check the currently in use production library.
- Validate all Phosphor icons are present. Check that any icons used are actually present in Phosphor.
Design Patterns
Baseplate is principally designed to help rapidly build B2B SaaS applications. In such applications, certain UI patterns appear over and over and over again. Conforming to them will ensure your application works like every other application your user base is used to. Implementing your own application specific UI won’t make your application “unique” it’ll make it difficult to use. Accordingly, we feel that conforming to these patterns helps users feel at home while supporting developers in quickly understanding your code.
Below we outline paradigms for common patterns – data tables, modals, forms, onboarding flows, dashboards, and notifications – that are implemented using Baseplate’s standard user interface stack (Joy UI components, Phosphor Icons, etc).
Data Tables
Use tables for structured, dense data. Data tables are ideal when users need to scan and compare many records (rows and columns) at once. They are commonly used on the “home” page of a feature to show all the relevant records for the user or customer. In general, we expect data tables to support header sorting, pagination, selection, and inline actions to enable efficient data management:
- Header Sorting: Sorting is active for important columns. For MVP, client-side sorting on fetched data is acceptable if dataset is small. If the data set is large you’ll need to fetch sorted data from the server via query parameters. Indicate sort order with an icon (using a Phosphor icon like CaretUp/CaretDown next to the column header). Only one column should sort at a time by default. Provide a clear default sort (e.g. newest first for date columns).
- Pagination & Scrolling: Avoid rendering extremely large tables in one go, use pagination for large datasets. In general, simple page controls with Prev/Next buttons, current page and total page numbers and ability to jump to a page are all that is needed. The current page should always be indicated. If all pages are not loaded on the client side, new pages can be handled with TanStack React Query by including the page in the query key (e.g. useQuery(['items', page], fetchPage)), which also provides caching. If using infinite scroll, use React Query’s fetchMore pattern or the useInfiniteQuery hook.
- Selection & Bulk Actions: Enable multi-row selection via checkboxes on each row if bulk operations are needed. Provide a top and bottom context toolbar or action button that appears when rows are selected. The most common action supported here is delete or archive for several items at once. Users should be able to checkbox-select multiple rows and click a single “Delete” action in the toolbars above and below the table. Always include a “Select All” checkbox in the header to select the current page of results for convenience.
- Row Actions: For actions on individual rows (like “Edit”, “Delete”), include buttons or icons at the end of the row for direct access. Icon buttons should set tooltips and use relevant Phosphor Icons for a consistent look. A pencil icon is used for edit, an eye icon for view and a trash icon for delete. These icon buttons should have accessible labels (e.g. <IconButton aria-label="Edit - [row item name]"><PencilIcon/></IconButton>) so screen readers announce their purpose.
- Loading, Empty, and Error States: While data is loading (e.g. awaiting a useQuery), provide a loading indicator. This could be a spinner or a skeleton row UI. Joy UI includes a Skeleton component which can be placed inside table cells to simulate content. For empty results, display a friendly message in a table row – e.g. “No records found.” possibly with an illustrative icon. This helps avoid a blank page confusion. In case of error (network failure, etc.), display an error state in place of the table (for instance, a Joy Alert component with color="danger" containing a brief message like “Failed to load data.”) and perhaps a retry button. Using React Query, you can check the error object from useQuery and conditionally render an error message UI.
- Example – Basic Table with React Query: Below is a simplified example of a data table component leveraging React Query and Joy UI:
import { useQuery } from '@tanstack/react-query';
import { Table, Sheet, Typography, IconButton, CircularProgress } from '@mui/joy';
import { PencilSimple, Trash } from 'phosphor-react';
function UsersTable() {
const { data, isLoading, error } = useQuery(['users'], fetchUsers);
if (isLoading) {
return <Typography><CircularProgress /> Loading users...</Typography>;
}
if (error) {
return <Typography color="danger">Error loading users.</Typography>;
}
return (
<Table hoverRow>
<thead>
<tr>
<th>Name</th>
<th>Email</th>
<th>Role</th>
<th></th>{/* Actions column */}
</tr>
</thead>
<tbody>
{data.map(user => (
<tr key={user.id}>
<td>{user.name}</td>
<td>{user.email}</td>
<td>{user.role}</td>
<td>
<IconButton aria-label="Edit">
<PencilSimple />
</IconButton>
<IconButton aria-label="Delete">
<Trash />
</IconButton>
</td>
</tr>
))}
</tbody>
</Table>
);
}
In this snippet, CircularProgress (Joy’s loading indicator) is shown during loading. Errors render a message. The table itself uses semantic <thead> and <tbody> for accessibility. The actions column contains Phosphor icon buttons with labels for screen readers.
- Design Considerations: Align text and numbers appropriately for readability. By default text should be left-aligned and numeric data right-aligned. Keep column headings aligned with their data. Use adequate cell padding and row striping (if needed) for visual clarity, leveraging Joy’s default Table styles or customizing via the theme.
- Responsive Tables: Tables can be challenging on small screens. For narrow viewports, consider limiting the number of visible columns to only the most essential. Less critical columns can be hidden or accessible via a detail view. An alternative is a stacked table pattern, where each row becomes a card with key-value pairs listed vertically (labels on the left, values on the right) for mobile. This sacrifices cross-row comparison but ensures all data is accessible. If horizontal scrolling is unavoidable for wide tables, implement sticky headers and maybe a sticky first column so context remains visible. Always indicate that a table scrolls (e.g., a subtle gradient or arrow hint at the edge) so the user knows to scroll for more columns.
Finally, always test your tables with realistic data. Ensure the UI handles long text (wrapping or truncating with ellipsis in cells) and that action buttons remain clickable and not cut off.
Modals (Dialogs)
Modals are used for focused tasks or critical information, such as creating or editing an item, showing details (i.e. viewing an item), or confirming an action. In Baseplate UIs, we use Joy UI’s Modal and ModalDialog components to implement modals. These components come with built-in accessibility features – they manage focus and ARIA roles automatically – ensuring that keyboard and screen reader users can interact properly.
- Usage Scenarios: Common modal use cases include:
- Edit/Create Forms: Clicking an “Add new X” or “Edit” button opens a modal with a form. This keeps the user on the same page context and allows focusing on the form.
- Confirmation Dialogs: To confirm destructive actions (e.g., deleting a record), use a modal that asks “Are you sure?” with Confirm and Cancel buttons.
- Detail Views: If a quick view of a record is needed without navigating to a new page, a modal can show read-only details and perhaps actions like “Close” or “Edit”.
- Structure: A Joy Modal requires a single child node, typically a ModalDialog which contains the actual content. Inside ModalDialog, use a ModalClose button (which renders a close “X” and hooks into the modal’s onClose) and then your content. Always provide a clear title for the modal, typically using a heading element (e.g., <Typography level="h4">Modal Title</Typography>). This title will be used to label the dialog for accessibility (Joy will apply aria-labelledby on the dialog).
- Focus Management: When a modal opens, focus should move to it and remain trapped until the modal is closed. Joy’s modal takes care of focus trapping and restores focus to the previously focused element on close. Ensure the first interactive element in the modal (often the close button or first field) is focusable. Every modal should have at least one focusable control (like a close or submit button). Avoid leaving users stuck with no way to interact via keyboard.
- Accessibility: Under the hood, Joy’s ModalDialog has role="dialog" and is labeled by the first heading inside it. If your modal contains additional descriptive text (like instructions), you can use aria-describedby on the ModalDialog to reference an element containing that text (though Joy may handle this automatically for content inside). The ModalClose button has appropriate aria-label by default (it likely uses “Close” internally). For custom close buttons, add aria-label="Close dialog". The backdrop that Joy renders will have aria-hidden="true" for background content. These defaults mean you usually don’t need to manually tweak ARIA attributes, just ensure you use the provided components as intended.
- Styling: Joy’s modal has variants (plain, soft, etc.) and sizes. By default, ModalDialog appears centered. Use the size="sm|md|lg" prop to adjust dimensions or layout="fullscreen" for mobile-fullscreen modals (useful for small devices to have the modal take up the whole screen, providing a responsive behavior).
- Example – Confirmation Modal:
import { Modal, ModalDialog, ModalClose, Button, Typography } from '@mui/joy';
import { useState } from 'react';
function DeleteConfirmModal({ item, onConfirm }) {
const [open, setOpen] = useState(false);
return (
<>
<Button color="danger" onClick={() => setOpen(true)}>Delete {item.name}</Button>
<Modal open={open} onClose={() => setOpen(false)}>
<ModalDialog variant="outlined" role="alertdialog">
<ModalClose />
<Typography id="confirm-title" component="h2">
Confirm Deletion
</Typography>
<Typography id="confirm-desc" textColor="text.tertiary">
Are you sure you want to delete <b>{item.name}</b>? This action cannot be undone.
</Typography>
<Button color="neutral" onClick={() => setOpen(false)}>Cancel</Button>
<Button color="danger" onClick={() => { onConfirm(); setOpen(false); }}>
Yes, delete
</Button>
</ModalDialog>
</Modal>
</>
);
}
In this example, we use role="alertdialog" for a confirmation (indicating to assistive tech that user attention is needed). The modal includes a title and description with associated id attributes, and those IDs could be passed to aria-labelledby and aria-describedby on ModalDialog if not set automatically. The Cancel and Yes buttons provide an explicit choice. We placed the modal trigger and modal in the same component for simplicity. In practice, manage state at a higher level if needed.
Modal Best Practices
- Use modals sparingly for critical or focused tasks.
- Do not overload a single modal with too much content or deeply nested scrolling regions (it can be confusing).
- If a form in a modal becomes very large, consider a dedicated page or a multi-step wizard instead.
- Always ensure modals are dismissible – via an explicit close button (the “X”), as well as by clicking the backdrop or pressing Escape (Joy modal supports both by default, which trigger onClose with reasons like "escapeKeyDown" or "backdropClick). However, for critical confirmations (like “Are you sure you want to delete?”), you might disable backdrop/Escape closing so the user must consciously choose.
Forms
Forms are where users input data - usability and feedback here are key. Baseplate uses React Hook Form for form state management and Zod for schema validation to ensure robust yet developer-friendly forms. Additionally, Joy UI provides form controls and styling, and React Hook Form’s integration with Joy is straightforward.
- Form State with React Hook Form: We recommend using useForm() from React Hook Form to manage form inputs. This provides out-of-the-box features like tracking dirtiness, validation states, and easy integration with uncontrolled Joy components via the register function. Define a Zod schema for form data and use zodResolver from @hookform/resolvers/zod to connect it. This setup will run Zod validations on form submit (and on blur/change if configured) and populate formState.errors with messages.
- Validation Strategy: For MVP, lean towards lenient validation that covers only truly essential rules. Overly strict validation will frustrate users and slow down development without significant benefit. Focus on required fields and basic format checks (e.g., valid email address) initially. You can always enhance validation later as the product requirements solidify. Using Zod means your schema can evolve – start simple, e.g. z.string().min(1) for required fields, and expand as needed. That said, for critical fields where errors might cause data issues (like an ID format or important numbers), enforce the rules from day one.
- Inline Validation & Feedback: Provide users immediate feedback for errors where possible. For instance, if a field is required, it’s good to show an error message like “This field is required” if they attempt to submit the form empty. With React Hook Form, you can either use handleSubmit and show errors after submit, or use the mode: 'onBlur' or 'onChange' settings for real-time validation. On the UI side, Joy’s form components have an error state styling you can activate. For example, wrapping an input in <FormControl error> ... </FormControl> will highlight it and show any helper text in an error color. You can use FormHelperText below an input to display the validation error message (e.g., “Email is invalid”). In fact, Joy’s FormHelperText can include an icon as well (perhaps a Phosphor warning icon) alongside the error text for clarity.
- Using Joy Form Components: Joy UI provides form controls like Input, Textarea, Select, Checkbox, etc. Use these instead of basic HTML elements to ensure consistent styling with the rest of the app. They support standard props like required, disabled, and have accessible markup. For instance, a <FormLabel> paired with an <Input> ensures the label is clickable and properly associated. If using register from React Hook Form, you can register Joy Inputs by spreading the props: <Input {...register('fieldName')}/>. For controlled components or more complex inputs, you might use Controller from React Hook Form to manage them.
- Disabled States: Make liberal use of the disabled attribute for form inputs and buttons when appropriate:
- Disable the Submit button when the form is not ready to be submitted (e.g., still loading required data, or if using isValid from RHF with mode: 'onChange' to disable until no client errors).
- Disable the entire form or certain fields while a submission is in progress (to prevent double submissions).
- Use contextual disabling – for example, if a checkbox “Enable advanced options” is unchecked, disable the advanced fields section.
- Joy’s inputs and buttons render a distinct disabled style automatically. Also ensure to handle disabled fields in your form logic (React Hook Form will by default not include disabled inputs in submission data, which often is fine).
- Submitting and Loading: When the form is submitted, provide feedback:
- Change the label of the submit button to indicate action (“Saving…”) or display a spinner inside it. You could use Joy’s <CircularProgress size="sm"> inside the button or use a loading prop if provided by Joy’s Button (Joy’s core API is still evolving, but it may have something like loading).
- Ensure the form cannot be resubmitted while the first submission is pending – disable the submit button and any relevant inputs.
- If using React Query’s useMutation for form submission, you can integrate that with the form – call the mutate function inside handleSubmit, and use the mutation’s isLoading to control the UI state (disable controls, show spinner).
- Success and Error Feedback: After submission, inform the user of the outcome:
- On success, you might close the modal (if form was in a modal) and show a toast notification saying “Saved successfully”blog.logrocket.com, or simply clear the form and show a success message inline.
- On error (e.g., server validation failed or network error), show an error message. If the error corresponds to specific fields (validation errors from backend), map them to those fields in RHF so they display under the appropriate input. For general errors (e.g., “Server unreachable”), you might show a Alert component or a toast.
Use a toast for confirmation for non-intrusive feedback. For instance, after saving a form, a small toast can appear “Profile updated” and auto-dismiss after a few seconds. Reserve modals or persistent messages for important errors or required next steps.
- Example – Login Form with React Hook Form & Zod:
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
import { FormControl, FormLabel, FormHelperText, Input, Button } from '@mui/joy';
const LoginSchema = z.object({
email: z.string().email("Enter a valid email"),
password: z.string().min(8, "Password must be 8+ characters"),
});
type LoginData = z.infer<typeof LoginSchema>;
function LoginForm({ onLogin }) {
const { register, handleSubmit, formState: { errors, isSubmitting } } = useForm<LoginData>({
resolver: zodResolver(LoginSchema)
});
const submit = (data: LoginData) => {
// Simulate login API call
return onLogin(data);
};
return (
<form onSubmit={handleSubmit(submit)}>
<FormControl error={Boolean(errors.email)}>
<FormLabel>Email</FormLabel>
<Input type="email" placeholder="name@company.com" {...register('email')} />
{errors.email && (
<FormHelperText>{errors.email.message}</FormHelperText>
)}
</FormControl>
<FormControl error={Boolean(errors.password)} sx={{ mt: 2 }}>
<FormLabel>Password</FormLabel>
<Input type="password" {...register('password')} />
{errors.password && (
<FormHelperText>{errors.password.message}</FormHelperText>
)}
</FormControl>
<Button type="submit" loading={isSubmitting} sx={{ mt: 2 }}>
{isSubmitting ? 'Logging in...' : 'Login'}
</Button>
</form>
);
}
In this code, we define a Zod schema for login. The form uses Joy’s FormControl, FormLabel, and FormHelperText for proper layout and error display. We mark FormControl with error if that field has an error, which automatically gives the input a red border and the helper text a red colormui.com. The submit button text changes when submitting. (Note: We used a hypothetical loading prop on Button for simplicity – if Joy’s Button doesn’t support it directly, we could conditionally render a spinner or disable the button.)
As the product matures, you might implement auto-save (saving fields as they change) or more complex validation logic (e.g., checking username availability via an API). These can be built on top of the basic patterns established. Start with a solid foundation: clear labels, correct inputs, basic validation, and good feedback.
Remember that forms are often the most interactive part of your UI – test them thoroughly, including keyboard-only usage and common error scenarios.
Onboarding Patterns
Onboarding flows are the guided experiences that help new users get started or existing users configure something step-by-step. Baseplate provides an out of the box onboarding flow that covers both initial customer setup (the first user for a given tenant) and initial user setup (all users). Onboarding is all about lowering friction to use by progressively getting the system setup and disclosing information and options rather than overwhelming the user all at once.
The exact method of onboarding is left to an implementing baseplate app. We typically split up onboarding into a multi-step wizards and app tour:
- Multi-Step Wizards: Breaks down the process of setting up a customer and user into multiple bite-sized steps. For example, an onboarding wizard for a new account might have steps for “Basic Info”, “Company Details”, “Invite Team”, etc. These use a Joy Stepper progress indicator to show the user’s progress (e.g., “Step 2 of 4”).
- As you implement, keep each step’s UI focused on a single topic or a few closely related inputs. Provide “Next” and “Back” buttons to navigate.
- The final step would have a “Finish” or “Submit”.
- It’s crucial to preserve the data as the user navigates (store it in component state or a context until final submission).
- Avoid asking for non-essential info in an onboarding – stick to what’s absolutely needed to get the user into the app.
- Progressive Disclosure: Use progressive disclosure techniques in the UI beyond just wizards. For example, in a form you might hide advanced settings under a “Advanced Options” toggle. Or an onboarding page might show a minimal “quick setup” with an option to expand “more settings”. This allows power users to access more configuration if they want, while keeping things simple for everyone else by default.
- In-app Tours: A quick tour overlay highlighting key UI areas can be helpful for new users (e.g., pointing out “This is your dashboard where you’ll see analytics.”). There are libraries for guided tours, but even a simple approach like highlighting a feature with a tooltip (“Start here!”) can onboard users. Use sparingly and ensure these hints can be dismissed.
- Onboarding State: If a user partially completes onboarding and comes back later, the progress and success of the onboarding process needs to be stored. We recommend storing this in the database and do so be default.
- Visual Design: Onboarding screens can be a bit more visually engaging than the app as a whole. If your have a budget for design - this is a good place to spend it. Illustrations, a different layout, video content all can make onboarding a more engaging experience. Use the Baseplate template as a start but feel free to let your imagination flow here.
Dashboard Components
Dashboards are often the first screen users see – they provide an overview of key information and metrics. For dashboards in Baseplate projects, the goal is to display essential cards, metrics, and charts in a clear, glanceable manner.
- Metrics and KPI Cards: Use cards to display key performance indicators (KPIs) or stats. Joy UI’s Card component can be a container for a metric. Typically, a KPI card might show a large number, a label, and maybe an icon or trend indicator. Keep these cards consistent in size and style for a clean layout. For example, you might have a card that shows “124 – New Signups This Week” with a small upwards arrow and “+5%” to indicate growth. Ensure each metric directly ties to a goal or user need – avoid vanity metrics. A good KPI is tied to a goal, actionable, and easy to interpret at a glanceuxdesign.cc. Don’t clutter the dashboard with too many numbers; pick the top 3-5 metrics that matter for the user’s context.
- Layout: Dashboards should be responsive (see Responsive Design below). On desktop, you might use a grid layout to place cards in a 2 or 3 column layout. On mobile, these would stack in a single column. Use Joy’s Grid or Stack (Flexbox) to achieve this. For example, two half-width cards side by side on desktop can collapse to full-width on mobile. Use consistent spacing between cards (Baseplate’s design likely follows an 8px or similar spacing gridmui.com). For MVP, you can hard-code some breakpoints (e.g., use CSS media queries or the Joy Grid with xs={12} and md={6} props for columns).
- Charts: Visualizing data with charts (line graphs, bar charts, pie charts) can provide insights at a glance. Instead of building charts from scratch, use a chart library that integrates with React (such as Chart.js via react-chartjs-2, Recharts, or Victory). Since Baseplate focuses on speed, pick a library with good defaults so you spend minimal time on configuration. Wrap charts in Card components with a title. Ensure charts have labels, axis titles, or tooltips to explain the data. For accessibility, include a text summary of what the chart shows (or at least ensure screen readers can get the data points, which is often tricky – an optional improvement might be a “View table data” link).
- Using React Query for Data: The dashboard will likely fetch summary data (counts, lists of recent items) from the backend. Use React Query to retrieve these asynchronously. Show a loading state (e.g., skeleton cards or spinners in each card) until data is ready. If some cards have data and others error out, handle each component separately so one failing API doesn’t blank the whole dashboard. For instance, if the “New Signups” count fails to load, you can show an error message or placeholder in that card while others still show their info.
- Consistency & Theming: All dashboard cards should use consistent background and text colors (which align with the theme). For example, use the theme’s surface color for cards (Joy’s default Card variant) and a consistent typography style for numbers (maybe use a larger size, like Joy’s Typography level="h2" or a custom style in theme for KPI numbers). If using icons (Phosphor Icons) alongside text, ensure they are the same color or complementary. Phosphor icons are available in different weights; Baseplate might choose one weight (regular or bold) for all icons for a unified look.
- Interactive Elements: Some dashboard components might allow user interaction, e.g., a filter dropdown to switch the time range of a chart (7 days vs 30 days) or a refresh button to re-fetch data. Use small, non-intrusive controls (like an icon button with a refresh icon placed in a corner of the card, perhaps semi-transparent until hovered). Ensure the intent is clear via tooltips or aria-labels (e.g., <IconButton aria-label="Refresh data"><ArrowsClockwise/></IconButton>).
- Example – Simple Dashboard Card:
<Card variant="outlined" sx={{ width: '240px', textAlign: 'center' }}>
<Typography level="body-sm" textColor="neutral.600">New Signups (7d)</Typography>
<Typography level="h3" component="div">124</Typography>
<Typography level="body-xs" textColor="success.600">
<ArrowUp size={16} /> 5% from last week
</Typography>
</Card>
This is a static example of a KPI card: it shows a label, the main number, and a small indication of change. In a real scenario, those values would be passed in as props or fetched, and the color of the change (green/red) might be determined by the data (positive or negative change).
- Dashboard Navigation: If the dashboard has multiple sections (e.g., different tabs for different data sets), ensure switching is easy and state is preserved if returning. However, for MVP, it’s often best to have a single, straightforward dashboard page.
Remember, the dashboard should tell a story at a glance: use it to surface the most relevant info and let users drill down elsewhere (perhaps with links or buttons to “View details” that take them to a detailed page or report for more).
Notification Systems (In-App & Toasts)
Effective feedback is crucial in UI. Baseplate UIs employ two main notification mechanisms: in-app notifications - via the notifications center - and toast notifications - brief messages that pop up to inform the user of completion of a system action.
In-App Notifications
In-app notifications are persistent messages that stay until read or dismissed. They are accessed through the bell icon at the top of the app which expands to provide a notifications panel.
In app notifications are used to provide persistent messages to users for material items. For example, an app might notify “Your subscription will expire in 3 days” or “New feature launched: ... [Learn more]”. The bell icon in the header shows a count of unread notifications. Clicking it open the notification panel allowing the user to review and mark as read notifications.
Each notification requires a title, a short description (which can include HTML) and an optional icon. They can be sent programmatically or via the System Administrator accessible application function.
Toast Notifications
Toasts (also called snackbars) are transient, auto-dismissing notifications used to confirm user actions or show minor alerts. For example, after saving changes, a toast in the corner might say “Profile updated”. The default behavior is that they will disappear after a few seconds without user intervention. Use toast notifications for success messages, low-severity error messages, or informational updates that don't require additional action. Because they vanish automatically, toasts should never carry critical information that a user might miss – anything mission-critical should use a persistent notification or dialog.
To implement toast notifications, use Material UI’s built-in Snackbar component. Generally, we recommend using the Baseplate standard <SnackbarProvider> to trigger toast. Toasts should be placed in a non-intrusive location and we default to the bottom right corner of the screen. These will render slightly above bottom on mobile (to avoid on-screen keyboards). We recommend using system standard styling for them and only overriding style if absolutely necessary (e.g.,red for error “Error”). You can use Joy’s Alert component inside a Snackbar for a quick way to get colored variants.
Toasts should use an ARIA live region (aria-live="polite") so screen readers have the option of announcing them when they appear but not the requirement to do so (or pull keyboard focus). Keep the message brief and clear, as users won't have time to read a long text.
For certain actions, notably deletion, if you want to provide an immediate “Undo” the toast UI is an effective place to do this. For example, when a user deletes an item, instead of immediately purging it, you can remove it from view and show a toast: “Item deleted. UNDO”. If they click UNDO within the toast window - here more like 5 seconds, it will restore the item in the UI. If Undo is not activated the automatic closing of the test triggers the backend deletion. We generally consider such functionality outside of MVP scope and not it here for the sake of completeness.
Finally, it’s worth noting that you should test the timing of toast auto-dismiss (usually 3-5 seconds). Too short and users might not read them; too long and they might stack up or annoy. Also test that multiple toasts queue gracefully (not overlaying each other in a confusing way). If using Storybook, simulate various messages (success, error, long text vs short text) to refine the appearance and behavior.
Modal and Banners
In some cases you may wish to use a modal for material and significant updates that require a large amount of text OR need to display a user interface component set to remedy an issue. These aren’t exactly “notification systems” but it’s worth noting you can use them as needed.
Finally, top-of-page banners can be used for system-wide alerts (e.g., “Maintenance in progress” or an offline indicator). Joy doesn’t have a Banner component explicitly, but you can use Alert with variant="soft" and place it at the top of your content. Banners are persistent until dismissed or the condition resolves. If you do implement a message in a banner we recommend a redundant In-App Notification as well.
Principles for Building MVP UIs
When building UIs for a Minimum Viable Product, speed and agility are key. However, we still want to enforce good practices that ensure quality and ease of future enhancement. Here are core principles:
Full vs. Lenient Validation
Not every piece of validation or every edge case needs to be built upfront. Aim for lenient validation in early versions – allow inputs that are “good enough” and avoid blocking the user unless absolutely necessary. For instance, if an email field roughly looks like an email (contains "@" and "."), that might be sufficient for an MVP instead of a complex regex that forbids rare but valid emails or ones your developers my hack together to use in testing. Overly strict rules can prevent legitimate input and frustrate users. On the other hand, do include full validation for critical domains – e.g., compliance rules for content or required fields that the app truly cannot function without.
User feedback trumps theoretical perfection at MVP stage. It’s better to get the product in use and discover what validations matter versus spending time upfront defining and implementing all constraints. Further, no matter how well you define validation and field restrictions your users will find edge cases and input weird data you’ve never thought of. In general our take is it’s better to get the application out the door an into users hands then perfectly define every use case. Once you have user data in-system it’s easy to look at the fields and see where users are inputting gibberish. At that point consider adding stricter checks or UI helpers. You’ll be far more informed on what’s important and what’s not.
Finally, lenient validation also means being forgiving: if the user input is slightly malformed you can accept it and clean it up either on the front or backend. Examples would be trimming white space or lowercasing emails. Could you require that client-side? Sure. Should you? Not until you’ve got clarity on how people are using the system.
Simplify for Speed
When trying to ship a MVP, simplicity is your friend. Here’s some basic philosophies we follow:
- Use existing components and patterns: Don’t reinvent UI elements that you can get from Joy UI or the broader MUI ecosystem. For example, need a date picker? MUI has one – it may not perfectly match your final design vision, but using it out-of-the-box is infinitely faster than crafting your own. Baseplate prioritizes using the stack’s capabilities to save time (that’s why we chose Joy, React, and the like - to leverage their solutions).
- Start with basic functionality: If a feature can be done in a straightforward way, do it in the straightforward way first. E.g., to implement filtering on a list, a simple text search field that filters results client-side may suffice for MVP. Only add complex multi-field filtering or advanced search logic if absolutely required. Wait until you get feedback that those features are needed before you implement them.
- Minimal configuration and options: In an MVP, we want to limit the number of choices we give the user. Not only is there an implementation cost to each of these choices may dilute attention from the core functionality of your application. Set sensible defaults and hide those thousands of configuration options for later (or better, never). As an example, instead of giving users a theme picker in the app we give them two options - light mode and dark mode.
- Favor clarity: A simple UI is less likely to confuse. Even if the design is somewhat plain - even if it sometime tips into ugly, that’s acceptable in an MVP. As an internal principle, if cutting a fancy animation or a clever but complex layout will save dev time and reduce bugs, cut it. A clean, straightforward interface that works is better than a flashy one that’s half-implemented or brittle.
- Leverage Storybook For Custom Components: Build your components in isolation using Storybook. This not only speeds up development (you can work on a component without running the whole app) but also encourages creating simple, reusable components.
Error Handling and Rollback
How your UI handles errors is crucial, especially in an MVP where things might not be perfectly stable. A few guidelines:
- Detect and inform: Whenever an async operation fails (data load, form save, etc.), always surface a message. Silent failures are the worst; users will be stuck or think the app is not responding. Use a toast or inline Alert component to say what went wrong in user-friendly terms. E.g., “Network error: failed to save. Check your connection and try again.” Avoid technical jargon or raw error dumps.
- Don't blame the user: Phrase error messages neutrally. Instead of “Invalid input”, say “Please enter a valid phone number” – it’s more guiding. Also, if the error is on us (server down, etc.), apologize and reassure (e.g., “Sorry, something went wrong on our end. Try again?”).
- Provide recovery actions: If possible, let the user correct the issue. For form errors, highlight the fields in error and allow resubmitting. For data load errors, give a “Retry” button. If an operation is not critical, the user should be able to continue using the app. For example, if loading a sidebar widget fails, you might show an error there but the main content works and user can still navigate.
- Atomicity and Rollback: Design UI actions to be atomic – either fully succeed or leave the system as it was. For example, if a multi-step process (like a wizard) fails halfway through, the system should not be left in a weird half-configured state. Ideally, use transactions or backend support to commit or rollback as a whole. On the UI side, if step 3 fails, you might keep the user on step 3 with an error message, rather than closing the wizard. If you had optimistic UI updates (like removing an item from a list before server confirms deletion), handle errors by restoring the item and informing the user the deletion didn’t happen. React Query’s useMutation with an onError handler can be used to rollback optimistic updates easily.
- Undo where appropriate: As mentioned, an “Undo” for destructive actions is great UX. It’s also a form of rollback – essentially you are offering the user a manual rollback. Build the UI to accommodate that (e.g., deletion puts item in a temporary state where it can be brought back).
- Global Error Handling: Sometimes errors are truly global (like the API auth token expired). In those cases, a global approach helps: e.g., a centralized axios interceptor or React Query’s onError handling to catch errors like 401 Unauthorized and redirect to login or show a modal “Session expired”. Baseplate’s structure has a middleware or context for such cross-cutting concerns, so utilize that to avoid scattering repeated error handling logic. For instance, a context provider can catch any error with a certain shape and display a generic message or log it.
- Logging: Although invisible to the user, ensure that errors are logged to console or a monitoring service (if available) to aid debugging. In MVP, you might simply console.error in the catch of an API call. As things evolve, integrate with a service (Sentry, etc.) so that developers can track issues post-release.
By making error handling a first-class part of the UI (rather than an afterthought), you build trust with users – they know that if something goes wrong, they’ll be informed and maybe even have a way to fix it or try again.
Accessibility Best Practices
Accessibility (a11y) should be considered from the start. It ensures our UIs can be used by people with disabilities and improves overall quality. Baseplate’s use of Joy UI gives us a head start (Joy components follow WAI-ARIA standards and include a lot of built-in accessibility), but we need to do our part:
- Keyboard Navigation: Every interactive element must be reachable and operable via keyboard alone. This means ensuring the tab order of the page flows logically (usually DOM order). Use semantic HTML elements (<button>, <a>, <input>, etc.) whenever possible because they are focusable by default and come with proper behaviors. If you create a custom component that is clickable but not a real button/link, add tabIndex={0} and handle key events (Enter/Space) to activate it. Avoid removing the outline on focus – that outline is critical for keyboard users to know where they are. If customizing focus style, ensure a visible focus indicator remains (Joy’s theme allows customizing focus ring thickness/color, but by default provides a focus outline)mui.commui.com.
- Focus Management: When dialogs or popovers open, focus should move into them and trap there; when they close, focus should return to a sensible place (usually the trigger button). Joy’s Modal and Drawer handle this for you. Similarly, if using a menu or select dropdown, ensure it’s focus trapped. Always test by keyboard: e.g., press Tab through your page and see if you can operate all controls and not get stuck. Also consider logical focus order – e.g., after submitting a form and showing a success message, it might be polite to shift focus to that message (an alert role region) so screen reader users know something happened.
- ARIA Roles and Labels: Use ARIA attributes to fill any accessibility gaps:
- If an element has no visible label but needs one (like an icon button with just an icon), use aria-label or aria-labelledby. For example, a close icon by itself should be <button aria-label="Close">×</button>developer.mozilla.org. MDN provides guidance that a button with only an SVG needs an accessible name via aria-labeldeveloper.mozilla.orgdeveloper.mozilla.org.
- Ensure form fields have labels. Using Joy’s <FormLabel for={id}> and giving the Input an id is one way. Or wrap the input in FormControl with a sibling FormLabel (Joy links them automatically).
- Use appropriate ARIA roles sparingly – if you’re building a custom component that behaves like a menu, add role="menu" and on items role="menuitem", etc., but only if you are also handling the keyboard interactions expected (arrow key navigation, etc.). A rule of thumb is: prefer using native elements; use ARIA roles only for custom widgets that don't have a native equivalent.
- Live regions: For dynamic content like validation errors or success messages that appear, use role="alert" or aria-live="assertive" on those elements so that screen readers announce them immediately. For example, if a form error appears on submit, a div with role="alert" containing the error text will be read out.
- Dialog roles: As mentioned in Modals, ensure modals have role="dialog" (Joy’s ModalDialog does this) and are labelled by their headerdeveloper.mozilla.org. This way screen readers announce “Dialog: Confirm Deletion…” etc.
- Icon-only elements: Mark decorative icons with aria-hidden="true" if they convey no information (e.g. an icon inside a button that also has text can be hidden from screen readers to avoid redundancy).
- Color Contrast: Adhere to WCAG guidelines for contrast. Text and interactive elements should have sufficient contrast against their background (generally 4.5:1 ratio for normal text)mui.com. Baseplate’s color palette should be chosen with this in mind (e.g., Joy’s default palette mostly meets contrasts, but if we customize colors, ensure we don’t pick light gray on white or similar). Test your pages with accessibility tools or browser dev tools’ contrast checker. Avoid using color alone to convey information (for instance, don't just rely on red text to indicate error; also show an icon or label it “Error:”).
- Screen Reader Considerations: Screen reader users navigate via semantic structure. Use proper heading levels (h1, h2, etc.) in a logical hierarchy (don’t skip from h1 to h4, for example)mui.com. Ensure interactive controls have clear labels (we covered that). For complex controls like tables, make sure to use <th> for headers and include scope="col" or aria-colindex as needed (though a simple table with proper <th> tags should be fine). If a layout is complex, consider adding landmark roles or using proper elements like <main>, <nav>, <aside> to help structure the page for assistive tech.
- Testing A11y: Use tools like Axe (browser extension) or Lighthouse’s accessibility audit during development. These can catch common issues like missing alt text or low contrast. In Storybook, you can add the Storybook A11y addon to get quick reports on each component. Additionally, try navigating your app with a screen reader (VoiceOver on Mac, NVDA or JAWS on Windows) to understand the experience. Even tabbing through and seeing if you can operate everything without a mouse will highlight a lot.
Accessibility is a vast topic, but by baking in these best practices in our Baseplate UI conventions, we ensure our MVPs are usable by more people and avoid costly retrofits later.
Responsive Design
Baseplate applications should be usable on a variety of screen sizes, from large desktop monitors to tablets and mobile phones out of the box. That concept, responsive design, adapts the layout and components to different sizes gracefully.
For MVPs we typically focus on two basic screen sizes - modern smartphone widths (~390px) and desktop (1024px). At a basic level we just want to ensure the app doesn’t break on small screens. That might mean some features are simplified on mobile (for example, complex tables cover to cards or have limited functionality).
In general for responsive design we want to use CSS layout systems rather than fixed pixel positioning. Joy UI includes a Grid component that wraps CSS Grid and a Stack (and Box for Flexbox) to easily create responsive layouts. For instance, to create a two-column form that becomes one column on mobile:
<Grid container spacing={2}>
<Grid xs={12} md={6}><TextField label="First Name" /></Grid>
<Grid xs={12} md={6}><TextField label="Last Name" /></Grid>
</Grid>
Here, each field spans 12 columns (full width) on extra-small screens (xs) and 6 columns (half) on medium screens and up (md). This automatically stacks them on small screens. The spacing={2} applies consistent gaps (which tie to theme spacing). Use similar patterns for other grids of cards, etc.
MUI (and Joy) define standard breakpoints (xs, sm, md, lg, xl). Baseplate’s default sticks with MUI’s default sizes (xs <600px, sm <900px, etc.) absent your adjustment of the theme. Use these breakpoints in sx or style overrides. For example, to hide an element on mobile: sx={{ display: { xs: 'none', sm: 'block' } }} which will not show it on xs screens. Conversely, you might have a simplified component that is only on mobile (display none on sm+).
Many components have built-in variants for small screens. For example, Joy’s ModalDialog has a layout="fullscreen" option for mobile which you can toggle based on screen size (maybe by using a media query hook or just always if the modal is primarily for mobile usage). Similarly, navigation drawers might switch from a persistent sidebar on desktop to a swipeable temporary drawer on mobile. If you use the out-of-the-box Baseplate navigation it will automatically update from sidebar menu (desktop) to a hamburger menu (mobile).
We covered some table paradigms in the Data Tables section and we’d repeat that advice here – hide non-critical columns, stack data, or provide an alternate view (like a summary card per row). It’s acceptable on MVP if some heavy admin tables are “best viewed on desktop” as long as they don’t completely malfunction on mobile (they might become horizontally scrollable and that’s okay). But core workflows (like basic data viewing, form submissions) should be doable on a phone in case a user needs to do something urgent on the go.
If your UI uses images, use responsive img tags or CSS background that can scale. Use max-width: 100% on images to ensure they shrink on smaller containers. For icons and vector art (Phosphor icons are SVGs), they will scale fine by font-size or width, but make sure not to use fixed px that break layout.
During development, regularly resize your browser or use device simulators to see how things look at different breakpoints. It's much easier to fix responsive issues as you build, rather than after everything is done. Storybook can also simulate different viewport sizes per story. Identify any overflow (e.g., a long word causing a scrollbar) and address it (perhaps by word-break CSS or by truncating text on small screens if needed).
Finally, note, that on small touch screens, certain UI patterns differ. For example, hover effects don't exist. Make sure any important actions don't rely solely on hover (like only showing a button on hover – that won't work on mobile). Instead, design it to be visible or accessible via tap. Also allow swiping gestures for carousel or drawer if applicable (Joy’s Drawer might have swipe support or it can be added).
Theming and Customization
Baseplate uses MUI Joy UI for a cohesive design out of the box, but every SaaS may have its own branding. Theming allows us to adjust the look (colors, typography, spacing) systematically. Rather than ad-hoc styling each component, leverage the theme to create a consistent design language:
Joy UI uses a design token system – essentially a set of variables for colors, font sizes, spacing, etc. By customizing these tokens, you propagate changes across all components using them. For example, if you want all primary buttons to have a green background instead of the default, you can override the theme.colorSchemes.light.palette.primary.* tokens, and instantly every <Button color="primary"> uses greenmui.commui.com. This is much better than manually applying styles to each button. We strongly encourage using extendTheme to define brand-specific tokens like primary/secondary colors, font families, border radius, etc., up front.
Maintaining consistent spacing, typography, and colors is key to a professional appearance. Use the theme’s spacing scale for margins/padding – e.g., MUI's design is often an 8px base grid. Joy provides spacing through either theme values or the spacing prop in Grid/Stack. Avoid arbitrary CSS values if a theme token exists. Similarly for typography, Joy defines several typography levels (h1...body-sm, etc.) that correspond to specific font sizes and weights. Use Typography with those levels instead of custom inline styles so that if the design adjusts globally (say, all body text becomes slightly larger), it can be done in one place.
For Baseplate applications, you’ll likely have or want your own custom theme. While you can edit this directly in Baseplate we’d recommend just setting up a visual style guide for your company or app and then letting Forge build it for you.
Implementation Consideration
While themes will control the majority of the look and feel of your application a few concerns will come up in implementation. Here’s what we’d recommend you think about as you build:
Phosphor Icons
Baseplate uses Phosphor icons as our icon library. While you can use whatever icons you like we recommend keeping their usage consistent across the app and theme. Set a default size for icons in buttons vs in text vs. for actions and stick to it. Phosphor icons have variants (bold, duotone, etc.); pick one style (e.g., regular outline) to use throughout for coherence. You can create a wrapper <Icon> component if needed that applies a certain size or color by default (for instance, always use currentColor so icons inherit the text color, which is usually desired). If you want to change icon color globally for something like "primary" color, you could style it via theme or context (though usually icons just inherit).
Dark Mode
Dark Mode support is provided out of the box via a switch in the lower left hand isde of the UI. Joy also supports light and dark color schemes for all components. That noted, you should definitely test dark mode to ensure all the components you build or use work in that as well as light mode. While you could skip dark mode for MVPs we find it’s so widely used, and frankly we like it, that it’s worth supporting from the start.
SX vs Theme
The sx prop can be used for one-off customizations but we recommend using it sparingly. If you find yourself writing the same sx repeatedly (e.g., giving every card a certain padding and border), that likely belongs in the theme or a component variant. The rule of thumb: if it’s truly unique to that usage, sx is fine. If it's a pattern (commonly reused style), add it to theme or make a wrapper component. For example, if all forms in your app use <Stack spacing={2}> and full width inputs, consider making a FormContainer component that encapsulates these styles, or set some global CSS via theme for forms.
Example – Extending Theme
import { extendTheme, CssVarsProvider } from '@mui/joy/styles';
const theme = extendTheme({
colorSchemes: {
light: {
palette: {
primary: {
500: '#004080', // custom brand blue
},
success: {
500: '#1B873F', // custom success green
},
},
},
},
fontFamily: {
body: "'Inter', var(--joy-fontFamily-fallback)",
},
components: {
JoyButton: {
defaultProps: {
variant: 'solid',
},
styleOverrides: {
root: {
textTransform: 'none', // e.g., disable uppercase transform
}
}
}
}
});
function App() {
return <CssVarsProvider theme={theme}>... </CssVarsProvider>
}
This snippet shows setting custom primary and success colors, changing the base font, and overriding Button to have no text transformation and use solid variant by default. By using extendTheme, we merge with Joy’s default theme, so we only specify what we want to change. The advantage is that if we later want to adjust, say, spacing scale or add a new component variant, it's all centralized. Again, we recommend just letting Forge set most of this stuff for you.
Finally, note that Baseplate’s design likely follows an 8px grid as mentioned. This is Joy’s default spacing unit (i.e., theme spacing 1 = 8px). So if you want pixel perfect use multiples of that in your designs (padding=2 => 16px, etc.) This uniformity makes the UI look balanced. For God’s sake, please resist the urge to arbitrarily make something 37px wide; stick to the scale unless there's a good reason.
Component Organization
Baseplate ships with a rigorously structured code base and Forge provides agent prompts that are highly detailed in code structure. Here we’ll highlight how UI components should be organized.
Directory Structure
All reusable UI components in a dedicated directory (e.g., src/components/). Each component gets its own folder or file depending on complexity. The Baseplate Source Code Layout guide suggests src/components houses reusable UI pieces with clear responsibilities. For example, you might have:
src/components/
DataTable/
DataTable.tsx
DataTable.stories.tsx
DataTable.test.tsx
UserForm/
UserForm.tsx
UserForm.stories.tsx
Nav/
Sidebar.tsx
Sidebar.stories.tsx
This groups relevant files and makes it easy to find things. Tests and Storybook stories live alongside the component implementation, so when you move or refactor a component, its tests and stories come along. Name component files in PascalCase (matching the component name) or index pattern if you prefer (e.g., index.tsx that exports the component, inside a folder named after the component).
Naming Conventions
Use clear, descriptive names for components and files and err on making them too long NOT too short. A file and component named InvoiceTable is more meaningful than something generic like TableComponent. Follow React community standards: components in PascalCase, instances in camelCase. While, yes, you should avoid very lengthy names almost always you’ll err on not long enough rather than too long. Here directory structure can be your friend with a component like Nav/Sidebar.tsx having the name “Sidebar” within Nav context. For CSS classes or ids, Joy uses its own, but if you have custom ones, consider a prefix.
Storybook Conventions
We’ve tried to document components in Storybook to aid both development and future documentation. We typically create a .stories.tsx file for each component. Use Storybook’s CSF format (Component Story Format) with a default export containing title (we group stories by component type or feature area) and the component. For example:
export default {
title: 'Data Display/DataTable',
component: DataTable,
};
const Template = (args) => <DataTable {...args} />;
export const Basic = Template.bind({});
Basic.args = { /* some sample props */ };
If you write your one stories ensure stories cover all key states: e.g., DataTable story might have “Loading”, “Empty”, “With Data” stories by mocking those props or using Storybook controls. Keep stories lightweight – they should not depend on real API calls (use mocks or static data). This not only helps developers see how the component behaves with various inputs but can serve as living documentation for designers or QA.
Reuse Principles (DRY): Don’t Repeat Yourself
If you notice the same structure or styling in multiple places, consider abstracting it. For example, if several pages show a panel with a title and some content in a card, create a Panel component that standardizes that look. However, balance this with You Aren’t Gonna Need It Again to avoid premature abstraction. It’s okay to duplicate a little markup in two places if you’re not sure they will always evolve together; you can abstract on the third usage. But definitely avoid copy-pasting large chunks of code – that’s technical debt.
Separation of Concerns
By default Baseplate separates components from data-fetching and mutation logic as well as data interfaces. Data fetching and mutations for domain objects is typically handled in the lib / api directory. Types in the lib / types directory and your UI code in the core React source code tree. You’ll end up using Supabase functions to fetch and update data in ways that look a lot like SQL. DON’T MIX THIS IN WITH YOUR UI CODE OR YOU’LL GO CRAZY DEBUGGING IT LATER.
Keeping this structure clean ensures a new team member can find things quickly. For instance, if I want to update how the date is formatted everywhere, I know to look in utils/date.ts rather than searching the whole codebase.
Finally, document your components in Storybook using the docs tab. If Storybook is overkill consider a simple markdown file in the repo, note any intricacies.
Conclusion
Following these UI coding paradigms will help Baseplate developers build polished, consistent UIs quickly. We focus on proven design patterns, sensible defaults, and the powerful tools in our stack (React, Joy UI, React Query, etc.) to deliver features fast while keeping the codebase maintainable. These guidelines, much like our database conventions, aim to make developers productive and ensure that even first-version products have a solid foundation for growth. By building with these principles in mind, we can iterate rapidly on MVPs that users find intuitive and that developers can easily refine as requirements evolve. Happy coding!
- © 2026 One to One Hundred. All Rights Reserved.
- Site by Arcbound.