> ## Documentation Index
> Fetch the complete documentation index at: https://docs.metabind.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Scroll chrome

> Modifiers for styling and controlling ScrollView background, indicators, edge effects, position, and snapping

export const PlatformStatuses = ({statuses}) => {
  const StatusBadge = ({status, label}) => {
    const styles = {
      green: {
        backgroundColor: '#dcfce7',
        color: '#166534'
      },
      orange: {
        backgroundColor: '#fed7aa',
        color: '#9a3412'
      },
      red: {
        backgroundColor: '#fecaca',
        color: '#991b1b'
      },
      gray: {
        backgroundColor: '#f3f4f6',
        color: '#4b5563'
      }
    };
    const baseStyle = {
      display: 'inline-flex',
      alignItems: 'center',
      padding: '0.125rem 0.625rem',
      borderRadius: '9999px',
      fontSize: '0.875rem',
      fontWeight: '500'
    };
    const colorStyle = styles[status] || styles.green;
    return <span style={{
      ...baseStyle,
      ...colorStyle
    }}>
        {label || status}
      </span>;
  };
  const STATUS_CONFIG = {
    supported: {
      label: "Supported",
      color: "green"
    },
    partial: {
      label: "Partial",
      color: "orange"
    },
    "not-implemented": {
      label: "Not Implemented",
      color: "gray"
    }
  };
  const renderCard = (platform, value) => {
    if (!value) return null;
    const {status, note} = typeof value === "string" ? {
      status: value
    } : value;
    const config = STATUS_CONFIG[status];
    if (!config) return null;
    const titleMap = {
      ios: "SwiftUI",
      android: "Jetpack Compose",
      web: "Web"
    };
    return <Card key={platform} title={titleMap[platform] || platform}>
          <StatusBadge status={config.color} label={config.label} />
          {note && <div style={{
      marginTop: '0.5rem',
      fontSize: '0.875rem',
      color: '#6b7280'
    }}>
              {note}
            </div>}
      </Card>;
  };
  if (statuses == null) {
    return null;
  }
  return <Columns cols="3">
      {Object.entries(statuses).map(([platform, value]) => renderCard(platform, value))}
    </Columns>;
};

export const ComposeJS = ({code, name, height}) => {
  const encodedCode = useMemo(() => {
    if (!code) return "";
    try {
      return btoa(code);
    } catch (e) {
      console.error("Failed to encode code", e);
      return "";
    }
  }, [code]);
  if (!encodedCode) {
    return null;
  }
  return <iframe src={`https://www.metabind.ai/embed?code=${encodedCode}&name=${name ?? 'Example'}`} loading="lazy" style={{
    width: "100%",
    height: height || '350px',
    border: "1px solid #e5e7eb",
    borderRadius: "var(--rounded-2xl,1rem)",
    overflow: "hidden"
  }} title="ComposeJS Preview" />;
};

These iOS-only modifiers configure scrollable containers — [ScrollView](/bindjs/components/ScrollView), [List](/bindjs/components/List), and `TextEditor`. They control the background, indicator visibility, bounce/stretch edge effects, programmatic position, and scroll snapping.

For carousel-style snapping, pair `scrollTargetLayout` (on the inner container) with `scrollTargetBehavior` (on the outer `ScrollView`). Most other modifiers are applied directly to the scrolling component.

## scrollContentBackground

Controls the visibility of the scroll content background.

```typescript theme={null}
.scrollContentBackground(visibility: "hidden" | "visible"): Component
```

<ParamField path="visibility" type="&#x22;hidden&#x22; | &#x22;visible&#x22;" required>
  Whether to show or hide the default background. Use `"hidden"` to remove the system background from Lists and other scrollable containers.
</ParamField>

<PlatformStatuses
  statuses={{
ios: { status: "supported" },
android: "not-implemented",
web: "not-implemented",
}}
/>

**Hide list background**

Remove the default grouped list background to show a custom background underneath.

```typescript theme={null}
List([
    Section("Settings", [
        Text("Account"),
        Text("Notifications"),
        Text("Privacy"),
    ])
])
    .scrollContentBackground("hidden")
    .background(Color("blue"))
```

**Transparent text editor**

```typescript theme={null}
const body = () => {
    const [text, setText] = useState("")
    return TextEditor({ text, setText })
        .scrollContentBackground("hidden")
        .background(Material("thin"))
}
```

## scrollPosition

Tracks and controls scroll position by child view ID.

```typescript theme={null}
.scrollPosition(props: {
    id: string | null;
    setId: (value: string | null) => void;
}): Component
```

