diff --git a/src/js/CheckboxTree.js b/src/js/CheckboxTree.js
index 21f52144..6cf71e79 100644
--- a/src/js/CheckboxTree.js
+++ b/src/js/CheckboxTree.js
@@ -12,6 +12,20 @@ import languageShape from './shapes/languageShape';
 import listShape from './shapes/listShape';
 import nodeShape from './shapes/nodeShape';
 
+const SUPPORTED_KEYS = [
+    'ArrowUp',
+    'ArrowDown',
+    'ArrowLeft',
+    'ArrowRight',
+    'End',
+    'Home',
+    'Enter',
+    ' ',
+];
+
+// Clamp a number so that it is within the range [min, max]
+const clamp = (n, min, max) => Math.min(Math.max(n, min), max);
+
 class CheckboxTree extends React.Component {
     static propTypes = {
         nodes: PropTypes.arrayOf(nodeShape).isRequired,
@@ -87,6 +101,7 @@ class CheckboxTree extends React.Component {
         });
 
         this.state = {
+            focusedNodeIndex: null,
             id: props.id || `rct-${nanoid(7)}`,
             model,
             prevProps: props,
@@ -97,6 +112,8 @@ class CheckboxTree extends React.Component {
         this.onNodeClick = this.onNodeClick.bind(this);
         this.onExpandAll = this.onExpandAll.bind(this);
         this.onCollapseAll = this.onCollapseAll.bind(this);
+        this.onFocus = this.onFocus.bind(this);
+        this.onKeyDown = this.onKeyDown.bind(this);
     }
 
     // eslint-disable-next-line react/sort-comp
@@ -158,6 +175,92 @@ class CheckboxTree extends React.Component {
         this.expandAllNodes(false);
     }
 
+    onFocus() {
+        const isFirstFocus = this.state.focusedNodeIndex === null;
+        if (isFirstFocus) {
+            this.setState({ focusedNodeIndex: 0 });
+        }
+    }
+
+    onKeyDown(e) {
+        const keyEligibleForFirstLetterNavigation = e.key.length === 1 &&
+            !e.ctrlKey && !e.metaKey && !e.altKey;
+        // abort early so that we don't try to intercept common browser keystrokes like alt+d
+        if (!SUPPORTED_KEYS.includes(e.key) && !keyEligibleForFirstLetterNavigation) {
+            return;
+        }
+
+        const { focusedNodeIndex, model } = this.state;
+        const currentlyFocusedNode = model.getNode(this.visibleNodes[focusedNodeIndex || 0]);
+        let newFocusedNodeIndex = focusedNodeIndex || 0;
+        const isExpandingEnabled = !this.props.expandDisabled && !this.props.disabled;
+
+        e.preventDefault(); // disable built-in scrolling
+        switch (e.key) {
+            case 'ArrowDown':
+                newFocusedNodeIndex += 1;
+                break;
+            case 'ArrowUp':
+                newFocusedNodeIndex -= 1;
+                break;
+            case 'Home':
+                newFocusedNodeIndex = 0;
+                break;
+            case 'End':
+                newFocusedNodeIndex = this.visibleNodes.length - 1;
+                break;
+            case 'ArrowRight':
+                if (currentlyFocusedNode && currentlyFocusedNode.isParent) {
+                    if (currentlyFocusedNode.expanded) {
+                        // we can increment focused index to get the first child
+                        // because visibleNodes is an pre-order traversal of the tree
+                        newFocusedNodeIndex += 1;
+                    } else if (isExpandingEnabled) {
+                        // expand the currently focused node
+                        this.onExpand({ value: currentlyFocusedNode.value, expanded: true });
+                    }
+                }
+                break;
+            case 'ArrowLeft':
+                if (!currentlyFocusedNode) {
+                    return;
+                }
+                if (currentlyFocusedNode.isParent && currentlyFocusedNode.expanded &&
+                    isExpandingEnabled) {
+                    // collapse the currently focused node
+                    this.onExpand({ value: currentlyFocusedNode.value, expanded: false });
+                } else {
+                    // Move focus to the parent of the current node, if any
+                    // parent is the first element to the left of the currently focused element
+                    // with a lower tree depth since visibleNodes is an pre-order traversal
+                    const parent = this.visibleNodes.slice(0, focusedNodeIndex)
+                        .reverse()
+                        .find(val => model.getNode(val).treeDepth < currentlyFocusedNode.treeDepth);
+                    if (parent) {
+                        newFocusedNodeIndex = this.visibleNodes.indexOf(parent);
+                    }
+                }
+                break;
+            default:
+                if (keyEligibleForFirstLetterNavigation) {
+                    const next = this.visibleNodes.slice((focusedNodeIndex || 0) + 1)
+                        .find((val) => {
+                            const { label } = model.getNode(val);
+                            // for now, we only support first-letter nav to
+                            // nodes with string labels, not jsx elements
+                            return label.startsWith ? label.startsWith(e.key) : false;
+                        });
+                    if (next) {
+                        newFocusedNodeIndex = this.visibleNodes.indexOf(next);
+                    }
+                }
+                break;
+        }
+
+        newFocusedNodeIndex = clamp(newFocusedNodeIndex, 0, this.visibleNodes.length - 1);
+        this.setState({ focusedNodeIndex: newFocusedNodeIndex });
+    }
+
     expandAllNodes(expand = true) {
         const { onExpand } = this.props;
 
@@ -207,10 +310,15 @@ class CheckboxTree extends React.Component {
             showNodeTitle,
             showNodeIcon,
         } = this.props;
-        const { id, model } = this.state;
+        const { focusedNodeIndex, id, model } = this.state;
         const { icons: defaultIcons } = CheckboxTree.defaultProps;
 
         const treeNodes = nodes.map((node) => {
+            const parentExpanded = parent.value ? model.getNode(parent.value).expanded : true;
+            if (parentExpanded) {
+                // visible only if parent is expanded or if there is no root parent
+                this.visibleNodes.push(node.value);
+            }
             const key = node.value;
             const flatNode = model.getNode(node.value);
             const children = flatNode.isParent ? this.renderTreeNodes(node.children, node) : null;
@@ -224,8 +332,6 @@ class CheckboxTree extends React.Component {
             const showCheckbox = onlyLeafCheckboxes ? flatNode.isLeaf : flatNode.showCheckbox;
 
             // Render only if parent is expanded or if there is no root parent
-            const parentExpanded = parent.value ? model.getNode(parent.value).expanded : true;
-
             if (!parentExpanded) {
                 return null;
             }
@@ -239,6 +345,7 @@ class CheckboxTree extends React.Component {
                     expandDisabled={expandDisabled}
                     expandOnClick={expandOnClick}
                     expanded={flatNode.expanded}
+                    hasFocus={this.visibleNodes[focusedNodeIndex] === node.value}
                     icon={node.icon}
                     icons={{ ...defaultIcons, ...icons }}
                     label={node.label}
@@ -261,7 +368,7 @@ class CheckboxTree extends React.Component {
         });
 
         return (
-            <ol>
+            <ol role="presentation">
                 {treeNodes}
             </ol>
         );
@@ -327,6 +434,9 @@ class CheckboxTree extends React.Component {
 
     render() {
         const { disabled, nodes, nativeCheckboxes } = this.props;
+        const { focusedNodeIndex } = this.state;
+        const isFirstFocus = focusedNodeIndex === null;
+        this.visibleNodes = []; // an pre-order traversal of the tree for keyboard support
         const treeNodes = this.renderTreeNodes(nodes);
 
         const className = classNames({
@@ -339,7 +449,15 @@ class CheckboxTree extends React.Component {
             <div className={className}>
                 {this.renderExpandAll()}
                 {this.renderHiddenInput()}
-                {treeNodes}
+                <div
+                    onFocus={this.onFocus}
+                    onKeyDown={this.onKeyDown}
+                    role="tree"
+                    // Only include top-level node in tab order if it has never gained focus before
+                    tabIndex={isFirstFocus ? 0 : -1}
+                >
+                    {treeNodes}
+                </div>
             </div>
         );
     }
diff --git a/src/js/NativeCheckbox.js b/src/js/NativeCheckbox.js
index 3a78561c..f6563300 100644
--- a/src/js/NativeCheckbox.js
+++ b/src/js/NativeCheckbox.js
@@ -30,7 +30,10 @@ class NativeCheckbox extends React.PureComponent {
         // Remove property that does not exist in HTML
         delete props.indeterminate;
 
-        return <input {...props} ref={(c) => { this.checkbox = c; }} type="checkbox" />;
+        // Since we already implement space toggling selection,
+        // the native checkbox no longer needs to be in the accessibility tree and in tab order
+        // I.e, this is purely for visual rendering
+        return <input {...props} ref={(c) => { this.checkbox = c; }} type="checkbox" aria-hidden tabIndex={-1} />;
     }
 }
 
diff --git a/src/js/TreeNode.js b/src/js/TreeNode.js
index 71ba66d7..7d2e1fd8 100644
--- a/src/js/TreeNode.js
+++ b/src/js/TreeNode.js
@@ -13,6 +13,7 @@ class TreeNode extends React.Component {
         disabled: PropTypes.bool.isRequired,
         expandDisabled: PropTypes.bool.isRequired,
         expanded: PropTypes.bool.isRequired,
+        hasFocus: PropTypes.bool.isRequired,
         icons: iconsShape.isRequired,
         isLeaf: PropTypes.bool.isRequired,
         isParent: PropTypes.bool.isRequired,
@@ -50,11 +51,23 @@ class TreeNode extends React.Component {
     constructor(props) {
         super(props);
 
+        this.nodeRef = React.createRef();
+
+        this.componentDidUpdate = this.componentDidUpdate.bind(this);
         this.onCheck = this.onCheck.bind(this);
         this.onClick = this.onClick.bind(this);
+        this.onKeyDown = this.onKeyDown.bind(this);
         this.onExpand = this.onExpand.bind(this);
     }
 
+    componentDidUpdate(prevProps) {
+        // Move focus for keyboard users
+        const isReceivingFocus = this.props.hasFocus && !prevProps.hasFocus;
+        if (isReceivingFocus) {
+            this.nodeRef.current.focus();
+        }
+    }
+
     onCheck() {
         const { value, onCheck } = this.props;
 
@@ -77,6 +90,16 @@ class TreeNode extends React.Component {
         onClick({ value, checked: this.getCheckState({ toggle: false }) });
     }
 
+    onKeyDown(e) {
+        if (e.key === ' ') {
+            e.preventDefault(); // prevent scrolling
+            e.stopPropagation(); // prevent parent nodes from toggling their checked state
+            if (!this.props.disabled) {
+                this.onCheck();
+            }
+        }
+    }
+
     onExpand() {
         const { expanded, value, onExpand } = this.props;
 
@@ -117,10 +140,13 @@ class TreeNode extends React.Component {
 
         return (
             <Button
+                // hide this button from the accessibility tree, as there is full keyboard control
+                aria-hidden
                 className="rct-collapse rct-collapse-btn"
                 disabled={expandDisabled}
                 title={lang.toggle}
                 onClick={this.onExpand}
+                tabIndex={-1}
             >
                 {this.renderCollapseIcon()}
             </Button>
@@ -178,21 +204,24 @@ class TreeNode extends React.Component {
         const { onClick, title } = this.props;
         const clickable = onClick !== null;
 
+        // Disable the lints about this control not being accessible
+        // We already provide full keyboard control, so this is clickable for mouse users
+        // eslint-disable-next-line max-len
+        /* eslint-disable jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */
         return (
             <span className="rct-bare-label" title={title}>
                 {clickable ? (
                     <span
                         className="rct-node-clickable"
                         onClick={this.onClick}
-                        onKeyPress={this.onClick}
-                        role="button"
-                        tabIndex={0}
                     >
                         {children}
                     </span>
                 ) : children}
             </span>
         );
+        // eslint-disable-next-line max-len
+        /* eslint-enable jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */
     }
 
     renderCheckboxLabel(children) {
@@ -226,13 +255,13 @@ class TreeNode extends React.Component {
 
         if (clickable) {
             render.push((
+                // We can disable the lint here, since keyboard functionality is already provided
+                // eslint-disable-next-line jsx-a11y/no-static-element-interactions
                 <span
                     key={1}
                     className="rct-node-clickable"
                     onClick={this.onClick}
                     onKeyPress={this.onClick}
-                    role="link"
-                    tabIndex={0}
                 >
                     {children}
                 </span>
@@ -267,11 +296,18 @@ class TreeNode extends React.Component {
             return null;
         }
 
-        return this.props.children;
+        return this.props.isParent ? (
+            <div role="group">
+                {this.props.children}
+            </div>
+        ) : (
+            this.props.children
+        );
     }
 
     render() {
         const {
+            checked,
             className,
             disabled,
             expanded,
@@ -285,9 +321,22 @@ class TreeNode extends React.Component {
             'rct-node-collapsed': !isLeaf && !expanded,
             'rct-disabled': disabled,
         }, className);
+        let ariaChecked = checked === 1 ? 'true' : 'false';
+        if (checked === 2) {
+            ariaChecked = 'mixed';
+        }
 
         return (
-            <li className={nodeClass}>
+            <li
+                aria-checked={ariaChecked}
+                aria-disabled={disabled}
+                aria-expanded={this.props.isParent ? expanded || false : null}
+                className={nodeClass}
+                onKeyDown={this.onKeyDown}
+                ref={this.nodeRef}
+                role="treeitem"
+                tabIndex={this.props.hasFocus ? 0 : -1}
+            >
                 <span className="rct-text">
                     {this.renderCollapseButton()}
                     {this.renderLabel()}
diff --git a/src/scss/react-checkbox-tree.scss b/src/scss/react-checkbox-tree.scss
index 39ff93f0..2bfa6323 100644
--- a/src/scss/react-checkbox-tree.scss
+++ b/src/scss/react-checkbox-tree.scss
@@ -5,8 +5,12 @@ $rct-clickable-hover: rgba($rct-icon-color, .1) !default;
 $rct-clickable-focus: rgba($rct-icon-color, .2) !default;
 
 .react-checkbox-tree {
+  // Unsure why these 2 lines cause the tree items to not render visually, once I added a div with role="tree" to wrap the tree
+  // would appreciate help to fix
+  /*
   display: flex;
   flex-direction: row-reverse;
+  */
   font-size: 16px;
 
   > ol {
diff --git a/test/CheckboxTree.js b/test/CheckboxTree.js
index 17ba31a2..8f98cd6e 100644
--- a/test/CheckboxTree.js
+++ b/test/CheckboxTree.js
@@ -178,7 +178,7 @@ describe('<CheckboxTree />', () => {
 
             assert.deepEqual(
                 wrapper.find(TreeNode).prop('children').props,
-                { children: [null, null] },
+                { children: [null, null], role: 'presentation' },
             );
         });
     });