How to get scrollbar to automatically scroll to bottom?

Godot 4.2 Stable

The game I’m working on is heavily menu-based, and I’m trying to find a way to get the scroll container to scroll to the bottom of its contents via script whenever a new menu pops up. Looking at the other answers on here talks about using functions that don’t appear to exist anymore. Anything associated with .value and max_value don’t work, although max value shows up as an integer when printed. What exactly am I missing?

Modify it via ScrollContainer.scroll_vertical instead of changing the ScrollBar would be fine.

I’m afraid this doesn’t work either. I tried multiple ways of using ScrollContainer, including set_deferred and across multiple scripts. I either get error messages or nothing happens.

Strangely, when I attach the function to a button it works perfectly, but I can’t seem to get it to automatically go. I’ve even tried to use signals to relay the command to a custom script attached to the container, but nothing seems to do the trick.

Update: I just added a print to the signal function and it appears to be calling correctly. It’s almost as if something is overwriting the scroll value after the function is called. Considering I have no other functions attached to the scroll value, any ideas on possible culprits?

well um, just set it to 9999 or really get the max scroll value and set_deffered the scroll_vertical of the scrollcontainer to that value

how to get the max scroll value?

the_scrollcontainer.get_v_scroll_bar().max_value

how to set the scroll_vertical to max value?

the_scrollcontainer.set_deferred("scroll_vertical", the_scrollcontainer.get_v_scroll_bar().max_value)

As I said, I tried set_deferred already. My function is calling properly, but something else appears to be overwriting the value after the function sets it.

I’ve come across a workaround by using a timer set for .01 seconds into runtime.

have you tried tweening the value to that?

1 Like

This solution works as well, and without the need for an extra node. Much appreciated

You just remind me of the scene tree timer:

await get_tree().create_timer(.01).timeout
the_scrollcontainer.set_deferred("scroll_vertical", the_scrollcontainer.get_v_scroll_bar().max_value)

or

get_tree().create_timer(.01).timeout.connect(
    func () -> void:
        the_scrollcontainer.set_deferred("scroll_vertical", the_scrollcontainer.get_v_scroll_bar().max_value)
)

Just put this in your ScrollContainer. Assuming you are adding some labels to the VBoxContainer when a button is pressed.

extends ScrollContainer

var start_text=100

func _enter_tree():
	set_deferred("scroll_vertical", 10000)

func _on_button_pressed():
	var label=Label.new()
	label.horizontal_alignment=HORIZONTAL_ALIGNMENT_CENTER
	start_text += 10
	label.text = str(start_text)
	$VBoxContainer.add_child(label)
	call_deferred("update_scroll")
	pass # Replace with function body.

func update_scroll():
	set_deferred("scroll_vertical", 10000)

I’m losing my mind. I’m using 4.3, using ChatGPT, I cannot get the scrollbar to go to the end.

I’m using

	await get_tree().create_timer(1).timeout
	scroll_container.set_deferred("scroll_vertical", 10000)

does not work. why is this so hard.

Here’s the function I used in my script to solve this problem. I’m taking a LineEdit’s submitted signal, making a new Label (of a custom extended class, CommandEntryLabel), assigning the submitted text to it, and appending it into a VBoxContainer within a ScrollContainer.

My best guess about what’s going on is that you tell it to scroll to what the maximum value is, but then a split second later it updates what that max value ACTUALLY is, which makes it appear like it didn’t really scroll to the bottom.

I hope this helps!

func _on_command_entry_text_submitted(command_text: String) -> void:
	var new_entry: CommandEntryLabel = CommandEntryLabel.new()
	new_entry.text = command_text
	%HistoryEntries.add_child(new_entry)
	%CommandEntry.text = ''
	
	# Waiting a split second after adding the text allows the program to catch up and properly
	# scroll all the way to the bottom. It's frustrating that I have to even do this hack,
	# but... here we are.
	get_tree().create_timer(.01).timeout.connect(
		func () -> void:
			%CommandHistory.scroll_vertical = %CommandHistory.get_v_scroll_bar().max_value
	)
1 Like
texts_container.add_child(message_scene)
await to_signal(RenderingServer.singleton, RenderingServer.FRAME_POST_DRAW)
texts_container.get_parent().get_parent().scroll_vertical = 10000

This is how I did it. Waiting for the next frame works well

Another option is to do two call_deferred – in other words, wait two process frames.

func post_message(msg: String):
  # ... post your message
  call_deferred("scroll_chat_to_bottom_1")

func scroll_chat_to_bottom_1():
  call_deferred("scroll_chat_to_bottom_2")

func scroll_chat_to_bottom_2():
  scroll_container.scroll_vertical = scroll_container.get_v_scroll_bar().max_value

This is also really low-cost, as it’s just a normal (efficient) call_deferred, twice, rather than spawning a timer or awaiting a signal.