In Flutter, why does a GestureDetector around a padded sticker miss taps on its padding, and what do HitTestBehavior opaque, translucent and deferToChild change?
answer
- hit test builds the path first
- default: deferToChild with a child
- translucent when there is no child
- opaque stops targets behind it
- arena winner is a separate question
basics
~20 sWith a child, GestureDetector defaults to HitTestBehavior.deferToChild, so it is hit only where a descendant is hit and padding misses. opaque claims its whole box and blocks targets behind; translucent claims it but lets targets behind be hit too.
solid answer
~40 sA `GestureDetector` only sees a pointer if its render object is on the hit-test path. Its `behavior` defaults to `deferToChild` when it has a child, so it counts as hit only where a descendant is hit; padding and gaps in a `Row` contain no child, so taps there fall through. `HitTestBehavior.opaque` makes the detector hit anywhere in its bounds and stops the hit test from reaching targets painted behind it in a `Stack`. `translucent` also receives events on empty space but still reports no hit upwards, so targets behind are hit too. Without a child the default is `translucent`. None of this decides which of two nested detectors wins; that is the gesture arena.
code
dart · 38 linesimport 'package:flutter/material.dart';
class StickerCanvas extends StatelessWidget {
const StickerCanvas({super.key, required this.onDeselect});
final VoidCallback onDeselect;
@override
Widget build(BuildContext context) {
return Stack(
children: [
// Bottom layer: tapping empty canvas clears the selection.
// No child, so a GestureDetector would already be translucent;
// opaque makes the intent explicit and blocks nothing above it.
Positioned.fill(
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: onDeselect,
),
),
// A sticker whose PNG has transparent margins inside a padded box.
Positioned(
left: 60,
top: 80,
child: GestureDetector(
// deferToChild (the default here) would miss taps on the padding.
behavior: HitTestBehavior.opaque,
onTap: () => debugPrint('sticker selected'),
child: Padding(
padding: const EdgeInsets.all(12),
child: Image.asset('assets/stickers/star.png', width: 80),
),
),
),
],
);
}
}go deeper
Remember that a tap on padding or empty space can miss a GestureDetector, and that behavior: HitTestBehavior.opaque makes the whole box respond.
Explain the hit-test path, the defaults with and without a child, and the difference between opaque and translucent inside a Stack.
Separate hit testing from arena resolution when debugging overlapping detectors, and use debugPaintPointersEnabled and the inspector to prove which boxes receive pointers.
Treat touch-target sizing and hit behaviour as a shared component concern, so custom controls do not each rediscover missed taps.
## Hit testing comes first Before any gesture is recognized, Flutter runs a **hit test**: starting at the root, each render object asks its children whether the pointer position lands on them, and every object that reports a hit is added to the **hit-test path**. Only objects on that path receive the pointer's events, and a `GestureDetector` can only start recognizing a gesture if its render object (a `RenderPointerListener`, created through `RawGestureDetector` and `Listener`) is on the path. `HitTestBehavior` decides how such a proxy render object answers the question "was I hit?" when the position is inside its bounds. ## The three values | Value | Receives events on its empty space? | Lets targets visually behind it be hit too? | |---|---|---| | `deferToChild` | no — only where a descendant is hit | yes, where no descendant is hit | | `opaque` | yes, everywhere in its bounds | no — it reports itself as hit, so the search stops | | `translucent` | yes, everywhere in its bounds | yes, where no descendant is hit | In source terms, `hitTestSelf` returns true only for `opaque`; `translucent` adds the object to the path even when nothing was hit, but still reports "not hit" upwards, so a parent such as `Stack` keeps looking at the children painted beneath. ## Defaults you must remember - `GestureDetector.behavior` is `null` by default, which becomes **`deferToChild` if there is a child** and **`translucent` if there is none**. - `Listener` defaults to `deferToChild`. - The `GestureDetector` inside `InkWell` is `opaque`. - `DragTarget` defaults to `translucent`; `Draggable` to `deferToChild`. ## Why the tap on the gap is lost With the default `deferToChild`, the detector is only hit where one of its descendants is hit. In a `Row` with `MainAxisAlignment.spaceBetween`, the gaps belong to the `Row`'s bounds but to no child, and a plain `Row` never reports itself hit. `Padding` is the same: the padded area contains no child. Transparent regions of an image still hit, because the `Image` render box fills its box. A `Container` with a `color` is tappable everywhere because it builds a `ColoredBox`, whose render object is itself `opaque`. The fix is one line: `behavior: HitTestBehavior.opaque` (the whole rectangle is yours) or `translucent` (yours, and whatever is underneath still gets a chance). ## Choosing between opaque and translucent 1. **Opaque** for a standalone target that should own its whole box: a row, a padded sticker, a full-screen tap-to-dismiss layer. 2. **Translucent** when an overlay must observe taps without stealing them from what lies beneath in a `Stack`, such as an invisible layer that logs taps or detects a double tap over a canvas. 3. **deferToChild** when only the painted parts should react — rare for touch targets, useful when detectors are tiled edge to edge. ## What `HitTestBehavior` does not do It governs **which objects are on the hit-test path**, not which recognizer wins among them. A parent and a child `GestureDetector` that both want a tap are both on the path whatever their behaviour; the gesture arena then picks one — by default the innermost tap recognizer. The framework docs state it directly: setting `opaque` or `translucent` "has no impact on parent-child relationships". Blocking all input to a subtree is a job for `AbsorbPointer` or `IgnorePointer`, not for this flag. ## Debugging Set `debugPaintPointersEnabled = true` to flash the boxes that receive pointers, and use the widget inspector to check the detector's actual size — a zero-size or tightly wrapped child is the other common reason a tap "goes nowhere".
- Why is a Container with a color tappable across its whole area even with the default behaviour?A `Container` with a `color` builds a `ColoredBox`, and its render object uses `HitTestBehavior.opaque`, so it reports a hit anywhere in its bounds. Because a descendant is hit, a `deferToChild` detector around it is hit too. Remove the colour and the same taps on empty space go nowhere.
- Does setting the parent GestureDetector to opaque stop a child GestureDetector from receiving the tap?No. Behaviour only controls whether the parent joins the hit-test path, and the child is on that path anyway. Both detectors send tap recognizers into the gesture arena, and by default the innermost one wins. To block input to a subtree, use `AbsorbPointer` or `IgnorePointer`.
saying these in an interview costs you the question
- GestureDetector covers its whole rectangle by default.
- translucent means the widget ignores touches on its empty space.
- opaque on a parent stops its children from getting taps.
- A GestureDetector without a child defaults to opaque.
- HitTestBehavior decides which detector wins the gesture arena.