Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).

## Unreleased

### Added

- Added scrollbar-visibility rule https://github.com/Textualize/textual/pull/6156

## [6.2.1] - 2025-10-01

- Fix inability to copy text outside of an input/textarea when it was focused https://github.com/Textualize/textual/pull/6148
Expand Down
27 changes: 27 additions & 0 deletions docs/examples/styles/scrollbar_visibility.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from textual.app import App
from textual.containers import Horizontal, VerticalScroll
from textual.widgets import Label

TEXT = """I must not fear.
Fear is the mind-killer.
Fear is the little-death that brings total obliteration.
I will face my fear.
I will permit it to pass over me and through me.
And when it has gone past, I will turn the inner eye to see its path.
Where the fear has gone there will be nothing. Only I will remain.
"""


class ScrollbarApp(App):
CSS_PATH = "scrollbar_visibility.tcss"

def compose(self):
yield Horizontal(
VerticalScroll(Label(TEXT * 10), classes="left"),
VerticalScroll(Label(TEXT * 10), classes="right"),
)


if __name__ == "__main__":
app = ScrollbarApp()
app.run()
11 changes: 11 additions & 0 deletions docs/examples/styles/scrollbar_visibility.tcss
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
VerticalScroll {
width: 1fr;
}

.left {
scrollbar-visibility: visible; # The default
}

.right {
scrollbar-visibility: hidden;
}
60 changes: 60 additions & 0 deletions docs/styles/scrollbar_visibility.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Scrollbar-visibility

The `scrollbar-visibility` is used to show or hide scrollbars.

If scrollbars are hidden, the user may still scroll the container using the mouse wheel / keys / and gestures, but
there will be no scrollbars shown.

## Syntax

--8<-- "docs/snippets/syntax_block_start.md"
scrollbar-visibility: hidden | visible;
--8<-- "docs/snippets/syntax_block_end.md"


### Values

| Value | Description |
| ------------------- | ---------------------------------------------------- |
| `hidden` | The widget's scrollbars will be hidden. |
| `visible` (default) | The widget's scrollbars will be displayed as normal. |


## Examples

The following example contains two containers with the same text.
The container on the right has its scrollbar hidden.

=== "Output"

```{.textual path="docs/examples/styles/scrollbar_visibility.py"}
```

=== "scrollbar_visibility.py"

```py
--8<-- "docs/examples/styles/scrollbar_visibility.py"
```

=== "scrollbar_visibility.tcss"

```css
--8<-- "docs/examples/styles/scrollbar_visibility.tcss"
```


## CSS

```css
scrollbar-visibility: visible;
scrollbar-visibility: hidden;
```



## Python

```py
widget.styles.scrollbar_visibility = "visible";
widget.styles.scrollbar_visibility = "hidden";
```
1 change: 1 addition & 0 deletions mkdocs-nav.yml
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ nav:
- "styles/scrollbar_colors/scrollbar_corner_color.md"
- "styles/scrollbar_gutter.md"
- "styles/scrollbar_size.md"
- "styles/scrollbar_visibility.md"
- "styles/text_align.md"
- "styles/text_opacity.md"
- "styles/text_overflow.md"
Expand Down
2 changes: 1 addition & 1 deletion src/textual/_compositor.py
Original file line number Diff line number Diff line change
Expand Up @@ -694,7 +694,7 @@ def add_widget(
if (
widget.show_vertical_scrollbar
or widget.show_horizontal_scrollbar
):
) and styles.scrollbar_visibility == "visible":
for chrome_widget, chrome_region in widget._arrange_scrollbars(
container_region
):
Expand Down
9 changes: 9 additions & 0 deletions src/textual/css/_styles_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
VALID_OVERLAY,
VALID_POSITION,
VALID_SCROLLBAR_GUTTER,
VALID_SCROLLBAR_VISIBILITY,
VALID_STYLE_FLAGS,
VALID_TEXT_ALIGN,
VALID_TEXT_OVERFLOW,
Expand All @@ -76,6 +77,7 @@
Display,
EdgeType,
Overflow,
ScrollbarVisibility,
TextOverflow,
TextWrap,
Visibility,
Expand Down Expand Up @@ -768,6 +770,13 @@ def process_color(self, name: str, tokens: list[Token]) -> None:
process_scrollbar_background_hover = process_color
process_scrollbar_background_active = process_color

