Automatically scrolling to new text

I’m using the (link:) changer to display a passage paragraph by paragraphm, with the reader clicking each time to reveal the next. But when the text expands beyond the bottom of the screen, the reader has to scroll down each time, which feels clunky. Is there a way to automatically scroll down when the text is revealed?

(I’m coming back to Twine after a long time away and getting to grips with the new version. It might be that this sort of thing is trickier to do in Harlowe?)

Twine Version: 2.3.7
Story Format: Harlowe

1 Like

You can use the <element>.scrollIntoView() function to scroll an element into view.

The following is one possible method for using the above function within a Harlowe project to achieve the effect you want.

1 Add an empty footer tagged Passage to your project, this will be used to represent the end of the current Passage. You can name this Passage whatever you want but in this example it is named PassageEnd.

2 Change the (link:) macro you are using to ‘reveal’ the next ‘section’ of content to include the following code.

(live: 100ms)[(stop:)<script>$('tw-include[title="PassageEnd"]')[0].scrollIntoView({ behavior: "smooth", block: "start" })</script>]

The above uses a HTML <script> element to call the previously mentioned function on the custom Harlowe <tw-include> element that represents the ‘footer’. A (live:) macro is needed to delay the function call until after the newly ‘reveal’ content has been rendered, you may need to change the delay to suit the method you are using to inject that ‘revealed’ content.

eg. The following is an example of the above solution in action.

A very long block of text that fills the current view-port....

(link: "continue")[\
	(show: ?next)\
	(live: 100ms)[(stop:)<script>$('tw-include[title="PassageEnd"]')[0].scrollIntoView({ behavior: "smooth", block: "start" })</script>]\
]

|next)[\
Another very long block of text that appears below the end of the current view-port....
]
2 Likes

Thanks so much for the help with this, The scroll works well and I think I understand how it’s working. However, it’s raised a new problem, which is that I was using (t8n: “dissolve”) with (link) to fade in the new text, and (t8n:) doesn’t work with (show:). And if I leave it where it was, then I think the (show:) overrides the (t8n:) because the new text is just snapping in:

