saleor/saleor-dashboard · Archived

saleor-dashboard-styles

Styling guide for Saleor Dashboard React components using macaw-ui design system.

First seen Jun 24, 2026

Installation

$ npx skills add saleor/saleor-dashboard --skill saleor-dashboard-styles

Summary

  • Styling guide for Saleor Dashboard React components using macaw-ui design system.
  • Use when creating, refactoring, or modifying React components that need styling - especially layout, spacing, colors, borders, or any visual changes.
  • Triggers on component creation, UI refactors, and style-related tasks.

Stronger alternatives

This repository is archived — consider an actively maintained alternative.

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Repository health

Stars 1.0K
License LICENSE
Default branch main
Open issues 108
Status Archived

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 21,178 B
  • docs SUMMARY.md 333 B

History

  1. First seen on skills.sh
  2. First recorded snapshot · 1 installs

SKILL.md

Saleor Dashboard Styling

Two strategies for styling components. Choose based on complexity.

Entity detail vs Configuration: Entity detail chrome and in-page settings use the **Detail family (DetailSettingsCard, DetailSectionNav, …). Configuration hubs use Settings** in src/components/Settings/. Do not mix. See [saleor-dashboard-entity-detail](./saleor-dashboard-entity-detail/SKILL.md) for page anatomy, card vs section criteria, primary/secondary headers, channel availability tiers, and Vercel-aligned restraint rules.

Elevation: Use shadows sparingly. When a surface truly floats (modal, popover, menu, setup checklist), follow [saleor-dashboard-smooth-shadow](./saleor-dashboard-smooth-shadow/SKILL.md) — never border + box-shadow on the same elevated node.

Strategy 1: Box Inline Props (Simple Styles)

Use <Box> from @saleor/macaw-ui-next when you need a few CSS properties (layout, spacing, colors).

import { Box, Text } from "@saleor/macaw-ui-next";

<Box display="flex" gap={2} alignItems="center" padding={4} backgroundColor="default1">
  <Text color="default2" size={2}>
    Label
  </Text>
</Box>;

Box supports sprinkle props for: display, flexDirection, alignItems, justifyContent, gap, padding, margin, width, height, borderRadius, backgroundColor, position, cursor, opacity, flexGrow, flexShrink, flexWrap, gridTemplateColumns, gridColumn, order.

All spacing/sizing props accept token numbers: 0 | 0.5 | 1 | 1.5 | 2 | 2.5 | 3 | 3.5 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 14 | 16 | 20 | 24 | 28 | 32 | 36 | 40 | 44 | 48 | 52

Responsive values work for layout props only (display, grid, flex, order, …):

<Box display={{ mobile: "none", tablet: "flex", desktop: "grid" }} />

Do not use responsive objects for spacing (padding, margin, gap). Macaw types allow it, but runtime sprinkles only accept state conditions (default / hover / …) for those props and will throw SprinklesError (breaks Storybook + Jest + the live app). Use a CSS module media query instead:

.root {
  padding-inline: var(--mu-spacing-6);
}
@media (max-width: 768px) {
  .root {
    padding-inline: var(--mu-spacing-3);
  }
}

Escape hatch for arbitrary CSS values via __ prefix:

<Box __width="25%" __transition="background-color 0.2s ease" __minWidth="200px" />

Hover/state-dependent values:

<Box backgroundColor={{ default: "transparent", hover: "default2" }} />

When Box Props Are Enough

  • Flex/grid layouts with spacing
  • Padding, margin, gap adjustments
  • Background and text colors from the design system
  • Border radius
  • Simple responsive breakpoints

Strategy 2: CSS Modules (Complex Styles)

Use .module.css when you need pseudo-selectors, animations, media queries, complex selectors, or more than ~5 CSS rules.

Create ComponentName.module.css next to ComponentName.tsx. One CSS file per component. Never share CSS module files across components.

/* SearchInput.module.css */
.input {
  flex: 1;
  border: none;
  outline: none;
  background-color: transparent;
  font-size: 14px;
  color: var(--mu-colors-text-default1);
  min-width: 0;
}

.input::placeholder {
  color: var(--mu-colors-text-default2);
}
// SearchInput.tsx
import styles from "./SearchInput.module.css";

<input className={styles.input} />;

