Configuration reference
English · 简体中文
Every option, its type and its default. If you are just starting, read Getting started first.
Top-level options
| Option | Default | Meaning |
|---|---|---|
profiles | app/desktop preset | The map of design canvases |
defaultProfile | app | The canvas ordinary CSS uses |
routes | [] | Reassign a canvas by selector, property name, breakpoint or file |
libraries | 'auto' | Component-library adaptation; all built-ins on by default |
atRuleName | adaptive | Custom at-rule name |
strategy | clamp | clamp, or the compatibility-oriented viewport |
unit | vw | vw, vi, cqw, cqi |
precision | 5 | 0–12 decimal places |
unitToConvert | ['px'] | The input unit(s) to read; a single string is accepted too |
rootValue | 16 | How many pixels one rem is worth; a function may choose it per file |
minPixelValue | 0 | Absolute values below this are not converted |
hairline | 1 | Hairline threshold that is never converted |
fontFluidity | 0.35 | Text fluidity ratio, 0–1 |
textProperties | font-related properties | Properties that use the rem/fluid text formula |
propList | ['*'] | Property list supporting * and ! |
selectorExclude | [] | Exclude by substring or regular expression |
valueExclude | [] | Exclude by value |
include / exclude | none | File string, regular expression, function, or an array |
transformCustomProperties | false | Whether to convert --token values |
preserveOriginal | false | Keep the original declaration in front as a fallback |
root | false | The optional root layout foundation |
unknownProfile | warn | warn, error, ignore |
What do I actually need to provide?
No top-level option is required. Start with adaptiveMatrix() to use the built-in preset. An empty profiles: {} is also equivalent to omitting it. Examples that show both fluid bounds are not mandatory templates.
| Intent | Smallest configuration |
|---|---|
| Use the built-in App/PC preset | adaptiveMatrix() |
| Use one custom canvas without bounds | adaptiveMatrix({ profiles: { mobile: 375 } }) |
| Set only a ceiling | adaptiveMatrix({ profiles: { mobile: { designWidth: 375, fluid: { maxWidth: 600 } } } }) |
| Set only a floor | adaptiveMatrix({ profiles: { mobile: { designWidth: 375, fluid: { minWidth: 320 } } } }) |
fluid, fluid.minWidth, and fluid.maxWidth are independently optional. An empty fluid: {} also means no bounds. A sole custom profile becomes defaultProfile automatically; with several custom profiles and no app, name the default because there is no unambiguous choice.
Only identity or calculation inputs are required when you author the corresponding nested object:
- A profile object needs
designWidth; a number is shorthand when no overrides are needed. - An object-form
queryneedscondition. - A route needs
profileand at least one matching channel. - A standalone custom library needs
nameanddesignWidth; anextendsentry inherits them. fluidandroothave no required members.
Optional top-level fields set to undefined by object composition are treated exactly like omission; null remains an error rather than silently selecting a default.
String file matchers are separator-portable: src/components/ also matches C:\\repo\\src\\components\\card.css, and a backslash spelling also matches a POSIX path. Regular expressions and predicate functions receive the original path unchanged, so existing host-specific logic keeps its exact contract.
List-shaped filters accept one item directly. propList: 'width', textProperties: 'font-size', selectorExclude: '.legacy', and valueExclude: /fixed/ are equivalent to one-element arrays; arrays are needed only for multiple entries.
propList example:
propList: ['*', '!border*', '!box-shadow']* is only needed when you want to include every property before applying exclusions. It is not required for an explicit allowlist: propList: 'width' or propList: ['width', 'height'] converts just those properties. An exclude-only ['!border*'] matches no property at all and is rejected; use ['*', '!border*'] to convert everything except borders.
Configuration is always validated first
CSS dimensions use JavaScript's finite numeric range during conversion. If an authored dimension or an intermediate calculation overflows, the CSS conversion path preserves that original token instead of emitting Infinity or NaN; other dimensions in the same declaration can still convert. For example, 1e308px 24px on a 375px canvas in viewport mode remains 1e308px 6.4vw. Preservation is not successful adaptation and is not a browser-rendering guarantee. Correct extreme input values upstream; each affected declaration emits one located compiler warning, including on cache hits. Enable the warnings gate in the CLI or programmatic API to reject such output.
A configuration usually lives in a .mjs file with no type checking behind it, and getting these fields wrong fails silently. So all of the following are errors raised before the first stylesheet is read:
| What you wrote | The consequence |
|---|---|
unit: 'vm' | Emits 4.267vm. That is not a length, so the browser drops the whole declaration and the element keeps its inherited value |
strategy: 'viewpoint' | Silently falls back to clamp, looking exactly like a setting that took effect |
unitToConvert: '' / [] | Matches no length, indistinguishable from not installing the plugin |
rootValue: 0 | Every rem reads as 0, and the writing end divides by zero |
atRuleName: 'media' | Every @media in the stylesheet is read as a canvas name and rewritten. At-keywords are case-insensitive, so MEDIA is the same collision |
atRuleName: ' canvas ' | Normalised to canvas; otherwise validation would pass while no @canvas block ever matched |
fontFluidity: NaN / rootMaxWidth: Infinity | Writes NaNrem or Infinitypx; the declaration is invalid and the browser drops it |
query: { type: 'media' } | Writes @media undefined, an invalid wrapper around otherwise valid rules |
propList: 16 | A JavaScript/JSON shape error that would otherwise escape later as an internal .every is not a function |
minPixeValue: 2 | An unknown field that would otherwise be spread into the options and ignored; the error suggests minPixelValue |
root.selector: '' | Compiles to :where(), which is a parse error — the whole foundation, safe-area variables included, is discarded |
textAnchorWidth: 0 | Division by zero, turning every text length into Infinity |
unit and strategy are validated at profile level too. Collection members and nested route, query, root, and library fields are checked with their full path (for example routes[0].media[1].minWidth), so JavaScript configs receive the same early protection as JSON files backed by the published schema.
Objects are closed at every level, just like the schema's additionalProperties: false: a misspelled top-level option or a field inside profiles, fluid, query, routes, media, root, or libraries is never silently ignored. When an unambiguous field is within two edits (three for a long name), the error names it; unrelated keys are rejected without inventing a guess.
unknownProfile
When the canvas name in @adaptive ghost does not exist:
| Value | Behaviour |
|---|---|
warn | Warn, and leave the at-rule as written |
error | Fail the build |
ignore | Say nothing, and leave the at-rule as written |
Both warn and ignore leave the original text, and a browser that cannot read @adaptive discards the whole block — everything inside it disappears. The only difference is whether anyone told you. Canvas names are case-sensitive (they are keys you wrote in profiles), so @adaptive PC does not find pc.
A block-less @adaptive pc; warns separately: with no block, nothing is compiled onto that canvas, and it is not rewritten into a @media.
unitToConvert and rootValue
unitToConvert?: string | readonly string[] // default 'px'
rootValue?: number | ((context: { file: string }) => number) // default 16By default only px is read. An array reads several units at once, which atomic CSS projects need — see Build tool integration:
unitToConvert: ['px', 'rem']The current profile's output unit is always treated as already converted, even if it also appears in unitToConvert. This lets compatibility-mode bare vw survive another build. Other pairs remain meaningful: a profile emitting cqi may still read authored vw when explicitly configured to do so.
There is exactly one conversion rule between units: rem becomes pixels via rootValue; every other unit is read at face value.
Every listed unit must be an unescaped CSS identifier (px, rem, rpx, dp, etc.). A percentage is not a unit token, and punctuation or whitespace cannot occur inside one; values such as %, px|rem, or two words are rejected instead of being compiled into a misleading regular expression.
em is read at face value too, deliberately. em is relative to the font size an element inherits, which is only known at runtime, and no build-time constant can stand in for it. Treating em as rem is correct only where the two happen to be equal — a minority of places in any stylesheet.
rootValue governs both ends:
- when reading, how many pixels
1remis worth; - when writing, what the static part of a text size is divided by to become
rem.
So a page with html { font-size: 62.5% } sets rootValue: 10, and 3.2rem and 32px produce exactly the same output, both correct. Configuring only one end would be wrong at the other, which is why there is no second option here.
rootValue is optional. When different sub-apps in one repository set different root font sizes, use a function and let the source path choose the ruler once for that file:
rootValue: ({ file }) => file.replaceAll('\\', '/').includes('/legacy/') ? 10 : 16The callback receives { file } with the host's original path separators and must return a positive finite number. A real PostCSS from path is required for a file-sensitive choice; without one the stylesheet still compiles, but emits one warning because the callback cannot distinguish files.
The minPixelValue and hairline thresholds are in pixels, not face value. A framework writing a hairline as 0.0625rem and you writing 1px are the same line, and hairline stops both.
AdaptiveRoute
One route object can be passed directly; use an array only when order between multiple routes matters. Internally both forms become the same ordered list.
interface AdaptiveRoute {
profile: string | false
file?: FileMatcher | FileMatcher[]
selector?: (string | RegExp) | (string | RegExp)[]
property?: string | string[]
media?: MediaMatcher | MediaMatcher[]
}
interface MediaMatcher {
minWidth?: number
maxWidth?: number
}Reassigns matching CSS to another canvas; profile: false keeps the pixels unconverted. Strings match by "contains" and regular expressions by test; property accepts unescaped custom-property prefixes beginning with --, matches them case-sensitively, and may use bare -- to claim every custom property; media matches the widths an enclosing @media confines the rule to — see Breakpoints.
Standard declaration names and propList patterns are ASCII case-insensitive, as CSS requires: FONT-SIZE is still text and keeps the zoomable rem + vw formula. Custom properties are the opposite: --Theme-gap and --theme-gap are different variables, so filters and routes preserve their spelling.
Every channel a route declares must match. To let a class name and a file match independently, write two routes.
Splitting two non-responsive ends by directory is the most common use:
adaptiveMatrix({
defaultProfile: 'pc',
profiles: {
pc: { designWidth: 1920, fluid: { minWidth: 1280, maxWidth: 2560 } },
mobile: { designWidth: 750, fluid: { minWidth: 320, maxWidth: 600 } },
},
routes: [{ profile: 'mobile', file: [/[\\/]mobile[\\/]/] }],
})Both canvases are resolved inside the same plugin instance, so they cannot overwrite each other and you do not need to register the plugin once per end — a piece of CSS is converted exactly once, and the first canvas that matches is the final answer.
Resolution priority, highest first:
- an enclosing
@adaptive <profile>— the author has already said so; - a matching
propertyroute; - a matching
selectorroute; - a matching
mediaroute; - a matching
fileroute; defaultProfile.
A selector beats a file path because the selector is part of the CSS itself, whereas a path only reflects how the build tool happened to arrange files at the time; once a bundler inlines a dependency, the path is gone. The reasoning for property names is the same and stronger: theme tokens are declared on :root and leave no trace of their origin beyond their name.
A selector beats a width band for a different reason: a component library is drawn on its own canvas at every viewport width, and a breakpoint does not change which design file the component came from. To override a library's own component at a breakpoint, say both — see below.
Breakpoints
A responsive stylesheet is one file holding two design files. The phone numbers were measured on a 750 mock; the numbers inside @media (min-width: 1024px) were measured on a 1440 one. Nothing in the CSS says so, and compiling the whole file against one canvas is not a near miss:
/* defaultProfile 'app': designWidth 750, fluid 320–600 */
@media (min-width: 1024px) {
.hero { padding: 40px } /* → clamp(17.07px, 5.33vw, 32px) */
}That rule is only ever live from 1024px up, which is past where the phone canvas stops scaling — so the clamp() is already pinned to its maximum everywhere the rule applies. The padding is a constant 32px at every width, forever. The compiler ran, the output looks compiled, and not one value moves.
A media route gives the breakpoint the design file it was drawn on:
adaptiveMatrix({
defaultProfile: 'app',
profiles: {
app: { designWidth: 750, fluid: { minWidth: 320, maxWidth: 600 } },
pc: { designWidth: 1440, fluid: { minWidth: 1024, maxWidth: 1920 } },
},
routes: [{ media: { minWidth: 1024 }, profile: 'pc' }],
})
// .hero → clamp(28.44px, 2.78vw, 53.33px)Matching is by implication, not by text. { minWidth: 1024 } claims any rule that cannot apply below 1024px, so it matches all of these:
| Query | Band it is live in | Claimed |
|---|---|---|
(min-width: 1024px) | 1024px and up | yes |
screen and (min-width: 1200px) | 1200px and up | yes |
(min-width: 64rem) | 1024px and up | yes |
(min-width: 1024px) and (max-width: 1600px) | 1024–1600px | yes |
(min-width: 1024px) and (orientation: landscape) | 1024px and up (landscape only) | yes |
(min-width: 768px) | 768px and up | no — it reaches below 1024 |
Nesting is conjunction, so @media (min-width: 900px) { @media (min-width: 1100px) { … } } is live from 1100px up and is claimed.
rem and em resolve at 16px, not at rootValue and not at the root element's font size. A media query is evaluated before any declaration could change font-size, so it cannot depend on the cascade it selects — 64rem is 1024px even in a stylesheet whose html is 62.5%. Utility frameworks write every breakpoint this way.
Width numbers use the same complete CSS grammar as declarations: signs, fractions and exponents are accepted, so (MIN-WIDTH: 1.024e3PX) is the same 1024px boundary. Feature names and units are ASCII case-insensitive. Unitless 0 is valid; any other unitless width, a malformed decimal, or a non-finite exponent makes the query unreadable rather than leaking NaN into routing and diagnostics.
A query the compiler cannot read — a comma, not, only, or an unsupported non-width feature — is claimed by nothing. Orientation is the deliberate exception: (orientation: landscape) / portrait is projected out while deriving the width band, because it can narrow which devices apply but can never make a 1024px minimum reach below 1024px. An orientation-only query therefore keeps the inherited canvas, while orientation + min-width can still prove a width route. Continuity analysis remains conservative and skips the group because it cannot guess the device's current orientation. @container never counts either; it bounds an element, and vw has never been about the element.
Contradictory readable bounds are different from an unreadable query. They form an empty interval, so no media route claims it and a converted rule gets one explicit unreachable warning (min-width: 1100px together with max-width: 900px). This is not reported as a pinned clamp: there is no viewport at which the rule exists. --fail-on warnings can enforce it in CI.
To redraw a component library's own component at a breakpoint, name both — a selector route on its own would apply at every width, and a width band on its own loses to the library:
routes: [{ selector: ['.van-'], media: { minWidth: 1024 }, profile: 'pc' }]The warning you get for free
You do not have to know about any of this to find the problem. When a rule generates a bounded length and its band lies entirely outside its canvas's fluid range, the compiler says so:
Every converted length here is a constant: this rule is live from 1024px up, but canvas
"app" stops scaling outside 320px–600px, so its bounded expression is pinned to its maximum across
that whole range. The numbers in a breakpoint are usually measured on a different design
file — give it one with a route: { media: { minWidth: 1024 }, profile: '…' }.This is arithmetic, not a heuristic: two numbers that do not overlap. It is reported once per canvas per band per file, and only for declarations whose conversion generated clamp(), min() or max() — a breakpoint that only changes display and color, deliberately static text, and an unbounded viewport expression have no generated bounds to pin. Selector- and property-routed declarations are checked against the canvas that actually converted them, including native CSS nesting.
libraries
type LibraryEntry = string | LibraryAdaptation
libraries?: LibraryEntry | readonly LibraryEntry[] | false // string 'auto' is the defaultDefault 'auto': every built-in is active, so a project using Vant or Element Plus needs no configuration. false turns the whole thing off. Pass one built-in or custom entry directly; use an array only when enabling more than one.
An inherited adaptation can be as small as { extends: 'vant' }; it receives name and designWidth from the built-in. A standalone custom adaptation still requires both because there is nowhere to infer them from.
Entries expand into routes appended after routes — explicit routes always win.
For the built-in list, the matching channels and how to override or extend, see Component libraries.
Profile
Use a number directly for a width-only profile. A file-sensitive design width resolver can also be the direct value. The object form is needed only for per-profile overrides:
profiles: {
mobile: 375,
desktop: { designWidth: 1440, unit: 'vi' },
embedded: ({ file }) => file.includes('/compact/') ? 320 : 375,
}interface AdaptiveProfile {
designWidth: number | ((context: { file: string; profile: string }) => number)
fluid?: { minWidth?: number; maxWidth?: number }
query?: string | {
type?: 'media' | 'container'
condition: string
name?: string
} | false
unit?: 'vw' | 'vi' | 'cqw' | 'cqi'
strategy?: 'clamp' | 'viewport'
fontFluidity?: number
textAnchorWidth?: number | ((context: { file: string; profile: string }) => number)
rootMaxWidth?: number
}Only designWidth is required in a profile. Omit fluid (or use fluid: {}) for an unbounded viewport expression, provide minWidth or maxWidth for a one-sided max()/min() limit, and provide both for clamp(). Supplied bounds must be positive finite numbers; when both exist, maxWidth must be greater than minWidth. The explicit strategy: 'viewport' compatibility mode remains unbounded regardless of fluid.
fontFluidity accepts the full range from 0 to 1. In adaptive mode, values below 1 retain a rem component in the preferred text expression; viewport compatibility mode bypasses this hybrid formula. Neither the ratio nor successful compilation guarantees accessible text resizing: verify zoom, clipping and reflow on the actual page.
Function-valued widths receive { file, profile }. A missing textAnchorWidth reuses the already-resolved designWidth exactly once rather than calling a dynamic resolver again. File-sensitive resolvers need a real PostCSS from path; an invalid return reports both profile and file.
query: false removes the @adaptive wrapper but keeps the rules inside it, which suits building separate artifacts with the profile chosen by environment.
An object query defaults to media when it has no type. Providing name is enough to select a named container query, so { name: 'workspace', condition: '(min-width: 400px)' } needs no redundant type: 'container'. An explicit type: 'media' together with name is rejected as a conflict.
Query conditions may use current or future CSS media/container-query syntax, but their structural boundary must be complete: strings, comments and () / [] component-value blocks must close, while an unescaped brace or top-level ; is rejected. This prevents a JavaScript configuration typo from ending the generated at-rule early or swallowing the rules that follow it without artificially freezing the query grammar.
textAnchorWidth defaults to designWidth and affects text only: text keeps a fixed rem component (so browser zoom keeps working), and a fixed length only means something relative to some width. A hand-written canvas anchoring to its own design width is correct; but when two canvases describe the same design in two sets of units (a library drawn on 375, pages drawn on 750, where Vant's 16px is the page's 32px), anchoring each to itself leaves the two misaligned at every viewport. Library canvases therefore always inherit the anchor of the profile they belong to, with nothing to configure. For the reasoning and the measurements see Which canvas the static part anchors to.
RootFoundationOptions
interface RootFoundationOptions {
selector?: string
center?: boolean
container?: boolean
containerName?: string
safeAreaVariables?: boolean
layer?: string | false
logical?: boolean
fixedContainingBlock?: boolean
injectTo?: FileMatcher | FileMatcher[]
}No global styles are injected by default. root: true enables the main plugin's default :root foundation; an object customises it. The same shorthand works in appPcPreset, where a root-only helper setting that actually configures or enables the foundation also enables it. container: false and fixedContainingBlock: false alone describe capabilities that are already off and therefore do not inject global CSS. rootSelector only overrides the default selector; it is not required to enable the foundation.
root itself has no required members. Prefer root: true when there is nothing to customise; root: {} remains equivalent. Set selector only when the layout is carried by another element such as #app.
containerName must be a non-reserved CSS custom identifier. Providing it automatically enables container, so a named container needs no redundant container: true; pairing it with an explicit container: false is a configuration error. layer is one dot-separated layer name such as adaptive-matrix or framework.layout; a space, comma, empty segment or CSS-wide keyword is rejected before it can produce an invalid container-name / @layer rule. A named container profile's query.name follows the same custom-identifier rule.
selector is inserted inside :where(...), so an explicitly supplied selector uses the same structural guard: strings, comments, parentheses and attribute brackets must close, and unescaped braces or a top-level semicolon are rejected before the foundation is generated.
injectTo
Limits which files receive the foundation; by default, all of them.
The foundation is global, but PostCSS only ever sees one file at a time and cannot deduplicate across files. A single-stylesheet project wants the default; in a Vue or Svelte project every component's <style> block is a separate file, so the default becomes one copy per component.
root: { selector: '#app', injectTo: 'src/styles/main' }Matching works exactly like include: a string matches by "contains", a regular expression tests the path, and a function decides for itself. The appPcPreset field is rootInjectTo.
A pattern that matches nothing is not an error — it just injects nothing. Use the CLI preview to confirm the added declarations appear in the entry file.
logical
Default true: the foundation is written with logical properties — inline-size, margin-inline, max-inline-size.
Set it to false and it writes width, margin-left / margin-right and max-width instead. The two are equivalent on a horizontal page, so this switch has exactly one purpose: a fallback for browsers that cannot read logical properties (below Safari 15 / iOS 15.0 / Chrome 89).
It deserves its own switch because it is the only syntax this plugin emits whose failure still leaves a page that looks fine: without margin-inline: auto the column is exactly the right width and sits against the left edge of the screen; without max-inline-size it goes full-bleed. Neither looks like a fault. For the complete failure list and the degradation path for each, see Browser support and degradation.
The appPcPreset field is rootLogical; likewise rootLayer, which passes through to layer.
fixedContainingBlock
When a profile sets rootMaxWidth, the page becomes a centred column, and position: fixed falls back to the viewport as its containing block — the fixed element sticks to the window edges, out of line with the content column it belongs to.
Enabled, the compiler publishes two variables:
| Variable | Meaning |
|---|---|
--adaptive-root-width | The root column width at the current breakpoint, or 100vw with no ceiling |
--adaptive-root-gutter | max(0px, (100vw - column width) / 2), i.e. the gutter on one side |
and does three things to rules that themselves declare position: fixed:
left/right/inset-inline-*, plus the inline components ofinset-inlineandinset, become the gutter when they are0, andcalc(original + gutter)when they are not;autoand CSS-wide keywords are left alone;width/inline-sizeand friends becomemin(100%, column width)when they are100%;- the block axis (
top/bottom) is untouched — a centred column only constrains the inline axis.
When the column equals the viewport, the gutter is 0, so narrow-screen output is identical to what you would have written by hand. The correction is idempotent and does not reprocess a value that already contains these variables.
Only the rule's winning local position declaration is considered. Order and !important are respected inside that declaration block; inheriting positioning or resolving a winner from another rule is not something this local transform can observe statically, and guessing would be worse than missing.
It is on by default whenever appPcPreset's root foundation is enabled — both of that preset's profiles set rootMaxWidth, which is exactly the configuration where the problem appears. Turn it off with appPcPreset({ root: true, fixedContainingBlock: false }) (add rootSelector: '#app' only when that is the actual layout root).
Outside that preset, enable it only when at least one profile sets rootMaxWidth. The compiler rejects fixedContainingBlock: true without such a profile: no centred column exists in that configuration, so the option would only rewrite declarations while keeping the gutter permanently at zero.
Legacy WebView mode
adaptiveMatrix({
...appPcPreset(),
strategy: 'viewport',
preserveOriginal: true,
})This emits the original px followed by the vw. Whether to use it should be decided by your real target browsers; modern projects should prefer the default clamp.
Use npx adaptive-matrix src/app.css --targets "ios_saf 13, chrome 90" to compare detected features in the bundled support table against explicit targets. Findings describe the impact and available configuration alternatives. Untracked syntax and rendering behavior are not certified by an empty report. See Browser support and degradation.