Message scroller
A scrolling list for messages. It sticks to the bottom as messages arrive, lets go when you scroll up, and offers a jump button with a count of what you missed.
Message 1
Message 2
Message 3
Message 4
Message 5
Message 6
Message 7
Message 8
Message 9
Message 10
Message 11
Message 12
Usage
use hydroxy_leptos::button::{Button, ButtonSize, ButtonVariant};
use hydroxy_leptos::message_scroller::MessageScroller;
use leptos::prelude::*;
pub fn demo() -> AnyView {
// Messages are numbered; older ones go below zero so prepending is visible.
let (ids, set_ids) = signal((1..=12).collect::<Vec<i32>>());
let oldest = StoredValue::new(1_i32);
view! {
<div style="display: grid; gap: var(--space-4); max-width: 520px;">
<div style="display: flex; flex-wrap: wrap; gap: var(--space-3);">
<Button
variant=ButtonVariant::Secondary
size=ButtonSize::Sm
on_click=move |_| set_ids.update(|v| v.push(v.last().copied().unwrap_or(0) + 1))
>
"New message"
</Button>
<Button
variant=ButtonVariant::Secondary
size=ButtonSize::Sm
on_click=move |_| {
let first = oldest.get_value();
oldest.set_value(first - 5);
set_ids.update(|v| {
v.splice(0..0, (first - 5)..first);
});
}
>
"Load 5 older"
</Button>
</div>
<div style="height: 320px; border: 1px solid var(--border-1); border-radius: var(--radius-lg); padding: var(--space-3);">
<MessageScroller
count=Signal::derive(move || ids.with(Vec::len))
last_key=Signal::derive(move || ids.with(|v| v.last().map(ToString::to_string)))
label="Demo messages"
>
<For
each=move || ids.get()
key=|id| *id
children=|id| {
view! {
<p style="margin: 0; padding: var(--space-3) var(--space-4); border-radius: var(--radius-md); background: var(--surface-sunken); font: var(--text-sm);">
"Message " {id}
</p>
}
}
/>
</MessageScroller>
</div>
</div>
}
.into_any()
}use dioxus::prelude::*;
use hydroxy_dioxus::button::{Button, ButtonSize, ButtonVariant};
use hydroxy_dioxus::message_scroller::MessageScroller;
#[component]
pub fn MessageScrollerDemo() -> Element {
// Messages are numbered; older ones go below zero so prepending is visible.
let mut ids = use_signal(|| (1..=12).collect::<Vec<i32>>());
let mut oldest = use_signal(|| 1_i32);
rsx! {
div { style: "display: grid; gap: var(--space-4); max-width: 520px;",
div { style: "display: flex; flex-wrap: wrap; gap: var(--space-3);",
Button {
variant: ButtonVariant::Secondary,
size: ButtonSize::Sm,
on_click: move |_| {
let next = ids.peek().last().copied().unwrap_or(0) + 1;
ids.write().push(next);
},
"New message"
}
Button {
variant: ButtonVariant::Secondary,
size: ButtonSize::Sm,
on_click: move |_| {
let first = *oldest.peek();
oldest.set(first - 5);
ids.write().splice(0..0, (first - 5)..first);
},
"Load 5 older"
}
}
div { style: "height: 320px; border: 1px solid var(--border-1); border-radius: var(--radius-lg); padding: var(--space-3);",
MessageScroller {
count: ids.read().len(),
last_key: ids.read().last().map(ToString::to_string),
label: "Demo messages",
for id in ids.read().iter() {
p {
key: "{id}",
style: "margin: 0; padding: var(--space-3) var(--space-4); border-radius: var(--radius-md); background: var(--surface-sunken); font: var(--text-sm);",
"Message {id}"
}
}
}
}
}
}
}
Anatomy
| Component | data-part | Renders |
|---|---|---|
MessageScroller | root | Positions the jump button. data-state: following or away |
| viewport | The scrolling role=log region. Focusable so the keyboard can scroll it |
| jump | Shown while away from the bottom: "Jump to latest" or "3 new messages" |
Keyboard
| Key | Does |
|---|---|
Arrow keys / Page keys | Scroll the focused list natively |
Enter / Space on the jump button | Return to the bottom and follow again |
Props
| Prop | Type | Notes |
|---|---|---|
count | usize | How many messages the list holds |
last_key | Option<String> | Key of the newest message. A new key means append; the same key with a larger count means older messages were prepended |
revision | usize | Bump when the newest message grows in place, such as streaming text |
threshold | f64 | Pixels from the bottom that still count as at the bottom. Default 48 |
label | String | Accessible name. Default "Messages" |