When to Use CSS Modules

  • Pseudo-selectors: ::placeholder, :hover, :focus, [data-state="open"]
  • Animations/transitions: @keyframes, complex transition
  • Media queries: @media (min-width: 960px)
  • Nested/compound selectors: .row:hover .icon
  • More than ~5 CSS rules for a single element

CSS Module Patterns

Hover reveal:

.row .icon {
  opacity: 0;
  transition: opacity 0.15s ease-in-out;
}
.row:hover .icon {
  opacity: 1;
}

Collapsible rotation:

.chevron {
  transition: transform 0.2s ease;
  transform: rotate(-90deg);
}
button[data-state="open"] .chevron {
  transform: rotate(0deg);
}

Responsive layout:

.sidebar {
  display: none;
}
@media (min-width: 1200px) {
  .sidebar {
    display: block;
  }
}

Design Tokens

Always use macaw CSS variables instead of hardcoded values (especially colors, spacing, borders).

In CSS Modules

Use var(--mu-*) variables. Read the full list from: node_modules/@saleor/macaw-ui/dist/style.css

Common patterns:

color: var(--mu-colors-text-default1);
background-color: var(--mu-colors-background-default2);
border: 1px solid var(--mu-colors-border-default1);

In TypeScript

Import vars from @saleor/macaw-ui-next for JS-accessible tokens:

import { vars } from "@saleor/macaw-ui-next";

vars.spacing[2]; // spacing token
vars.colors.border.default1; // border color
vars.colors.text.default2; // text color

Full TypeScript token structure: node_modules/@saleor/macaw-ui/dist/theme/contract.css.d.ts

Foldable sections (DetailGroupBox)

For expandable/collapsible sections on detail and create pages, use DetailGroupBox — not the legacy @dashboard/components/Accordion wrapper and not a one-off Macaw Accordion with custom chrome.

Component: [src/components/DetailGroupBox/DetailGroupBox.tsx](../../src/components/DetailGroupBox/DetailGroupBox.tsx)

Reference usages:

  • Shipping zone rates: src/shipping/components/ShippingZoneRates/ShippingZoneRateItem.tsx
  • Shipping postal codes: src/shipping/components/ShippingZonePostalCodes/ShippingZonePostalCodes.tsx
  • Countries list, metadata editors, translation groups
import { DetailGroupBox } from "@dashboard/components/DetailGroupBox/DetailGroupBox";
import { Title2 } from "@dashboard/components/Title2/Title2";
import { Box } from "@saleor/macaw-ui-next";

<DetailGroupBox
  groupId="unique-section-id"
  dataTestId="my-section"
  triggerButtonTestId="my-section-expand"
  defaultExpanded={false}
  marginTop={4}
  headerStart={<Title2>{title}</Title2>}
  headerEnd={optionalMetaOrActions}
>
  <Box padding={5}>{/* section body */}</Box>
</DetailGroupBox>;

Rules

  • Header title: prefer Title2 for the foldable label (same as shipping rates)
  • groupId: stable unique string (or entity id when listing many foldables)
  • defaultExpanded: false for optional/advanced content; true only when the section is primary
  • Do not full-bleed: nest DetailGroupBox inside DashboardCard.Content (or another padded container). Shipping zone rates/postal codes do this — a bare DetailGroupBox as a page sibling of cards spans the content column edge-to-edge and looks wrong
  • Body padding: content area has no built-in padding — wrap children in <Box padding={5}> (or match the surrounding list/table pattern)
  • Actions in the header: put icon buttons in headerEnd and call event.stopPropagation() so they don’t toggle expand/collapse
  • Do not use @dashboard/components/Accordion for new UI — that is the older bordered accordion pattern

When to use what

Need Use
Foldable section inside a card (advanced settings, rate row, postal ranges) DetailGroupBox inside DashboardCard.Content
Full always-visible settings block on entity detail DetailSettingsCard (+ DetailSetting* rows) — see entity-detail skill
Full always-visible block on Configuration hub SettingsSection + SettingsToggleRow
Shipping-style card with subtitle hints DashboardCard + DashboardCard.Subtitle
Nested disclose inside an already-padded surface DetailGroupBox with marginTop={0} when it is the only content