<ParamField path="props" type="object" required>
  <Expandable title="properties">
    <ParamField path="id" type="string | null" required>
      The ID of the currently visible child view. Set programmatically to scroll to a specific child. `null` when no child is tracked.
    </ParamField>

    <ParamField path="setId" type="(value: string | null) => void" required>
      Callback that fires when the visible child changes as the user scrolls.
    </ParamField>
  </Expandable>
</ParamField>

<PlatformStatuses
  statuses={{
ios: { status: "supported" },
android: "not-implemented",
web: "not-implemented",
}}
/>

**Track scroll position**

```typescript theme={null}
const body = () => {
    const [scrollId, setScrollId] = useState(null)
    return ScrollView([
        ForEach(items, (item) =>
            Text(item.name)
                .padding(16)
                .id(item.id)
        )
    ]).scrollPosition({ id: scrollId, setId: setScrollId })
}
```

**Programmatic scroll**

```typescript theme={null}
const body = () => {
    const [scrollId, setScrollId] = useState(null)
    return VStack([
        Button("Scroll to top", () => setScrollId("item-0")),
        ScrollView([
            ForEach(items, (item) =>
                Text(item.name)
                    .padding(16)
                    .id(item.id)
            )
        ]).scrollPosition({ id: scrollId, setId: setScrollId })
    ])
}
```

