VaneUI

VaneUI

Customization

Variant Inheritance

How components inherit colors from ancestor layouts via CSS custom-property cascade.

Edit this page

VaneUI components inherit their colors from ancestor components through native CSS custom-property cascade. This page explains how inheritance works, when components set their own colors vs. inherit, and how to control the behavior.

How It Works

VaneUI's color system has three layers:

  1. :root fallbacks — set --text-color, --bg-color, --border-color etc. to the primary outline palette (dark text, white background, light border)
  2. Direct CSS rules — when a component has data-variant + data-appearance attributes, a CSS rule fires and rewrites those variables on the element
  3. CSS cascade — children that don't set their own variables inherit from the nearest ancestor that did
:root → --text-color: dark, --bg-color: white
<Card filled primary> → --text-color: white, --bg-color: dark (CSS rule fires)
<Button> → inherits white text from Card (no attrs, no rule)
<Button primary outline> → --text-color: dark (explicit attrs, own rule fires)
<Text> → inherits white text from Card (inherit mode)
<Mark> → --text-color: amber (warning) (identity, own rule fires)

Which Components Set vs. Inherit

Components That Inherit (no data attributes by default)

These components use the primary + outline defaults from the library baseline. Because primary + outline matches the :root palette, VaneUI skips data-attribute emission — letting the component inherit from its nearest ancestor that DID set variables.

Button, Card, Badge, Code, Kbd, Input, IconButton, NavLink, Icon

A default <Button> inside a <Card filled primary> inherits the Card's white text and dark background — it automatically looks right on the dark surface. No configuration needed.

Components That Set Their Own Colors (identity components)

These components have defaults that deviate from the primary + outline baseline. VaneUI detects this and emits data attributes so the component's own CSS rule fires, pinning its colors regardless of context.

ComponentDefault AppearanceWhy
Markwarning + outlineYellow highlighter must be visible everywhere
Chipsecondary + outlineMuted tag should look consistent
Linklink + outlineBlue hyperlink must be recognizable
Checkboxprimary + filledChecked state needs non-white background

A <Mark> inside a <Card filled danger> still renders in its warning (yellow) palette — it doesn't turn red.

Components in Inherit Mode (typography)

Text, Title, SectionTitle, PageTitle, Label, List, ListItem, Divider, Blockquote

These components default to inherit — they never emit data attributes and always read colors from their nearest ancestor. This is how <Text> inside a <Card filled primary> automatically gets white text without any props.

Explicit Props Always Win

When you explicitly set an appearance or variant on a component, VaneUI emits data attributes and the component's own CSS rule fires — overriding any inherited values:

react-icon
<Card filled primary>
{/* Inherits from Card — white text on dark background */}
<Text>I'm white</Text>
{/* Explicit props — own CSS rule fires, dark text */}
<Text primary outline>I'm dark, even inside a filled Card</Text>
{/* Explicit different appearance — own CSS rule fires */}
<Text success>I'm green</Text>
</Card>

Nested Layouts

When multiple layout components are nested, each child inherits from its nearest ancestor that sets variables — not from the outermost ancestor:

react-icon
<Card filled primary>
{/* Card sets: --text-color = white, --bg-color = dark */}
<Stack outline primary>
{/* Stack sets its own: --text-color = dark, --bg-color = white */}
{/* (explicit outline primary → own CSS rule fires) */}
<Text>I'm dark (inherits from Stack, not Card)</Text>
</Stack>
<Stack filled danger>
{/* Stack sets its own: --text-color = white, --bg-color = red */}
<Text>I'm white on red (inherits from danger Stack)</Text>
</Stack>
</Card>

ThemeProvider Overrides

ThemeProvider.themeDefaults overrides are treated as user intent — they cause data-attribute emission even if the resolved value matches baseline:

react-icon
{/* All Badges in this subtree render as danger, with data attributes */}
<ThemeProvider themeDefaults={{ badge: { danger: true } }}>
<Card filled primary>
{/* This Badge renders red (danger), not inherited from Card */}
<Badge>Alert</Badge>
</Card>
</ThemeProvider>

