Component Principles
The core ideas behind how Marigold components are designed, styled, and composed.
Marigold components share a consistent set of design principles that shape their API. Understanding these principles helps you use the system effectively and avoid common pitfalls. This page introduces the core pillars: accessibility, theming, composition, layout, and shared form field patterns.
Accessible by Default
Every Marigold component is built on top of React Aria, a library of accessible UI primitives. This means keyboard navigation, focus management, and ARIA attributes are handled for you out of the box.
We rename some React Aria props for a cleaner developer experience:
| React Aria | Marigold |
|---|---|
isDisabled | disabled |
isPending | loading |
isReadOnly | readOnly |
isRequired | required |
This gives you a solid foundation, but accessibility is a shared responsibility. You still need to provide proper labels, manage ARIA live regions for dynamic content, and test with assistive technologies.
For the full picture, see the Accessibility page.
Theming
Marigold components are unstyled by default. They provide structure and behavior, but their visual appearance comes entirely from a theme. A theme is a collection of style definitions that covers colors, typography, spacing, and visual variations for each component. It is passed to the MarigoldProvider at the root of your application. Every component within it automatically picks up its styles from there.
Because styling is owned by the theme, components don't accept className or style props. Instead, their appearance is controlled through two props: variant and size.
variantdefines the visual intent of a component, such asprimary,secondary,ghost, ordestructive.sizecontrols the physical dimensions and density, such asdefault,small, orlarge.
These props map to styles defined in the theme. When you write variant="ghost", you're selecting a set of pre-defined styles that the theme author has designed to work together.
import { Eye, Pencil, Trash2 } from 'lucide-react';import { Inline, Menu } from '@marigold/components';export default () => ( <Inline space="group" alignX="center"> <Menu label="Actions" variant="default" size="default"> <Menu.Section title="Event"> <Menu.Item id="view"> <Eye /> View Details </Menu.Item> <Menu.Item id="edit"> <Pencil /> Edit Event </Menu.Item> </Menu.Section> <Menu.Section title="Danger Zone"> <Menu.Item id="cancel" variant="destructive"> <Trash2 /> Cancel Event </Menu.Item> </Menu.Section> </Menu> <Menu label="Actions" variant="ghost" size="small"> <Menu.Section title="Event"> <Menu.Item id="view"> <Eye /> View Details </Menu.Item> <Menu.Item id="edit"> <Pencil /> Edit Event </Menu.Item> </Menu.Section> <Menu.Section title="Danger Zone"> <Menu.Item id="cancel" variant="destructive"> <Trash2 /> Cancel Event </Menu.Item> </Menu.Section> </Menu> </Inline>);How it Works
The chain from prop to rendered style looks like this:
- You pass
variantandsizeto a component. - The component looks up the matching styles from the current theme.
- The theme contains style definitions for each component, written with
cva(class variance authority). - Based on your
variantandsize, the matching Tailwind CSS classes are selected and applied to the component.
For example, a simplified theme entry for a Menu trigger button looks like this:
// themes/theme-rui/src/components/Menu.styles.ts
export const Menu = {
button: cva({
base: ['...base styles'],
variants: {
variant: {
default: '...surface styles with border',
ghost: '...transparent with hover background',
},
size: {
default: 'h-button p-squish-relaxed',
small: 'h-button-small px-3',
},
},
}),
};If the available variants don't cover your use case, you can extend the theme using the extendTheme function. This lets you add new variants and sizes to any component without modifying the design system itself.
Composition
Complex components in Marigold are assembled from smaller building blocks using dot notation. Instead of configuring everything through a single component's props, you compose the component from its parts.
Take the Menu component: it's made up of Menu.Item for individual actions and Menu.Section for grouping related items with a heading.
<Menu label="Actions">
<Menu.Section title="Event">
<Menu.Item id="view">View Details</Menu.Item>
<Menu.Item id="edit">Edit Event</Menu.Item>
</Menu.Section>
<Menu.Section title="Danger Zone">
<Menu.Item id="cancel" variant="destructive">
Cancel Event
</Menu.Item>
</Menu.Section>
</Menu>This pattern has several advantages:
- Readability: the structure of the UI is visible in the JSX tree.
- Flexibility: you control the order, nesting, and content of each part.
- Type safety: each sub-component has its own typed props, so you get autocompletion and validation.
You'll find this pattern across the design system. For example, Dialog uses Dialog.Trigger, Dialog.Title, Dialog.Content, and Dialog.Actions. Table uses Table.Header, Table.Body, Table.Row, and Table.Cell.
Layout
Components in Marigold never set their own outer spacing. A Button doesn't know or care whether it has margin around it. Instead, layout is always the responsibility of a parent layout component.
Marigold provides layout primitives like Inline, Stack, Grid, and Columns that accept props such as space and alignX to control how children are arranged.
import { Button, Inline } from '@marigold/components';export default () => ( <Inline space="related" alignX="center" alignY="center"> <Button variant="primary">Save Changes</Button> <Button variant="secondary">Cancel</Button> </Inline>);In the example above, the Inline component handles the horizontal arrangement. The space prop controls the gap between the buttons using a semantic spacing token (related), and alignX centers them horizontally.
All layout components share this same set of props. Beyond space and alignX, you will find alignY for cross-axis alignment. The space prop accepts semantic tokens that describe the relationship between elements rather than a fixed pixel value. For the full token vocabulary and how to choose the right one, see the Spacing page.
This separation has an important benefit: components remain portable. The same Button works in a dialog footer, a toolbar, or a form without any layout adjustments.
Need something that doesn't exist?
If you find yourself reaching for wrapper elements or custom CSS to achieve a layout, it might indicate a missing feature in the design system. Get in touch with the design system team so we can help.
For a deeper look at how spacing works, check out the Spacing and Layouts pages.
Form Fields
Marigold's form components share a uniform API. Once you learn how one form field works, the same knowledge transfers to all of them.
Every form field supports the same anatomy through three props:
labelprovides a visible label associated with the input.descriptionadds help text below the field for additional context.errorMessagereplaces the description when the field is in an error state.
They also share a consistent set of state props:
disabledprevents interaction with the field.readOnlyallows the value to be read but not changed.requiredmarks the field as mandatory.errorputs the field into an error state, triggering the error message.
Additionally, all form fields accept a width prop to control their sizing, supporting both fixed values and fractional widths like 1/2 or 1/3.
For a complete guide to form field anatomy, width control, states, and validation, see the Form Fields page.