def process_scrollbar_visibility(self, name: str, tokens: list[Token]) -> None:
"""Process scrollbar visibility rules."""
self.styles._rules["scrollbar_visibility"] = cast(
ScrollbarVisibility,
self._process_enum(name, tokens, VALID_SCROLLBAR_VISIBILITY),
)

process_link_color = process_color
process_link_background = process_color
process_link_color_hover = process_color
Expand Down
1 change: 1 addition & 0 deletions src/textual/css/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@
VALID_TEXT_WRAP: Final = {"wrap", "nowrap"}
VALID_TEXT_OVERFLOW: Final = {"clip", "fold", "ellipsis"}
VALID_EXPAND: Final = {"greedy", "optimal"}
VALID_SCROLLBAR_VISIBILITY: Final = {"visible", "hidden"}

HATCHES: Final = {
"left": "╲",
Expand Down
11 changes: 9 additions & 2 deletions src/textual/css/styles.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
VALID_OVERLAY,
VALID_POSITION,
VALID_SCROLLBAR_GUTTER,
VALID_SCROLLBAR_VISIBILITY,
VALID_TEXT_ALIGN,
VALID_TEXT_OVERFLOW,
VALID_TEXT_WRAP,
Expand Down Expand Up @@ -153,11 +154,10 @@ class RulesMap(TypedDict, total=False):
scrollbar_background: Color
scrollbar_background_hover: Color
scrollbar_background_active: Color

scrollbar_gutter: ScrollbarGutter

scrollbar_size_vertical: int
scrollbar_size_horizontal: int
scrollbar_visibility: ScrollbarVisibility

align_horizontal: AlignHorizontal
align_vertical: AlignVertical
Expand Down Expand Up @@ -242,6 +242,7 @@ class StylesBase:
"scrollbar_background",
"scrollbar_background_hover",
"scrollbar_background_active",
"scrollbar_visibility",
"link_color",
"link_background",
"link_color_hover",
Expand Down Expand Up @@ -424,6 +425,10 @@ class StylesBase:
"""Set the width of the vertical scrollbar (measured in cells)."""
scrollbar_size_horizontal = IntegerProperty(default=1, layout=True)
"""Set the height of the horizontal scrollbar (measured in cells)."""
scrollbar_visibility = StringEnumProperty(
VALID_SCROLLBAR_VISIBILITY, "visible", layout=True
)
"""Sets the visibility of the scrollbar."""

align_horizontal = StringEnumProperty(
VALID_ALIGN_HORIZONTAL, "left", layout=True, refresh_children=True
Expand Down Expand Up @@ -1153,6 +1158,8 @@ def append_declaration(name: str, value: str) -> None:
append_declaration(
"scrollbar-size-vertical", str(self.scrollbar_size_vertical)
)
if "scrollbar_visibility" in rules:
append_declaration("scrollbar-visibility", self.scrollbar_visibility)

if "box_sizing" in rules:
append_declaration("box-sizing", self.box_sizing)
Expand Down
1 change: 1 addition & 0 deletions src/textual/css/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
TextWrap = Literal["wrap", "nowrap"]
TextOverflow = Literal["clip", "fold", "ellipsis"]
Expand = Literal["greedy", "expand"]
ScrollbarVisibility = Literal["visible", "hidden"]

Specificity3 = Tuple[int, int, int]
Specificity6 = Tuple[int, int, int, int, int, int]
Expand Down
2 changes: 0 additions & 2 deletions src/textual/dom.py
Original file line number Diff line number Diff line change
Expand Up @@ -1723,8 +1723,6 @@ def _update_styles(self) -> None:

Should be called whenever CSS classes / pseudo classes change.
"""
if not self.is_attached or not self.screen.is_mounted:
return
try:
self.app.update_styles(self)
except NoActiveAppError:
Expand Down
6 changes: 4 additions & 2 deletions src/textual/widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -3430,7 +3430,11 @@ def scroll_to_widget(
return False

while isinstance(widget.parent, Widget) and widget is not self:
if not region:
break

container = widget.parent

if widget.styles.dock != "none":
scroll_offset = Offset(0, 0)
else:
Expand All @@ -3454,13 +3458,11 @@ def scroll_to_widget(

# Adjust the region by the amount we just scrolled it, and convert to
# its parent's virtual coordinate system.

region = (
(
region.translate(-scroll_offset)
.translate(container.styles.margin.top_left)
.translate(container.styles.border.spacing.top_left)
.translate(-widget.scroll_offset)
.translate(container.virtual_region_with_margin.offset)
)
.grow(container.styles.margin)
Expand Down
Loading
Loading