Skip to content

transition-mixins — cross-port transition class names

styles/theme/common/transition.scss is the single source of cross-port transition class names. It solves a real pain point:

Vue2’s transition class names are <name>-enter / <name>-leave (the starting state of enter / leave), while Vue3 and React changed them to <name>-enter-from / <name>-leave-from. Both ports express the same semantics (“enter start state” / “leave start state”) with different names. Previously 16 components hand-wrote duplicate selectors file by file, and fixing one place missed another.

Mixin Parameters Generated class names Purpose
lh-enter-both $selector (required), $child (optional) #{$selector}-enter, #{$selector}-enter-from Duplicate-writes the enter start state
lh-leave-both $selector, $child? #{$selector}-leave, #{$selector}-leave-from Duplicate-writes the leave start state
lh-transition-ends $selector, $child? #{$selector}-enter, #{$selector}-enter-from, #{$selector}-leave-to Transition start/end states (enter start duplicated + the leave end state, which is named identically on both ports)

All three receive a style block through @content.

Pass the second argument when the transition state needs to live on a descendant node; the generated selector then gets a #{$child} suffix:

// without $child: the state lives on the selector itself
@include lh-enter-both('.lh-backtop-fade') { ... }
// → .lh-backtop-fade-enter, .lh-backtop-fade-enter-from { ... }
// with $child: the state lives on the inner panel
@include lh-enter-both('.lh-drawer-fade', '.lh-drawer__panel--right') { ... }
// → .lh-drawer-fade-enter .lh-drawer__panel--right,
// .lh-drawer-fade-enter-from .lh-drawer__panel--right { ... }
  • -enter-to / -leave-to are named identically on both ports and need no duplication.
  • Only the paired start states need duplicating: -enter / -enter-from and -leave / -leave-from.
  • lh-transition-ends covers the common case where “the enter start and the leave end are the same thing” (a fade: opacity 0 → 1 → 0) — one mixin covers both the duplicated enter start state and the leave end state.

An ordinary transition (backtop fade-in / fade-out)

Section titled “An ordinary transition (backtop fade-in / fade-out)”
.lh-backtop-fade {
@include lh-transition-ends('.lh-backtop-fade') {
opacity: 0;
}
}

Compile result:

.lh-backtop-fade-enter,
.lh-backtop-fade-enter-from,
.lh-backtop-fade-leave-to {
opacity: 0;
}

Only the enter start state (dialog fade-in; the end state is not needed)

Section titled “Only the enter start state (dialog fade-in; the end state is not needed)”
.lh-dialog-fade {
@include lh-enter-both('.lh-dialog-fade') {
opacity: 0;
transform: scale(0.9);
}
}

State on the inner panel (drawer slide-in)

Section titled “State on the inner panel (drawer slide-in)”
.lh-drawer-fade {
@include lh-enter-both('.lh-drawer-fade', '.lh-drawer__panel--right') {
transform: translateX(100%);
}
}

Vue3’s <transition name="lh-drawer-fade"> adds lh-drawer-fade-enter-from to the root element during the enter phase; the selector’s descendant matches the panel, so the panel translates — the root element itself does not move, avoiding a transform on the teleported root node interfering with positioning.

styles/drawer/index.scss
@use 'theme/common/transition' as *;
.lh-drawer-fade {
@include lh-enter-both('.lh-drawer-fade', '.lh-drawer__panel--right') {
transform: translateX(100%);
}
@include lh-leave-both('.lh-drawer-fade', '.lh-drawer__panel--right') {
transform: translateX(100%);
}
}

UI layer (Vue3):

<transition name="lh-drawer-fade">
<div v-if="visible" class="lh-drawer">...</div>
</transition>

UI layer (Vue2):

<transition name="lh-drawer-fade">
<div v-show="visible" class="lh-drawer">...</div>
</transition>

Both ports use the same name, and the duplicated selectors generated by the mixins each match their own port’s class names. This is how “distributed injection, single-sourced content” lands for transition class names.

  • Component styles must never hand-write duplicate transition class names — always @include (red line 2). verify:styles item 5 (transition class names single-sourced) scans for bare -enter-from / -leave-from literals.
  • The @use path: @use 'theme/common/transition' as *; (the relative path from each component’s scss, or the bare specifier at build time, is decided by the injection strategy).
  • The React case: the React build uses headless transitions (state-driven className), and the mixin-generated -enter-from class names apply just the same.
  • The $child suffix is a descendant selector: it is not an & concatenation — the generated selector is .parent-enter .child and relies on the root element carrying the transition class name.

Copyright © 2024-2026 FengMo WenXue