<Note>
  Children must have `.id()` set for scroll position tracking to work. Use with [scrollTargetLayout](#scrolltargetlayout) and [scrollTargetBehavior](#scrolltargetbehavior) for snapping scroll views.
</Note>

## scrollIndicators

Controls the visibility of scroll indicators on a scroll view.

```typescript theme={null}
.scrollIndicators(visibility: 'automatic' | 'visible' | 'hidden' | 'never')
.scrollIndicators(props: { visibility?: 'automatic' | 'visible' | 'hidden' | 'never'; axes?: Axis })
```

<ParamField path="visibility" type="string" required>
  The scroll indicator visibility. One of:

  * `"automatic"` — system default behavior (show when scrolling, hide when idle)
  * `"visible"` — always show scroll indicators
  * `"hidden"` — hide scroll indicators when possible
  * `"never"` — never show scroll indicators
</ParamField>

<ParamField path="props" type="object" required>
  Object form for axis-targeted control.

  <Expandable title="props">
    <ParamField path="visibility" type="string" optional default="automatic">
      The scroll indicator visibility (`"automatic"`, `"visible"`, `"hidden"`, or `"never"`).
    </ParamField>

    <ParamField path="axes" type="Axis" optional>
      Which axes to apply the visibility to. See [Axis](/bindjs/types/Axis). Defaults to both axes.
    </ParamField>
  </Expandable>
</ParamField>

<PlatformStatuses
  statuses={{
ios: { status: "supported" },
android: "not-implemented",
web: "not-implemented",
}}
/>

**Hide scroll indicators**

```typescript theme={null}
ScrollView([
    ForEach(items, (item) =>
        Text(item.name).padding(12)
    )
]).scrollIndicators("hidden")
```

**Never show indicators**

```typescript theme={null}
ScrollView([
    ForEach(items, (item) =>
        Text(item.name).padding(12)
    )
]).scrollIndicators("never")
```

**Hide only vertical indicators**

```typescript theme={null}
ScrollView({ axes: "vertical" }, [
    ForEach(items, (item) =>
        Text(item.name).padding(12)
    )
]).scrollIndicators({
    visibility: "hidden",
    axes: "vertical"
})
```

<Note>
  The difference between `"hidden"` and `"never"` is that `"hidden"` allows the system to show indicators in certain accessibility contexts, while `"never"` unconditionally hides them.
</Note>

## scrollEdgeEffectHidden

Hides the scroll edge bounce or stretch effect.

```typescript theme={null}
.scrollEdgeEffectHidden(isHidden?: boolean): Component
```

<ParamField path="isHidden" type="boolean" optional default="true">
  Whether to hide the edge effect. Defaults to `true` when called without arguments.
</ParamField>

<PlatformStatuses
  statuses={{
ios: { status: "supported" },
android: "not-implemented",
web: "not-implemented",
}}
/>

**Hide the bounce effect**

```typescript theme={null}
ScrollView([
    ForEach(items, (item) =>
        Text(item.name).padding(12)
    )
]).scrollEdgeEffectHidden()
```

**Conditionally hide**

```typescript theme={null}
ScrollView([
    ForEach(items, (item) =>
        Text(item.name).padding(12)
    )
]).scrollEdgeEffectHidden(false)
```

## scrollEdgeEffectStyle

Sets the visual style for the scroll edge effect.

```typescript theme={null}
.scrollEdgeEffectStyle(props: {
    style?: "automatic" | "soft" | "hard";
    edges?: EdgeSet;
}): Component
```

<ParamField path="props" type="object" required>
  <Expandable title="properties">
    <ParamField path="style" type="&#x22;automatic&#x22; | &#x22;soft&#x22; | &#x22;hard&#x22;" optional>
      The edge effect style. `"soft"` provides a subtle effect, `"hard"` provides a sharp effect, and `"automatic"` uses the system default.
    </ParamField>

    <ParamField path="edges" type="EdgeSet" optional>
      Which edges to apply the style to. Defaults to all edges. See [EdgeSet](/bindjs/types/EdgeSet).
    </ParamField>
  </Expandable>
</ParamField>

<PlatformStatuses
  statuses={{
ios: { status: "supported" },
android: "not-implemented",
web: "not-implemented",
}}
/>

**Soft edge effect**

```typescript theme={null}
ScrollView([
    ForEach(items, (item) =>
        Text(item.name).padding(12)
    )
]).scrollEdgeEffectStyle({ style: "soft" })
```

**Hard effect on specific edges**

```typescript theme={null}
ScrollView([
    ForEach(items, (item) =>
        Text(item.name).padding(12)
    )
]).scrollEdgeEffectStyle({ style: "hard", edges: "vertical" })
```

## scrollTargetLayout

Marks a container's children as scroll snap targets.

```typescript theme={null}
.scrollTargetLayout(isEnabled?: boolean): Component
```

<ParamField path="isEnabled" type="boolean" optional default="true">
  Whether the layout acts as a scroll target. Defaults to `true`.
</ParamField>

<PlatformStatuses
  statuses={{
ios: { status: "supported" },
android: "not-implemented",
web: { status: "supported" },
}}
/>

**Horizontal card carousel**

Apply `.scrollTargetLayout()` to the inner container (e.g., HStack) and `.scrollTargetBehavior("viewAligned")` to the outer ScrollView.

```typescript theme={null}
ScrollView({ axis: "horizontal" }, [
    HStack({ spacing: 16 }, items.map((item) =>
        VStack([
            Image({ url: item.image })
                .resizable()
                .scaledToFill()
                .frame({ width: 280, height: 180 })
                .clipped(),
            Text(item.title)
                .font("headline")
                .padding(12),
        ])
            .background(Color("white"))
            .cornerRadius(12)
            .shadow({ radius: 4 })
    )).scrollTargetLayout()
]).scrollTargetBehavior("viewAligned")
```

**Disable snapping**

```typescript theme={null}
HStack({ spacing: 16 }, items.map((item) =>
    Text(item.name)
        .frame({ width: 200, height: 100 })
)).scrollTargetLayout(false)
```

## scrollTargetBehavior

Sets scroll snapping behavior for a `ScrollView`.

```typescript theme={null}
.scrollTargetBehavior(behavior: "viewAligned" | "paging"): Component
```

<ParamField path="behavior" type="&#x22;viewAligned&#x22; | &#x22;paging&#x22;" required>
  The snapping behavior:

  * `"viewAligned"` -- snaps to child views marked with `.scrollTargetLayout()`
  * `"paging"` -- snaps to page boundaries (one screen width or height at a time)
</ParamField>

<PlatformStatuses
  statuses={{
ios: { status: "supported" },
android: "not-implemented",
web: { status: "supported" },
}}
/>

**View-aligned snapping**

Combine with [scrollTargetLayout](#scrolltargetlayout) on the inner container to snap to individual child views.

```typescript theme={null}
ScrollView({ axis: "horizontal" }, [
    HStack({ spacing: 16 }, items.map((item) =>
        Text(item.name)
            .frame({ width: 300, height: 200 })
            .background(Color("blue"))
            .cornerRadius(12)
    )).scrollTargetLayout()
]).scrollTargetBehavior("viewAligned")
```

**Paging behavior**

```typescript theme={null}
ScrollView({ axis: "horizontal" }, [
    HStack({ spacing: 0 }, pages.map((page) =>
        Image({ url: page.url })
            .resizable()
            .scaledToFill()
            .frame({ width: 375, height: 600 })
            .clipped()
    ))
]).scrollTargetBehavior("paging")
```

## See also

* [ScrollView](/bindjs/components/ScrollView) — scrollable container
* [List](/bindjs/components/List) — scrollable list with native row chrome
* [List chrome](/bindjs/modifiers/list-chrome) — list-specific row and style modifiers
* [Axis](/bindjs/types/Axis) — axis values for indicator targeting
* [EdgeSet](/bindjs/types/EdgeSet) — edge selectors for edge effects
