When the button is shown as a toolbar item, the transition and associated animation is controlled by the native toolbar and I don’t think there is much you can do to change the animation effect.
- The transition seems to be a blur effect.
- It is important to include an
.animationmodifier, otherwise there is no animation at all. However, changing the animation style to something like.bouncyhas no effect – it seems that the style of animation is controlled by the native toolbar.
Here is a standalone example to show the native animation effect (this is just an elaborated version of what you showed in the question):
struct ContentView: View {
@State private var text = ""
var body: some View {
NavigationStack {
VStack {
TextField("Text", text: $text)
.textFieldStyle(.roundedBorder)
.toolbarVisibility(.visible, for: .automatic)
.padding()
Spacer()
}
.toolbar {
if !text.isEmpty {
ToolbarItem(placement: .topBarTrailing) {
Button("Delete", systemImage: "trash", role: .destructive) {
text = ""
}
}
}
}
.animation(.default, value: text.isEmpty)
.toolbarVisibility(.visible, for: .automatic)
}
}
}

The native animation can be made a bit more interesting if a ToolbarItemGroup is used, together with a second state variable that is used to change what is shown inside the group:
- the group is only shown when the text is not empty
- the group shows a
Spacerwhen first shown, then the spacer is replaced by the button - the switch can be performed using
.taskandwithAnimation.
@State private var isButtonShowing = false
VStack {
// ... as before
}
.toolbar {
if !text.isEmpty {
ToolbarItemGroup(placement: .topBarTrailing) {
if isButtonShowing {
Button("Delete", systemImage: "trash", role: .destructive) {
text = ""
}
} else {
Spacer()
}
}
}
}
.animation(.default, value: text.isEmpty)
.toolbarVisibility(.visible, for: .automatic)
.task(id: text.isEmpty) {
withAnimation {
isButtonShowing = !text.isEmpty
}
}

The difference is not huge, but the animation now has a bit of a bounce to it.
An alternative way to show the button would be to use either .safeAreaBar or .overlay, instead of as a toolbar item. This way, you get much more control over the style of transition and animation.
In order to make sure the button looks the same as a toolbar button, I would suggest using a custom ButtonStyle. For example:
struct RoundGlassButton: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
configuration.label
.font(.title2)
.labelStyle(.iconOnly)
.frame(minWidth: 44, minHeight: 44)
.contentShape(.circle)
.glassEffect(.regular.interactive(), in: .circle)
}
}
This is how the button with custom style can then be shown with a bouncy animation:
struct ContentView: View {
@State private var text = ""
var body: some View {
NavigationStack {
VStack {
TextField("Text", text: $text)
.textFieldStyle(.roundedBorder)
.toolbarVisibility(.visible, for: .automatic)
.padding()
Spacer()
}
}
.safeAreaBar(edge: .top, alignment: .trailing) {
if !text.isEmpty {
Button("Delete", systemImage: "trash", role: .destructive) {
text = ""
}
.buttonStyle(RoundGlassButton())
.padding(.horizontal, 16)
.transition(.asymmetric(
insertion: .scale.animation(.bouncy(duration: 0.3, extraBounce: 0.25)),
removal: .scale.combined(with: .opacity).animation(.easeInOut(duration: 0.3))
))
}
}
}
}