(t8n: "dissolve")+(link: "...")[(show: ?next)(live: 100ms)[(stop:)<script>$('tw-include[title="PassageEnd"]')[0].scrollIntoView({ behavior: "smooth", block: "start" })</script>]]|next)[.

I don’t fully understand the logic of why we need to use (show:), but I do see that this isn’t working without it.

Any suggestions?

The solution for scrolling an element into view doesn’t require the usage of the (show:) macro, I was only using that macro to reveal ‘hidden’ content because it is the simplest method for making content appear in a specific location.

You can use whatever method you want to ‘reveal’ the next block of text as long as you call the following after (or maybe before) doing that ‘reveal’…

(live: 100ms)[(stop:)<script>$('tw-include[title="PassageEnd"]')[0].scrollIntoView({ behavior: "smooth", block: "start" })</script>]

… however the new content needs to be fully rendered before you can scroll the target element into view, because otherwise the target element won’t be positioned correctly.

1 Like

Thank you again, very much. It’s all working now. This is what I ended up using:

(live: 100ms)[(stop:)<script>$('tw-include[title="PassageEnd"]')[0].scrollIntoView({ behavior: "smooth", block: "start" })</script>](t8n: "dissolve")+(link: "...")[New section text here]

I tried doing something similar myself with what you originally provided, but couldn’t get it working. Either I was getting my brackets in a muddle and the script broke, or, more likely, it does all need to happen in a particular order and I wasn’t getting it right. I tired moving the (live:) section to between the (t8n:) and the (link:), and also to after the (link:), and it doesn’t work in either of those places. Interestingly, there also doesn’t need to be a + between the (live:) section and the (t8n:) – a plus there is just printed as text. If anyone is able to explain why either of these things is the case, I’d be grateful, as it would help me understand the syntax here better.

Fascinating. It’s not actually working perfectly. It’s not working for the final section of the page and I have no idea why. With the below code, each new paragraph appears with a fade transition and scrolls down to show the paragraph, but the final paragraph snaps in and doesn’t scroll.

“Once there was a storyteller. When she was born, a forest spirit who had taken a liking to the storyteller’s mother gave the baby a gift: The child would grow up to be the greatest storyteller in the land, and people would travel from miles around just to hear her thrilling, strange and beautiful tales(live: 100ms)[(stop:)<script>$('tw-include[title="PassageEnd"]')[0].scrollIntoView({ behavior: "smooth", block: "start" })</script>](t8n: "dissolve")+(link: "...")[.

“But there was another spirit, a river spirit, who was unhappy. The forest and river spirits had once been the closest of friends. But the river spirit said something that upset the forest spirit, just once, and the forest spirit began  spending more time with humans, and with the storyteller’s mother in particular, and the river spirit grew jealous. So when the storyteller was born, the river spirit gave her a curse: whenever she tried to speak, great flocks of birds would begin calling, and no one would be able to hear her(live: 100ms)[(stop:)<script>$('tw-include[title="PassageEnd"]')[0].scrollIntoView({ behavior: "smooth", block: "start" })</script>](t8n: "dissolve")+(link: "...")[.

“And so it came to pass. The child grew, and grew full of astonishing stories, stories she couldn’t wait to tell the world. But whenever she opened her mouth, flocks of birds would surround her, and open their beaks, and drown out her words. Sometimes it was a murder of crows cawing raucously, sometimes a murmuration of starlings twittering incessantly – but always, the storyteller would be silenced(live: 100ms)[(stop:)<script>$('tw-include[title="PassageEnd"]')[0].scrollIntoView({ behavior: "smooth", block: "start" })</script>](t8n: "dissolve")+(link: "...")[.

“When the storyteller came of age (around 13 years old in those days), she sought help. She went to an old wise woman in the village with a plea in her eyes. (She couldn’t write down her problem either, or the birds would snatch it away.) Luckily, the wise woman knew exactly what was wrong, and had been waiting for this day(live: 100ms)[(stop:)<script>$('tw-include[title="PassageEnd"]')[0].scrollIntoView({ behavior: "smooth", block: "start" })</script>](t8n: "dissolve")+(link: "...")[.

“‘You’ve been cursed,’ said the wise woman. ‘It’s not your fault. When you were born, the forest and river spirits were fighting, and your curse was the result. They’re fighting still. I’m afraid the curse can only be lifted if you can bring them back together(live: 100ms)[(stop:)<script>$('tw-include[title="PassageEnd"]')[0].scrollIntoView({ behavior: "smooth", block: "start" })</script>](t8n: "dissolve")+(link: "...")[’.”

“The storyteller couldn’t speak, but she was full of compassion. She hated to see people fighting, and when her siblings argued she would be the one to go to them and hold them and show them they were loved, until the tears stopped and they no longer wanted to fight(live: 100ms)[(stop:)<script>$('tw-include[title="PassageEnd"]')[0].scrollIntoView({ behavior: "smooth", block: "start" })</script>](t8n: "dissolve")+(link: "...")[.

“So the storyteller went to the wood. She went to each tree and put her arms around them and let them know that they were loved. And inside the tree, the green spirit’s heart began to beat. And rhen the storyteller went to the river, and stepped into it and opened her arms and let it know that it was loved. And beneath the river, the blue spirit’s heart began to beat(live: 100ms)[(stop:)<script>$('tw-include[title="PassageEnd"]')[0].scrollIntoView({ behavior: "smooth", block: "start" })</script>](t8n: "dissolve")+(link: "...")[.”

“The change didn’t come overnight. It took many weeks of winning the trust of the forest and the river. Often the storyteller felt like giving up, often she felt an anger rising in her. But eventually, one day, a quiet, grey sort of day, the spirit of the forest and the spirit of the river came out of their hiding places and saw each other. And began to move towards each other. And the curse was lifted(live: 100ms)[(stop:)<script>$('tw-include[title="PassageEnd"]')[0].scrollIntoView({ behavior: "smooth", block: "start" })</script>](t8n: "dissolve")+(link: "...")[.

“As the storyteller saw the spirits embracing, she felt the words bubbling inside her belly. She needed to speak. And there were no birds to be seen. She opened her mouth(live: 100ms)[(stop:)<script>$('tw-include[title="PassageEnd"]')[0].scrollIntoView({ behavior: "smooth", block: "start" })</script>](t8n: "dissolve")+(link: "...")[.

“And this is the story she told...”

[[Listen|Listen 3]]]]]]]]]]]

note: I found your code easier to read after I used Escaped line break markup to reformat each ‘block’ of content to appear like so.

“Once there was a storyteller. When she was born, a forest spirit who had taken a liking to the storyteller’s mother gave the baby a gift: The child would grow up to be the greatest storyteller in the land, and people would travel from miles around just to hear her thrilling, strange and beautiful tales\
(live: 100ms)[(stop:)<script>$('tw-include[title="PassageEnd"]')[0].scrollIntoView({ behavior: "smooth", block: "start" })</script>]\
(t8n: "dissolve")+(link: "...")[.

Because the final paragraph doesn’t include a copy of the (live:) macro call that is being used to ‘scroll’ each paragraph into focus.

eg. your final paragraph should looks something the following…

(t8n: "dissolve")+(link: "...")[.

“And this is the story she told...”

[[Listen|Listen 3]]\
(live: 100ms)[(stop:)<script>$('tw-include[title="PassageEnd"]')[0].scrollIntoView({ behavior: "smooth", block: "start" })</script>]\
]]]]]]]]]
  1. The ‘delayed scroll’ (live:) macro call needs to be within the same block as the ‘just revealed’ content that you want it to scroll into place.
  2. You can only use the plus (+) operator to join specific Changer based macros together, and while the (live:) macro may be listed as a Changer it isn’t really one due to its nature, which is why you can’t join it with the other two Changer macros you are using. eg. the (t8n:) and (link:) macros.

Thank you, that’s all working now, and I think I grasp it conceptually too. My problem was that I thought that (live:) was modifying how the (link:) worked when clicked, but I now see that it’s a macro that runs when it’s revealed, which explains why I kept getting the implementation wrong. (I’m probably using the wrong terms here too.)

I think I’m getting on top of the escaped line break markup too.

Thank you again.