# Merid > Merid is an accessible React component library (`@meridui/react`) built on plain CSS, cascade layers and `--mrd-*` design tokens: 1px hairlines, one cool accent, soft grey trays and calm motion. React 18 and 19, server components, light and dark themes. This file contains the AI rules, the design contract and every page of the English docs at https://meridui.dev/docs. Each page starts with its source URL. --- # Merid UI rules This project uses Merid (`@meridui/react`) for its UI. Follow these rules when you write or change interface code. Docs: https://meridui.dev/docs · full text for LLMs: https://meridui.dev/llms-full.txt ## Imports - Import components from the package root only: `import { Button, Dialog, Field, Input } from "@meridui/react";`. Never deep-import from `@meridui/react/dist/...` or copy component source into the project. - Import the stylesheet once, at the app entry (root layout, `main.tsx`): `import "@meridui/react/styles.css";`. Do not import it per component. - Compound components use dot parts (`Dialog.Root`, `Dialog.Content`, `Select.Item`). In React Server Component files use the flat exports (`DialogRoot`, `DialogContent`, …). - For router links use `asChild`: ``. ## Compose before you create 1. Check whether a Merid component already does the job (Button, IconButton, Link, Field, Input, Textarea, Select, NativeSelect, Checkbox, Radio, Switch, SegmentedControl, Card, Stack, Grid, Container, Section, Heading, Text, Table, Badge, Avatar, Tabs, Accordion, Alert, `ToastProvider` + `useToast`, Progress, Spinner, Skeleton, EmptyState, Dialog, AlertDialog, Drawer, Popover, Tooltip, DropdownMenu, Breadcrumb, Pagination, Stepper, SidebarNav, Separator, Kbd, Code, VisuallyHidden). 2. If not, compose existing components (for example a settings row is `Section` + `Field` + `Switch`; a toolbar is `Stack direction="row"` + `Button`/`IconButton`). 3. Only then write a new component, and style it with Merid tokens as below. Do not wrap Merid components just to rename them or restyle them with overrides. 4. Look up props before using them (docs page or the `get_component` MCP tool). Do not invent props, variants or sizes. ## Styling: tokens only - Colours come only from semantic `--mrd-*` tokens: `--mrd-ink`, `--mrd-body`, `--mrd-muted` for text; `--mrd-bg`, `--mrd-surface`, `--mrd-tray`, `--mrd-subtle` for surfaces; `--mrd-line` for borders; `--mrd-accent*` for interactive and selected states; `--mrd-danger*`, `--mrd-warning-*`, `--mrd-success*` for status. - No raw hex, `rgb()`, `hsl()` or named colours in components or CSS. No primitive palette tokens (`--mrd-blue-500`) either; they do not follow themes or accent presets. - Spacing comes from the scale (`--mrd-space-1` … `--mrd-space-28`, or the `gap`/`padding` props of `Stack`, `Grid`, `Card`, `Section`). No arbitrary pixel values such as `margin: 13px` or Tailwind arbitrary values like `p-[13px]`. - Radius from `--mrd-radius-*`, shadows from `--mrd-shadow-*`, type from `--mrd-text-*` / `--mrd-leading-*` / `--mrd-weight-*`, motion from `--mrd-duration*` / `--mrd-ease`. - No inline `style={{ … }}` for colour, spacing, radius or typography. Use component props, a CSS class that reads tokens, or `data-*` attributes. (Setting a single CSS variable inline, such as `style={{ "--mrd-button-height": "32px" }}`, is the one allowed exception.) - Borders are 1px `--mrd-line`. Never 2px, never dark borders. Separate regions by surface (`--mrd-tray`) before adding a border. - One `variant="primary"` button per view. No gradients. Font weight 700 is never used. - Theme, accent and density are attributes, not classes: `data-theme="dark"`, `data-accent="violet"`, `data-density="compact"` on `` or any subtree. - Write your own CSS outside Merid's layers (or in a later layer). Never use `!important` to beat Merid; its rules live in `@layer merid.*` and lose to unlayered CSS. ## Accessibility - Every form control has a visible label: wrap it in `Field label="…"` (which wires `id`, `aria-describedby`, invalid and required) or use `Label`. - Icon-only actions use `IconButton` with a `label`. Never a bare ``. ## Compose before you create 1. Check whether a Merid component already does the job (Button, IconButton, Link, Field, Input, Textarea, Select, NativeSelect, Checkbox, Radio, Switch, SegmentedControl, Card, Stack, Grid, Container, Section, Heading, Text, Table, Badge, Avatar, Tabs, Accordion, Alert, `ToastProvider` + `useToast`, Progress, Spinner, Skeleton, EmptyState, Dialog, AlertDialog, Drawer, Popover, Tooltip, DropdownMenu, Breadcrumb, Pagination, Stepper, SidebarNav, Separator, Kbd, Code, VisuallyHidden). 2. If not, compose existing components (for example a settings row is `Section` + `Field` + `Switch`; a toolbar is `Stack direction="row"` + `Button`/`IconButton`). 3. Only then write a new component, and style it with Merid tokens as below. Do not wrap Merid components just to rename them or restyle them with overrides. 4. Look up props before using them (docs page or the `get_component` MCP tool). Do not invent props, variants or sizes. ## Styling: tokens only - Colours come only from semantic `--mrd-*` tokens: `--mrd-ink`, `--mrd-body`, `--mrd-muted` for text; `--mrd-bg`, `--mrd-surface`, `--mrd-tray`, `--mrd-subtle` for surfaces; `--mrd-line` for borders; `--mrd-accent*` for interactive and selected states; `--mrd-danger*`, `--mrd-warning-*`, `--mrd-success*` for status. - No raw hex, `rgb()`, `hsl()` or named colours in components or CSS. No primitive palette tokens (`--mrd-blue-500`) either; they do not follow themes or accent presets. - Spacing comes from the scale (`--mrd-space-1` … `--mrd-space-28`, or the `gap`/`padding` props of `Stack`, `Grid`, `Card`, `Section`). No arbitrary pixel values such as `margin: 13px` or Tailwind arbitrary values like `p-[13px]`. - Radius from `--mrd-radius-*`, shadows from `--mrd-shadow-*`, type from `--mrd-text-*` / `--mrd-leading-*` / `--mrd-weight-*`, motion from `--mrd-duration*` / `--mrd-ease`. - No inline `style={{ … }}` for colour, spacing, radius or typography. Use component props, a CSS class that reads tokens, or `data-*` attributes. (Setting a single CSS variable inline, such as `style={{ "--mrd-button-height": "32px" }}`, is the one allowed exception.) - Borders are 1px `--mrd-line`. Never 2px, never dark borders. Separate regions by surface (`--mrd-tray`) before adding a border. - One `variant="primary"` button per view. No gradients. Font weight 700 is never used. - Theme, accent and density are attributes, not classes: `data-theme="dark"`, `data-accent="violet"`, `data-density="compact"` on `` or any subtree. - Write your own CSS outside Merid's layers (or in a later layer). Never use `!important` to beat Merid; its rules live in `@layer merid.*` and lose to unlayered CSS. ## Accessibility - Every form control has a visible label: wrap it in `Field label="…"` (which wires `id`, `aria-describedby`, invalid and required) or use `Label`. - Icon-only actions use `IconButton` with a `label`. Never a bare ` ); } ``` ## Variants, sizes and state Variants and sizes are props. Under the hood they become data attributes — never modifier classes — which makes them easy to target from your own CSS. ```tsx // renders Home ``` ## Passing props across the boundary Props passed from a server component to a client component must be serialisable. Strings, numbers, plain objects and React elements are fine. Functions are not, so event handlers such as `onOpenChange` must be defined in a client component. ```tsx // app/settings/page.tsx — a server component import { Button, Card, DialogContent, DialogDescription, DialogRoot, DialogTitle, DialogTrigger } from "@meridui/react"; import { DeleteAccount } from "./delete-account"; // "use client" inside: it passes onClick export default function Settings() { return (

