Tailwind Translate: 16 Uses, and the One That Had to Be Inline
Centering, drawers and hover swaps across 8 components — plus the containing-block defect a no-op translate-x-0 shipped, and why a computed value can't be a class.
Tailwind's translate-x-* and translate-y-* utilities move an element along an axis without affecting layout — nothing reflows, nothing around it shifts. This storefront uses them 16 times across 8 components, and 7 of those are one idiom: centering. One of them also shipped a visible bug, because a translate does something to its descendants that nothing about the class name suggests.
The short answer
translate-x-<n> and translate-y-<n> compile to CSS translate (v4) / transform: translateX() (v3), moving an element from where layout already put it. Values come from the spacing scale (translate-x-5 → 1.25rem), as fractions of the element's own size (-translate-x-1/2 → -50%), or translate-x-full (100%). Prefix with - to negate. The element's layout box does not move.
The census
16 translate instances, 9 distinct values, 8 components
4 -translate-y-1/2 3 -translate-x-1/2
3 translate-x-0 1 translate-x-full
1 translate-x-5 1 -translate-x-5
1 translate-y-5 1 -translate-y-5
1 translate-y-0
Split by what the translate is actually for:
| Purpose | Instances | Where |
|---|---|---|
| Centering against an anchor | 7 | TemplateGallery ×2, NewsletterForm, YourProductsGrid, Testimonials, HeroBackground, PreviewModal |
| Off-canvas drawer slide | 3 | Header |
| Hover icon swap | 6 | Trust |
Three jobs, and only the second two are animations. The largest group isn't motion at all.
The centering idiom: 7 of 16 uses, and it isn't animation
// src/components/molecules/TemplateGallery.tsx — the carousel's prev button
className="absolute top-1/2 left-3 grid size-10 -translate-y-1/2 place-items-center rounded-full bg-white/90 ..."
top-1/2 puts the button's top edge at the midpoint of its container. That is not centered — it's half the button's height too low. -translate-y-1/2 pulls it back up by half of its own height, and the two together centre it exactly, without anyone needing to know what that height is.
That last part is the whole reason the idiom exists. Percentage top/left resolve against the parent; percentage translate resolves against the element itself. It's the only pair of properties that can express "half of me" in CSS without measuring, which is why this appears 7 times here and in every codebase:
// src/components/organisms/Testimonials.tsx
<div className="absolute bottom-0 left-1/2 -translate-x-1/2">
// src/components/molecules/HeroBackground.tsx — centered glow
className="absolute top-[-32px] left-1/2 h-[140px] w-[320px] -translate-x-1/2 sm:top-[-56px] sm:h-[180px] sm:w-[520px]"
// src/components/molecules/NewsletterForm.tsx — submit button inside the input
className="focus:shadow-focused absolute top-1/2 right-1 flex h-10 min-w-12 -translate-y-1/2 items-center justify-center rounded-full bg-primary ..."
The HeroBackground case shows why it beats a hard-coded offset: the glow's width changes at sm (w-[320px] → w-[520px]), and -translate-x-1/2 stays correct across both without a second responsive variant. A ml-[-160px] would have needed sm:ml-[-260px] beside it, and would drift the next time the width changed.
Where flexbox or grid already owns the layout, none of this is needed — place-items-center on that same gallery button centres the icon inside it. The translate idiom is specifically for absolute/fixed elements, which have no parent layout to defer to.
The drawer: translate-x-full and the one real slide
// src/components/organisms/Header.tsx
className={`fixed inset-y-0 right-0 z-9999 flex w-[85%] max-w-xs flex-col justify-between overflow-y-auto bg-white px-6 py-8 shadow-2xl transition-[transform,visibility] duration-300 ease-in-out xl:pointer-events-auto xl:visible xl:static xl:inset-auto xl:z-auto xl:max-w-none xl:flex-1 xl:translate-x-0 xl:flex-row ... ${navOpen ? "visible translate-x-0" : "pointer-events-none invisible translate-x-full"}`}
Three things worth pulling out of that string:
translate-x-full is 100% of the element, not of the viewport. The drawer is w-[85%] max-w-xs, so translate-x-full moves it exactly its own width to the right — just past the edge, whatever that width resolves to. A viewport-relative value like translate-x-[100vw] would overshoot on a wide screen and animate for longer than the drawer is visible.
Transform is the right property to animate. transition-[transform,visibility] animates the slide on the compositor; animating right or margin-left instead would relayout on every frame. This is the main practical reason to reach for translate over a positional offset in motion.
invisible is in the transition list, and that was a bug fix. The comment above the class says it: pointer-events-none stops the mouse but not the Tab key, so the closed drawer's links stayed focusable off-screen and a keyboard user on a phone tabbed through invisible nav links. A translate moves an element out of sight but leaves it in the accessibility tree and the tab order. invisible removes it; putting visibility in the transition means the flip happens only after the slide finishes, so the outgoing drawer stays visible while it animates.
The desktop bar is the same element. xl:translate-x-0 unconditionally resets the transform above xl, where the drawer becomes a static horizontal nav. This is why translate-x-0 has 3 uses — a translate often needs explicit cancellation at a breakpoint or state, not just application.
The hover swap: 6 uses, 2 elements, one icon
Trust stacks two copies of the same arrow glyph and slides them past each other on hover:
// src/components/organisms/Trust.tsx
<span className="relative flex h-5 w-5 items-center justify-center overflow-hidden">
<svg className="absolute inset-0 m-auto h-4 w-4 transition-all duration-300 ease-in-out
group-hover:translate-x-5 group-hover:-translate-y-5" ... />
<svg className="absolute inset-0 m-auto h-4 w-4 -translate-x-5 translate-y-5
transition-all duration-300 ease-in-out
group-hover:translate-x-0 group-hover:translate-y-0" ... />
</span>
The first arrow sits at rest and exits up-and-right on hover; the second starts parked down-and-left and arrives at 0,0. The effect is one arrow travelling diagonally through the box with no gap. Three details make it work:
overflow-hiddenon therelativeparent clips both arrows at the 5×5 boundary, so neither is visible in transit.- The resting offsets (
-translate-x-5 translate-y-5) are plain utilities, notgroup-hoverones — the incoming arrow is translated by default andgroup-hover:translate-x-0cancels it. Both arrows are always translated; hover only swaps which values apply. 5(1.25rem) exceeds the 5×5 box (1.25rem), so each arrow is fully outside the clip at its extreme.
Accounting for all 6: 2 on the outgoing arrow's hover state, 2 resting on the incoming one, 2 on its hover state. No element here is translated once.
The defect: a translate makes the element a containing block for fixed children
This one shipped, and the class name gives no hint of it.
SavedTemplates renders a slide-up sheet. It was originally inline in the header markup, and it rendered visibly broken on mobile — a full-width button spilling across the page and no backdrop at all. The fix is a portal, and the reason is in the code:
// src/components/molecules/SavedTemplates.tsx
{/*
* PORTALLED TO document.body ON PURPOSE — do not inline this back into
* the header markup. `Header`'s nav wrapper carries `xl:translate-x-0`
* (and `translate-x-full` on mobile) for its slide-in drawer, and a
* non-`none` transform makes that element the containing block for any
* `position: fixed` descendant. Rendered in place, the overlay and panel
* size themselves against the nav box instead of the viewport, which
* shipped as a visibly broken sheet: a full-width "View all saved"
* button spilling across the page and no backdrop.
*/}
{open && createPortal(<>...</>, document.body)}
Any non-none transform — including translate-x-0 — makes that element the containing block for every position: fixed descendant. fixed inset-0 on the backdrop then means "fill the nav wrapper," not "fill the viewport."
translate-x-0 is the nastiest version, because it is a no-op visually while being a real value in the cascade. Here is what it actually compiles to in this build's CSS:
.xl\:translate-x-0 { --tw-translate-x: 0; translate: var(--tw-translate-x) var(--tw-translate-y) }
translate: 0 — not translate: none. Zero pixels of movement, and a new containing block for every fixed child. (In v4 these utilities set the standalone translate property rather than transform, which changes nothing about this: per the CSS Transforms spec, transform, translate, rotate and scale all establish one when their value isn't none.) The desktop-reset class that makes the drawer work above xl is what broke a sheet nested inside it.
There is no warning for this. The markup is valid, the CSS is valid, and the only symptom is a fixed child sized against the wrong box.
Comparison: moving an element without moving layout
| Approach | Reflows layout | Compositor-friendly | Knows "half of me" | Used here |
|---|---|---|---|---|
translate-x-* / translate-y-* | No | Yes | Yes (-1/2, full) | 16× |
left-* / top-* on absolute | No (but repaints) | No | No — resolves against parent | As the anchor, paired with translate |
ml-* / mt-* | Yes | No | No | Never for motion |
inset-0 + h-full w-full | No | — | N/A | Where fill, not offset, is wanted |
Keyframes with transform | No | Yes | Yes | 1× — the marquee animation |
That last row is the exception to the utility-first rule. The Carousel marquee lives in globals.css, not in a class:
/* src/app/globals.css */
@keyframes marquee {
from { transform: translateX(0); }
to { transform: translateX(-100%); }
}
A utility can set one static value per state. A continuous loop needs two keyframes and no state change between them, which is not a thing a class can express — so it becomes a keyframe that happens to use the same CSS property.
The one translate that cannot be a utility
// src/components/molecules/TemplateGallery.tsx
<div
className="flex transition-transform duration-500 ease-out motion-reduce:transition-none"
ref={trackRef}
style={{ transform: `translateX(-${index * 100}%)` }}
>
The gallery track slides by -100% per slide index — an inline style, in a codebase that is otherwise entirely utility classes. It has to be, and the reason is Tailwind's build model: class names are discovered by scanning source files as plain text. translate-x-[-${index * 100}%] is not a class name in the source; it's a template literal that only becomes one at runtime, long after the CSS was generated. The utility would never be emitted and the element would never move.
The rule is that an arbitrary value must be literally written out to exist. A value computed per render belongs in style, and that is the correct call rather than a workaround — the alternative is enumerating -translate-x-[0%] through -translate-x-[300%] as safelisted classes for a carousel whose slide count comes from the catalog.
Worth noting what stays a utility here: transition-transform duration-500 ease-out and motion-reduce:transition-none. Only the value is dynamic, so only the value goes inline — the animation's shape, and the reduced-motion opt-out, remain classes.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
A fixed child sizes itself against a parent, not the viewport | An ancestor has a non-none transform — even translate-x-0 | Portal the child to document.body, or remove the transform |
-translate-x-1/2 centers nothing | No left-1/2 (or left-[50%]) anchor beside it | The idiom is both halves: left-1/2 -translate-x-1/2 |
translate-x-full overshoots or undershoots | full is 100% of the element, not the viewport | Intended for off-canvas by own width; use a viewport unit if you really mean the screen |
| Off-screen translated links are still tabbable | Translate moves pixels, not the tab order | Add invisible (and put visibility in the transition so it flips after the slide) |
A dynamic translate-x-[${n}%] does nothing | Tailwind scans source text; a runtime-built class was never generated | Use an inline style for the computed value |
| The slide animation stutters | A layout property (left, margin) is being animated | Animate transform instead — transition-[transform] |
| A translate persists at a larger breakpoint | Transforms aren't reset by a breakpoint automatically | Cancel it explicitly, e.g. xl:translate-x-0 |
| An element overlaps its neighbour after translating | Expected — translate doesn't change the layout box | Use margin or a layout change if siblings must move too |
Frequently asked questions
What's the difference between translate-x-full and translate-x-[100%] in Tailwind?
Nothing — both resolve to 100% of the element's own width. full is the named shorthand for the fractional scale (1/2, full), which measures against the element itself, unlike left-* percentages which measure against the parent.
Why do left-1/2 and -translate-x-1/2 have to be used together?
They resolve against different boxes. left-1/2 puts the element's left edge at the parent's midpoint; -translate-x-1/2 then pulls it back by half of its own width. Only the second one can express "half of me" without knowing the element's size, which is what makes the pair work for content of unknown width.
Does a translate affect surrounding elements? No. The element's layout box stays exactly where layout put it, so nothing reflows and nothing shifts — which is also why a translated element can overlap its neighbours. If siblings need to move too, the translate is the wrong tool.
Can I use a computed value in a Tailwind translate class?
Not if it's computed at runtime. Arbitrary values must appear literally in the source for the class scanner to find them, so a template-literal class name is never generated. Put the computed transform in an inline style and keep the transition and reduced-motion handling as classes, the way this repo's carousel track does.
Templates in this post
ASoc Reach, ASoc Realm and ASoc Relay are Next.js + Tailwind landing page templates built on the same transform conventions audited above — left-1/2 -translate-x-1/2 for anchored centering, translate-x-full drawers that animate on the compositor, and motion-reduce opt-outs on every transition.
Browse the full sets: Next.js landing page templates, Tailwind landing page templates.
