upp-scrollable
A scroll container component that provides configurable scroll behavior for its content. It supports horizontal scrolling, vertical scrolling, both directions, or no scrolling at all. CSS classes are applied dynamically based on the selected direction, and it integrates with viewService for viewport-aware adjustments.
When to Use
Use upp-scrollable to wrap content that may overflow its container and needs controlled scroll behavior. It is particularly useful inside upp-panel-content or any fixed-height container where you want explicit control over which scroll directions are enabled.
Demo
Source Code
- HTML
- TypeScript
- SCSS
<h2>upp-scrollable</h2>
<p class="demo-description">Message feed with configurable scroll direction via <code>upp-scrollable</code>.</p>
<div class="demo-controls">
<span class="control-label">Direction:</span>
<ion-button size="small" [fill]="direction === 'none' ? 'solid' : 'outline'" (click)="setDirection('none')">none</ion-button>
<ion-button size="small" [fill]="direction === 'x' ? 'solid' : 'outline'" (click)="setDirection('x')">x</ion-button>
<ion-button size="small" [fill]="direction === 'y' ? 'solid' : 'outline'" (click)="setDirection('y')">y</ion-button>
<ion-button size="small" [fill]="direction === 'both' ? 'solid' : 'outline'" (click)="setDirection('both')">both</ion-button>
</div>
<div class="demo-section">
<h3>Messages (scrollbar={{ direction }})</h3>
<div class="feed-container">
<upp-scrollable [scrollbar]="direction">
<div [class]="direction === 'x' ? 'messages-row' : 'messages-column'">
<div class="message-card" *ngFor="let msg of messages">
<div class="message-header">
<span class="message-sender">{{ msg.sender }}</span>
<span class="message-time">{{ msg.time }}</span>
</div>
<div class="message-text">{{ msg.text }}</div>
</div>
</div>
</upp-scrollable>
</div>
</div>
import { Component } from '@angular/core';
import { ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
interface Message {
sender: string;
time: string;
text: string;
}
@Component({
selector: 'demo-upp-scrollable',
templateUrl: './demo-upp-scrollable.html',
styleUrls: ['../demo-common.scss', './demo-upp-scrollable.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class DemoUppScrollableComponent {
direction: 'none' | 'x' | 'y' | 'both' = 'y';
messages: Message[] = [
{ sender: 'Alice', time: '09:01', text: 'Good morning! Ready for the standup?' },
{ sender: 'Bob', time: '09:02', text: 'Almost there, give me 2 minutes.' },
{ sender: 'Carol', time: '09:03', text: 'I pushed the fix for the login bug last night.' },
{ sender: 'Dave', time: '09:04', text: 'Great, I\'ll review it after the meeting.' },
{ sender: 'Alice', time: '09:05', text: 'Let\'s also discuss the new dashboard layout.' },
{ sender: 'Eve', time: '09:06', text: 'I have some mockups ready to share.' },
{ sender: 'Bob', time: '09:07', text: 'Perfect. Can you drop the link in the channel?' },
{ sender: 'Carol', time: '09:10', text: 'The API response times improved by 30% after the cache change.' },
{ sender: 'Dave', time: '09:12', text: 'Nice! We should document the caching strategy.' },
{ sender: 'Alice', time: '09:14', text: 'Agreed. I\'ll add it to the wiki this afternoon.' },
{ sender: 'Eve', time: '09:15', text: 'Also, the staging server needs a restart.' },
{ sender: 'Bob', time: '09:17', text: 'I\'ll handle that right after standup.' },
{ sender: 'Carol', time: '09:18', text: 'Don\'t forget to update the env variables.' },
{ sender: 'Dave', time: '09:20', text: 'Has anyone looked at the new feature request from the client?' },
{ sender: 'Alice', time: '09:22', text: 'Yes, it\'s basically a bulk import tool.' },
{ sender: 'Eve', time: '09:23', text: 'We could reuse the CSV parser from the reports module.' },
{ sender: 'Bob', time: '09:25', text: 'Good idea. Let me check if it supports custom delimiters.' },
{ sender: 'Carol', time: '09:27', text: 'It does. I added that last sprint.' },
{ sender: 'Dave', time: '09:30', text: 'Ok, let\'s wrap up. Everyone knows their tasks?' },
{ sender: 'Alice', time: '09:31', text: 'All clear. Talk later!' },
];
constructor(private change: ChangeDetectorRef) {}
setDirection(dir: 'none' | 'x' | 'y' | 'both') {
this.direction = dir;
this.change.markForCheck();
}
}
:host {
display: block;
padding: 16px;
}
.control-label {
display: flex;
align-items: center;
font-size: 13px;
font-weight: 500;
color: var(--ion-color-medium, #888);
margin-right: 4px;
}
.feed-container {
height: 350px;
border: 1px solid var(--ion-color-light-shade, #d7d8da);
border-radius: 10px;
overflow: hidden;
background: var(--ion-color-light, #f4f5f8);
}
.messages-column {
display: flex;
flex-direction: column;
gap: 8px;
padding: 12px;
}
.messages-row {
display: flex;
flex-direction: row;
gap: 8px;
padding: 12px;
width: max-content;
}
.messages-row .message-card {
min-width: 240px;
max-width: 240px;
}
.message-card {
background: #fff;
border-radius: 8px;
padding: 10px 14px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
}
.message-header {
display: flex;
justify-content: space-between;
align-items: baseline;
margin-bottom: 4px;
}
.message-sender {
font-weight: 600;
font-size: 13px;
color: var(--ion-color-primary, #3880ff);
}
.message-time {
font-size: 11px;
color: var(--ion-color-medium, #888);
}
.message-text {
font-size: 13px;
color: var(--ion-color-dark, #333);
line-height: 1.4;
}
API Reference
upp-scrollable
| Property | Type | Default | Description |
|---|---|---|---|
scrollbar | 'none' | 'x' | 'y' | 'both' | 'none' | Controls which scroll directions are enabled. 'none' hides all scrollbars, 'x' enables horizontal only, 'y' enables vertical only, and 'both' enables both directions. |
The component dynamically applies CSS classes based on the scrollbar value:
scrollbar value | CSS classes applied |
|---|---|
'none' | --no-scrollbar-x --no-scrollbar-y |
'x' | --no-scrollbar-y |
'y' | --no-scrollbar-x |
'both' | (none) |