Overview
FractionalTranslation applies a translation transformation before painting its child.
The translation is expressed as an offset scaled to the child’s size. For example, an offset with x of 0.25 will result in a horizontal translation of one quarter the width of the child.
Hit tests will only be detected inside the bounds of the FractionalTranslation, even if the contents are offset such that they overflow.
See: https://api.flutter.dev/flutter/widgets/FractionalTranslation-class.html
When to use it?
- When you need to position relative to the child’s own size
- For size-independent movement in responsive animations
- When dynamically adjusting overlay or tooltip positions
- When moving based on the child widget’s center point or edges
- For implementing slide in/out animations
Basic Usage
// Move right by half, down by quarter
FractionalTranslation({
translation: { x: 0.5, y: 0.25 },
child: Container({
width: 100,
height: 100,
color: 'blue'
})
})
// Center (move left and up by half of own size)
FractionalTranslation({
translation: { x: -0.5, y: -0.5 },
child: Container({
width: 50,
height: 50,
color: 'red'
})
})
// Hide completely upward
FractionalTranslation({
translation: { x: 0, y: -1.0 },
child: Container({
width: 200,
height: 80,
color: 'green'
})
})
Props
translation (required)
Value: { x: number, y: number }
The translation offset expressed as a fraction of the child’s size.
x
: Horizontal translation ratio relative to width (-∞ ~ +∞)y
: Vertical translation ratio relative to height (-∞ ~ +∞)
Value meanings:
0.0
: No translation1.0
: Move by the widget’s own width (x) or height (y)-1.0
: Move by the widget’s own width (x) or height (y) in opposite direction0.5
: Move by half of the widget’s own width (x) or height (y)
FractionalTranslation({
translation: { x: 0.25, y: -0.5 }, // Right by 1/4, up by 1/2
child: child
})
FractionalTranslation({
translation: { x: -1.0, y: 0 }, // Completely to the left
child: child
})
child
Value: Widget | undefined
The child widget to which the translation transformation will be applied.
Practical Examples
Example 1: Slide In Animation
const SlideInPanel = ({ isVisible, children }) => {
const [isAnimating, setIsAnimating] = useState(false);
useEffect(() => {
if (isVisible) {
setIsAnimating(true);
setTimeout(() => setIsAnimating(false), 300);
}
}, [isVisible]);
return AnimatedContainer({
duration: Duration.milliseconds(300),
curve: Curves.easeInOut,
child: FractionalTranslation({
translation: {
x: isVisible ? 0 : 1.0, // Slide in from off-screen
y: 0
},
child: Container({
width: 300,
padding: EdgeInsets.all(20),
decoration: BoxDecoration({
color: 'white',
borderRadius: BorderRadius.only({
topLeft: Radius.circular(16),
topRight: Radius.circular(16)
}),
boxShadow: [
BoxShadow({
color: 'rgba(0,0,0,0.2)',
blurRadius: 10,
offset: { x: 0, y: -2 }
})
]
}),
child: Column({
mainAxisSize: MainAxisSize.min,
children: children
})
})
})
});
};
Example 2: Custom Tooltip
const CustomTooltip = ({ text, position, isVisible }) => {
// Calculate translation based on tooltip position
const getTranslation = () => {
switch (position) {
case 'top': return { x: -0.5, y: -1.1 }; // Above, center aligned
case 'bottom': return { x: -0.5, y: 0.1 }; // Below, center aligned
case 'left': return { x: -1.1, y: -0.5 }; // Left, vertically centered
case 'right': return { x: 0.1, y: -0.5 }; // Right, vertically centered
default: return { x: 0, y: 0 };
}
};
return AnimatedOpacity({
opacity: isVisible ? 1.0 : 0.0,
duration: Duration.milliseconds(200),
child: FractionalTranslation({
translation: getTranslation(),
child: Container({
padding: EdgeInsets.symmetric({ horizontal: 12, vertical: 8 }),
decoration: BoxDecoration({
color: 'rgba(0,0,0,0.8)',
borderRadius: BorderRadius.circular(6)
}),
child: Text(text, {
style: TextStyle({
color: 'white',
fontSize: 12
})
})
})
})
});
};
Example 3: Overlay Notification
const OverlayNotification = ({ message, type, onDismiss }) => {
const [isExiting, setIsExiting] = useState(false);
const handleDismiss = () => {
setIsExiting(true);
setTimeout(onDismiss, 300);
};
useEffect(() => {
const timer = setTimeout(handleDismiss, 4000);
return () => clearTimeout(timer);
}, []);
return FractionalTranslation({
translation: {
x: 0,
y: isExiting ? -1.2 : 0 // Slide out upward
},
child: AnimatedContainer({
duration: Duration.milliseconds(300),
margin: EdgeInsets.all(16),
padding: EdgeInsets.all(16),
decoration: BoxDecoration({
color: type === 'success' ? '#4CAF50' : '#F44336',
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow({
color: 'rgba(0,0,0,0.2)',
blurRadius: 8,
offset: { x: 0, y: 2 }
})
]
}),
child: Row({
children: [
Icon({
icon: type === 'success' ? Icons.check : Icons.error,
color: 'white'
}),
SizedBox({ width: 12 }),
Expanded({
child: Text(message, {
style: TextStyle({ color: 'white' })
})
}),
GestureDetector({
onTap: handleDismiss,
child: Icon({
icon: Icons.close,
color: 'white',
size: 18
})
})
]
})
})
});
};
Example 4: Image Gallery Preview
const ImagePreview = ({ image, isExpanded, onToggle }) => {
return GestureDetector({
onTap: onToggle,
child: AnimatedContainer({
duration: Duration.milliseconds(400),
curve: Curves.easeInOut,
child: FractionalTranslation({
translation: isExpanded
? { x: -0.5, y: -0.5 } // Move to center
: { x: 0, y: 0 }, // Original position
child: Transform.scale({
scale: isExpanded ? 2.0 : 1.0,
child: Container({
width: 120,
height: 120,
decoration: BoxDecoration({
borderRadius: BorderRadius.circular(8),
boxShadow: isExpanded ? [
BoxShadow({
color: 'rgba(0,0,0,0.3)',
blurRadius: 20,
offset: { x: 0, y: 10 }
})
] : []
}),
child: ClipRRect({
borderRadius: BorderRadius.circular(8),
child: Image({
src: image.url,
objectFit: 'cover'
})
})
})
})
})
})
});
};
Example 5: Floating Action Menu
const FloatingActionMenu = ({ isOpen, actions }) => {
return Column({
mainAxisSize: MainAxisSize.min,
children: [
// Action buttons (appear from bottom to top)
...actions.map((action, index) =>
AnimatedContainer({
duration: Duration.milliseconds(200 + index * 50),
child: FractionalTranslation({
translation: {
x: 0,
y: isOpen ? 0 : 1.5 + (index * 0.2) // Hide below
},
child: Container({
margin: EdgeInsets.only({ bottom: 16 }),
child: Row({
mainAxisAlignment: MainAxisAlignment.end,
children: [
// Label
AnimatedOpacity({
opacity: isOpen ? 1.0 : 0.0,
duration: Duration.milliseconds(150),
child: Container({
padding: EdgeInsets.symmetric({
horizontal: 12,
vertical: 8
}),
decoration: BoxDecoration({
color: 'rgba(0,0,0,0.7)',
borderRadius: BorderRadius.circular(4)
}),
child: Text(action.label, {
style: TextStyle({
color: 'white',
fontSize: 12
})
})
})
}),
SizedBox({ width: 16 }),
// Action button
FloatingActionButton({
mini: true,
onPressed: action.onPressed,
backgroundColor: action.color,
child: Icon(action.icon)
})
]
})
})
})
})
),
// Main FAB
FloatingActionButton({
onPressed: () => setIsOpen(!isOpen),
child: AnimatedRotation({
turns: isOpen ? 0.125 : 0, // 45 degree rotation
duration: Duration.milliseconds(200),
child: Icon(Icons.add)
})
})
]
});
};
Understanding Coordinates
Translation Directions
// Positive directions
FractionalTranslation({
translation: { x: 1.0, y: 1.0 }, // Move right and down
child: child
})
// Negative directions
FractionalTranslation({
translation: { x: -1.0, y: -1.0 }, // Move left and up
child: child
})
// Single direction movement
FractionalTranslation({
translation: { x: 0, y: -0.5 }, // Move up by half only
child: child
})
Center Alignment Pattern
// Position at parent center
Center({
child: FractionalTranslation({
translation: { x: -0.5, y: -0.5 },
child: Container({
width: 100,
height: 100,
color: 'blue'
})
})
})
Important Notes
- Hit tests only work within the original position boundaries
- Children can overflow parent area, so consider clipping
- Excessive movement can harm user experience
- Consider performance when using with animations
- Understand direction correctly when using negative values
Related Widgets
- Transform.translate: Absolute pixel-based translation
- Positioned: Absolute positioning within Stack
- Align: Alignment-based positioning
- AnimatedPositioned: Animated positioning
- Offset: Coordinate offset representation class