Split Color
A ::before pseudo-element clipped with clip-path: inset() splits the text into two tones; @keyframes splitWave continuously animates the split line — pure CSS, zero dependencies.
You'll get access to the interactive demo with a free account.
This effect is part of Effect.Labs — 811 vanilla effects, some free, some premium. Text has 56 effects, including 4 free. Explore the category →
3 usage examples



How it works
The text is rendered twice in pixel-perfect overlay. The <span class="text-split"> element displays the upper half in its base color (color: #6366f1). Its ::before pseudo-element reproduces the same content via content: attr(data-text), positioned at position: absolute; left: 0; top: 0, and masked by clip-path: inset(50% 0 0) — which clips the top 50%, leaving only the lower half of the pseudo-element visible in the secondary color (color: #d946ef). The visual result is a single piece of text whose two halves have different colors.
The @keyframes splitWave animation oscillates the pseudo-element's clip-path between two states: inset(50% 0 0) (split at exactly 50% height) and inset(40% 0 10%) (the visible band shifts up 10%, its bottom edge also rises 10%). The colored band moves slightly upward and back, creating a gentle wave over a 2 s ease-in-out infinite cycle. The boundary is never a sharp line but shaped by the glyphs themselves.
The JS exposes a utility function splitChars(el) that wraps each character of the element's text in an individual <span class="char">. Combining the class text-split-hover on the element and the class scattered (added via JS) lets each character fade (opacity: 0.3) and move (transform) with the defined transitions — cubic-bezier(.68, -.55, .265, 1.55) for bounce. The ::before pseudo-element keeps displaying the full text via data-text, maintaining the two-tone effect even during the scatter.
The variable el = getElementById('fadeInText') at the script's top level is dormant: it returns null when no element carries that id, with no runtime error. Since the two-tone animation starts purely via CSS, no JS function call is needed to initialize it — adding the classes demo-text text-split and the data-text attribute is all that is required.
Accessibility
- prefers-reduced-motion: absent from the code — the
splitWaveanimation runs even when the OS requests reduced motion. Recommended fix:@media (prefers-reduced-motion: reduce) { .text-split::before { animation: none; } }. - The
data-textattribute duplicates the visible text content: screen readers may announce the text twice. Wrap the element in a container witharia-labeland addaria-hidden="true"on the colored<span>to avoid duplication. - Default color contrast on background
#0a0a0f:#6366f1(upper half) → ratio ≈ 5.8:1 (WCAG AA ✓);#d946ef(lower half) → ratio ≈ 5.1:1 (WCAG AA ✓). Ratios vary with your background — verify with your custom colors. - Keyboard navigation: the effect is purely decorative by default, no focus management is required. If you activate the hover/scatter mode (
text-split-hover), make the element focusable (tabindex="0") and add an explicit:focus-visiblestate. - Semantics:
<span>is a neutral element (no implicit role). If the two-tone text is a page or section title, wrap it in the appropriate heading tag (<h1>–<h3>) and apply the classes on an inner<span>.
Browser compatibility
Relies on clip-path: inset() and content: attr() — supported in all modern browsers since 2017–2018. Zero external dependencies.
If <code>clip-path</code> is unsupported (IE 11, very old browsers), the text renders in the pseudo-element's color (<code>#d946ef</code>) without the split — readable and error-free.
The code
HTML structure to paste into your page (CSS + JS available with a premium account):
<div class="demo-preview">
<!-- Two-tone text — CSS-only animation -->
<span class="demo-text text-split" data-text="YOUR TEXT">YOUR TEXT</span>
<!-- Hover/scatter variant (add class + call splitChars(el) in JS) -->
<!-- <span class="demo-text text-split text-split-hover"
data-text="YOUR TEXT">YOUR TEXT</span> -->
</div>
Full HTML + CSS + JS, copy-paste ready — with hundreds of premium effects.
Customize
Options passed to the API or data-* attributes:
| Option / property | Default | Effect |
|---|---|---|
| color on .text-split | #6366f1 | Color of the upper text half (base element). Change the hex value directly in the CSS. |
| color on .text-split::before | #d946ef | Color of the lower half (::before pseudo-element). Should contrast with the base color for the effect to be legible. |
| font-size on .demo-text | 2.5rem | Text size. The effect reads best at large sizes (4–10 rem) where the two-tone boundary is clearly visible. |
| animation-duration in .text-split::before | 2s | Duration of one wave cycle. 1 s = fast and dynamic; 4–6 s = slow and ambient. Replace the first value of the animation shorthand. |
| inset() in @keyframes splitWave | inset(50% 0 0) / inset(40% 0 10%) | First value = boundary position (50% = midpoint). Increase to shrink the lower zone, decrease to expand it. Remove the animation and set a fixed value for a static split. |
| data-text on the element | "SPLIT" | Text content of the ::before pseudo-element. Must always match the element's visible textContent — a mismatch offsets the two layers. |
| font-weight on .demo-text | 800 | Font weight. At 800–900 the two colors are clearly distinct. Below 600, the boundary may look blurry on fonts with low weight contrast. |
| .text-split-hover class | absent | Add this class to the element and call splitChars(el) in JS to enable per-character scatter on hover. Then toggle .scattered via JS to trigger the fade + transform animation. |
FAQ
splitWave animation is driven entirely by CSS. The data-text attribute must match the element's visible text, and the two classes demo-text text-split are all that is needed. The JS in the snippet is only useful if you want to activate the per-character scatter via splitChars(el).clip-path in @keyframes splitWave. For example, inset(30% 0 0) expands the lower zone to 70% of the height; inset(70% 0 0) reduces it to 30%. To remove the animation and lock a static boundary, drop the animation property and write .text-split::before { clip-path: inset(50% 0 0); } directly.clip-path: inset() applies a single rectangular clip across the entire pseudo-element. On two lines, the boundary cuts at the same absolute level — line 1 may be fully upper-color and line 2 fully lower-color depending on the chosen value. For per-line control, a background: linear-gradient() combined with -webkit-background-clip: text; -webkit-text-fill-color: transparent gives more granular results.