Run the following command. New project? Run npx shadcn@latest init first to set up Tailwind and path aliases.
npx shadcn@latest add @aicanvas/slice-type
2
For dark mode, add the dark class to your <html> element:
Optional
<html class="dark">
Install with AI Canvas MCP
With AI Canvas MCP, your AI knows every component we ship. Ask for one inside Claude Code, Codex, or Cursor and it installs the component you pick. Works with any AI Canvas account, free or premium.
Slice Type is a typographic magic trick: at rest you read one ambiguous word, but as you hover the letters slice horizontally — LIGHT lifting up, NIGHT sinking down — and the shared glyphs resolve into two separate words. The split is driven by Motion handling the vertical translations, with a canvas layer for the gradient masking that makes the cut feel clean. Use it for hero sections, brand sites, and editorial layouts where the headline itself is the surprise.
Built with
MotionCanvasTailwind CSS
Frequently asked questions
Is Slice Type free to use?
Yes. Slice Type is part of the free AI Canvas library and is open source under the MIT license, so you can use it in personal and commercial projects.
How do I install Slice Type?
One command: npx shadcn@latest add https://aicanvas.me/r/slice-type.json, free with a free AI Canvas account. Or connect the AI Canvas MCP server and ask your AI editor to install Slice Type for you. You can also copy the source straight from the Code tab, no account needed. It works in any React project with Tailwind CSS.
What is Slice Type built with?
Slice Type is built with React and TypeScript, using Motion, Canvas, and Tailwind CSS.
Where would I use Slice Type?
Common uses include Hero section, Editorial, and Brand site. Like every AI Canvas component, it is self-contained and drops into any React project.
Can I remix Slice Type with AI?
Yes. Slice Type ships with one comprehensive AI prompt written against the real source code. Open "Remix with AI" on this page to read and copy it into Claude, Cursor, ChatGPT, or any AI tool. Prompts are for remixing your own variation; for the exact component, install it with the one-command CLI.
One comprehensive prompt, written against the real source code. Works in Claude, Cursor, ChatGPT, or any AI tool you use.
This prompt is for remixing. Use it to build your own variation of Slice Type. Results depend on the model you use, and no prompt in the world is 100% exact.
Want the exact component?
One command installs it, pixel-perfect. Copy, paste into your project, done.
npx shadcn@latest add @aicanvas/slice-type
AI prompt for Slice Type
Before writing any code, verify the project has Tailwind CSS v4, TypeScript, and React set up. If missing, use the shadcn CLI to scaffold them.
Build a single-file React component called `SliceType` with `'use client'` at the top. Install dependency: `framer-motion`.
---
## Concept
A typographic magic trick using two words that share a tail skeleton: `LIGHT` and `NIGHT`. At rest you see one ambiguous hybrid word — the top half of LIGHT fused with the bottom half of NIGHT. The shared I/G/H/T letters align seamlessly; only the L/N leading glyph forms an ambiguous splice at the midline. On hover, the top word lifts up and the bottom word sinks down, both fully revealed. Simultaneously, the background inverts from dark to light — NIGHT becomes LIGHT.
---
## Always dark at rest — no theme prop
The component is always in **NIGHT mode at rest** (dark background, light text) regardless of the site's global theme. On hover it inverts to **LIGHT mode** in sync with the word reveal. Do not use a theme toggle or detect the global dark class.
```ts
const DARK_BG = '#0A0A0A'
const LIGHT_BG = '#EFEEE6'
const DARK_FG = '#EFEEE6'
const LIGHT_FG = '#0A0A0A'
```
Drive background and text color from the same `engageSmooth` MotionValue using a `mix(a, b, t)` sRGB hex lerp:
```ts
const bgColor = useTransform(engageSmooth, (e) => mix(DARK_BG, LIGHT_BG, e))
const fgColor = useTransform(engageSmooth, (e) => mix(DARK_FG, LIGHT_FG, e))
```
Set these on the root `<motion.div>` as `backgroundColor: bgColor, color: fgColor`. All text elements use `color: 'inherit'`.
---
## Layout
Root: `<motion.div>` with `flex min-h-screen w-full items-center justify-center overflow-hidden`, `touchAction: 'none'`, `cursor: 'pointer'`, and the animated bg/fg colors.
Inside, a single `<div ref={containerRef} className="relative">` that holds everything:
1. **Spacer** — an invisible `<span>` rendering `NIGHT` (the wider word) to set the container width.
2. **Top word wrapper** — a `<motion.div>` with `position: absolute; inset: 0` carrying the clip-path and vertical lift. Inside it:
- `<motion.span ref={lRef}>` for the **L**, with `position: absolute; top: 0; left: 0` and an animated `x`.
- `<span ref={ightRef}>` for **IGHT**, with `position: absolute; top: 0; right: 0`.
3. **Bottom word** — a `<motion.span>` with `position: absolute; inset: 0; textAlign: 'right'` carrying its own clip-path and vertical shift. Renders `NIGHT`.
---
## Typography
```ts
const sharedTextStyle: React.CSSProperties = {
fontFamily: 'var(--font-sans), ui-sans-serif, system-ui, -apple-system, Segoe UI, sans-serif',
fontWeight: 900,
fontSize: 'clamp(3.5rem, 18vw, 11rem)',
lineHeight: 0.92,
letterSpacing: '-0.04em',
color: 'inherit',
whiteSpace: 'nowrap',
userSelect: 'none',
}
```
---
## Engagement and animation
```ts
const OPEN_OFFSET = 0.65 // fraction of element height — how far words travel apart
const engage = useMotionValue(0)
const engageSmooth = useSpring(engage, { stiffness: 140, damping: 18, mass: 0.9 })
```
**Top word clip + lift:**
```ts
const topClip = useTransform(engageSmooth, (e) => `inset(0 0 ${50 * (1 - e)}% 0)`)
const topY = useTransform(engageSmooth, (e) => `${-OPEN_OFFSET * 100 * e}%`)
```
**Bottom word clip + sink:**
```ts
const botClip = useTransform(engageSmooth, (e) => `inset(${50 * (1 - e)}% 0 0 0)`)
const botY = useTransform(engageSmooth, (e) => `${OPEN_OFFSET * 100 * e}%`)
```
---
## The L alignment trick
The L and IGHT are **separate absolutely-positioned elements** — not a single text run. At rest:
- L is pinned at `left: 0` (same x as NIGHT's N). This merges L's top-half vertical stroke with N's bottom-half vertical into one continuous line.
- IGHT is pinned at `right: 0`. Its characters match NIGHT's IGHT exactly.
- Real layout space exists between L and IGHT — equal to N's width minus L's width.
On hover the L translates right to its natural touching-IGHT position so the word reads LIGHT.
**Measure L's natural position dynamically** (font-size is fluid):
```ts
const naturalLeftMV = useMotionValue(0)
useLayoutEffect(() => {
const measure = () => {
const cRect = containerRef.current!.getBoundingClientRect()
const lRect = lRef.current!.getBoundingClientRect()
const iRect = ightRef.current!.getBoundingClientRect()
const ightLeft = iRect.left - cRect.left
naturalLeftMV.set(Math.max(0, ightLeft - lRect.width))
}
measure()
const ro = new ResizeObserver(measure)
ro.observe(containerRef.current!)
return () => ro.disconnect()
}, [])
```
**L's x-translate** — includes a tiny nudge to compensate for font side-bearing:
```ts
const L_REST_NUDGE_PX = -1.5
const lX = useTransform(
[engageSmooth, naturalLeftMV],
([e, natural]) => `${L_REST_NUDGE_PX * (1 - e) + (natural as number) * (e as number)}px`,
)
```
---
## Intro teaser (once on mount)
After 700ms, play one open-and-close cycle so users discover the interaction:
```ts
const INTRO_DELAY_MS = 700, INTRO_HOLD_MS = 1100, INTRO_PEAK = 0.7, INTRO_DURATION_S = 0.9
```
Use `animate(engage, INTRO_PEAK, { duration: INTRO_DURATION_S, ease: [0.22, 1, 0.36, 1] })`, await it, hold, then close. Skip if `prefers-reduced-motion` is set.
---
## Pointer events
Set on the root motion.div: `onPointerEnter/Leave/Down/Up/Cancel` → `engage.set(1)` or `engage.set(0)`.
---
## sRGB hex lerp helper
```ts
function mix(a: string, b: string, t: number): string {
const pa = parseInt(a.slice(1), 16), pb = parseInt(b.slice(1), 16)
const ar = (pa >> 16) & 255, ag = (pa >> 8) & 255, ab = pa & 255
const br = (pb >> 16) & 255, bg = (pb >> 8) & 255, bb = pb & 255
const r = Math.round(ar + (br - ar) * t)
const g = Math.round(ag + (bg - ag) * t)
const bl = Math.round(ab + (bb - ab) * t)
return `#${((r << 16) | (g << 8) | bl).toString(16).padStart(6, '0')}`
}
```
Single file, default export, no external fonts needed.