Typography & semantics (shared)

  • Prefer macaw Text tokens (size, fontWeight, color) over browser defaults or ad-hoc CSS font-size.
  • Section card titles on entity detail: always Text size={5} fontWeight="bold" as="h2" — owned by DetailSettingsCard (and matching secondary cards). Pass title content only.
  • Foldable nested rows: Title2 inside DetailGroupBox — do not invent a third heading style.
  • Column / table headers (datagrid, list headers) stay secondary (size={2}3, regular/medium) — they are not section titles.
  • One visual system for the same role across collections, categories, channels, shipping — if a title looks “off”, fix the shared primitive, not a one-off page style.

Interactive affordances (hover / focus)

Every clickable control must show a hover (and focus-visible) state. Prefer one of:

Pattern When
Underline on hover Text links, channel name links, product name rows, microcopy links
Color change on hover Icon buttons, chips, rows that don’t read as links (default2default1, or background tint)
Both Dense ops lists where underline alone is easy to miss

Do not ship interactive elements that only change the cursor. Focus-visible outlines stay required for keyboard users (outline / macaw focus rings).

Links in UI components — prefer normal text color

Inside cards, sidebars, provenance, tooltips, and other product UI chrome, prefer normal text colors over accent blue:

Use Color Hover
In-component navigation (Link color="secondary") default1 Underline (built into Link when not already underlined)
Hint / subtitle embeds MicrocopyLinkinherit Underline
Read-only / clickable channel ChannelDisplay / ChannelDetailsLink / ClickableChannel Underline on name (or color on ClickableChannel)
Marketing / rare emphasis Link default primary (accent1) Underline
// ✅ Preferred in entity detail / sidebar chrome
<Link href={orderUrl} color="secondary">
  <Text size={3} as="span">#{orderNumber}</Text>
</Link>

// ✅ Channel with globe icon
<ChannelDetailsLink channel={channel} size={3} color="default1" fontWeight="regular" />

// ❌ Avoid for body/meta links in cards
<Link href={orderUrl}>#{orderNumber}</Link> // accent blue

Channel display primitives live in src/components/Channel/Channel.tsx:

  • ChannelDisplay — read-only name + globe icon (optional inactive suffix)
  • ChannelDetailsLink — same chrome, links to channel details
  • ClickableChannel — same chrome, links to orders filtered by channel (permission-gated)

Do not hand-roll Globe + blue Link for channel names.

Price inputs (PriceFieldV2) — preferred

Use [PriceFieldV2](../../src/components/PriceFieldV2/PriceFieldV2.tsx) for all new money/amount fields (channel prices, voucher fixed amounts, cost prices, etc.). Do not add new type="number" money inputs or revive legacy PriceField.

Concern How
Component PriceFieldV2 — right-aligned, tabular nums, currency as endAdornment
Typing / paste normalization formatPriceInput via usePriceFieldV2 (US/EU/Swiss separators, currency decimal places)
Blur Pads to currency decimals (10.210.20 for USD)
Spreadsheet column paste sanitizeSpreadsheetPrice + applySpreadsheetColumnPaste / bulk-publish multi-field paste
Currency prop Pass currency code (USD, PLN) as currencySymbol — drives both adornment and decimal precision

Canonical references:

  • Bulk publish review: BulkPublishReviewRow.tsx (PriceFieldV2 + TSV paste)
  • Voucher fixed amount per channel: VoucherFixedAmountChannelList.tsx
import { PriceFieldV2 } from "@dashboard/components/PriceFieldV2/PriceFieldV2";

<PriceFieldV2
  currencySymbol={channel.currencyCode}
  value={price}
  onChange={setPrice}
  aria-label={…}
/>

Default (medium) input size for channel amount lists — size="small" feels cramped next to currency adornments. Use small only in dense dialogs (e.g. bulk publish).

Row-list UIs with aligned amount columns should support spreadsheet paste (onPasteCapture + src/utils/spreadsheetPaste/). See also docs/follow-ups/spreadsheet-paste-reuse.md.

Channel amount lists (Pricing / Order value / voucher amounts)

Canonical references: VoucherFixedAmountChannelList, OrderValue (shipping method), PricingCard (shipping method).

