diff --git a/app/javascript/mastodon/components/column.jsx b/app/javascript/mastodon/components/column.jsx
deleted file mode 100644
index abc87a57e5..0000000000
--- a/app/javascript/mastodon/components/column.jsx
+++ /dev/null
@@ -1,72 +0,0 @@
-import PropTypes from 'prop-types';
-import { PureComponent } from 'react';
-
-import { supportsPassiveEvents } from 'detect-passive-events';
-
-import { scrollTop } from '../scroll';
-
-const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
-
-export default class Column extends PureComponent {
-
- static propTypes = {
- children: PropTypes.node,
- label: PropTypes.string,
- bindToDocument: PropTypes.bool,
- };
-
- scrollTop () {
- let scrollable = null;
-
- if (this.props.bindToDocument) {
- scrollable = document.scrollingElement;
- } else {
- scrollable = this.node.querySelector('.scrollable');
- }
-
- if (!scrollable) {
- return;
- }
-
- this._interruptScrollAnimation = scrollTop(scrollable);
- }
-
- handleWheel = () => {
- if (typeof this._interruptScrollAnimation !== 'function') {
- return;
- }
-
- this._interruptScrollAnimation();
- };
-
- setRef = c => {
- this.node = c;
- };
-
- componentDidMount () {
- if (this.props.bindToDocument) {
- document.addEventListener('wheel', this.handleWheel, listenerOptions);
- } else {
- this.node.addEventListener('wheel', this.handleWheel, listenerOptions);
- }
- }
-
- componentWillUnmount () {
- if (this.props.bindToDocument) {
- document.removeEventListener('wheel', this.handleWheel, listenerOptions);
- } else {
- this.node.removeEventListener('wheel', this.handleWheel, listenerOptions);
- }
- }
-
- render () {
- const { label, children } = this.props;
-
- return (
-
- {children}
-
- );
- }
-
-}
diff --git a/app/javascript/mastodon/components/column.tsx b/app/javascript/mastodon/components/column.tsx
new file mode 100644
index 0000000000..01c75d85c0
--- /dev/null
+++ b/app/javascript/mastodon/components/column.tsx
@@ -0,0 +1,52 @@
+import { forwardRef, useRef, useImperativeHandle } from 'react';
+import type { Ref } from 'react';
+
+import { scrollTop } from 'mastodon/scroll';
+
+export interface ColumnRef {
+ scrollTop: () => void;
+ node: HTMLDivElement | null;
+}
+
+interface ColumnProps {
+ children?: React.ReactNode;
+ label?: string;
+ bindToDocument?: boolean;
+}
+
+export const Column = forwardRef(
+ ({ children, label, bindToDocument }, ref: Ref) => {
+ const nodeRef = useRef(null);
+
+ useImperativeHandle(ref, () => ({
+ node: nodeRef.current,
+
+ scrollTop() {
+ let scrollable = null;
+
+ if (bindToDocument) {
+ scrollable = document.scrollingElement;
+ } else {
+ scrollable = nodeRef.current?.querySelector('.scrollable');
+ }
+
+ if (!scrollable) {
+ return;
+ }
+
+ scrollTop(scrollable);
+ },
+ }));
+
+ return (
+
+ {children}
+
+ );
+ },
+);
+
+Column.displayName = 'Column';
+
+// eslint-disable-next-line import/no-default-export
+export default Column;
diff --git a/app/javascript/mastodon/features/directory/index.tsx b/app/javascript/mastodon/features/directory/index.tsx
index d0e57600bb..ef2649a27f 100644
--- a/app/javascript/mastodon/features/directory/index.tsx
+++ b/app/javascript/mastodon/features/directory/index.tsx
@@ -15,7 +15,8 @@ import {
changeColumnParams,
} from 'mastodon/actions/columns';
import { fetchDirectory, expandDirectory } from 'mastodon/actions/directory';
-import Column from 'mastodon/components/column';
+import { Column } from 'mastodon/components/column';
+import type { ColumnRef } from 'mastodon/components/column';
import { ColumnHeader } from 'mastodon/components/column_header';
import { LoadMore } from 'mastodon/components/load_more';
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
@@ -49,7 +50,7 @@ export const Directory: React.FC<{
const intl = useIntl();
const dispatch = useAppDispatch();
- const column = useRef(null);
+ const column = useRef(null);
const [orderParam, setOrderParam] = useSearchParam('order');
const [localParam, setLocalParam] = useSearchParam('local');
diff --git a/app/javascript/mastodon/features/link_timeline/index.tsx b/app/javascript/mastodon/features/link_timeline/index.tsx
index 262a21afcc..1b3f287177 100644
--- a/app/javascript/mastodon/features/link_timeline/index.tsx
+++ b/app/javascript/mastodon/features/link_timeline/index.tsx
@@ -5,7 +5,8 @@ import { useParams } from 'react-router-dom';
import ExploreIcon from '@/material-icons/400-24px/explore.svg?react';
import { expandLinkTimeline } from 'mastodon/actions/timelines';
-import Column from 'mastodon/components/column';
+import { Column } from 'mastodon/components/column';
+import type { ColumnRef } from 'mastodon/components/column';
import { ColumnHeader } from 'mastodon/components/column_header';
import StatusListContainer from 'mastodon/features/ui/containers/status_list_container';
import type { Card } from 'mastodon/models/status';
@@ -17,7 +18,7 @@ export const LinkTimeline: React.FC<{
const { url } = useParams<{ url: string }>();
const decodedUrl = url ? decodeURIComponent(url) : undefined;
const dispatch = useAppDispatch();
- const columnRef = useRef(null);
+ const columnRef = useRef(null);
const firstStatusId = useAppSelector((state) =>
decodedUrl
? // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
diff --git a/app/javascript/mastodon/features/lists/index.tsx b/app/javascript/mastodon/features/lists/index.tsx
index cf413a1fe0..25a537336e 100644
--- a/app/javascript/mastodon/features/lists/index.tsx
+++ b/app/javascript/mastodon/features/lists/index.tsx
@@ -11,7 +11,7 @@ import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
import SquigglyArrow from '@/svg-icons/squiggly_arrow.svg?react';
import { fetchLists } from 'mastodon/actions/lists';
import { openModal } from 'mastodon/actions/modal';
-import Column from 'mastodon/components/column';
+import { Column } from 'mastodon/components/column';
import { ColumnHeader } from 'mastodon/components/column_header';
import { Icon } from 'mastodon/components/icon';
import ScrollableList from 'mastodon/components/scrollable_list';
diff --git a/app/javascript/mastodon/features/lists/members.tsx b/app/javascript/mastodon/features/lists/members.tsx
index 184b54b92d..97b730f436 100644
--- a/app/javascript/mastodon/features/lists/members.tsx
+++ b/app/javascript/mastodon/features/lists/members.tsx
@@ -20,7 +20,7 @@ import {
import type { ApiAccountJSON } from 'mastodon/api_types/accounts';
import { Avatar } from 'mastodon/components/avatar';
import { Button } from 'mastodon/components/button';
-import Column from 'mastodon/components/column';
+import { Column } from 'mastodon/components/column';
import { ColumnHeader } from 'mastodon/components/column_header';
import { ColumnSearchHeader } from 'mastodon/components/column_search_header';
import { FollowersCounter } from 'mastodon/components/counters';
diff --git a/app/javascript/mastodon/features/lists/new.tsx b/app/javascript/mastodon/features/lists/new.tsx
index cf39331d7c..100f126c37 100644
--- a/app/javascript/mastodon/features/lists/new.tsx
+++ b/app/javascript/mastodon/features/lists/new.tsx
@@ -14,7 +14,7 @@ import { fetchList } from 'mastodon/actions/lists';
import { createList, updateList } from 'mastodon/actions/lists_typed';
import { apiGetAccounts } from 'mastodon/api/lists';
import type { RepliesPolicyType } from 'mastodon/api_types/lists';
-import Column from 'mastodon/components/column';
+import { Column } from 'mastodon/components/column';
import { ColumnHeader } from 'mastodon/components/column_header';
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
import { useAppDispatch, useAppSelector } from 'mastodon/store';
diff --git a/app/javascript/mastodon/features/notifications_v2/index.tsx b/app/javascript/mastodon/features/notifications_v2/index.tsx
index 730d95bcd5..bb476fe51f 100644
--- a/app/javascript/mastodon/features/notifications_v2/index.tsx
+++ b/app/javascript/mastodon/features/notifications_v2/index.tsx
@@ -36,7 +36,8 @@ import { useAppDispatch, useAppSelector } from 'mastodon/store';
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
import { submitMarkers } from '../../actions/markers';
-import Column from '../../components/column';
+import { Column } from '../../components/column';
+import type { ColumnRef } from '../../components/column';
import { ColumnHeader } from '../../components/column_header';
import { LoadGap } from '../../components/load_gap';
import ScrollableList from '../../components/scrollable_list';
@@ -96,7 +97,7 @@ export const Notifications: React.FC<{
selectNeedsNotificationPermission,
);
- const columnRef = useRef(null);
+ const columnRef = useRef(null);
const selectChild = useCallback((index: number, alignTop: boolean) => {
const container = columnRef.current?.node as HTMLElement | undefined;
diff --git a/app/javascript/mastodon/features/onboarding/follows.tsx b/app/javascript/mastodon/features/onboarding/follows.tsx
index 02a8d01ba9..6ff0d31fd7 100644
--- a/app/javascript/mastodon/features/onboarding/follows.tsx
+++ b/app/javascript/mastodon/features/onboarding/follows.tsx
@@ -14,7 +14,7 @@ import { fetchSuggestions } from 'mastodon/actions/suggestions';
import { markAsPartial } from 'mastodon/actions/timelines';
import { apiRequest } from 'mastodon/api';
import type { ApiAccountJSON } from 'mastodon/api_types/accounts';
-import Column from 'mastodon/components/column';
+import { Column } from 'mastodon/components/column';
import { ColumnHeader } from 'mastodon/components/column_header';
import { ColumnSearchHeader } from 'mastodon/components/column_search_header';
import ScrollableList from 'mastodon/components/scrollable_list';
diff --git a/app/javascript/mastodon/features/onboarding/profile.tsx b/app/javascript/mastodon/features/onboarding/profile.tsx
index e4d9137e9e..1e5e868f18 100644
--- a/app/javascript/mastodon/features/onboarding/profile.tsx
+++ b/app/javascript/mastodon/features/onboarding/profile.tsx
@@ -13,7 +13,7 @@ import EditIcon from '@/material-icons/400-24px/edit.svg?react';
import PersonIcon from '@/material-icons/400-24px/person.svg?react';
import { updateAccount } from 'mastodon/actions/accounts';
import { Button } from 'mastodon/components/button';
-import Column from 'mastodon/components/column';
+import { Column } from 'mastodon/components/column';
import { ColumnHeader } from 'mastodon/components/column_header';
import { Icon } from 'mastodon/components/icon';
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
diff --git a/app/javascript/mastodon/features/ui/components/column_loading.tsx b/app/javascript/mastodon/features/ui/components/column_loading.tsx
index d9563dda7a..8b20e76ffb 100644
--- a/app/javascript/mastodon/features/ui/components/column_loading.tsx
+++ b/app/javascript/mastodon/features/ui/components/column_loading.tsx
@@ -1,4 +1,4 @@
-import Column from 'mastodon/components/column';
+import { Column } from 'mastodon/components/column';
import { ColumnHeader } from 'mastodon/components/column_header';
import type { Props as ColumnHeaderProps } from 'mastodon/components/column_header';