Danger zone

{/* Interactive Merid components work here directly — with flat part names */} What gets deleted Projects, files and billing history.
); } ``` ## Styles The stylesheet is static CSS, so it is collected at build time and never depends on rendering. There is no style injection at runtime and no flash of unstyled content during streaming. ## Portals Overlays render into `document.body` through a portal after hydration. On the server they render nothing, which keeps the initial HTML free of closed dialogs and menus. See [Portal](https://meridui.dev/docs/components/portal) for how portalled content keeps its subtree's theme, accent, density and direction. --- # Browser support Merid supports the last two major versions of evergreen browsers and the Safari releases that are still receiving security updates. | Browser | Minimum version | | --- | --- | | Chrome and Edge | 111 | | Firefox | 113 | | Safari (macOS and iOS) | 16.4 | Internet Explorer and legacy Edge are not supported. ## CSS features in use The minimum versions follow from a short list of CSS features that Merid depends on: - **Cascade layers** (`@layer`) for the override model. - **`:where()`** for zero-specificity base styles. - **`color-mix()`** for a few derived tints. - **`:focus-visible`** for keyboard-only focus rings. - **`text-wrap: balance`** for headings. Where it is missing, headings simply wrap normally. ## React versions React 18.2 and newer are supported, including React 19. Merid does not rely on experimental React APIs. ## Testing Components are tested in jsdom for behaviour and accessibility, and checked manually in the browsers above before each minor release, with VoiceOver on macOS and iOS and NVDA on Windows. --- # Accessibility Merid targets conformance with the **Web Content Accessibility Guidelines (WCAG) 2.2, level AA**. Accessibility is part of each component's definition of done, not a later pass. ## What every component guarantees - **Semantics.** Native elements are used wherever they exist. Where they do not, roles, states and properties follow the WAI-ARIA Authoring Practices Guide. - **Keyboard.** Everything that can be done with a pointer can be done with a keyboard. Each component page documents its keys. - **Focus.** Focus is always visible: a 2px accent outline offset by 3px, or an accent border with a soft halo on inputs. Overlays trap focus while open and return it to the trigger when closed. - **Target size.** Interactive targets are at least 24 × 24 CSS pixels, and controls grow on coarse pointers. - **Zoom and reflow.** Layouts work at 200% zoom and at 320 CSS pixels wide without horizontal scrolling. - **Motion.** When `prefers-reduced-motion` is set, all transitions and animations are removed, including the skeleton shimmer. - **Contrast.** Body and UI text meet 4.5:1. The muted tone is reserved for meta text of 13px and larger. See [Color](https://meridui.dev/docs/foundations/color) for measured ratios. ## How it is tested Every component has automated tests that run axe against its rendered states, and interaction tests that drive it with the keyboard. Before a minor release, components are checked manually with VoiceOver and NVDA. ## Your responsibilities Merid cannot label your content for you. Give icon-only buttons an `aria-label`, give every form field a visible label, and write link text that makes sense out of context. ## Known limitations Known issues are tracked in the repository with the `accessibility` label. If you find a barrier, please [open an issue](https://github.com/ahmetcantryk/merid/issues/new/choose) — accessibility bugs are treated with the same priority as functional ones. --- # Versioning Merid follows [Semantic Versioning](https://semver.org). Releases are prepared with Changesets and published to npm with provenance. ## What counts as public API - Component names, props and their documented values. - The exported TypeScript types. - Documented `.mrd-` class names, part classes and data attributes. - Token names (`--mrd-*`) and the cascade layer names. Token *values* are design decisions. A value may change in a minor release when it fixes a contrast problem or brings a component back in line with the design contract. Such changes are always listed in the changelog. ## Before 1.0 While the version is `0.x`, a minor release (`0.1` → `0.2`) may contain breaking changes. Each one is marked in the changelog with a migration note. Patch releases never break. ## Deprecation policy 1. A deprecated API keeps working for at least one minor release before 1.0, and one major release after. 2. Deprecated props and components are marked with `@deprecated` in the types, so your editor strikes them through. 3. In development builds, the first use logs a single console warning naming the replacement. 4. The docs page shows a *Deprecated* status and the migration path. ## Support window Only the latest minor release receives fixes. Security fixes are backported to the previous minor release for three months after a new minor ships. --- # Changelog All notable changes to `@meridui/react`. The same notes are kept in `CHANGELOG.md` in the repository. ## 0.1.0 *28 September 2026 — first public release.* ### Added - 46 components: layout (Container, Section, Stack, Grid, Separator), typography (Heading, Text, Link, Code, Kbd, Label, VisuallyHidden), actions (Button, IconButton), forms (Field, Input, Textarea, NativeSelect, Select, Checkbox, RadioGroup, Switch, SegmentedControl), navigation (Tabs, Breadcrumb, Pagination, SidebarNav, Stepper), overlays (Dialog, AlertDialog, Drawer, Popover, Tooltip, DropdownMenu, Portal), feedback (Alert, Toast, Progress, Spinner, Skeleton, EmptyState) and data display (Card, Badge, Avatar, Table, Accordion). - Design tokens for colour, typography, spacing, radius, shadow, motion, layout and layers, with light and dark values. Text and fill pairs meet WCAG AA contrast; `--mrd-accent-solid` / `--mrd-accent-solid-hover` are the fills that carry on-accent text. - `tokens.css` and `styles.css` entry points, organised in the `merid.tokens`, `merid.base` and `merid.components` cascade layers. - Opt-in base styles through the `mrd-root` class. - `asChild` on Dialog, Drawer and Popover Trigger/Close, AlertDialog Trigger/Action/Cancel and DropdownMenu.Trigger, to render your own `Button`. - `Dialog.Content size` (`sm`, `md`, `lg`, `full`) and `Drawer.Content size` (`sm`, `md`, `lg`); `AlertDialog.Action tone` (`primary`, `danger`). Types `DialogSize`, `DrawerSize`, `AlertDialogActionTone`. - `Field` wiring for Input, Textarea, NativeSelect, Checkbox, Switch and RadioGroup, with `aria-invalid` for semantics and `data-invalid` for styling. - `"use client"` on every interactive module, so components import directly into React Server Components. - Bundled Geist and Geist Mono variable fonts under the SIL Open Font License 1.1. - Documentation site with foundations, accessibility statement and versioning policy. --- # Roadmap The roadmap is a statement of intent, not a promise of dates. Priorities are discussed openly in GitHub Discussions. ## 0.2 — Forms Button, IconButton, Input, Textarea, Select, Checkbox, Radio, Switch, SegmentedControl and Field, with consistent labelling, helper text and error messaging. ## 0.3 — Structure and data Card, Tabs, Accordion, Table, Badge, Avatar, Skeleton, Toast and Menu. A documented pattern for empty and loading states. ## 0.4 — Navigation Breadcrumbs, Pagination and a sidebar navigation pattern for application shells. ## Towards 1.0 - A stable API reviewed against real product use. - Visual regression tests for every component in both themes. - A migration guide from 0.x. ## Not planned - Charts. Merid will document how to style a charting library with its tokens instead. - A CSS-in-JS build. Plain CSS is a core decision. - Multiple accent colours per theme. --- # FAQ ## Can I use Merid without React? The stylesheet can be used on its own with the documented class names and data attributes, but the behaviour — focus management, keyboard handling, ARIA wiring — lives in the React components. Only React is supported. ## Does it work with Tailwind CSS? Yes. Declare Merid's cascade layers before Tailwind's utilities layer so utilities still win. See [Styling and CSS layers](https://meridui.dev/docs/styling). ## Can I change the accent colour? Yes, through `--mrd-accent` and its related tokens. Merid is designed around one accent, so there is no second brand colour to configure. ## Why no gradients? Gradients compete with content and age quickly. The only gradient in the library is the skeleton shimmer, and it is removed under reduced motion. ## Is there a Figma library? Not yet. The tokens are documented precisely enough to rebuild them as variables, and a Figma library is under consideration after 1.0. ## How large is the stylesheet? The full stylesheet is plain CSS without generated utilities. Import `tokens.css` alone if you only need the variables. ## Who maintains Merid? Merid is an independent open-source project. See [Contributing](https://meridui.dev/docs/contributing) if you would like to help. --- # Contributing Contributions are welcome — bug reports, documentation fixes and components alike. The full guide lives in `CONTRIBUTING.md`; this page is the short version. ## Set up ```bash git clone https://github.com/ahmetcantryk/merid.git cd merid npm install npm run dev ``` The documentation site runs at `http://localhost:3210`. The library lives in `packages/react`, the site in `apps/docs`. ## Before you open a pull request - Read the [design contract](https://meridui.dev/docs/foundations/principles). Changes that add colours, radii or shadows outside the tokens will not be merged. - Run `npm run lint`, `npm run typecheck` and `npm test`. - Add a changeset with `npx changeset` for any change that affects the published package. - For new components, open an issue first so the API can be agreed before you write code. ## Code of conduct Everyone taking part is expected to follow the Contributor Covenant. Reports go through GitHub's private channels described in `CODE_OF_CONDUCT.md`. --- # Design tokens Every colour, size, shadow and duration in Merid is a CSS custom property. Components never contain a raw value that is not also a token, so changing a token changes every place it is used. ## Naming All tokens share the `--mrd-` prefix and fall into three tiers. | Tier | Example | Purpose | | --- | --- | --- | | Primitive | `--mrd-space-4` | A raw step on a scale. Has no meaning on its own. | | Semantic | `--mrd-accent`, `--mrd-tray` | Named for its role. Changes between light and dark. | | Component | `--mrd-control-md` | Sizes shared by a family of components. | Prefer semantic tokens in your own code. `var(--mrd-tray)` follows dark mode; a hex value does not. ## Categories - [Color](https://meridui.dev/docs/foundations/color) — accent, surfaces, lines, text tones and status. - [Typography](https://meridui.dev/docs/foundations/typography) — font stacks, nine sizes with line height and tracking, three weights. - [Spacing](https://meridui.dev/docs/foundations/spacing) — a 4 and 8 pixel scale plus layout widths. - [Radius](https://meridui.dev/docs/foundations/radius) — nine corner sizes that step down as surfaces nest. - [Elevation](https://meridui.dev/docs/foundations/elevation) — ink-tinted shadows and the focus halo. - [Motion](https://meridui.dev/docs/foundations/motion) — durations and the single easing curve. - [Breakpoints and layers](https://meridui.dev/docs/foundations/layout) — the two breakpoints and the z-index scale. ## Consuming tokens Import the tokens without the component rules when you only need the variables: ```tsx import "@meridui/react/tokens.css"; ``` Then use them anywhere CSS accepts a value: ```css .panel { padding: var(--mrd-space-7); border-radius: var(--mrd-radius-card); background: var(--mrd-tray); color: var(--mrd-body); } ``` ## Overriding tokens Set a token on `:root` to change it globally, or on any element to scope the change to that subtree. Tokens live in the `merid.tokens` layer, so a plain unlayered declaration always wins. See [Styling and CSS layers](https://meridui.dev/docs/styling). --- # Color Merid's palette is one cool blue and a set of ink-tinted neutrals. The accent marks interaction and selection; neutrals carry everything else. Status colours appear as soft tints with strong text, never as large saturated fills. ## Rules - Use the accent for things people can act on, and for the current selection. Do not use it for decoration or emphasis. - Separate regions with `--mrd-tray` before reaching for a border. When a border is needed it is 1px `--mrd-line`. - Build hierarchy with the three text tones — `ink`, `body`, `muted` — and weight, not with colour. - A page has at most one filled colour block. ## Tokens ## Contrast Ratios below are measured against the page background (`--mrd-bg`) unless noted. The AA thresholds are 4.5:1 for normal text and 3:1 for large text and user-interface components. | Pair | Light | Dark | Use | | --- | --- | --- | --- | | ink on bg | 18.74 | 17.03 | Headings, strong text | | body on bg | 6.94 | 8.24 | Body text | | body on tray | 6.42 | 7.39 | Text on recessed sections | | accent-strong on accent-soft | 6.98 | — | Selected items, info tints | | danger / warning / success strong on soft | 5.94 / 5.46 / 5.30 | — | Notices | | muted on bg | 3.21 | 4.25 | Meta text only | | accent on bg | 4.32 | 6.20 | Links, focus ring, controls | | on-accent on accent | 4.32 | 3.13 | Primary button label | > **Known contrast gaps in 0.1.** Three pairs fall short of 4.5:1 for normal-size text: the muted tone on the page, the light accent used as link text, and white labels on the accent fill (most noticeably in dark mode). They meet the 3:1 threshold for large text and UI components. Fixes to `--mrd-muted`, `--mrd-accent` and the dark `--mrd-on-accent` are tracked for the next minor release and will be listed in the changelog. Until then, avoid muted text for anything a reader must understand. ## Using colour in your own components ```css .notice { background: var(--mrd-warning-soft); color: var(--mrd-warning-strong); border-radius: var(--mrd-radius-lg); } ``` --- # Dark mode Every semantic colour token has a dark value. Components never need a dark variant: they read the tokens, and the tokens change. ## How the theme is chosen 1. If an element has `data-theme="dark"`, it and its descendants use dark values. 2. If an element has `data-theme="light"`, they use light values, even when the system prefers dark. 3. Otherwise the tokens follow `prefers-color-scheme`. Because the attribute works on any element, you can render a dark panel inside a light page, or the other way round. ```tsx
{/* everything in here uses dark tokens */}
``` ## Adding a theme switch Store the user's choice, apply it to `` before the first paint, and fall back to the system preference when nothing is stored. ```html ``` The script is wrapped in `try` because storage can be unavailable in private windows or when site data is blocked. Place it in the document `head` so it runs before the body renders. ## What changes in dark mode - Surfaces step lighter as they rise: page `#0b0d12`, surface `#12151c`, tray `#161a22`. - Shadows switch from ink tints to deeper black, and elevated surfaces gain a 1px `--mrd-line` ring, because shadows alone are hard to see on dark backgrounds. - The accent lightens to `#6b8aff` to keep contrast against the dark page. - Status tints become translucent so they sit on any surface. ## Checking your own components Test both themes whenever you add colour. A quick way is to toggle `data-theme` on `` in the browser's element inspector. --- # Typography Merid sets text in **Geist** and code, labels and token names in **Geist Mono**, both bundled as variable fonts. Numbers are tabular everywhere, so figures line up in tables and prices do not shift as they change. ## Scale Sizes are tokens: `--mrd-text-*` for the size, with matching `--mrd-leading-*` for line height and `--mrd-tracking-*` for letter spacing where it applies. The display and section sizes use `clamp()` and scale with the viewport. Specimens above show them at a representative desktop size. ## Weights - **400** (`--mrd-weight-regular`) — body text. - **500** (`--mrd-weight-medium`) — all interface text: buttons, labels, navigation, tabs. - **600** (`--mrd-weight-semibold`) — headings and figures that need emphasis. Weight 700 is never used. When 600 is not enough contrast, change the tone instead. ## Rules - Large headings are tight: negative tracking and a line height close to 1. Body text is relaxed at 1.6. - Headings use `text-wrap: balance` so short titles do not leave a single word on the last line. - Keep reading width at or below `--mrd-prose` (720px). - Use the three text tones for hierarchy: `--mrd-ink` for headings, `--mrd-body` for text, `--mrd-muted` for meta. ## Font stacks ```css --mrd-font-sans: "Geist", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; --mrd-font-mono: "Geist Mono", ui-monospace, SFMono-Regular, Menlo, monospace; ``` --- # Spacing Spacing is built on a 4px base with an 8px rhythm. The scale skips steps on purpose: fewer choices make layouts more consistent. ## Scale ## Layout tokens | Token | Value | Use | | --- | --- | --- | | `--mrd-container` | 1160px | Maximum content width | | `--mrd-gutter` | 24px, 20px at 640px and below | Horizontal page padding | | `--mrd-section` | 112px, 80px at 920px, 64px at 640px | Vertical section padding | | `--mrd-prose` | 720px | Maximum reading width | ## Rules - Prefer air. Sections are separated by generous padding, not by rules or boxes. - Space inside a component is smaller than the space between components. - Card padding is `--mrd-space-7` (28px); dialog padding is `--mrd-space-8` (32px). - Use `gap` on flex and grid containers rather than margins on children. --- # Radius Merid's corners are generous and consistent. Larger surfaces get larger radii, and radius steps down as elements nest, so inner corners stay concentric with outer ones. ## Scale ## Nesting The rule of thumb is **20 → 14 → 12 → 8**. A card uses `--mrd-radius-card` (20px); a window frame inside it uses `--mrd-radius-xl` (14px); a button inside that uses `--mrd-radius-lg` (12px); a small tile inside a button uses `--mrd-radius-sm` (8px). - **Do:** Step radius down as you nest, so the gap between two edges looks even all the way round. - **Don't:** Give an inner element the same radius as its container. The inner corner will look pinched. ## Pills `--mrd-radius-full` is for elements whose shape is defined by their height: pills, badges, switches and avatars. Do not use it on buttons with text; Merid buttons use `--mrd-radius-lg`. --- # Elevation Elevation in Merid comes from surfaces first and shadows second. A card on the page sits on a grey tray; a card on a tray flips to a white surface with a soft shadow. Borders are the last resort. ## Shadow scale All shadows are long, soft and faint, tinted with ink (`rgb(15 18 25 / …)`) rather than black. The only saturated shadow is the primary button's accent glow. ## Choosing a level | Surface | Shadow | | --- | --- | | Selected chip in a segmented control | `--mrd-shadow-xs` | | Card on a tray, interactive card on hover | `--mrd-shadow-md` | | Popover, toast, tooltip | `--mrd-shadow-lg` | | Menu | `--mrd-shadow-xl` | | Dialog | `--mrd-shadow-2xl` | ## In dark mode Shadows become black at 40–60% opacity, and elevated surfaces add a 1px ring through `--mrd-elevated-ring`. Include it next to the shadow so your own surfaces behave the same way: ```css .floating-panel { box-shadow: var(--mrd-shadow-lg), var(--mrd-elevated-ring); } ``` ## Focus halo `--mrd-focus-ring` is a 4px accent halo at low opacity, used together with an accent border on inputs. Other controls use a 2px accent outline offset by 3px. --- # Motion Motion in Merid is quiet. It confirms that something happened and never asks to be watched. ## Tokens Enter animations use a 220ms fade: opacity from 0 to 1 while the element rises 4px into place. ## Patterns - **Hover** tints the background or lifts the element by 2–3px. - **Press** scales to `.97` over 100ms. - **Open** fades and rises; overlays fade their backdrop at the same time. - **Rotate** is reserved for disclosure icons, such as the accordion plus turning 45°. ## Reduced motion When the user has asked for reduced motion, Merid removes transitions and animations entirely, including the skeleton shimmer. State still changes instantly, so nothing is lost. ```css @media (prefers-reduced-motion: reduce) { .your-component { transition: none; } } ``` Merid's base layer applies this to everything inside `.mrd-root`; every component also drops its own motion under reduced motion. --- # Breakpoints and layers ## Breakpoints Merid uses two breakpoints, in CSS only. Components adapt to the space they have; there is no JavaScript breakpoint API. | Width | What changes | | --- | --- | | 920px and below | Multi-column layouts collapse; section padding drops to 80px. | | 640px and below | Mobile pass: gutter becomes 20px, section padding 64px, dialogs become bottom sheets. | Custom properties cannot be used inside media queries, so the values are written out: ```css @media (max-width: 920px) { /* layout collapse */ } @media (max-width: 640px) { /* mobile pass */ } ``` ## Coarse pointers On touch screens (`pointer: coarse`), controls grow to be easier to hit: the default button becomes 48px tall, inputs 50px and segmented pills 40px. ## Z-index scale | Token | Value | Use | | --- | --- | --- | | `--mrd-z-sticky` | 50 | Sticky headers and toolbars | | `--mrd-z-overlay` | 100 | Dialogs, drawers and their backdrops | | `--mrd-z-popover` | 105 | Popovers, menus and select lists, above overlays so they work inside dialogs | | `--mrd-z-toast` | 110 | Toasts, above overlays | | `--mrd-z-tooltip` | 120 | Tooltips, above everything | Use these tokens instead of arbitrary numbers so your own layers stack predictably with Merid's. --- # Principles These ten rules are the design contract. Every component, doc page and example follows them. If a value is not defined by a token, it is derived from an existing one — never invented. ## 1. One accent, used sparingly A single cool blue carries interactivity and selection. Everything else is ink-tinted neutrals. ## 2. Separate by surface before border Cards sit on a tray over the page; on a tray section they flip to a white surface and a soft shadow. Borders are the last resort. ## 3. Hairlines only Every border is 1px `--mrd-line`. Selection is a 1.5px accent ring. Never 2px, never dark borders. ## 4. Soft, long, faint shadows Shadows are always tinted with ink, never black. The only saturated shadow is the primary button's glow. ## 5. Generous rounding that steps down Radius steps down as surfaces nest: 20, then 14, then 12, then 8. ## 6. Tight headings, relaxed body Headings are 600 with negative tracking and balanced wrapping; body text has a 1.6 line height. Weight 700 is never used. ## 7. Three text tones Ink, body and muted. Hierarchy comes from tone and weight, not colour. ## 8. Quiet interaction Hover is a background tint or a 2–3px lift; press is a `.97` scale; everything takes 150–200ms. Reduced motion removes all of it. ## 9. Air Big section padding, wide gaps, content capped at 1160px and reading width at or below 720px. ## 10. One filled block per page At most one filled colour block on a page. No gradients — the skeleton shimmer is the only exception. --- # Components Merid's components are grouped by what they do. Each has its own page with a live preview, props, keyboard interactions and usage guidance. --- AccordionBasic, AccordionMultiple, AccordionNonCollapsible, } from "@/examples/accordion"; # Accordion Stacked disclosure sections that show one or more panels at a time. ```tsx accordionBasicCode ``` ## Import ```tsx import { Accordion } from "@meridui/react"; ``` ## Anatomy ```tsx ``` - **Root** decides whether one (`type="single"`) or many (`type="multiple"`) items can be open. - **Item** groups a trigger and its content under a unique `value`. - **Trigger** is a `}> 2 members are waiting for approval. ``` ### Without icon or live region `icon={null}` hides the icon; any node replaces it. `live="off"` renders a plain note with no role. ```tsx Plain note without an icon or live region. ``` ## API reference Renders a `div`; the ref and other div attributes are forwarded (the native `title` attribute is replaced by the `title` prop). | Prop | Type | Default | Description | | --- | --- | --- | --- | | `tone` | `"info" \| "success" \| "warning" \| "danger"` | `"info"` | Colour tone and default icon. | | `title` | `ReactNode` | | Medium-weight first line. | | `icon` | `ReactNode \| null` | | Leading icon. Defaults to a tone glyph; null hides it. | | `action` | `ReactNode` | | Trailing slot, e.g. an action button or dismiss IconButton. | | `live` | `"polite" \| "assertive" \| "off"` | | Live-region behaviour. Defaults to polite for info/success and assertive for warning/danger. | | `children` | `ReactNode` | | Body text. | ## Styling | Hook | Values | | --- | --- | | `.mrd-alert` | Root | | `.mrd-alert__icon` | Icon slot | | `.mrd-alert__content`, `__title`, `__body` | Text column | | `.mrd-alert__action` | Trailing action slot | | `data-tone` | `info` · `success` · `warning` · `danger` | Component variables: `--mrd-alert-bg` and `--mrd-alert-fg`, set per tone to the matching `-soft` and `-strong` tokens. ```css .billing .mrd-alert[data-tone="info"] { --mrd-alert-bg: var(--mrd-tray); --mrd-alert-fg: var(--mrd-ink); } ``` ## Accessibility - `polite` renders `role="status"`, `assertive` renders `role="alert"`. Live regions only announce content that changes after they mount, so render the alert when the event happens rather than toggling `hidden`. - Use `live="off"` for static notes present on page load, so screen readers do not interrupt. - The icon is decorative; the title and body carry the meaning, so do not rely on colour alone. ## Guidelines - **Do:** Say what happened and what to do next in one or two sentences: “Sync failed. Check your connection.” - **Don't:** Stack several alerts at the top of a page, or use `danger` for anything that is not an error. - **Do:** Use an Alert for messages tied to the current page that should stay visible. - **Don't:** Use it for short-lived confirmations — those are a [Toast](https://meridui.dev/docs/components/toast). ## Related - [Toast](https://meridui.dev/docs/components/toast) — transient notifications. - [AlertDialog](https://meridui.dev/docs/components/alert-dialog) — confirmations that need an answer. - [Badge](https://meridui.dev/docs/components/badge) — compact status labels. --- # AlertDialog A modal confirmation that requires a response. It does not close on outside press and focuses Cancel first. ## Import ```tsx import { AlertDialog } from "@meridui/react"; ``` ```tsx Delete “Northwind”? The project and its 24 files are removed permanently. This cannot be undone. Cancel Delete project ``` ## Anatomy ```tsx ``` Root, Trigger, Title, Description and Footer are the same parts as [Dialog](https://meridui.dev/docs/components/dialog). **Cancel** is a secondary Button that dismisses and receives initial focus; **Action** is a primary (or `tone="danger"`) Button that confirms and closes. There is no icon close button. ## Examples ### Destructive confirmation The example above is the canonical use: name the object, state the consequence, and repeat the verb on the Action button. ### Keeping the dialog open while working Call `event.preventDefault()` in the Action's `onClick` to keep the dialog open, then close it yourself with controlled state once the work finishes. ```tsx const [open, setOpen] = useState(false); Delete project? Cancel { event.preventDefault(); await deleteProject(); setOpen(false); }} > Delete ``` ## API reference ### AlertDialog.Root | Prop | Type | Default | Description | | --- | --- | --- | --- | | `open` | `boolean` | | Controlled open state. | | `defaultOpen` | `boolean` | `false` | Initial open state when uncontrolled. | | `onOpenChange` | `(open: boolean) => void` | | Called when the open state should change. | | `children` | `ReactNode` | | Trigger, Content and anything else sharing this dialog's state. | ### AlertDialog.Content Accepts every `
` attribute except `role`. Outside press never closes it. | Prop | Type | Default | Description | | --- | --- | --- | --- | | `closeOnEscape` | `boolean` | `true` | Close on Escape. | | `initialFocus` | `RefObject` | | Element to focus when opened; defaults to [data-autofocus] (the Cancel button), then the first tabbable element. | | `container` | `Element \| null` | `document.body` | Portal target. `undefined` uses `document.body`; `null` renders nothing until the target exists. | | `showClose` | `boolean` | `false` | Render the standard top-right icon close button automatically. Skipped while you render your own icon `Close`. | | `closeLabel` | `string` | `"Close"` | Accessible name of the automatic close button. | | `ref` | `Ref` | | Forwarded ref to the dialog element. | ### AlertDialog.Action Accepts every ` ``` ## Import ```tsx import { Button } from "@meridui/react"; ``` ## Examples ### Variants `primary` is the single accent action on a screen. `secondary` (the default) is for everything else; `ghost` sits quietly in toolbars and dense rows; `danger` confirms destructive actions; `link` looks like inline text but keeps button semantics. ```tsx ``` ### Sizes `sm` is 36px, `md` 44px (default) and `lg` 50px tall. Match the size of neighbouring inputs so a row of controls shares one baseline. ```tsx ``` ### With icons `leadingIcon` and `trailingIcon` take any node. They are wrapped in `aria-hidden` spans, so the label alone names the button. ```tsx ``` ### Loading `loading` overlays a spinner, keeps the button's width so the layout does not jump, sets `aria-busy` and `aria-disabled`, and swallows clicks. The button stays focusable, so keyboard focus is not lost mid-request. ```tsx const [saving, setSaving] = useState(false); ``` ### Disabled `disabled` is the native attribute: the button leaves the tab order and ignores clicks. Prefer explaining why an action is unavailable over silently disabling it. ```tsx ``` ### Full width `fullWidth` stretches the button to its container — useful in narrow forms and on mobile. ```tsx ``` ### As a router link Use `asChild` to render your router's link with button styling, so navigation stays a real link: ```tsx import Link from "next/link"; ``` ## API reference `Button` forwards its ref to the ` Edit profile Changes are visible to everyone in your workspace. Viewer Editor Admin Cancel ``` ## Anatomy ```tsx Cancel ``` - **Root** holds the open state. It renders no element. - **Trigger** is a ` Controlled dialog Open state lives in the parent component. ``` ### Initial focus Focus moves to the element passed as `initialFocus`, then to an element with `data-autofocus`, then to the first tabbable element. ```tsx Rename ``` ### Blocking dismissal Set `closeOnOutsidePress={false}` or `closeOnEscape={false}` when losing unsaved work would be costly. Prefer [AlertDialog](https://meridui.dev/docs/components/alert-dialog) for confirmations. ## API reference ### Dialog.Root | Prop | Type | Default | Description | | --- | --- | --- | --- | | `open` | `boolean` | | Controlled open state. | | `defaultOpen` | `boolean` | `false` | Initial open state when uncontrolled. | | `onOpenChange` | `(open: boolean) => void` | | Called when the open state should change. | | `children` | `ReactNode` | | Trigger, Content and anything else sharing this dialog's state. | ### Dialog.Trigger Accepts every ` Filters Narrow the list of invoices. Reset ``` ## Anatomy ```tsx ``` Root, Trigger, Title, Description, Close and Footer are the [Dialog](https://meridui.dev/docs/components/dialog) parts. The Footer is pushed to the bottom of the sheet. ## Examples ### Sides ```tsx … … ``` ### Sizes `size` sets the sheet width: `sm` 320px, `md` 420px (default), `lg` 560px, always capped to the viewport minus 48px. ```tsx … … … ``` ### Controlled ```tsx const [open, setOpen] = useState(false); … ``` ## API reference ### Drawer.Root | Prop | Type | Default | Description | | --- | --- | --- | --- | | `open` | `boolean` | | Controlled open state. | | `defaultOpen` | `boolean` | `false` | Initial open state when uncontrolled. | | `onOpenChange` | `(open: boolean) => void` | | Called when the open state should change. | | `children` | `ReactNode` | | Trigger, Content and anything else sharing this drawer's state. | ### Drawer.Content Accepts every `
` attribute except `role`. | Prop | Type | Default | Description | | --- | --- | --- | --- | | `side` | `"left" \| "right"` | `"right"` | Edge the sheet slides in from. | | `size` | `DrawerSize` | `"md"` | Sheet width: `sm` 320px, `md` 420px, `lg` 560px, capped to the viewport minus 48px. `DrawerSize` is `"sm" \| "md" \| "lg"`. | | `closeOnOutsidePress` | `boolean` | `true` | Close when the backdrop is pressed. | | `closeOnEscape` | `boolean` | `true` | Close on Escape. | | `initialFocus` | `RefObject` | | Element to focus when opened; defaults to [data-autofocus], then the first tabbable element. | | `container` | `Element \| null` | `document.body` | Portal target. `undefined` uses `document.body`; `null` renders nothing until the target exists. | | `showClose` | `boolean` | `true` | Render the standard top-right icon close button automatically. Skipped while you render your own icon `Close`. | | `closeLabel` | `string` | `"Close"` | Accessible name of the automatic close button. | | `ref` | `Ref` | | Forwarded ref to the drawer element. | Trigger, Title, Description, Close and Footer take the same props as in [Dialog](https://meridui.dev/docs/components/dialog#api-reference). ## Styling | Class | Element | |---|---| | `.mrd-drawer__backdrop` | Fixed backdrop; aligns the sheet to its side | | `.mrd-drawer` | The sheet: 320 / 420 / 560px by `data-size`, capped at `100vw - 48px`, full height | Both carry `data-side="left" | "right"` and `data-state="open"`; the sheet also carries `data-size`. Title, description, footer and close use the `.mrd-dialog__*` classes. Motion is removed under `prefers-reduced-motion`. ## Accessibility Follows the [WAI-ARIA Dialog (Modal) pattern](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/): `role="dialog"`, `aria-modal="true"`, focus trap, scroll lock and focus return. | Keys | Action | | --- | --- | | `Space` `Enter` | On the trigger, opens the drawer. | | `Tab` | Moves focus forward, wrapping inside the drawer. | | `Shift` `Tab` | Moves focus backwards, wrapping inside the drawer. | | `Esc` | Closes the drawer and returns focus to the trigger. | ## Guidelines - **Do:** Use a drawer for secondary content that benefits from keeping the page in view: filters, details, settings. - **Don't:** Put a multi-step flow in a drawer; use a page. ## Related - [Dialog](https://meridui.dev/docs/components/dialog) - [SidebarNav](https://meridui.dev/docs/components/sidebar-nav) for persistent navigation. --- DropdownMenuBasic, DropdownMenuCheckboxes, DropdownMenuPlacement, } from "@/examples/dropdown-menu"; # DropdownMenu A list of actions or toggles opened from a button. ```tsx dropdownMenuBasicCode ``` ## Import ```tsx import { DropdownMenu } from "@meridui/react"; ``` ## Anatomy ```tsx ``` - **Root** holds the open state. It renders no element. - **Trigger** is a `} /> ``` ## Import ```tsx import { EmptyState } from "@meridui/react"; ``` ## Examples ### Plain `variant="plain"` drops the tray background — use it inside a card or table frame that already has a surface. `titleLevel` sets the heading level to fit the page outline. ```tsx } title="No results for “quarterly”" description="Try a shorter search or check the spelling." action={} titleLevel={2} /> ``` ### Title only Only `title` is required. ```tsx ``` ## API reference Renders a `div`; the ref and other div attributes are forwarded (the native `title` attribute is replaced by the `title` prop). | Prop | Type | Default | Description | | --- | --- | --- | --- | | `title` | `ReactNode` | | Short headline, e.g. "No invoices yet". | | `description` | `ReactNode` | | One or two sentences explaining what to do next. | | `icon` | `ReactNode` | | Icon shown in a soft accent tile. Decorative. | | `action` | `ReactNode` | | Primary action(s), usually a Button. | | `titleLevel` | `1 \| 2 \| 3 \| 4 \| 5 \| 6` | `3` | Heading level for title. | | `variant` | `"tray" \| "plain"` | `"tray"` | Surface: tray is a recessed panel, plain has no background. | ## Styling | Hook | Values | | --- | --- | | `.mrd-empty-state` | Root; `data-variant` = `tray` · `plain` | | `.mrd-empty-state__icon` | Accent-soft icon tile | | `.mrd-empty-state__title` | Heading | | `.mrd-empty-state__description` | Body text | | `.mrd-empty-state__action` | Action row | EmptyState has no component variables. ## Accessibility - The title is a real heading (`h3` by default); set `titleLevel` so it fits the surrounding outline. - The icon is `aria-hidden`; the title must carry the meaning on its own. - When the empty state replaces results after a search, announce the change — for example render the result count in a polite live region. ## Guidelines - **Do:** Say what is missing and how to fill it: “No invoices yet” plus a “New invoice” button. - **Don't:** Show a bare “No data” or a large illustration with no way forward. ## Related - [Table](https://meridui.dev/docs/components/table) — the most common place an empty state appears. - [Skeleton](https://meridui.dev/docs/components/skeleton) — while data is still loading, before you know it is empty. - [Button](https://meridui.dev/docs/components/button) — the action. --- # Field Wraps a single control with its label, helper text and error message, and wires them together — `htmlFor`, `aria-describedby`, `aria-invalid`, `required` and `disabled` — through context. ```tsx ``` ## Import ```tsx import { Field } from "@meridui/react"; ``` ## Examples ### Error When `error` is set, the control gets `aria-invalid="true"` and the invalid style, and the message is appended to its `aria-describedby`. Pass `undefined`, `null` or `false` to clear it. ```tsx ``` ### Controlled input Field does not own the value. Control the child as usual and derive `error` from your state. ```tsx const [name, setName] = useState(""); const error = name.length > 0 && name.length < 3 ? "Use at least 3 characters." : undefined; setName(event.target.value)} /> ``` ### Uncontrolled input With `defaultValue` and `name`, the control keeps its own state and submits with the form. ```tsx