Hover it for the shadow; the border color comes from woven.token().
Component styles
Each CSS module, and each <style lang="scss"> block in a Vue, Svelte or Astro component, is a separate Sass compile. When that file loads the main entry, @wovenui/core, every component repeats the tokens, the reset and the base styles. Worse, a component that loads core without your configuration emits the default tokens again, after the configured ones in the global stylesheet: a card compiled that way carries --wu-brand-500: #007bff while the app is configured with #4361ee. The @wovenui/core/mixins entry fixes both. It has the same API as @wovenui/core and outputs no CSS.
The pattern
Section titled “The pattern”Three files: one partial with the configuration, one global stylesheet loaded once by the app, and any number of components.
@forward "@wovenui/core/mixins" with ($brand: #4361ee);// src/styles/global.scss, loaded once by the app@use "woven";@use "@wovenui/core";@use "../styles/woven" as *;
.card { @include apply(p(4), bg(surface), rounded(md), shadow(md) hover, p(6) md);}- The partial holds the configuration. It forwards the mixins entry with your
with()settings. Components and the global stylesheet load the partial, so both see the same configured modules. - The global stylesheet emits the CSS. Loading
@wovenui/corethere writes the tokens, the reset, the base styles and, when enabled, the utility classes, with the configured brand: the compiled file has--wu-brand-500: #4361ee. - Components write their own rules only.
apply(), every mixin,token()and the configuration variables come through the partial.
@use "@wovenui/core/mixins" as *;
.card { @include apply(p(4), bg(surface), rounded(md), shadow(md) hover, p(6) md);}<article class="card"> <strong>Card title</strong> <p>Only the .card rules are compiled; the tokens come from the page.</p></article>.card { padding: var(--wu-space-4); background-color: var(--wu-bg-surface); border-radius: var(--wu-radius-md);}.card:hover { box-shadow: var(--wu-shadow-md);}@media (min-width: 768px) { .card { padding: var(--wu-space-6); }}Why @use "woven" comes first
Section titled “Why @use "woven" comes first”@wovenui/core forwards the same modules as @wovenui/core/mixins. Sass loads a module once per compile and accepts its configuration only on the first load. With @use "woven" first, the partial configures the modules and @wovenui/core reuses them. With the lines swapped, core loads with the defaults and the partial’s with() fails:
// src/styles/global.scss: wrong order@use "@wovenui/core";@use "woven";Error: This module was already loaded, so it can't be configured using "with".1 │ @forward "@wovenui/core/mixins" with ($brand: #4361ee); │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ new load2 │ @forward "mixins"; │ ━━━━━━━━━━━━━━━━━ original loadThe first frame is _woven.scss, the second is core’s index.scss. Configuring core directly after loading it fails with the same message. See Configuration for the with() rules.
What a component emits
Section titled “What a component emits”Card.module.scss above compiles to its own rules and nothing else:
.card { padding: var(--wu-space-4); background-color: var(--wu-bg-surface); border-radius: var(--wu-radius-md);}.card:hover { box-shadow: var(--wu-shadow-md);}@media (min-width: 768px) { .card { padding: var(--wu-space-6); }}The same card compiled through the main entry (@use "@wovenui/core" as *;) carries the whole foundation layer with it. Measured with Dart Sass 1.105.1 and wc -c:
| Card compiled through | Expanded | Compressed |
|---|---|---|
@wovenui/core/mixins (the partial) |
247 bytes | 206 bytes |
@wovenui/core |
48,953 bytes | 43,193 bytes |
Through the main entry, every component repeats those 43 KB. Through the partial, it adds only its own rules.
What stays in the global stylesheet
Section titled “What stays in the global stylesheet”The global stylesheet is the only file that emits the foundation layer, so load it on every page:
- Tokens. Every
--wu-*token: the fixed:rootblock, the light and dark themes, and the high-contrast blocks when set. Component rules read them throughvar(). - Reset and base layer.
@layer reset, base, components, the base element styles and.sr-only. @propertyregistrations. 13 registrations: the transform variables,--wu-gapand the transition group variables.
Component mixins that read the registered variables need the global stylesheet on the page:
| Mixins | Variables they read |
|---|---|
Transforms: translate-x(), scale(), rotate(), skew-x() and the rest |
--wu-translate-x, --wu-scale-x, --wu-rotate, --wu-skew-x … |
gap(), gap-x() and fill() |
--wu-gap |
transition-colors, transition-opacity, transition-shadow, transition-transform |
--wu-transition-colors … |
For example, @include apply(translate-x(4) hover); in a component writes transform: translateX(var(--wu-translate-x)) translateY(var(--wu-translate-y)) .... The unset variables take their registered initial values from the @property rules in the global stylesheet.
Keyframes are the exception: an animation mixin adds its keyframes in whichever compile uses it. A component with @include animate-spin; emits:
.spinner { animation: anim-spin-normal 1s linear infinite;}@keyframes anim-spin-normal { from { transform: rotate(0deg); } to { transform: rotate(360deg); }}Classes and feature flags
Section titled “Classes and feature flags”$generate-utility-classesaffects the global stylesheet only. With$generate-utility-classes: truein the partial, the global stylesheet grows from 48,706 to 1,180,439 bytes (expanded) and the card stays 247 bytes, byte for byte the same. Components never emit classes.- Feature flags apply everywhere. The partial configures the mixins too. With
$feature-flags: ("theme": false),apply(p(4), rounded(md))in a component givespadding: 1rem; border-radius: 0.5rem;, andbg(surface)raises an error because no tokens are emitted.
Namespaced loading
Section titled “Namespaced loading”Load the partial with a namespace to keep component files free of global names:
@use "../styles/woven" as woven;
.card { @include woven.apply(p(4), bg(surface), shadow(md) hover); border-color: woven.token(border-regular);}.card { padding: var(--wu-space-4); background-color: var(--wu-bg-surface);}.card:hover { box-shadow: var(--wu-shadow-md);}.card { border-color: var(--wu-border-regular);}@use "@wovenui/core/mixins" as woven;
.card { @include woven.apply(p(4), bg(surface), shadow(md) hover); border-color: woven.token(border-regular);}<article class="card border"> <strong>Namespaced card</strong> <p>Hover it for the shadow; the border color comes from woven.token().</p></article>.card { padding: var(--wu-space-4); background-color: var(--wu-bg-surface);}.card:hover { box-shadow: var(--wu-shadow-md);}.card { border-color: var(--wu-border-regular);}In a namespaced load the items reach apply() as text, which has three limits. Each is set by Sass, and each has a working form.
| Limit | Fails | Works |
|---|---|---|
| A map cannot be written into a text item | woven.apply(animate-spin((duration: 2s))): (duration: 2s) isn't a valid CSS value. |
woven.apply(woven.animate-spin((duration: 2s))) |
opacity() with anything but a number is the Sass color function |
woven.apply(opacity(inherit)): $color: inherit is not a color. |
woven.apply(woven.opacity(inherit)), or opacity(50) for a key |
A color function other than rgb() or rgba() takes no shade |
woven.apply(bg(oklch(60% 0.15 250), 600)): error, the color takes no shade in namespaced apply() |
apply(bg(oklch(60% 0.15 250), 600)) after @use ... as *, which gives #1272c1 |
Everything else matches apply() after @use ... as *. See apply() for the full syntax.
Tokens in plain component CSS
Section titled “Tokens in plain component CSS”token() works in any declaration of a component:
@use "../styles/woven" as *;
.panel { padding: token(space-4); color: token(text-muted);}.panel { padding: var(--wu-space-4); color: var(--wu-text-muted);}@use "@wovenui/core/mixins" as *;
.panel { padding: token(space-4); color: token(text-muted); background: token(bg-alternate); border-radius: token(radius-md);}<div class="panel"> Plain CSS declarations that read WovenUI tokens.</div>.panel { padding: var(--wu-space-4); color: var(--wu-text-muted); background: var(--wu-bg-alternate); border-radius: var(--wu-radius-md);}Unlike a hand-written var(--wu-...), token() is checked at compile time. A typo stops the build:
Error: "token(text-mutd): unknown token. Close matches: text-muted." ╷4 │ color: token(text-mutd); │ ^^^^^^^^^^^^^^^^ ╵The tokens page lists every name.
Per framework
Section titled “Per framework”Every framework uses the same three files. Only the place the component styles live and the file that loads global.scss change.
| Framework | Component styles | Global stylesheet loaded in | Guide |
|---|---|---|---|
| React | CSS module, src/components/Card.module.scss |
src/main.tsx |
React |
| Vue | <style scoped lang="scss"> in src/components/Card.vue |
src/main.js |
Vue |
| Svelte | <style lang="scss"> in src/lib/Card.svelte, compiled through vitePreprocess in svelte.config.js |
src/main.js |
Svelte |
| Astro | <style lang="scss"> in src/components/Card.astro, scoped to the component |
src/layouts/Layout.astro |
Astro |
Svelte compiles <style lang="scss"> only through a preprocessor. Without vitePreprocess, apply() reaches the Svelte compiler as plain CSS and no rule is written. For installing the package and the Sass version, see Installation.