Piece Rule
Paste hint band Optional Text size={2} in a tinted band (padding Y spacing-3). One short line — not a second card header. Multi-column lists (Order value min/max) should mention tab-separated rows, not only “a column”.
Column label row (subheader) Always include, even for single-amount lists (Pricing: Channel name / Price). Keep the band compact (Text size={2}, Y ~spacing-1) — never reuse value-row min-height or value-row Y padding.
Value rows Default-size PriceFieldV2 (not small); row min-height ~3.75rem, Y spacing-2. Amount columns ≥15rem so ~5 digits + decimals + currency adornment fit without clipping.
Shared grid Share grid-template-columns between label row and value rows; split padding/min-height rules so labels stay short.

Anti-patterns:

  • One CSS rule for .headerRow, .row { min-height: 3.25rem; padding: … } — labels inherit a tall input row.
  • Skipping the label row on single-amount channel lists (“obvious” Price column) — keep Channel name / Price so Pricing matches Order value. Prefer the same on voucher amount lists when you touch them.
  • Material-UI Table / TableCell for channel amount or breakdown lists — prefer macaw Box + CSS grid like OrderValue / PricingCard / ShippingZoneRateChannelTable. Fighting MUI head padding is a losing battle.

Entity detail settings card (DetailSettingsCard)

Primary bordered settings surface on entity detail pages. Full rules (primary vs secondary header, card vs section, Vercel restraint) live in [saleor-dashboard-entity-detail](./saleor-dashboard-entity-detail/SKILL.md).

Piece Style
Card shell DetailSettingsCard.module.cssdefault1 body, 8px radius, 1px border
Primary header Tinted default2 band; align-items: center; title left, headerEnd right
Title Always Text size={5} fontWeight="bold" as="h2" — string and ReactNode titles
Header with action .headerWithEnd — same Y padding as title-only; card coerces headerEnd buttons to size="small"; right inset spacing-4
Leading copy intro prop — white band + bottom border below header (not under title in tinted band)
Optional in title DetailSettingsCardTitle optional + DetailSettingsOptionalLabel (size={2}, default2)
Body .content padding 5/6; contentFlush for lists and upload zones

Secondary sidebar ops cards (AssignListCard, ChannelInventoryCard) use white headers with meta on the right — not DetailSettingsCard.

In-card assignable lists (AssignableList*)

Flush lists inside DetailSettingsCard (products in a collection, attribute values, voucher codes, product-type attributes). The primitive owns padding, heading height, hover-reveal delete, and pagination. Feature views pass columns, rows, and the assign action.

Piece Token / rule
Composition AssignableListCard + AssignableListTable (or MUI tableStyles.assignableTable) + AssignableListPagination
Header ± button Same height either way — DetailSettingsCard coerces headerEnd to size="small"
Search Card search slot — Y spacing-3, X spacing-6. Do not pad SearchInput in the view.
Heading row Compact subhead: Y spacing-1, Text size={2} — same as Pricing / channel column labels
Rows density="compact" (text) or "media" (50px thumbnails)
Hover Row delete opacity 0 → 1 on hover or delete-control focus (not row :focus-within)
Pagination Component draws the top border. Last table row has no bottom border. `inset="card" \ "nested" \ "drag"`

Layout numbers: src/components/AssignableListTable/assignableListTableLayout.ts. Full composition and anti-patterns: [saleor-dashboard-entity-detail](./saleor-dashboard-entity-detail/SKILL.md) → In-card assignable lists.

Do not add a one-off .listHeader / .pagination { border-top } in a feature module.css.

Anti-patterns

  • No inline style={{}} - Use Box props or CSS Modules instead
  • No plain .css files for components - Use .module.css to scope styles
  • No hardcoded colors - Use var(--mu-colors-*) or Box color props
  • No shared CSS modules - Each component gets its own .module.css file
  • Combining Box + CSS Modules is OK - Use Box for layout, CSS Module for complex styling within the same component
  • No legacy @dashboard/components/Accordion for new foldable sections - Use DetailGroupBox (see above)
  • No border + box-shadow on elevated surfaces - See [saleor-dashboard-smooth-shadow](./saleor-dashboard-smooth-shadow/SKILL.md)
  • No bare browser heading styles for card titles - Do not style only string titles; the primitive must style all title nodes
  • No new type="number" money fields - Use PriceFieldV2 (see above)
  • No tall column-label bands in channel amount lists - Label rows stay compact (spacing-1 Y); do not reuse value-row min-height (see Channel amount lists above)