The Baseline Rule

The data-attribute gate uses a simple rule:

Emit data-appearance and data-variant when the resolved values deviate from primary + outline — either because the user explicitly set props, because themeDefaults changed them, or because the component's library defaults are non-baseline (identity components).

This means:

  • <Button> → primary + outline (baseline) → no attrs → inherits
  • <Button filled> → primary + filled (filled ≠ outline) → attrs emitted → own rule
  • <Button danger> → danger + outline (danger ≠ primary) → attrs emitted → own rule
  • <Mark> → warning + outline (warning ≠ primary) → attrs emitted → own rule
  • <Text> → inherit + outline (inherit excluded) → no attrs → inherits

Granular Inheritance Props

By default, the inherit appearance keyword inherits everything — color, size, background, and border — from the nearest ancestor. But sometimes you need selective inheritance: a Link inside a Title should inherit font-size (so the link matches the heading size) but keep its own link-blue color.

VaneUI provides four independent boolean toggle props for this:

PropWhat it inheritsNegative toggle
inheritSizeFont-size and line-height from parentnoInheritSize
inheritColorText color via CSS variable cascadenoInheritColor
inheritBgBackground color via CSS variable cascadenoInheritBg
inheritBorderBorder color via CSS variable cascadenoInheritBorder

How inherit expands

When a component has inherit appearance (the default for Text, Title, Label, List, Divider, Blockquote), VaneUI expands it into color, background, and border inheritance — but not size:

<Text inherit>
↓ expands to:
inheritColor + inheritBg + inheritBorder
(NOT inheritSize — size uses own --fs variable so <Text sm> works as expected)

Size inheritance is separate — only inline components like Link, Code, Kbd, and Mark have inheritSize: true in their defaults. You can also set it explicitly:

react-icon
<Card filled primary>
{/* Inherits color (white) but uses own md size */}
<Text inherit>Inherited color, own size</Text>
{/* Explicit inheritSize — also inherits font-size from parent */}
<Text inherit inheritSize>Inherited color AND size</Text>
</Card>

Link, Mark — exact size inheritance via inheritSize

Link and Mark have their own appearance (Link = link, Mark = warning) so the inherit expansion does NOT fire. Instead, they have inheritSize: true set explicitly in their defaults — they render at the exact font-size of the nearest typography ancestor.

react-icon
<Title lg>
Check the <Link href="/docs">documentation</Link> for details
</Title>
  • Link renders at the Title's lg font-size (inherited 1:1) but stays link-blue (own appearance)
  • Mark renders at parent size with its own warning highlight color

Code, Kbd — em-relative geometry

Code and Kbd use a different mechanism: their .vane-code / .vane-kbd rules override --spacing to 0.25em locally, so the entire geometry pipeline (font-size, padding, border-radius, gap) resolves in em — proportional to the parent's font-size.

react-icon
<Title lg>
Run <Code>npm install</Code> to add the package
</Title>
  • Code renders at 87.5% of Title's lg font-size (Code's default md ratio = 0.875×)
  • The size prop adjusts the ratio: xs = 0.625, sm = 0.75, md = 0.875 (default), lg = 1 (matches parent), xl = 1.125

So inline Code feels right-sized in every context — body text at 14 px / 16 px, headings at 21 px / 24 px, hero displays at 42 px / 48 px.

Opting out (Link, Mark)

Use noInheritSize to keep Link or Mark at its own size instead of the parent's:

react-icon
<Title lg>
Heading with <Link noInheritSize href="/docs">fixed-size link</Link>
</Title>

The Link renders at its default md size while the Title is lg. For Code/Kbd, the size prop already controls the ratio — pass any of xs / sm / md / lg / xl to adjust.

Responsive overrides inheritSize

Title, PageTitle, and SectionTitle have responsive: true in their defaults. Responsive sizing takes priority over inheritSize — a responsive heading always uses its viewport-scaled size, even if inheritSize is set via the inherit expansion.