tactile 0.3.1
tactile: ^0.3.1 copied to clipboard
Make any widget feel physical: it tilts toward your finger, depresses at the press point, and a glare tracks where you press. Compositor-only, no shaders.
tactile #
Make any Flutter widget feel physical when you touch it. It tilts toward your finger, depresses at the exact press point, and a specular highlight tracks where you press.
No shaders. Compositor-only. Works on iOS, Android, web, macOS, Windows, and Linux at full frame rate.
Tactile(
onTap: () => print('tapped'),
child: const FlutterLogo(size: 120),
)
Why #
Flutter has neumorphism packages for the static soft-extruded look. None of
them make interaction press-position-aware — the visual response that
follows where and how you touch. That's what tactile does, and it works on
any child, not just buttons.
Install #
flutter pub add tactile
Then import 'package:tactile/tactile.dart';.
Usage #
Wrap anything:
Tactile(
borderRadius: BorderRadius.circular(16),
onTap: () {},
child: myWidget,
)
Feel presets #
A TactileFeel bundles the press parameters; named presets cover the common
cases, and Tactile.from applies one:
Tactile.from(TactileFeel.subtle, child: ...) // restrained — cards, dense UI
Tactile.from(TactileFeel.crisp, child: ...) // quick and snappy
Tactile.from(TactileFeel.playful, child: ...) // exaggerated — hero buttons
Tactile.from(TactileFeel.heavy, child: ...) // deep, slow, with a haptic
Tactile.subtle(child: ...) // shorthand, still supported
Tactile.playful(child: ...)
Start from a preset and tweak one knob — individual parameters always win:
Tactile(feel: TactileFeel.playful, tilt: 0.1, child: ...)
Haptics #
Opt into haptic feedback, fired only on a confirmed interaction — a real tap, keyboard activation, or a completed long-press escalation. A press that turns into a scroll never buzzes:
Tactile(onTap: () {}, haptics: TactileHaptics.light, child: ...)
TactileHaptics is none (default), light, medium, heavy, or
selection. With longPressEscalation: true, holding deepens the press and
fires onLongPress plus a stronger haptic when it clicks into place:
Theme #
Set a default feel — and a default TactileStyle for the styled components —
once for a whole subtree with TactileTheme. Individual widgets still override
it:
TactileTheme(
data: const TactileThemeData(feel: TactileFeel.crisp),
child: MyApp(),
)
Styled components #
Batteries-included widgets built on Tactile that own their surface, so
they also morph their neumorphic shadows from extruded to flush as you press:
TactileButton(onTap: () {}, child: const Text('Press me'))
TactileCard(child: ...) // leans like a held card
TactileTile( // list row
leading: const Icon(Icons.bolt),
title: const Text('Title'),
subtitle: const Text('Subtitle'),
trailing: const Icon(Icons.chevron_right),
onTap: () {},
)
Pass a TactileStyle to control the surface (color, gradient,
borderRadius, elevation, lightDirection) and the feel (tilt, depress,
glareIntensity). Neumorphic shadows read best when color is close to the
surrounding background.
Tuning #
| Parameter | Default | What it does |
|---|---|---|
tilt |
0.15 |
Max perspective rotation toward the press point (radians). 0 disables. |
depress |
0.04 |
Fractional scale-in at the touch point. 0.04 → shrinks to 96%. |
glare |
true |
Moving specular highlight overlay. |
glareColor |
Colors.white |
Highlight color. |
glareIntensity |
0.35 |
Peak glare opacity at full press. |
borderRadius |
BorderRadius.zero |
Clips the glare to the child's rounded shape. |
springBack |
true |
Release uses spring physics (vs. reversed curve). |
pressCurve |
Curves.easeOut |
Curve while pressing in. |
pressDuration |
90ms |
Press-in duration. |
haptics |
none |
Haptic on a confirmed press: light/medium/heavy/selection. |
longPressEscalation |
false |
Hold to deepen the press; fires onLongPress on completion. |
enabled |
true |
When false, no effects and callbacks don't fire. |
Any of these can be left unset and supplied by a feel: bundle or a
TactileTheme; an explicit value here always wins.
How it works #
The press point drives everything — touch position is normalized to [-1, 1]
from the child's center, and a single press-progress value scales both the tilt
magnitude and depress depth, so release springs them back together:
- Tilt — a perspective
Matrix4rotation about X/Y toward the finger. - Depress —
Transform.scalewith its origin offset to the touch point, so the child shrinks toward your finger rather than its center. - Glare — a radial gradient painted on top, centered at the finger and
clipped to
borderRadius. - Spring-back — a
SpringSimulationreturns press-progress to rest on release, with a little overshoot.
Effects are transform + overlay only and wrapped in a RepaintBoundary, so they
run on the compositor and never touch the child's layout.
Accessibility #
- Respects the platform reduce-motion setting: tilt and glare are dropped, leaving only a quiet depress as the press affordance.
- Exposes button semantics and a tap action when
onTapis provided. - Supports keyboard activation (Enter/Space) when focused — which also fires the configured haptic.
- Haptics fire only on a confirmed interaction, so a press that becomes a scroll never produces stray feedback.
enabled: falsedisables all effects and callbacks.- Tracks a single pointer; multi-touch can't tear the effect in two directions.
- The effect follows your finger as you drag, while
onTapis suppressed once the press becomes a drag (so dragging never counts as a tap).
Scrollables:
Tactiledrives its press through a gesture recognizer that joins the gesture arena, so inside aListView/PageViewit yields to the scroll's drag (springing back) instead of animating alongside it. Taps and standalone finger-tracking are unaffected.
Example #
A gallery with recordable scenes lives in example/. Run it with:
cd example && flutter run
Status #
The core Tactile wrapper, the styled components (TactileButton,
TactileCard, TactileTile) with neumorphic shadow-morph, gesture-arena
coexistence with scrollables, feel presets (TactileFeel), app-wide theming
(TactileTheme), haptics, and long-press escalation are all in place.
Development #
This repo is pinned to a Flutter version with FVM
(see .fvmrc). With FVM installed:
fvm install # gets the pinned Flutter version
fvm flutter pub get
fvm flutter test
fvm flutter analyze
(Plain flutter works too if your global SDK satisfies the constraints in
pubspec.yaml.)
Support #
If tactile saved you time, you can buy me a coffee ☕.
License #
MIT © 2026