Repository: holoviz/holoviews
Pull request #5388 (2022-08-12 15:44:48)

Compatibility changes with bokeh 3.0
For now, this is only done to be able to run panel tests in https://github.com/holoviz/panel/pull/3752. 

With these changes, it is also possible to run the following with Bokeh version 3.0:
``` python
import holoviews as hv
hv.extension("bokeh")
```

I have been pretty brutal and removed what is not needed, it could be added back in the future.

Widgetbox does not exist anymore, see https://github.com/bokeh/bokeh/issues/9334.

basnijholt: Until this is done, we should probably pin bokeh, https://github.com/holoviz/holoviews/pull/5507.
Hoxbro: `hv.Gridspace` definitely does not work right now, which is why some of the tests fails right now. 
![image](https://user-images.githubusercontent.com/19758978/200378764-742746e0-f345-4019-85e1-b42cfc0d083e.png)

``` python
import holoviews as hv

hv.extension("bokeh")

data = {(i, j): hv.Curve(range(i + j)) for i in range(1, 3) for j in range(2, 4)}
(hv.GridSpace(data) + hv.GridSpace(data) + hv.GridSpace(data))
```

Hoxbro: In the previous versions of Bokeh, `gridplot` returned `Column`/`Row`. With Bokeh 3.0, the output is now `GridPlot`. 

`GridPlot` don't seem to merge tools of nested `GridPlot` like `Column`/`Row`, which is why in the previous comment, there were _a lot_ of toolbars. 

This has been "fixed" by removing all the toolbars with `toolbar_location=None` except the outer one. This gives the expected output:

![image](https://user-images.githubusercontent.com/19758978/200578210-bebed974-e57f-45a5-a3b6-b0c2a81e100d.png)




The lack of merging toolbars of GridPlot seems to have been there before version 3.0:

<details>

<summary> Code </summary>

``` python
from bokeh.io import output_notebook, show
from bokeh.layouts import Row, gridplot
from bokeh.plotting import figure

output_notebook()


def fig():
    fig = figure(height=200, width=200)
    a = fig.line(x=list(range(10)), y=list(range(10)))
    return fig


def gp(func):
    plots = [[func(), func()]]
    return gridplot(plots)


def row(func):
    plots = [func(), func()]
    return Row(*plots)


# show(row(fig))
# show(gp(fig))
# show(gp(lambda: row(fig)))
show(gp(lambda: gp(fig)))
```

</details>

![image](https://user-images.githubusercontent.com/19758978/200578569-47a0a7eb-1f21-49b5-82db-f0c175dc7c82.png)

@mattpap is this the expected behavior, or should I open an issue about it on Bokeh?
codecov-commenter: # [Codecov](https://codecov.io/gh/holoviz/holoviews/pull/5388?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=holoviz) Report
> Merging [#5388](https://codecov.io/gh/holoviz/holoviews/pull/5388?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=holoviz) (bb8044e) into [master](https://codecov.io/gh/holoviz/holoviews/commit/b66d49389fe76e6be227d80ec40d031033565dd7?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=holoviz) (b66d493) will **decrease** coverage by `0.09%`.
> The diff coverage is `95.22%`.

```diff
@@            Coverage Diff             @@
##           master    #5388      +/-   ##
==========================================
- Coverage   88.09%   88.00%   -0.10%     
==========================================
  Files         302      302              
  Lines       62269    62290      +21     
==========================================
- Hits        54858    54816      -42     
- Misses       7411     7474      +63     
```


| [Impacted Files](https://codecov.io/gh/holoviz/holoviews/pull/5388?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=holoviz) | Coverage Δ | |
|---|---|---|
| [holoviews/tests/plotting/bokeh/test\_renderer.py](https://codecov.io/gh/holoviz/holoviews/pull/5388/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=holoviz#diff-aG9sb3ZpZXdzL3Rlc3RzL3Bsb3R0aW5nL2Jva2VoL3Rlc3RfcmVuZGVyZXIucHk=) | `98.18% <ø> (ø)` | |
| [holoviews/plotting/bokeh/util.py](https://codecov.io/gh/holoviz/holoviews/pull/5388/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=holoviz#diff-aG9sb3ZpZXdzL3Bsb3R0aW5nL2Jva2VoL3V0aWwucHk=) | `61.51% <67.85%> (+0.01%)` | :arrow_up: |
| [holoviews/plotting/bokeh/element.py](https://codecov.io/gh/holoviz/holoviews/pull/5388/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=holoviz#diff-aG9sb3ZpZXdzL3Bsb3R0aW5nL2Jva2VoL2VsZW1lbnQucHk=) | `89.42% <85.71%> (+0.55%)` | :arrow_up: |
| [holoviews/tests/plotting/bokeh/test\_elementplot.py](https://codecov.io/gh/holoviz/holoviews/pull/5388/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=holoviz#diff-aG9sb3ZpZXdzL3Rlc3RzL3Bsb3R0aW5nL2Jva2VoL3Rlc3RfZWxlbWVudHBsb3QucHk=) | `95.08% <92.45%> (ø)` | |
| [holoviews/plotting/bokeh/annotation.py](https://codecov.io/gh/holoviz/holoviews/pull/5388/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=holoviz#diff-aG9sb3ZpZXdzL3Bsb3R0aW5nL2Jva2VoL2Fubm90YXRpb24ucHk=) | `84.42% <100.00%> (+1.14%)` | :arrow_up: |
| [holoviews/plotting/bokeh/graphs.py](https://codecov.io/gh/holoviz/holoviews/pull/5388/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=holoviz#diff-aG9sb3ZpZXdzL3Bsb3R0aW5nL2Jva2VoL2dyYXBocy5weQ==) | `91.32% <100.00%> (ø)` | |
| [holoviews/plotting/bokeh/links.py](https://codecov.io/gh/holoviz/holoviews/pull/5388/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=holoviz#diff-aG9sb3ZpZXdzL3Bsb3R0aW5nL2Jva2VoL2xpbmtzLnB5) | `69.75% <100.00%> (-0.55%)` | :arrow_down: |
| [holoviews/plotting/bokeh/plot.py](https://codecov.io/gh/holoviz/holoviews/pull/5388/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=holoviz#diff-aG9sb3ZpZXdzL3Bsb3R0aW5nL2Jva2VoL3Bsb3QucHk=) | `91.84% <100.00%> (-0.03%)` | :arrow_down: |
| [holoviews/tests/plotting/bokeh/test\_barplot.py](https://codecov.io/gh/holoviz/holoviews/pull/5388/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=holoviz#diff-aG9sb3ZpZXdzL3Rlc3RzL3Bsb3R0aW5nL2Jva2VoL3Rlc3RfYmFycGxvdC5weQ==) | `100.00% <100.00%> (ø)` | |
| [...oviews/tests/plotting/bokeh/test\_boxwhiskerplot.py](https://codecov.io/gh/holoviz/holoviews/pull/5388/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=holoviz#diff-aG9sb3ZpZXdzL3Rlc3RzL3Bsb3R0aW5nL2Jva2VoL3Rlc3RfYm94d2hpc2tlcnBsb3QucHk=) | `100.00% <100.00%> (ø)` | |
| ... and [39 more](https://codecov.io/gh/holoviz/holoviews/pull/5388/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=holoviz) | |

:mega: We’re building smart automated test selection to slash your CI/CD build times. [Learn more](https://about.codecov.io/iterative-testing/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=holoviz)

mattpap: > GridPlot don't seem to merge tools of nested GridPlot like Column/Row, which is why in the previous comment, there were a lot of toolbars.

Neither `GridPlot` nor `Column` or `Row` does any merging of tools. That's a responsibility of higher level layout functions in `bokeh.layouts`.
Hoxbro: Sorry meant: "`gridplot` don't seem to merge tools of nested `GridPlot` like `Column/Row`". 

I don't know how you define higher-level layout functions.  Is `gridplot` a higher-level layout function?  

I would expect that when passing in `GridPlot` (the output of `gridplot`) into `gridplot` it would merge / hide the inner toolbar like it does with `Row` and `Column`. (The code is the same as my previous comment)

![image](https://user-images.githubusercontent.com/19758978/200601326-2a7b72e1-b1aa-4d32-a93f-2c084e0206f1.png)

![image](https://user-images.githubusercontent.com/19758978/200601515-177fd119-347f-4d07-a1f3-c8bf7ee6b8f2.png)

Hoxbro: For some reason, this does not work anymore:


Bokeh 2.4
![ Screenshot 2022-11-09 20 04 29](https://user-images.githubusercontent.com/19758978/200919359-f7a7d49d-5769-4ea7-a5a8-14bbb1791bc1.png)


Bokeh 3.0
![ Screenshot 2022-11-09 20 04 49](https://user-images.githubusercontent.com/19758978/200919293-9b6906bc-74d1-4cb0-b9b1-a253f5c1365f.png)


``` python
import holoviews as hv
import numpy as np

hv.extension("bokeh")

xs = np.linspace(0, np.pi * 4, 100)
data = (xs, np.sin(xs))
hv.Points(data).opts(height=200, responsive=True, title="stretch width")
```

 
mattpap: This happens when an expanding plot is added to a non-expanding container. The plot will work fine on its own, assuming CSS allows for that (usually true in width and depends in height), however not when wrapped. The previous layout allowed expanding children to expand the parent, but that's not possible anymore with the CSS layout. One has to assign sizing modes outside-in. There's new `inherit` sizing mode to help with this.
mattpap: > I don't know how you define higher-level layout functions. Is gridplot a higher-level layout function?

Models are generally considered low-level and any functions on top of them (so e.g. anything in `bokeh.layouts`) is high-level.

> I would expect that when passing in GridPlot (the output of gridplot) into gridplot it would merge / hide the inner toolbar like it does with Row and Column. (The code is the same as my previous comment)

`GridPlot` was meant to only allow `Plot` children, but for backwards compatibility it allows other UI components. I didn't consider usage like this, i.e. sub grid plots, so there is not support for merging their toolbars. I suppose this should be considered as a feature request, because this is a territory of merging already merged toolbars. There's also a question of scoping, i.e. I'm not sure if such "deep" merging should happen by default. I need to give this some though.

However, I specifically designed `GridPlot` to not do any automated merging, so that users can implement their own strategies for merging tools. Thus the merger function is implemented as a part of the high-level API in `gridplot()` (separately in bokeh and bokehjs).
Hoxbro: > Models are generally considered low-level and any functions on top of them (so e.g. anything in bokeh.layouts) is high-level.

Thank you for the explanation. I'm still pretty new to this world. :slightly_smiling_face: 

> However, I specifically designed GridPlot to not do any automated merging, so that users can implement their own strategies for merging tools. Thus the merger function is implemented as a part of the high-level API in gridplot() (separately in bokeh and bokehjs).

Yeah, I saw it in the migration guide after I wrote the original message.    
Hoxbro: ## Notebook issues
Two problems that do not work in a notebook, but work fine outside it, e.g. when running it with `panel serve`. I suspect the problems are related to `pyviz_comms` but I could be wrong. 

### Streaming
It raises an error when moving the point around.

<details>

<summary>Code</summary>


``` python
import holoviews as hv
from holoviews.streams import PointDraw

hv.extension("bokeh")

points = hv.Points([(0, 0), (0.5, 0.5), (1, 0)])
point_stream = PointDraw(data=points.columns(), source=points)
points.opts(active_tools=["point_draw"])
```
</details>

<details>

<summary>Traceback</summary>

``` python-traceback
Traceback (most recent call last):
  File "/home/shh/miniconda3/envs/bkdev/lib/python3.9/site-packages/pyviz_comms/__init__.py", line 347, in _handle_msg
    self._on_msg(msg)
  File "/home/shh/Repos/bokeh3/panel/panel/viewable.py", line 302, in _on_msg
    patch.apply_to_document(doc, comm.id if comm else None)
  File "/home/shh/Repos/bokeh3/bokeh/src/bokeh/protocol/messages/patch_doc.py", line 104, in apply_to_document
    invoke_with_curdoc(doc, lambda: doc.apply_json_patch(self.payload, setter=setter))
  File "/home/shh/Repos/bokeh3/bokeh/src/bokeh/document/callbacks.py", line 405, in invoke_with_curdoc
    return f()
  File "/home/shh/Repos/bokeh3/bokeh/src/bokeh/protocol/messages/patch_doc.py", line 104, in <lambda>
    invoke_with_curdoc(doc, lambda: doc.apply_json_patch(self.payload, setter=setter))
  File "/home/shh/Repos/bokeh3/bokeh/src/bokeh/document/document.py", line 373, in apply_json_patch
    patch: PatchJson = deserializer.deserialize(patch_json)
  File "/home/shh/Repos/bokeh3/bokeh/src/bokeh/core/serialization.py", line 491, in deserialize
    return self.decode(obj.content, obj.buffers)
  File "/home/shh/Repos/bokeh3/bokeh/src/bokeh/core/serialization.py", line 506, in decode
    return self._decode(obj)
  File "/home/shh/Repos/bokeh3/bokeh/src/bokeh/core/serialization.py", line 547, in _decode
    return {key: self._decode(val) for key, val in obj.items()}
  File "/home/shh/Repos/bokeh3/bokeh/src/bokeh/core/serialization.py", line 547, in <dictcomp>
    return {key: self._decode(val) for key, val in obj.items()}
  File "/home/shh/Repos/bokeh3/bokeh/src/bokeh/core/serialization.py", line 549, in _decode
    return [self._decode(entry) for entry in obj]
  File "/home/shh/Repos/bokeh3/bokeh/src/bokeh/core/serialization.py", line 549, in <listcomp>
    return [self._decode(entry) for entry in obj]
  File "/home/shh/Repos/bokeh3/bokeh/src/bokeh/core/serialization.py", line 547, in _decode
    return {key: self._decode(val) for key, val in obj.items()}
  File "/home/shh/Repos/bokeh3/bokeh/src/bokeh/core/serialization.py", line 547, in <dictcomp>
    return {key: self._decode(val) for key, val in obj.items()}
  File "/home/shh/Repos/bokeh3/bokeh/src/bokeh/core/serialization.py", line 528, in _decode
    return self._decode_map(cast(MapRep, obj))
  File "/home/shh/Repos/bokeh3/bokeh/src/bokeh/core/serialization.py", line 579, in _decode_map
    return { self._decode(key): self._decode(val) for key, val in entries }
  File "/home/shh/Repos/bokeh3/bokeh/src/bokeh/core/serialization.py", line 579, in <dictcomp>
    return { self._decode(key): self._decode(val) for key, val in entries }
  File "/home/shh/Repos/bokeh3/bokeh/src/bokeh/core/serialization.py", line 536, in _decode
    return self._decode_ndarray(cast(NDArrayRep, obj))
  File "/home/shh/Repos/bokeh3/bokeh/src/bokeh/core/serialization.py", line 640, in _decode_ndarray
    decoded = self._decode(array)
  File "/home/shh/Repos/bokeh3/bokeh/src/bokeh/core/serialization.py", line 530, in _decode
    return self._decode_bytes(cast(BytesRep, obj))
  File "/home/shh/Repos/bokeh3/bokeh/src/bokeh/core/serialization.py", line 589, in _decode_bytes
    id = data["id"]
KeyError: 'id'
```
</details>

https://user-images.githubusercontent.com/19758978/200924612-804f817b-3d0a-4971-8087-133199ff34b4.mp4

### Widgets
The widget does not update the plot and gives an error in the console. A minor note is the formatting around the widget seems to be gone when compared to the previous version of holoviews/bokeh.


 

<details>

<summary>Code</summary>

``` python
import pandas as pd
import numpy as np
import holoviews as hv
import panel as pn

hv.extension("bokeh")

df = pd.DataFrame(np.random.randint(0, 10, size=(100, 3)), columns=list("ABC"))
ds = hv.Dataset(df, ['A', "B"], ["C"])
ds.aggregate(function=np.mean).to(hv.Scatter, 'A', 'C')

```
</details>

<details>

<summary>Console</summary>

``` console
Uncaught TypeError: e is undefined
    apply_state http://localhost:8888/lab/tree/Untitled.ipynb line 2 > injectedScript:1669
    set_state http://localhost:8888/lab/tree/Untitled.ipynb line 2 > injectedScript:1669
    anonymous http://localhost:8888/lab/tree/Untitled.ipynb line 2 > injectedScript line 596 > Function:13
    execute http://localhost:8888/lab/tree/Untitled.ipynb line 2 > injectedScript:596
    n http://localhost:8888/lab/tree/Untitled.ipynb line 2 > injectedScript:359
    emit http://localhost:8888/lab/tree/Untitled.ipynb line 2 > injectedScript:323
    emit http://localhost:8888/lab/tree/Untitled.ipynb line 2 > injectedScript:323
    _setv http://localhost:8888/lab/tree/Untitled.ipynb line 2 > injectedScript:322
    setv http://localhost:8888/lab/tree/Untitled.ipynb line 2 > injectedScript:322
    set http://localhost:8888/lab/tree/Untitled.ipynb line 2 > injectedScript:322
    _slide http://localhost:8888/lab/tree/Untitled.ipynb line 2 > injectedScript:978
    render http://localhost:8888/lab/tree/Untitled.ipynb line 2 > injectedScript:978
    st http://localhost:8888/lab/tree/Untitled.ipynb line 2 > injectedScript:979
    st http://localhost:8888/lab/tree/Untitled.ipynb line 2 > injectedScript:979
    st http://localhost:8888/lab/tree/Untitled.ipynb line 2 > injectedScript:979
    ut http://localhost:8888/lab/tree/Untitled.ipynb line 2 > injectedScript:979
    ut http://localhost:8888/lab/tree/Untitled.ipynb line 2 > injectedScript:979
    Z http://localhost:8888/lab/tree/Untitled.ipynb line 2 > injectedScript:979
    o http://localhost:8888/lab/tree/Untitled.ipynb line 2 > injectedScript:979
Untitled.ipynb line 2 > injectedScript:1669:342
    apply_state http://localhost:8888/lab/tree/Untitled.ipynb line 2 > injectedScript:1669
    set_state http://localhost:8888/lab/tree/Untitled.ipynb line 2 > injectedScript:1669
    anonymous http://localhost:8888/lab/tree/Untitled.ipynb line 2 > injectedScript line 596 > Function:13
    execute http://localhost:8888/lab/tree/Untitled.ipynb line 2 > injectedScript:596
    n http://localhost:8888/lab/tree/Untitled.ipynb line 2 > injectedScript:359
    emit http://localhost:8888/lab/tree/Untitled.ipynb line 2 > injectedScript:323
    emit http://localhost:8888/lab/tree/Untitled.ipynb line 2 > injectedScript:323
    _setv http://localhost:8888/lab/tree/Untitled.ipynb line 2 > injectedScript:322
    setv http://localhost:8888/lab/tree/Untitled.ipynb line 2 > injectedScript:322
    set http://localhost:8888/lab/tree/Untitled.ipynb line 2 > injectedScript:322
    _slide http://localhost:8888/lab/tree/Untitled.ipynb line 2 > injectedScript:978
    render http://localhost:8888/lab/tree/Untitled.ipynb line 2 > injectedScript:978
    st http://localhost:8888/lab/tree/Untitled.ipynb line 2 > injectedScript:979
    forEach self-hosted:203
    st http://localhost:8888/lab/tree/Untitled.ipynb line 2 > injectedScript:979
    forEach self-hosted:203
    st http://localhost:8888/lab/tree/Untitled.ipynb line 2 > injectedScript:979
    ut http://localhost:8888/lab/tree/Untitled.ipynb line 2 > injectedScript:979
    forEach self-hosted:203
    ut http://localhost:8888/lab/tree/Untitled.ipynb line 2 > injectedScript:979
    Z http://localhost:8888/lab/tree/Untitled.ipynb line 2 > injectedScript:979
    o http://localhost:8888/lab/tree/Untitled.ipynb line 2 > injectedScript:979

```
</details>

<details>

<summary>Previous version layout</summary>

![image](https://user-images.githubusercontent.com/19758978/200926755-bdee9b2e-63b3-4ad7-8078-55b647e33c5d.png)


</details>


https://user-images.githubusercontent.com/19758978/200925588-1c779ce8-4aa9-4ec1-b3dd-8b925bb253f5.mp4
Hoxbro: The workaround made in https://github.com/holoviz/holoviews/pull/5465, does not work anymore. The type of `dmaps` has changed from a `Column` to a `GridPlot`, which could be a reason why. 

<details>

<summary>Code</summary>

``` python
import numpy as np
import holoviews as hv
from holoviews.streams import RangeX

hv.extension("bokeh")

def cos(x_range):
    t = np.linspace(x_range[0], x_range[1], 500)
    return hv.Curve((t, np.cos(t)))


def sin(x_range):
    t = np.linspace(x_range[0], x_range[1], 500)
    return hv.Curve((t, np.sin(t)))


dmap1 = hv.DynamicMap(cos, streams=[RangeX(x_range=(1, 10))])
dmap2 = hv.DynamicMap(sin, streams=[RangeX(x_range=(1, 10))])
dmaps = dmap1 + dmap2
```
</details>

<details>

<summary>Console</summary>

``` console
Uncaught TypeError: plot.x_range.plots is undefined
    anonymous bokeh.js:6
    execute customjs.js:22
    _process_event model.js:33
    trigger document.js:34
    trigger_event model.js:40
    trigger_ranges_update_event plot_canvas.js:430
    reset_range plot_canvas.js:426
    reset plot_canvas.js:142
    doit reset_tool.js:9
    connect_signals action_tool.js:9
    new_slot view.js:33
    emit signaling.js:50
    _clicked click_button.js:7
    initialize tool_button.js:44
bokeh.js:6:19

```
</details>


https://user-images.githubusercontent.com/19758978/200932070-d339df28-17a3-4049-9c9b-fa0c8cfe9f74.mp4



basnijholt: Is there any estimate on when HoloViews plans to support Bokeh 3?

I am really looking forward to this because it solves https://github.com/holoviz/holoviews/issues/5360.
Hoxbro: The work for Holoviews to work with Bokeh 3 is almost done. What is missing is:
- fixing the merge conflicts, 
- the issues mentioned above, 
- Some changes to make it backward compatible with Bokeh 2.4.

The main blocker for Holoviews to work with Bokeh 3.0 is that Panel 1.0 is needed, which is more of an undertaking. We are working on it, but it is not ready yet. You can follow the progress here: https://github.com/holoviz/panel/issues/4097
Hoxbro: @philippjfr this should be ready to be merged. These changes make Holoviews compatible with Bokeh 2 / Panel 0.14 and (almost) Bokeh 3 / Panel 1.

The almost is because of the problems mentioned in:
- https://github.com/holoviz/holoviews/pull/5388#issuecomment-1309233647
- https://github.com/holoviz/holoviews/pull/5388#issuecomment-1309286680
- https://github.com/holoviz/holoviews/pull/5388#issuecomment-1309325288

I will open a new issue after this PR is merged.
philippjfr: > For some reason, this does not work anymore:
> 
> Bokeh 2.4 ![ Screenshot 2022-11-09 20 04 29](https://user-images.githubusercontent.com/19758978/200919359-f7a7d49d-5769-4ea7-a5a8-14bbb1791bc1.png)
> 
> Bokeh 3.0 ![ Screenshot 2022-11-09 20 04 49](https://user-images.githubusercontent.com/19758978/200919293-9b6906bc-74d1-4cb0-b9b1-a253f5c1365f.png)
> 
> ```python
> import holoviews as hv
> import numpy as np
> 
> hv.extension("bokeh")
> 
> xs = np.linspace(0, np.pi * 4, 100)
> data = (xs, np.sin(xs))
> hv.Points(data).opts(height=200, responsive=True, title="stretch width")
> ```

Probably to do with the way the `HoloViews` pane in Panel wraps the component. Hopefully an easy fix.
philippjfr: >Two problems that do not work in a notebook, but work fine outside it, e.g. when running it with panel serve. I suspect the problems are related to pyviz_comms but I could be wrong.

Would have to see the full traceback.

>The widget does not update the plot and gives an error in the console. A minor note is the formatting around the widget seems to be gone when compared to the previous version of holoviews/bokeh.

Will need investigation. The formatting will be restored when we finish the CSS PR and add WidgetBox styles correctly.
philippjfr: >The workaround made in #5465, does not work anymore. The type of dmaps has changed from a Column to a GridPlot, which could be a reason why.

This one is a little frustrating, looks like `Range.plots` was removed in https://github.com/bokeh/bokeh/pull/11927/ and now only a `DataRange1d` keeps track of the plots it has been attached to. Not sure how to work around this yet.

Patch:
diff --git a/examples/gallery/demos/bokeh/choropleth_data_link.ipynb b/examples/gallery/demos/bokeh/choropleth_data_link.ipynb
index 55279c110..bb1f6a576 100644
--- a/examples/gallery/demos/bokeh/choropleth_data_link.ipynb
+++ b/examples/gallery/demos/bokeh/choropleth_data_link.ipynb
@@ -41,10 +41,12 @@
     "            for cid, county in counties.items()\n",
     "            if county[\"state\"] == \"tx\"]\n",
     "\n",
-    "county_data = [(county['detailed name'], county['Unemployment']) for county in counties]\n",
     "\n",
-    "choropleth = hv.Polygons(counties, ['lons', 'lats'], [('detailed name', 'County'), 'Unemployment'], label='Texas Unemployment')\n",
-    "table = hv.Table(county_data, [('detailed name', 'County'), 'Unemployment'])\n",
+    "detailed_name = 'detailed_name' if counties[0].get('detailed_name') else 'detailed name' # detailed name was changed in Bokeh 3.0\n",
+    "county_data = [(county[detailed_name], county['Unemployment']) for county in counties]\n",
+    "\n",
+    "choropleth = hv.Polygons(counties, ['lons', 'lats'], [(detailed_name, 'County'), 'Unemployment'], label='Texas Unemployment')\n",
+    "table = hv.Table(county_data, [(detailed_name, 'County'), 'Unemployment'])\n",
     "\n",
     "print(len(choropleth.data), len(table))"
    ]
@@ -86,5 +88,5 @@
   }
  },
  "nbformat": 4,
- "nbformat_minor": 2
+ "nbformat_minor": 4
 }
diff --git a/examples/gallery/demos/bokeh/texas_choropleth_example.ipynb b/examples/gallery/demos/bokeh/texas_choropleth_example.ipynb
index 04c6180bd..4c8964dd4 100644
--- a/examples/gallery/demos/bokeh/texas_choropleth_example.ipynb
+++ b/examples/gallery/demos/bokeh/texas_choropleth_example.ipynb
@@ -42,7 +42,8 @@
     "            for cid, county in counties.items()\n",
     "            if county[\"state\"] == \"tx\"]\n",
     "\n",
-    "choropleth = hv.Polygons(counties, ['lons', 'lats'], [('detailed name', 'County'), 'Unemployment'])"
+    "detailed_name = 'detailed_name' if counties[0].get('detailed_name') else 'detailed name' # detailed name was changed in Bokeh 3.0\n",
+    "choropleth = hv.Polygons(counties, ['lons', 'lats'], [(detailed_name, 'County'), 'Unemployment'])"
    ]
   },
   {
@@ -72,5 +73,5 @@
   }
  },
  "nbformat": 4,
- "nbformat_minor": 2
+ "nbformat_minor": 4
 }
diff --git a/examples/gallery/demos/matplotlib/texas_choropleth_example.ipynb b/examples/gallery/demos/matplotlib/texas_choropleth_example.ipynb
index 955132ec8..eb182b08d 100644
--- a/examples/gallery/demos/matplotlib/texas_choropleth_example.ipynb
+++ b/examples/gallery/demos/matplotlib/texas_choropleth_example.ipynb
@@ -38,11 +38,14 @@
     "from bokeh.sampledata.us_counties import data as counties\n",
     "from bokeh.sampledata.unemployment import data as unemployment\n",
     "\n",
+    "\n",
     "counties = [dict(county, Unemployment=unemployment[cid])\n",
     "            for cid, county in counties.items()\n",
     "            if county[\"state\"] == \"tx\"]\n",
     "\n",
-    "choropleth = hv.Polygons(counties, ['lons', 'lats'], [('detailed name', 'County'), 'Unemployment'])"
+    "\n",
+    "detailed_name = 'detailed_name' if counties[0].get('detailed_name') else 'detailed name' # detailed name was changed in Bokeh 3.0\n",
+    "choropleth = hv.Polygons(counties, ['lons', 'lats'], [(detailed_name, 'County'), 'Unemployment'])"
    ]
   },
   {
@@ -72,5 +75,5 @@
   }
  },
  "nbformat": 4,
- "nbformat_minor": 2
+ "nbformat_minor": 4
 }
diff --git a/examples/user_guide/Linked_Brushing.ipynb b/examples/user_guide/Linked_Brushing.ipynb
index cf5b01d3e..beb7ea3f3 100644
--- a/examples/user_guide/Linked_Brushing.ipynb
+++ b/examples/user_guide/Linked_Brushing.ipynb
@@ -275,7 +275,8 @@
     "            for cid, county in counties.items()\n",
     "            if county[\"state\"] == \"tx\"]\n",
     "\n",
-    "choropleth = hv.Polygons(counties, ['lons', 'lats'], [('detailed name', 'County'), 'Unemployment'])\n",
+    "detailed_name = 'detailed_name' if counties[0].get('detailed_name') else 'detailed name' # detailed name was changed in Bokeh 3.0\n",
+    "choropleth = hv.Polygons(counties, ['lons', 'lats'], [(detailed_name, 'County'), 'Unemployment'])\n",
     "hist = choropleth.hist('Unemployment', adjoin=False, normed=False)"
    ]
   },
diff --git a/examples/user_guide/Network_Graphs.ipynb b/examples/user_guide/Network_Graphs.ipynb
index cb4ddd35c..2fcc4594d 100644
--- a/examples/user_guide/Network_Graphs.ipynb
+++ b/examples/user_guide/Network_Graphs.ipynb
@@ -470,5 +470,5 @@
   }
  },
  "nbformat": 4,
- "nbformat_minor": 2
+ "nbformat_minor": 1
 }
diff --git a/holoviews/plotting/bokeh/annotation.py b/holoviews/plotting/bokeh/annotation.py
index 3376bd4f5..a2eb3a1ae 100644
--- a/holoviews/plotting/bokeh/annotation.py
+++ b/holoviews/plotting/bokeh/annotation.py
@@ -7,16 +7,7 @@ import numpy as np
 from bokeh.models import BoxAnnotation, Span, Arrow, Slope
 from panel.models import HTML
 
-try:
-    from bokeh.models.arrow_heads import TeeHead, NormalHead
-    arrow_start = {'<->': NormalHead, '<|-|>': NormalHead}
-    arrow_end = {'->': NormalHead, '-[': TeeHead, '-|>': NormalHead,
-                 '-': None}
-except ImportError:
-    from bokeh.models.arrow_heads import OpenHead, NormalHead
-    arrow_start = {'<->': NormalHead, '<|-|>': NormalHead}
-    arrow_end = {'->': NormalHead, '-[': OpenHead, '-|>': NormalHead,
-                 '-': None}
+from bokeh.models import TeeHead, NormalHead
 from bokeh.transform import dodge
 
 from ...core.util import datetime_types, dimension_sanitizer
@@ -28,6 +19,10 @@ from .styles import base_properties, fill_properties, line_properties, text_prop
 from .plot import BokehPlot
 from .util import date_to_integer
 
+arrow_start = {'<->': NormalHead, '<|-|>': NormalHead}
+arrow_end = {'->': NormalHead, '-[': TeeHead, '-|>': NormalHead,
+                '-': None}
+
 
 class TextPlot(ElementPlot, AnnotationPlot):
 
diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py
index a87d91325..cba14bb70 100644
--- a/holoviews/plotting/bokeh/element.py
+++ b/holoviews/plotting/bokeh/element.py
@@ -7,7 +7,6 @@ import param
 import numpy as np
 import bokeh
 import bokeh.plotting
-
 from bokeh.core.properties import value
 from bokeh.document.events import ModelChangedEvent
 from bokeh.models import (
@@ -16,8 +15,9 @@ from bokeh.models import (
 )
 from bokeh.models.axes import CategoricalAxis, DatetimeAxis
 from bokeh.models.formatters import (
-    FuncTickFormatter, TickFormatter, MercatorTickFormatter
+    TickFormatter, MercatorTickFormatter
 )
+from bokeh.models.layouts import Tabs
 from bokeh.models.mappers import (
     LinearColorMapper, LogColorMapper, CategoricalColorMapper
 )
@@ -26,7 +26,6 @@ from bokeh.models.tickers import (
     Ticker, BasicTicker, FixedTicker, LogTicker, MercatorTicker
 )
 from bokeh.models.tools import Tool
-from bokeh.models.widgets import Panel, Tabs
 
 from packaging.version import Version
 
@@ -47,14 +46,19 @@ from .styles import (
 )
 from .tabular import TablePlot
 from .util import (
-    TOOL_TYPES, bokeh_version, date_to_integer, decode_bytes, get_tab_title,
+    TOOL_TYPES, bokeh_version, bokeh3, date_to_integer, decode_bytes, get_tab_title,
     glyph_order, py2js_tickformatter, recursive_model_update,
     theme_attr_json, cds_column_replace, hold_policy, match_dim_specs,
     compute_layout_properties, wrap_formatter, match_ax_type,
-    prop_is_none, remove_legend
+    prop_is_none, remove_legend, property_to_dict
 )
 
-
+if bokeh3:
+    from bokeh.models.formatters import CustomJSTickFormatter
+    from bokeh.models.layouts import TabPanel
+else:
+    from bokeh.models.formatters import FuncTickFormatter as CustomJSTickFormatter
+    from bokeh.models.layouts import Panel as TabPanel
 
 try:
     TOOLS_MAP = Tool._known_aliases
@@ -501,13 +505,18 @@ class ElementPlot(BokehPlot, GenericElementPlot):
 
         properties.update(**self._plot_properties(key, element))
 
+        if bokeh3:
+            figure = bokeh.plotting.figure
+        else:
+            figure = bokeh.plotting.Figure
+
         with warnings.catch_warnings():
             # Bokeh raises warnings about duplicate tools but these
             # are not really an issue
             warnings.simplefilter('ignore', UserWarning)
-            return bokeh.plotting.Figure(x_axis_type=x_axis_type,
-                                         y_axis_type=y_axis_type, title=title,
-                                         **properties)
+            return figure(x_axis_type=x_axis_type,
+                          y_axis_type=y_axis_type, title=title,
+                          **properties)
 
 
     def _plot_properties(self, key, element):
@@ -572,7 +581,7 @@ class ElementPlot(BokehPlot, GenericElementPlot):
                 plot.toolbar.active_scroll = tool
             if isinstance(tool, tools.Tap):
                 plot.toolbar.active_tap = tool
-            if isinstance(tool, tools.Inspection):
+            if isinstance(tool, tools.InspectTool):
                 plot.toolbar.active_inspect.append(tool)
 
 
@@ -664,7 +673,7 @@ class ElementPlot(BokehPlot, GenericElementPlot):
             formatter = wrap_formatter(formatter, axis)
             if formatter is not None:
                 axis_props['formatter'] = formatter
-        elif FuncTickFormatter is not None and ax_mapping and isinstance(dimension, Dimension):
+        elif CustomJSTickFormatter is not None and ax_mapping and isinstance(dimension, Dimension):
             formatter = None
             if dimension.value_format:
                 formatter = dimension.value_format
@@ -675,7 +684,7 @@ class ElementPlot(BokehPlot, GenericElementPlot):
                        'converted to tick formatter. ' % dimension.name)
                 jsfunc = py2js_tickformatter(formatter, msg)
                 if jsfunc:
-                    formatter = FuncTickFormatter(code=jsfunc)
+                    formatter = CustomJSTickFormatter(code=jsfunc)
                     axis_props['formatter'] = formatter
 
         if axis == 'x':
@@ -868,8 +877,8 @@ class ElementPlot(BokehPlot, GenericElementPlot):
             elif not (fixed_height and fixed_width):
                 # Set initial aspect
                 aspect = self.get_aspect(xspan, yspan)
-                width = plot.frame_width or plot.plot_width or 300
-                height = plot.frame_height or plot.plot_height or 300
+                width = plot.frame_width or plot.width or 300
+                height = plot.frame_height or plot.height or 300
 
                 if not (fixed_width or fixed_height) and not self.responsive:
                     fixed_height = True
@@ -877,11 +886,11 @@ class ElementPlot(BokehPlot, GenericElementPlot):
                 if fixed_height:
                     plot.frame_height = height
                     plot.frame_width = int(height/aspect)
-                    plot.plot_width, plot.plot_height = None, None
+                    plot.width, plot.height = None, None
                 elif fixed_width:
                     plot.frame_width = width
                     plot.frame_height = int(width*aspect)
-                    plot.plot_width, plot.plot_height = None, None
+                    plot.width, plot.height = None, None
                 else:
                     plot.aspect_ratio = 1./aspect
 
@@ -1244,8 +1253,8 @@ class ElementPlot(BokehPlot, GenericElementPlot):
             # Ensure that data is populated before updating glyph
             dataspecs = glyph.dataspecs()
             for spec in dataspecs:
-                new_spec = filtered.get(spec)
-                old_spec = getattr(glyph, spec)
+                new_spec = property_to_dict(filtered.get(spec))
+                old_spec = property_to_dict(getattr(glyph, spec))
                 new_field = new_spec.get('field') if isinstance(new_spec, dict) else new_spec
                 old_field = old_spec.get('field') if isinstance(old_spec, dict) else old_spec
                 if (data is None) or (new_field not in data or new_field in source.data or new_field == old_field):
@@ -1266,9 +1275,18 @@ class ElementPlot(BokehPlot, GenericElementPlot):
             server = self.renderer.mode == 'server'
             with hold_policy(self.document, 'collect', server=server):
                 empty_data = {c: [] for c in columns}
-                event = ModelChangedEvent(self.document, source, 'data',
-                                          source.data, empty_data, empty_data,
-                                          setter='empty')
+                if bokeh3:
+                    event = ModelChangedEvent(
+                        document=self.document,
+                        model=source,
+                        attr='data',
+                        new=empty_data,
+                        setter='empty'
+                    )
+                else:
+                    event = ModelChangedEvent(
+                        self.document, source, 'data', source.data, empty_data, empty_data, setter='empty'
+                    )
                 self.document.callbacks._held_events.append(event)
 
         if legend is not None:
@@ -2177,12 +2195,17 @@ class OverlayPlot(GenericOverlayPlot, LegendPlot):
         if 'legend_items' not in self.handles:
             self.handles['legend_items'] = []
         legend_items = self.handles['legend_items']
-        legend_labels = {tuple(sorted(i.label.items())) if isinstance(i.label, dict) else i.label: i
-                         for i in legend_items}
+        legend_labels = {
+            tuple(sorted(property_to_dict(i.label).items()))
+            if isinstance(property_to_dict(i.label), dict) else i.label: i
+            for i in legend_items
+        }
         for item in legend.items:
-            label = tuple(sorted(item.label.items())) if isinstance(item.label, dict) else item.label
-            if not label or (isinstance(item.label, dict) and not item.label.get('value', True)):
+            item_label = property_to_dict(item.label)
+            label = tuple(sorted(item_label.items())) if isinstance(item_label, dict) else item_label
+            if not label or (isinstance(item_label, dict) and not item_label.get('value', True)):
                 continue
+
             if label in legend_labels:
                 prev_item = legend_labels[label]
                 prev_item.renderers[:] = list(util.unique_iterator(prev_item.renderers+item.renderers))
@@ -2200,8 +2223,9 @@ class OverlayPlot(GenericOverlayPlot, LegendPlot):
             if (item in filtered or not item.renderers or
                 not any(r.visible or 'hv_legend' in r.tags for r in item.renderers)):
                 continue
-            if isinstance(item.label, dict) and 'value' in item.label and self.legend_labels:
-                label = item.label['value']
+            item_label = property_to_dict(item.label)
+            if isinstance(item_label, dict) and 'value' in item_label and self.legend_labels:
+                label = item_label['value']
                 item.label = {'value': self.legend_labels.get(label, label)}
             renderers += item.renderers
             filtered.append(item)
@@ -2343,7 +2367,7 @@ class OverlayPlot(GenericOverlayPlot, LegendPlot):
                 title = subplot._format_title(key, dimensions=False)
                 if not title:
                     title = get_tab_title(key, frame, self.hmap.last)
-                panels.append(Panel(child=child, title=title))
+                panels.append(TabPanel(child=child, title=title))
             self._merge_tools(subplot)
 
         if self.tabs:
diff --git a/holoviews/plotting/bokeh/graphs.py b/holoviews/plotting/bokeh/graphs.py
index 8f893c1c5..e6af38c28 100644
--- a/holoviews/plotting/bokeh/graphs.py
+++ b/holoviews/plotting/bokeh/graphs.py
@@ -20,6 +20,7 @@ from .styles import (
     base_properties, line_properties, fill_properties, text_properties,
     rgba_tuple
 )
+from .util import bokeh3
 
 
 class GraphPlot(CompositeElementPlot, ColorbarPlot, LegendPlot):
@@ -187,12 +188,15 @@ class GraphPlot(CompositeElementPlot, ColorbarPlot, LegendPlot):
         if nodes.dtype.kind not in 'uif':
             node_indices = {v: i for i, v in enumerate(nodes)}
             index = np.array([node_indices[n] for n in nodes], dtype=np.int32)
-            layout = {str(node_indices[k]): (y, x) if self.invert_axes else (x, y)
+            layout = {node_indices[k]: (y, x) if self.invert_axes else (x, y)
                       for k, (x, y) in zip(nodes, node_positions)}
         else:
             index = nodes.astype(np.int32)
-            layout = {str(k): (y, x) if self.invert_axes else (x, y)
+            layout = {k: (y, x) if self.invert_axes else (x, y)
                       for k, (x, y) in zip(index, node_positions)}
+        if not bokeh3:
+            layout = {str(k): v for k, v in layout.items()}
+
         point_data = {'index': index}
 
         # Handle node colors
diff --git a/holoviews/plotting/bokeh/links.py b/holoviews/plotting/bokeh/links.py
index 3e9e8a4a8..1228b3dc6 100644
--- a/holoviews/plotting/bokeh/links.py
+++ b/holoviews/plotting/bokeh/links.py
@@ -1,8 +1,9 @@
 import numpy as np
 
-from bokeh.models import CustomJS, ToolbarBox
+from bokeh.models import CustomJS
 from bokeh.models.tools import RangeTool
 
+from .util import bokeh3
 from ...core.util import isscalar
 from ..links import (
     Link, RectanglesTableLink, DataLink, RangeToolLink,
@@ -10,6 +11,11 @@ from ..links import (
 )
 from ..plot import GenericElementPlot, GenericOverlayPlot
 
+if bokeh3:
+    from bokeh.models import Toolbar
+else:
+    from bokeh.models import ToolbarBox as Toolbar  # Not completely correct
+
 
 class LinkCallback:
 
@@ -131,7 +137,7 @@ class RangeToolLinkCallback(LinkCallback):
     """
 
     def __init__(self, root_model, link, source_plot, target_plot):
-        toolbars = list(root_model.select({'type': ToolbarBox}))
+        toolbars = list(root_model.select({'type': Toolbar}))
         axes = {}
         if 'x' in link.axes:
             axes['x_range'] = target_plot.handles['x_range']
@@ -139,7 +145,9 @@ class RangeToolLinkCallback(LinkCallback):
             axes['y_range'] = target_plot.handles['y_range']
         tool = RangeTool(**axes)
         source_plot.state.add_tools(tool)
-        if toolbars:
+        if bokeh3 and toolbars:
+            toolbars[0].tools.append(tool)
+        elif toolbars:
             toolbar = toolbars[0].toolbar
             toolbar.tools.append(tool)
 
@@ -185,7 +193,7 @@ class DataLinkCallback(LinkCallback):
             renderer.update(data_source=src_cds)
         else:
             renderer.update(source=src_cds)
-        if hasattr(renderer, 'view'):
+        if not bokeh3 and hasattr(renderer, 'view'):
             renderer.view.update(source=src_cds)
         target_plot.handles['source'] = src_cds
         target_plot.handles['cds'] = src_cds
diff --git a/holoviews/plotting/bokeh/plot.py b/holoviews/plotting/bokeh/plot.py
index 9869607cb..93cc345a4 100644
--- a/holoviews/plotting/bokeh/plot.py
+++ b/holoviews/plotting/bokeh/plot.py
@@ -8,7 +8,7 @@ from bokeh.layouts import gridplot
 from bokeh.models import (
     ColumnDataSource, Column, Row, Div, Title, Legend, Axis, ColorBar
 )
-from bokeh.models.widgets import Panel, Tabs
+from bokeh.models.layouts import Tabs
 
 from ...selection import NoOpSelectionDisplay
 from ...core import (
@@ -30,10 +30,15 @@ from ..plot import (
 from ..util import attach_streams, displayable, collate
 from .links import LinkCallback
 from .util import (
-    filter_toolboxes, make_axis, update_shared_sources, empty_plot,
+    bokeh3, filter_toolboxes, make_axis, update_shared_sources, empty_plot,
     decode_bytes, theme_attr_json, cds_column_replace, get_default
 )
 
+if bokeh3:
+    from bokeh.models.layouts import TabPanel
+else:
+    from bokeh.models.layouts import Panel as TabPanel
+
 
 class BokehPlot(DimensionedPlot, CallbackPlot):
     """
@@ -266,6 +271,8 @@ class BokehPlot(DimensionedPlot, CallbackPlot):
 
         if 'title' in self.handles:
             title_div = self.handles['title']
+        elif bokeh3:
+            title_div = Div(width=width, styles={"white-space": "nowrap"})  # so it won't wrap long titles easily
         else:
             title_div = Div(width=width, style={"white-space": "nowrap"})  # so it won't wrap long titles easily
         title_div.text = title_tags
@@ -303,7 +310,7 @@ class BokehPlot(DimensionedPlot, CallbackPlot):
                         renderer.update(data_source=new_source)
                     else:
                         renderer.update(source=new_source)
-                    if hasattr(renderer, 'view'):
+                    if not bokeh3 and hasattr(renderer, 'view'):
                         renderer.view.update(source=new_source)
                     plot.handles['source'] = plot.handles['cds'] = new_source
                     plots.append(plot)
@@ -575,9 +582,10 @@ class GridPlot(CompositePlot, GenericCompositePlot):
             else:
                 passed_plots.append(None)
 
+        toolbar_location = None if bokeh3 else self.toolbar
         plot = gridplot(plots[::-1], merge_tools=self.merge_tools,
                         sizing_mode=self.sizing_mode,
-                        toolbar_location=self.toolbar)
+                        toolbar_location=toolbar_location)
         plot = self._make_axes(plot)
 
         title = self._get_title_div(self.keys[-1])
@@ -629,7 +637,10 @@ class GridPlot(CompositePlot, GenericCompositePlot):
             if self.shared_yaxis:
                 x_axis.margin = (0, 0, 0, 50)
                 r1, r2 = r1[::-1], r2[::-1]
-            plot = gridplot([r1, r2])
+            if bokeh3:
+                plot = gridplot([r1, r2], toolbar_location=None)
+            else:
+                plot = gridplot([r1, r2])
         elif y_axis:
             models = [y_axis, plot]
             if self.shared_yaxis: models = models[::-1]
@@ -943,7 +954,7 @@ class LayoutPlot(CompositePlot, GenericLayoutPlot):
         # Wrap in appropriate layout model
         if self.tabs:
             plots = filter_toolboxes([p for t, p in tab_plots])
-            panels = [Panel(child=child, title=t) for t, child in tab_plots]
+            panels = [TabPanel(child=child, title=t) for t, child in tab_plots]
             layout_plot = Tabs(tabs=panels, sizing_mode=sizing_mode)
         else:
             plot_grid = filter_toolboxes(plot_grid)
diff --git a/holoviews/plotting/bokeh/util.py b/holoviews/plotting/bokeh/util.py
index 0c2eea911..c0c27f6b8 100644
--- a/holoviews/plotting/bokeh/util.py
+++ b/holoviews/plotting/bokeh/util.py
@@ -14,17 +14,16 @@ import numpy as np
 
 from bokeh.core.json_encoder import serialize_json # noqa (API import)
 from bokeh.core.validation import silence
-from bokeh.layouts import WidgetBox, Row, Column
+from bokeh.layouts import Row, Column
 from bokeh.models import tools
 from bokeh.models import (
-    Model, ToolbarBox, FactorRange, Range1d, Plot, Spacer, CustomJS,
+    Model, FactorRange, Range1d, Plot, Spacer, CustomJS,
     GridBox, DatetimeAxis, CategoricalAxis
 )
 from bokeh.models.formatters import (
-    FuncTickFormatter, TickFormatter, PrintfTickFormatter
+    TickFormatter, PrintfTickFormatter
 )
-from bokeh.models.widgets import DataTable, Tabs, Div
-from bokeh.plotting import Figure
+from bokeh.models.widgets import DataTable, Div
 from bokeh.themes.theme import Theme
 from bokeh.themes import built_in_themes
 from packaging.version import Version
@@ -39,7 +38,21 @@ from ...core.spaces import get_nested_dmaps, DynamicMap
 from ..util import dim_axis_label
 
 bokeh_version = Version(bokeh.__version__)
-
+bokeh3 = bokeh_version >= Version("3.0")
+
+if bokeh3:
+    from bokeh.models.formatters import CustomJSTickFormatter
+    from bokeh.models import Toolbar, Tabs, GridPlot
+    from bokeh.plotting import figure
+    class WidgetBox: pass  # Does not exist in Bokeh 3
+
+else:
+    from bokeh.layouts import WidgetBox
+    from bokeh.models.formatters import FuncTickFormatter as CustomJSTickFormatter
+    from bokeh.models.widgets import Tabs
+    from bokeh.models import ToolbarBox as Toolbar  # Not completely correct
+    from bokeh.plotting import Figure as figure
+    class GridPlot: pass  # Does not exist in Bokeh 2
 
 TOOL_TYPES = {
     'pan': tools.PanTool,
@@ -131,9 +144,9 @@ def layout_padding(plots, renderer):
         for c, p in enumerate(row):
             if p is None:
                 p = empty_plot(widths[c], heights[r])
-            elif hasattr(p, 'plot_width') and p.plot_width == 0 and p.plot_height == 0:
-                p.plot_width = widths[c]
-                p.plot_height = heights[r]
+            elif hasattr(p, 'width') and p.width == 0 and p.height == 0:
+                p.width = widths[c]
+                p.height = heights[r]
             expanded_plots[r].append(p)
     return expanded_plots
 
@@ -141,21 +154,21 @@ def layout_padding(plots, renderer):
 def compute_plot_size(plot):
     """
     Computes the size of bokeh models that make up a layout such as
-    figures, rows, columns, widgetboxes and Plot.
+    figures, rows, columns, and Plot.
     """
-    if isinstance(plot, GridBox):
+    if isinstance(plot, (GridBox, GridPlot)):
         ndmapping = NdMapping({(x, y): fig for fig, y, x in plot.children}, kdims=['x', 'y'])
         cols = ndmapping.groupby('x')
         rows = ndmapping.groupby('y')
         width = sum([max([compute_plot_size(f)[0] for f in col]) for col in cols])
         height = sum([max([compute_plot_size(f)[1] for f in row]) for row in rows])
         return width, height
-    elif isinstance(plot, (Div, ToolbarBox)):
-        # Cannot compute size for Div or ToolbarBox
+    elif isinstance(plot, (Div, Toolbar)):
+        # Cannot compute size for Div or Toolbar
         return 0, 0
-    elif isinstance(plot, (Row, Column, WidgetBox, Tabs)):
+    elif isinstance(plot, (Row, Column, Tabs, WidgetBox)):
         if not plot.children: return 0, 0
-        if isinstance(plot, Row) or (isinstance(plot, ToolbarBox) and plot.toolbar_location not in ['right', 'left']):
+        if isinstance(plot, Row) or (isinstance(plot, Toolbar) and plot.toolbar_location not in ['right', 'left']):
             w_agg, h_agg = (np.sum, np.max)
         elif isinstance(plot, Tabs):
             w_agg, h_agg = (np.max, np.max)
@@ -163,13 +176,13 @@ def compute_plot_size(plot):
             w_agg, h_agg = (np.max, np.sum)
         widths, heights = zip(*[compute_plot_size(child) for child in plot.children])
         return w_agg(widths), h_agg(heights)
-    elif isinstance(plot, Figure):
-        if plot.plot_width:
-            width = plot.plot_width
+    elif isinstance(plot, figure):
+        if plot.width:
+            width = plot.width
         else:
             width = plot.frame_width + plot.min_border_right + plot.min_border_left
-        if plot.plot_height:
-            height = plot.plot_height
+        if plot.height:
+            height = plot.height
         else:
             height = plot.frame_height + plot.min_border_bottom + plot.min_border_top
         return width, height
@@ -345,14 +358,20 @@ def compute_layout_properties(
                        'provide a numeric value, \'equal\' or '
                        '\'square\'.')
 
-    return ({'aspect_ratio': aspect_ratio,
-             'aspect_scale': aspect_scale,
-             'match_aspect': match_aspect,
-             'sizing_mode' : sizing_mode},
-            {'frame_width' : frame_width,
-             'frame_height': frame_height,
-             'plot_height' : height,
-             'plot_width'  : width})
+    aspect_info = {
+        'aspect_ratio': aspect_ratio,
+        'aspect_scale': aspect_scale,
+        'match_aspect': match_aspect,
+        'sizing_mode' : sizing_mode
+    }
+    dimension_info =  {
+        'frame_width' : frame_width,
+        'frame_height': frame_height,
+        'height' : height,
+        'width'  : width
+    }
+
+    return aspect_info, dimension_info
 
 
 @contextmanager
@@ -434,17 +453,17 @@ def make_axis(axis, size, factors, dim, flip=False, rotation=0,
         height = int(axis_height + np.abs(np.sin(rotation)) *
                      ((nchars*tick_px)*0.82)) + tick_px + label_px
         opts = dict(x_axis_type='auto', x_axis_label=axis_label,
-                    x_range=ranges, y_range=ranges2, plot_height=height,
-                    plot_width=size)
+                    x_range=ranges, y_range=ranges2, height=height,
+                    width=size)
     else:
         # Adjust width to compensate for label rotation
         align = 'left' if flip else 'right'
         width = int(axis_height + np.abs(np.cos(rotation)) *
                     ((nchars*tick_px)*0.82)) + tick_px + label_px
         opts = dict(y_axis_label=axis_label, x_range=ranges2,
-                    y_range=ranges, plot_width=width, plot_height=size)
+                    y_range=ranges, height=size, width=width)
 
-    p = Figure(toolbar_location=None, tools=[], **opts)
+    p = figure(toolbar_location=None, tools=[], **opts)
     p.outline_line_alpha = 0
     p.grid.grid_line_alpha = 0
 
@@ -523,7 +542,7 @@ def pad_width(model, table_padding=0.85, tabs_padding=1.2):
     elif isinstance(model, (WidgetBox, Div)):
         width = model.width
     elif model:
-        width = model.plot_width
+        width = model.width
     else:
         width = 0
     return width
@@ -532,7 +551,7 @@ def pad_width(model, table_padding=0.85, tabs_padding=1.2):
 def pad_plots(plots):
     """
     Accepts a grid of bokeh plots in form of a list of lists and
-    wraps any DataTable or Tabs in a WidgetBox with appropriate
+    wraps any DataTable or Tabs in a Column with appropriate
     padding. Required to avoid overlap in gridplot.
     """
     widths = []
@@ -542,7 +561,9 @@ def pad_plots(plots):
             width = pad_width(p)
             row_widths.append(width)
         widths.append(row_widths)
-    plots = [[WidgetBox(p, width=w) if isinstance(p, (DataTable, Tabs)) else p
+
+    layout = Column if bokeh3 else WidgetBox
+    plots = [[layout(p, width=w) if isinstance(p, (DataTable, Tabs)) else p
               for p, w in zip(row, ws)] for row, ws in zip(plots, widths)]
     return plots
 
@@ -556,7 +577,7 @@ def filter_toolboxes(plots):
         plots = [filter_toolboxes(plot) for plot in plots]
     elif hasattr(plots, 'children'):
         plots.children = [filter_toolboxes(child) for child in plots.children
-                          if not isinstance(child, ToolbarBox)]
+                          if not isinstance(child, Toolbar)]
     return plots
 
 
@@ -961,9 +982,27 @@ def wrap_formatter(formatter, axis):
         msg = f'{axis}formatter could not be converted to tick formatter. '
         jsfunc = py2js_tickformatter(formatter, msg)
         if jsfunc:
-            formatter = FuncTickFormatter(code=jsfunc)
+            formatter = CustomJSTickFormatter(code=jsfunc)
         else:
             formatter = None
     else:
         formatter = PrintfTickFormatter(format=formatter)
     return formatter
+
+
+def property_to_dict(x):
+    """
+    Convert Bokeh's property Field and Value to a dictionary
+
+    Was added in bokeh 3.0
+    """
+
+    try:
+        from bokeh.core.property.vectorization import Field, Unspecified, Value
+
+        if isinstance(x, (Field, Value)):
+            x = {k: v for k, v in x.__dict__.items() if v != Unspecified}
+    except ImportError:
+        pass
+
+    return x
diff --git a/holoviews/plotting/util.py b/holoviews/plotting/util.py
index 93b458a15..ed1436c2d 100644
--- a/holoviews/plotting/util.py
+++ b/holoviews/plotting/util.py
@@ -699,8 +699,14 @@ def _list_cmaps(provider=None, records=False):
 
     if 'matplotlib' in provider:
         try:
+            import matplotlib as mpl
             from matplotlib import cm
-            if hasattr(cm, '_cmap_registry'):
+
+            # matplotlib.colormaps has been introduced in matplotlib 3.5
+            # matplotlib.cm._cmap_registry was removed in matplotlib 3.6
+            if hasattr(mpl, "colormaps"):
+                mpl_cmaps = list(mpl.colormaps)
+            elif hasattr(cm, '_cmap_registry'):
                 mpl_cmaps = list(cm._cmap_registry)
             else:
                 mpl_cmaps = list(cm.cmaps_listed)+list(cm.datad)


Test patch:
diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
index b41a4d39a..ae80325aa 100644
--- a/.github/workflows/test.yaml
+++ b/.github/workflows/test.yaml
@@ -27,7 +27,7 @@ jobs:
       - name: pre-commit
         uses: pre-commit/action@v3.0.0
   test_suite:
-    name: Pytest on ${{ matrix.python-version }}, ${{ matrix.os }}
+    name: Pytest on ${{ matrix.python-version }}, Bokeh ${{ matrix.bokeh-version }}, ${{ matrix.os }}
     needs: [pre_commit]
     runs-on: ${{ matrix.os }}
     strategy:
@@ -36,12 +36,17 @@ jobs:
         os: ['ubuntu-latest', 'macos-latest', 'windows-latest']
         # Run on the full set on schedule, workflow_dispatch and push&tags events, otherwise on a subset.
         python-version: ${{ ( github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || ( github.event_name == 'push' && github.ref_type == 'tag' ) ) && fromJSON('["3.7", "3.8", "3.9", "3.10", "3.11"]') || fromJSON('["3.7", "3.9", "3.11"]') }}
+        bokeh-version: ['2', '3']
+        exclude:
+            # Bokeh 3 does not support Python 3.7
+          - bokeh-version: '3'
+            python-version: '3.7'
     timeout-minutes: 120  # Because slow conda solve on Python 3.7
     defaults:
       run:
         shell: bash -el {0}
     env:
-      DESC: "Python ${{ matrix.python-version }} tests"
+      DESC: "Python ${{ matrix.python-version }} - Bokeh ${{ matrix.bokeh-version }} tests"
       PYTHON_VERSION: ${{ matrix.python-version }}
       SETUPTOOLS_ENABLE_FEATURES: "legacy-editable"
       DISPLAY: ":99.0"
@@ -51,7 +56,7 @@ jobs:
     steps:
       - uses: pyviz-dev/holoviz_tasks/install@v0.1a9
         with:
-          name: unit_test_suite
+          name: unit_test_suite_bokeh${{ matrix.bokeh-version }}
           python-version: ${{ matrix.python-version }}
           channel-priority: strict
           channels: pyviz/label/dev,conda-forge,nodefaults
@@ -64,6 +69,12 @@ jobs:
         run: |
           conda activate test-environment
           bokeh sampledata
+      - name: Install bokeh 3.0 compatibility packages
+        if: matrix.bokeh-version == '3'
+        run: |
+          conda activate test-environment
+          conda uninstall panel bokeh --force -y --offline || echo "packages already removed"
+          pip install "git+https://github.com/holoviz/panel.git@branch-1.0"
       - name: doit test_unit
         run: |
           conda activate test-environment
@@ -73,7 +84,7 @@ jobs:
         run: |
           conda activate test-environment
           mkdir -p ~/.jupyter/
-          echo "c.ExecutePreprocessor.startup_timeout=120" >> ~/.jupyter/jupyter_nbconvert_config.py
+          echo "c.ExecutePreprocessor.startup_timeout=600" >> ~/.jupyter/jupyter_nbconvert_config.py
           doit test_examples
       - name: test examples - python 3.11
         # Should be removed when numba support python 3.11
@@ -81,7 +92,7 @@ jobs:
         run: |
           conda activate test-environment
           mkdir -p ~/.jupyter/
-          echo "c.ExecutePreprocessor.startup_timeout=120" >> ~/.jupyter/jupyter_nbconvert_config.py
+          echo "c.ExecutePreprocessor.startup_timeout=600" >> ~/.jupyter/jupyter_nbconvert_config.py
           pytest -n auto --dist loadscope --nbval-lax examples/reference/elements
       - name: codecov
         run: |
diff --git a/holoviews/tests/plotting/bokeh/test_barplot.py b/holoviews/tests/plotting/bokeh/test_barplot.py
index 0dd411647..5cdffc66e 100644
--- a/holoviews/tests/plotting/bokeh/test_barplot.py
+++ b/holoviews/tests/plotting/bokeh/test_barplot.py
@@ -2,6 +2,7 @@ import numpy as np
 
 from holoviews.core.overlay import NdOverlay
 from holoviews.element import Bars
+from holoviews.plotting.bokeh.util import property_to_dict
 
 from bokeh.models import CategoricalColorMapper, LinearColorMapper
 
@@ -160,8 +161,8 @@ class TestBarPlot(TestBokehPlot):
         cds = plot.handles['cds']
         glyph = plot.handles['glyph']
         self.assertEqual(cds.data['color'], np.array(['#000', '#F00', '#0F0']))
-        self.assertEqual(glyph.fill_color, {'field': 'color'})
-        self.assertEqual(glyph.line_color, 'black')
+        self.assertEqual(property_to_dict(glyph.fill_color), {'field': 'color'})
+        self.assertEqual(property_to_dict(glyph.line_color), 'black')
 
     def test_bars_linear_color_op(self):
         bars = Bars([(0, 0, 0), (0, 1, 1), (0, 2, 2)],
@@ -174,8 +175,8 @@ class TestBarPlot(TestBokehPlot):
         self.assertEqual(cmapper.low, 0)
         self.assertEqual(cmapper.high, 2)
         self.assertEqual(cds.data['color'], np.array([0, 1, 2]))
-        self.assertEqual(glyph.fill_color, {'field': 'color', 'transform': cmapper})
-        self.assertEqual(glyph.line_color, 'black')
+        self.assertEqual(property_to_dict(glyph.fill_color), {'field': 'color', 'transform': cmapper})
+        self.assertEqual(property_to_dict(glyph.line_color), 'black')
 
     def test_bars_categorical_color_op(self):
         bars = Bars([(0, 0, 'A'), (0, 1, 'B'), (0, 2, 'C')],
@@ -187,8 +188,8 @@ class TestBarPlot(TestBokehPlot):
         self.assertTrue(cmapper, CategoricalColorMapper)
         self.assertEqual(cmapper.factors, ['A', 'B', 'C'])
         self.assertEqual(cds.data['color'], np.array(['A', 'B', 'C']))
-        self.assertEqual(glyph.fill_color, {'field': 'color', 'transform': cmapper})
-        self.assertEqual(glyph.line_color, 'black')
+        self.assertEqual(property_to_dict(glyph.fill_color), {'field': 'color', 'transform': cmapper})
+        self.assertEqual(property_to_dict(glyph.line_color), 'black')
 
     def test_bars_line_color_op(self):
         bars = Bars([(0, 0, '#000'), (0, 1, '#F00'), (0, 2, '#0F0')],
@@ -197,8 +198,8 @@ class TestBarPlot(TestBokehPlot):
         cds = plot.handles['cds']
         glyph = plot.handles['glyph']
         self.assertEqual(cds.data['line_color'], np.array(['#000', '#F00', '#0F0']))
-        self.assertNotEqual(glyph.fill_color, {'field': 'line_color'})
-        self.assertEqual(glyph.line_color, {'field': 'line_color'})
+        self.assertNotEqual(property_to_dict(glyph.fill_color), {'field': 'line_color'})
+        self.assertEqual(property_to_dict(glyph.line_color), {'field': 'line_color'})
 
     def test_bars_fill_color_op(self):
         bars = Bars([(0, 0, '#000'), (0, 1, '#F00'), (0, 2, '#0F0')],
@@ -207,8 +208,8 @@ class TestBarPlot(TestBokehPlot):
         cds = plot.handles['cds']
         glyph = plot.handles['glyph']
         self.assertEqual(cds.data['fill_color'], np.array(['#000', '#F00', '#0F0']))
-        self.assertEqual(glyph.fill_color, {'field': 'fill_color'})
-        self.assertNotEqual(glyph.line_color, {'field': 'fill_color'})
+        self.assertEqual(property_to_dict(glyph.fill_color), {'field': 'fill_color'})
+        self.assertNotEqual(property_to_dict(glyph.line_color), {'field': 'fill_color'})
 
     def test_bars_alpha_op(self):
         bars = Bars([(0, 0, 0), (0, 1, 0.2), (0, 2, 0.7)],
@@ -217,7 +218,7 @@ class TestBarPlot(TestBokehPlot):
         cds = plot.handles['cds']
         glyph = plot.handles['glyph']
         self.assertEqual(cds.data['alpha'], np.array([0, 0.2, 0.7]))
-        self.assertEqual(glyph.fill_alpha, {'field': 'alpha'})
+        self.assertEqual(property_to_dict(glyph.fill_alpha), {'field': 'alpha'})
 
     def test_bars_line_alpha_op(self):
         bars = Bars([(0, 0, 0), (0, 1, 0.2), (0, 2, 0.7)],
@@ -226,8 +227,8 @@ class TestBarPlot(TestBokehPlot):
         cds = plot.handles['cds']
         glyph = plot.handles['glyph']
         self.assertEqual(cds.data['line_alpha'], np.array([0, 0.2, 0.7]))
-        self.assertEqual(glyph.line_alpha, {'field': 'line_alpha'})
-        self.assertNotEqual(glyph.fill_alpha, {'field': 'line_alpha'})
+        self.assertEqual(property_to_dict(glyph.line_alpha), {'field': 'line_alpha'})
+        self.assertNotEqual(property_to_dict(glyph.fill_alpha), {'field': 'line_alpha'})
 
     def test_bars_fill_alpha_op(self):
         bars = Bars([(0, 0, 0), (0, 1, 0.2), (0, 2, 0.7)],
@@ -236,8 +237,8 @@ class TestBarPlot(TestBokehPlot):
         cds = plot.handles['cds']
         glyph = plot.handles['glyph']
         self.assertEqual(cds.data['fill_alpha'], np.array([0, 0.2, 0.7]))
-        self.assertNotEqual(glyph.line_alpha, {'field': 'fill_alpha'})
-        self.assertEqual(glyph.fill_alpha, {'field': 'fill_alpha'})
+        self.assertNotEqual(property_to_dict(glyph.line_alpha), {'field': 'fill_alpha'})
+        self.assertEqual(property_to_dict(glyph.fill_alpha), {'field': 'fill_alpha'})
 
     def test_bars_line_width_op(self):
         bars = Bars([(0, 0, 1), (0, 1, 4), (0, 2, 8)],
@@ -246,7 +247,7 @@ class TestBarPlot(TestBokehPlot):
         cds = plot.handles['cds']
         glyph = plot.handles['glyph']
         self.assertEqual(cds.data['line_width'], np.array([1, 4, 8]))
-        self.assertEqual(glyph.line_width, {'field': 'line_width'})
+        self.assertEqual(property_to_dict(glyph.line_width), {'field': 'line_width'})
 
     def test_op_ndoverlay_value(self):
         colors = ['blue', 'red']
diff --git a/holoviews/tests/plotting/bokeh/test_boxwhiskerplot.py b/holoviews/tests/plotting/bokeh/test_boxwhiskerplot.py
index 485f120aa..96274e290 100644
--- a/holoviews/tests/plotting/bokeh/test_boxwhiskerplot.py
+++ b/holoviews/tests/plotting/bokeh/test_boxwhiskerplot.py
@@ -3,6 +3,7 @@ import datetime as dt
 import numpy as np
 
 from holoviews.element import BoxWhisker
+from holoviews.plotting.bokeh.util import property_to_dict
 
 from .test_plot import TestBokehPlot, bokeh_renderer
 
@@ -64,7 +65,7 @@ class TestBoxWhiskerPlot(TestBokehPlot):
         self.assertTrue(cmapper, LinearColorMapper)
         self.assertEqual(cmapper.low, 0)
         self.assertEqual(cmapper.high, 4)
-        self.assertEqual(glyph.fill_color, {'field': 'box_color', 'transform': cmapper})
+        self.assertEqual(property_to_dict(glyph.fill_color), {'field': 'box_color', 'transform': cmapper})
 
     def test_box_whisker_categorical_color_op(self):
         a = np.repeat(np.arange(5), 5)
@@ -77,7 +78,7 @@ class TestBoxWhiskerPlot(TestBokehPlot):
         self.assertEqual(source.data['box_color'], b[::5])
         self.assertTrue(cmapper, CategoricalColorMapper)
         self.assertEqual(cmapper.factors, ['A', 'B', 'C', 'D', 'E'])
-        self.assertEqual(glyph.fill_color, {'field': 'box_color', 'transform': cmapper})
+        self.assertEqual(property_to_dict(glyph.fill_color), {'field': 'box_color', 'transform': cmapper})
 
     def test_box_whisker_alpha_op(self):
         a = np.repeat(np.arange(5), 5)
@@ -87,7 +88,7 @@ class TestBoxWhiskerPlot(TestBokehPlot):
         source = plot.handles['vbar_1_source']
         glyph = plot.handles['vbar_1_glyph']
         self.assertEqual(source.data['box_alpha'], np.arange(5)/10.)
-        self.assertEqual(glyph.fill_alpha, {'field': 'box_alpha'})
+        self.assertEqual(property_to_dict(glyph.fill_alpha), {'field': 'box_alpha'})
 
     def test_box_whisker_line_width_op(self):
         a = np.repeat(np.arange(5), 5)
@@ -97,4 +98,4 @@ class TestBoxWhiskerPlot(TestBokehPlot):
         source = plot.handles['vbar_1_source']
         glyph = plot.handles['vbar_1_glyph']
         self.assertEqual(source.data['box_line_width'], np.arange(5))
-        self.assertEqual(glyph.line_width, {'field': 'box_line_width'})
+        self.assertEqual(property_to_dict(glyph.line_width), {'field': 'box_line_width'})
diff --git a/holoviews/tests/plotting/bokeh/test_callbacks.py b/holoviews/tests/plotting/bokeh/test_callbacks.py
index 07f5cf0b7..0d88c1143 100644
--- a/holoviews/tests/plotting/bokeh/test_callbacks.py
+++ b/holoviews/tests/plotting/bokeh/test_callbacks.py
@@ -148,7 +148,7 @@ class TestResetCallback(CallbackTestCase):
 class TestPointerCallbacks(CallbackTestCase):
 
     def test_pointer_x_datetime_out_of_bounds(self):
-        points = Points([(dt.datetime(2017, 1, 1), 1), (dt.datetime(2017, 1, 3), 3)])
+        points = Points([(dt.datetime(2017, 1, 1), 1), (dt.datetime(2017, 1, 3), 3)]).opts(padding=0)
         PointerX(source=points)
         plot = bokeh_server_renderer.get_plot(points)
         set_curdoc(plot.document)
diff --git a/holoviews/tests/plotting/bokeh/test_elementplot.py b/holoviews/tests/plotting/bokeh/test_elementplot.py
index 19dbfaf21..590609f9c 100644
--- a/holoviews/tests/plotting/bokeh/test_elementplot.py
+++ b/holoviews/tests/plotting/bokeh/test_elementplot.py
@@ -10,6 +10,7 @@ from holoviews.core.util import dt_to_int
 from holoviews.element import Curve, Image, Scatter, Labels
 from holoviews.streams import Stream, PointDraw
 from holoviews.plotting.util import process_cmap
+from holoviews.plotting.bokeh.util import bokeh3
 from holoviews.util import render
 
 from .test_plot import TestBokehPlot, bokeh_renderer
@@ -19,10 +20,16 @@ import panel as pn
 
 from bokeh.document import Document
 from bokeh.models import tools
-from bokeh.models import (FuncTickFormatter, PrintfTickFormatter,
+from bokeh.models import (PrintfTickFormatter,
                             NumeralTickFormatter, LogTicker,
                             LinearColorMapper, LogColorMapper, EqHistColorMapper)
 
+if bokeh3:
+    from bokeh.models.formatters import CustomJSTickFormatter
+else:
+    from bokeh.models.formatters import FuncTickFormatter as CustomJSTickFormatter
+
+
 class TestElementPlot(LoggingComparisonTestCase, TestBokehPlot):
 
     def test_element_show_frame_disabled(self):
@@ -192,7 +199,7 @@ class TestElementPlot(LoggingComparisonTestCase, TestBokehPlot):
         curve = Curve(range(10)).opts(xformatter=formatter)
         plot = bokeh_renderer.get_plot(curve)
         xaxis = plot.handles['xaxis']
-        self.assertIsInstance(xaxis.formatter, FuncTickFormatter)
+        self.assertIsInstance(xaxis.formatter, CustomJSTickFormatter)
 
     def test_element_yformatter_function(self):
         try:
@@ -204,7 +211,7 @@ class TestElementPlot(LoggingComparisonTestCase, TestBokehPlot):
         curve = Curve(range(10)).opts(yformatter=formatter)
         plot = bokeh_renderer.get_plot(curve)
         yaxis = plot.handles['yaxis']
-        self.assertIsInstance(yaxis.formatter, FuncTickFormatter)
+        self.assertIsInstance(yaxis.formatter, CustomJSTickFormatter)
 
     def test_element_xformatter_instance(self):
         formatter = NumeralTickFormatter()
@@ -308,7 +315,7 @@ class TestElementPlot(LoggingComparisonTestCase, TestBokehPlot):
             return f'{x}'
         curve = Curve(range(10), kdims=[Dimension('x', value_format=formatter)])
         plot = bokeh_renderer.get_plot(curve).state
-        self.assertIsInstance(plot.xaxis[0].formatter, FuncTickFormatter)
+        self.assertIsInstance(plot.xaxis[0].formatter, CustomJSTickFormatter)
 
     def test_element_formatter_yaxis(self):
         try:
@@ -319,7 +326,7 @@ class TestElementPlot(LoggingComparisonTestCase, TestBokehPlot):
             return f'{x}'
         curve = Curve(range(10), vdims=[Dimension('y', value_format=formatter)])
         plot = bokeh_renderer.get_plot(curve).state
-        self.assertIsInstance(plot.yaxis[0].formatter, FuncTickFormatter)
+        self.assertIsInstance(plot.yaxis[0].formatter, CustomJSTickFormatter)
 
     def test_element_xticks_datetime(self):
         dates = [(dt.datetime(2016, 1, i), i) for i in range(1, 4)]
@@ -479,8 +486,8 @@ class TestElementPlot(LoggingComparisonTestCase, TestBokehPlot):
     def test_element_aspect(self):
         curve = Curve([1, 2, 3]).opts(aspect=2)
         plot = bokeh_renderer.get_plot(curve)
-        self.assertEqual(plot.state.plot_height, None)
-        self.assertEqual(plot.state.plot_width, None)
+        self.assertEqual(plot.state.height, None)
+        self.assertEqual(plot.state.width, None)
         self.assertEqual(plot.state.frame_height, 300)
         self.assertEqual(plot.state.frame_width, 600)
         self.assertEqual(plot.state.aspect_ratio, None)
@@ -488,8 +495,8 @@ class TestElementPlot(LoggingComparisonTestCase, TestBokehPlot):
     def test_element_aspect_width(self):
         curve = Curve([1, 2, 3]).opts(aspect=2, width=400)
         plot = bokeh_renderer.get_plot(curve)
-        self.assertEqual(plot.state.plot_height, None)
-        self.assertEqual(plot.state.plot_width, None)
+        self.assertEqual(plot.state.height, None)
+        self.assertEqual(plot.state.width, None)
         self.assertEqual(plot.state.frame_height, 200)
         self.assertEqual(plot.state.frame_width, 400)
         self.assertEqual(plot.state.aspect_ratio, None)
@@ -498,8 +505,8 @@ class TestElementPlot(LoggingComparisonTestCase, TestBokehPlot):
     def test_element_aspect_height(self):
         curve = Curve([1, 2, 3]).opts(aspect=2, height=400)
         plot = bokeh_renderer.get_plot(curve)
-        self.assertEqual(plot.state.plot_height, None)
-        self.assertEqual(plot.state.plot_width, None)
+        self.assertEqual(plot.state.height, None)
+        self.assertEqual(plot.state.width, None)
         self.assertEqual(plot.state.frame_height, 400)
         self.assertEqual(plot.state.frame_width, 800)
         self.assertEqual(plot.state.aspect_ratio, None)
@@ -508,8 +515,8 @@ class TestElementPlot(LoggingComparisonTestCase, TestBokehPlot):
     def test_element_aspect_width_height(self):
         curve = Curve([1, 2, 3]).opts(aspect=2, height=400, width=400)
         plot = bokeh_renderer.get_plot(curve)
-        self.assertEqual(plot.state.plot_height, 400)
-        self.assertEqual(plot.state.plot_width, 400)
+        self.assertEqual(plot.state.height, 400)
+        self.assertEqual(plot.state.width, 400)
         self.assertEqual(plot.state.frame_height, None)
         self.assertEqual(plot.state.frame_width, None)
         self.assertEqual(plot.state.aspect_ratio, None)
@@ -518,8 +525,8 @@ class TestElementPlot(LoggingComparisonTestCase, TestBokehPlot):
     def test_element_aspect_frame_width(self):
         curve = Curve([1, 2, 3]).opts(aspect=2, frame_width=400)
         plot = bokeh_renderer.get_plot(curve)
-        self.assertEqual(plot.state.plot_height, None)
-        self.assertEqual(plot.state.plot_width, None)
+        self.assertEqual(plot.state.height, None)
+        self.assertEqual(plot.state.width, None)
         self.assertEqual(plot.state.frame_height, 200)
         self.assertEqual(plot.state.frame_width, 400)
         self.assertEqual(plot.state.aspect_ratio, None)
@@ -527,8 +534,8 @@ class TestElementPlot(LoggingComparisonTestCase, TestBokehPlot):
     def test_element_aspect_frame_height(self):
         curve = Curve([1, 2, 3]).opts(aspect=2, frame_height=400)
         plot = bokeh_renderer.get_plot(curve)
-        self.assertEqual(plot.state.plot_height, None)
-        self.assertEqual(plot.state.plot_width, None)
+        self.assertEqual(plot.state.height, None)
+        self.assertEqual(plot.state.width, None)
         self.assertEqual(plot.state.frame_height, 400)
         self.assertEqual(plot.state.frame_width, 800)
         self.assertEqual(plot.state.aspect_ratio, None)
@@ -536,8 +543,8 @@ class TestElementPlot(LoggingComparisonTestCase, TestBokehPlot):
     def test_element_aspect_frame_width_frame_height(self):
         curve = Curve([1, 2, 3]).opts(aspect=2, frame_height=400, frame_width=400)
         plot = bokeh_renderer.get_plot(curve)
-        self.assertEqual(plot.state.plot_height, None)
-        self.assertEqual(plot.state.plot_width, None)
+        self.assertEqual(plot.state.height, None)
+        self.assertEqual(plot.state.width, None)
         self.assertEqual(plot.state.frame_height, 400)
         self.assertEqual(plot.state.frame_width, 400)
         self.assertEqual(plot.state.aspect_ratio, None)
@@ -546,8 +553,8 @@ class TestElementPlot(LoggingComparisonTestCase, TestBokehPlot):
     def test_element_data_aspect(self):
         curve = Curve([0, 0.5, 1, 1.5]).opts(data_aspect=1.5)
         plot = bokeh_renderer.get_plot(curve)
-        self.assertEqual(plot.state.plot_height, None)
-        self.assertEqual(plot.state.plot_width, None)
+        self.assertEqual(plot.state.height, None)
+        self.assertEqual(plot.state.width, None)
         self.assertEqual(plot.state.frame_height, 300)
         self.assertEqual(plot.state.frame_width, 400)
         self.assertEqual(plot.state.aspect_scale, 1.5)
@@ -555,8 +562,8 @@ class TestElementPlot(LoggingComparisonTestCase, TestBokehPlot):
     def test_element_data_aspect_width(self):
         curve = Curve([0, 0.5, 1, 1.5]).opts(data_aspect=2, width=400)
         plot = bokeh_renderer.get_plot(curve)
-        self.assertEqual(plot.state.plot_height, None)
-        self.assertEqual(plot.state.plot_width, None)
+        self.assertEqual(plot.state.height, None)
+        self.assertEqual(plot.state.width, None)
         self.assertEqual(plot.state.frame_height, 400)
         self.assertEqual(plot.state.frame_width, 400)
         self.assertEqual(plot.state.aspect_scale, 2)
@@ -565,8 +572,8 @@ class TestElementPlot(LoggingComparisonTestCase, TestBokehPlot):
     def test_element_data_aspect_height(self):
         curve = Curve([0, 0.5, 1, 1.5]).opts(data_aspect=2, height=400)
         plot = bokeh_renderer.get_plot(curve)
-        self.assertEqual(plot.state.plot_height, None)
-        self.assertEqual(plot.state.plot_width, None)
+        self.assertEqual(plot.state.height, None)
+        self.assertEqual(plot.state.width, None)
         self.assertEqual(plot.state.frame_height, 400)
         self.assertEqual(plot.state.frame_width, 400)
         self.assertEqual(plot.state.aspect_scale, 2)
@@ -576,8 +583,8 @@ class TestElementPlot(LoggingComparisonTestCase, TestBokehPlot):
         curve = Curve([0, 2, 3]).opts(data_aspect=2, height=400, width=400)
         plot = bokeh_renderer.get_plot(curve)
         x_range, y_range = plot.handles['x_range'], plot.handles['y_range']
-        self.assertEqual(plot.state.plot_height, 400)
-        self.assertEqual(plot.state.plot_width, 400)
+        self.assertEqual(plot.state.height, 400)
+        self.assertEqual(plot.state.width, 400)
         self.assertEqual(plot.state.aspect_scale, 2)
         self.assertEqual(x_range.start, -2)
         self.assertEqual(x_range.end, 4)
@@ -587,8 +594,8 @@ class TestElementPlot(LoggingComparisonTestCase, TestBokehPlot):
     def test_element_data_aspect_frame_width(self):
         curve = Curve([1, 2, 3]).opts(data_aspect=2, frame_width=400)
         plot = bokeh_renderer.get_plot(curve)
-        self.assertEqual(plot.state.plot_height, None)
-        self.assertEqual(plot.state.plot_width, None)
+        self.assertEqual(plot.state.height, None)
+        self.assertEqual(plot.state.width, None)
         self.assertEqual(plot.state.frame_height, 800)
         self.assertEqual(plot.state.frame_width, 400)
         self.assertEqual(plot.state.aspect_scale, 2)
@@ -596,8 +603,8 @@ class TestElementPlot(LoggingComparisonTestCase, TestBokehPlot):
     def test_element_data_aspect_frame_height(self):
         curve = Curve([1, 2, 3]).opts(data_aspect=2, frame_height=400)
         plot = bokeh_renderer.get_plot(curve)
-        self.assertEqual(plot.state.plot_height, None)
-        self.assertEqual(plot.state.plot_width, None)
+        self.assertEqual(plot.state.height, None)
+        self.assertEqual(plot.state.width, None)
         self.assertEqual(plot.state.frame_height, 400)
         self.assertEqual(plot.state.frame_width, 200)
         self.assertEqual(plot.state.aspect_scale, 2)
@@ -605,8 +612,8 @@ class TestElementPlot(LoggingComparisonTestCase, TestBokehPlot):
     def test_element_data_aspect_frame_width_frame_height(self):
         curve = Curve([1, 2, 3]).opts(data_aspect=2, frame_height=400, frame_width=400)
         plot = bokeh_renderer.get_plot(curve)
-        self.assertEqual(plot.state.plot_height, None)
-        self.assertEqual(plot.state.plot_width, None)
+        self.assertEqual(plot.state.height, None)
+        self.assertEqual(plot.state.width, None)
         self.assertEqual(plot.state.frame_height, 400)
         self.assertEqual(plot.state.frame_width, 400)
         self.assertEqual(plot.state.aspect_scale, 2)
@@ -618,8 +625,8 @@ class TestElementPlot(LoggingComparisonTestCase, TestBokehPlot):
     def test_element_responsive(self):
         curve = Curve([1, 2, 3]).opts(responsive=True)
         plot = bokeh_renderer.get_plot(curve)
-        self.assertEqual(plot.state.plot_height, None)
-        self.assertEqual(plot.state.plot_width, None)
+        self.assertEqual(plot.state.height, None)
+        self.assertEqual(plot.state.width, None)
         self.assertEqual(plot.state.frame_height, None)
         self.assertEqual(plot.state.frame_width, None)
         self.assertEqual(plot.state.sizing_mode, 'stretch_both')
@@ -627,8 +634,8 @@ class TestElementPlot(LoggingComparisonTestCase, TestBokehPlot):
     def test_element_width_responsive(self):
         curve = Curve([1, 2, 3]).opts(width=400, responsive=True)
         plot = bokeh_renderer.get_plot(curve)
-        self.assertEqual(plot.state.plot_height, None)
-        self.assertEqual(plot.state.plot_width, 400)
+        self.assertEqual(plot.state.height, None)
+        self.assertEqual(plot.state.width, 400)
         self.assertEqual(plot.state.frame_height, None)
         self.assertEqual(plot.state.frame_width, None)
         self.assertEqual(plot.state.sizing_mode, 'stretch_height')
@@ -636,8 +643,8 @@ class TestElementPlot(LoggingComparisonTestCase, TestBokehPlot):
     def test_element_height_responsive(self):
         curve = Curve([1, 2, 3]).opts(height=400, responsive=True)
         plot = bokeh_renderer.get_plot(curve)
-        self.assertEqual(plot.state.plot_height, 400)
-        self.assertEqual(plot.state.plot_width, None)
+        self.assertEqual(plot.state.height, 400)
+        self.assertEqual(plot.state.width, None)
         self.assertEqual(plot.state.frame_height, None)
         self.assertEqual(plot.state.frame_width, None)
         self.assertEqual(plot.state.sizing_mode, 'stretch_width')
@@ -645,8 +652,8 @@ class TestElementPlot(LoggingComparisonTestCase, TestBokehPlot):
     def test_element_frame_width_responsive(self):
         curve = Curve([1, 2, 3]).opts(frame_width=400, responsive=True)
         plot = bokeh_renderer.get_plot(curve)
-        self.assertEqual(plot.state.plot_height, None)
-        self.assertEqual(plot.state.plot_width, None)
+        self.assertEqual(plot.state.height, None)
+        self.assertEqual(plot.state.width, None)
         self.assertEqual(plot.state.frame_height, None)
         self.assertEqual(plot.state.frame_width, 400)
         self.assertEqual(plot.state.sizing_mode, 'stretch_height')
@@ -654,8 +661,8 @@ class TestElementPlot(LoggingComparisonTestCase, TestBokehPlot):
     def test_element_frame_height_responsive(self):
         curve = Curve([1, 2, 3]).opts(frame_height=400, responsive=True)
         plot = bokeh_renderer.get_plot(curve)
-        self.assertEqual(plot.state.plot_height, None)
-        self.assertEqual(plot.state.plot_width, None)
+        self.assertEqual(plot.state.height, None)
+        self.assertEqual(plot.state.width, None)
         self.assertEqual(plot.state.frame_height, 400)
         self.assertEqual(plot.state.frame_width, None)
         self.assertEqual(plot.state.sizing_mode, 'stretch_width')
@@ -663,8 +670,8 @@ class TestElementPlot(LoggingComparisonTestCase, TestBokehPlot):
     def test_element_aspect_responsive(self):
         curve = Curve([1, 2, 3]).opts(aspect=2, responsive=True)
         plot = bokeh_renderer.get_plot(curve)
-        self.assertEqual(plot.state.plot_height, None)
-        self.assertEqual(plot.state.plot_width, None)
+        self.assertEqual(plot.state.height, None)
+        self.assertEqual(plot.state.width, None)
         self.assertEqual(plot.state.frame_height, None)
         self.assertEqual(plot.state.frame_width, None)
         self.assertEqual(plot.state.sizing_mode, 'scale_both')
@@ -673,8 +680,8 @@ class TestElementPlot(LoggingComparisonTestCase, TestBokehPlot):
         curve = Curve([1, 2, 3]).opts(aspect=2, width=400, responsive=True)
         plot = bokeh_renderer.get_plot(curve)
         self.log_handler.assertContains('WARNING', "responsive mode could not be enabled")
-        self.assertEqual(plot.state.plot_height, None)
-        self.assertEqual(plot.state.plot_width, None)
+        self.assertEqual(plot.state.height, None)
+        self.assertEqual(plot.state.width, None)
         self.assertEqual(plot.state.frame_height, 200)
         self.assertEqual(plot.state.frame_width, 400)
         self.assertEqual(plot.state.sizing_mode, 'fixed')
@@ -686,16 +693,16 @@ class TestElementPlot(LoggingComparisonTestCase, TestBokehPlot):
         self.assertEqual(plot.state.frame_height, 400)
         self.assertEqual(plot.state.frame_width, 800)
         self.log_handler.assertContains('WARNING', "responsive mode could not be enabled")
-        self.assertEqual(plot.state.plot_height, None)
-        self.assertEqual(plot.state.plot_width, None)
+        self.assertEqual(plot.state.height, None)
+        self.assertEqual(plot.state.width, None)
         self.assertEqual(plot.state.sizing_mode, 'fixed')
         self.log_handler.assertContains('WARNING', "uses those values as frame_width/frame_height instead")
 
     def test_element_width_height_responsive(self):
         curve = Curve([1, 2, 3]).opts(height=400, width=400, responsive=True)
         plot = bokeh_renderer.get_plot(curve)
-        self.assertEqual(plot.state.plot_height, 400)
-        self.assertEqual(plot.state.plot_width, 400)
+        self.assertEqual(plot.state.height, 400)
+        self.assertEqual(plot.state.width, 400)
         self.log_handler.assertContains('WARNING', "responsive mode could not be enabled")
         self.assertEqual(plot.state.frame_height, None)
         self.assertEqual(plot.state.frame_width, None)
@@ -705,8 +712,8 @@ class TestElementPlot(LoggingComparisonTestCase, TestBokehPlot):
         curve = Curve([1, 2, 3]).opts(aspect=2, frame_width=400, responsive=True)
         plot = bokeh_renderer.get_plot(curve)
         self.log_handler.assertContains('WARNING', "responsive mode could not be enabled")
-        self.assertEqual(plot.state.plot_height, None)
-        self.assertEqual(plot.state.plot_width, None)
+        self.assertEqual(plot.state.height, None)
+        self.assertEqual(plot.state.width, None)
         self.assertEqual(plot.state.frame_height, 200)
         self.assertEqual(plot.state.frame_width, 400)
         self.assertEqual(plot.state.sizing_mode, 'fixed')
diff --git a/holoviews/tests/plotting/bokeh/test_errorbarplot.py b/holoviews/tests/plotting/bokeh/test_errorbarplot.py
index d9f43802c..b108b5d75 100644
--- a/holoviews/tests/plotting/bokeh/test_errorbarplot.py
+++ b/holoviews/tests/plotting/bokeh/test_errorbarplot.py
@@ -3,6 +3,7 @@ import numpy as np
 from bokeh.models import CategoricalColorMapper, LinearColorMapper
 
 from holoviews.element import ErrorBars
+from holoviews.plotting.bokeh.util import property_to_dict
 
 from .test_plot import TestBokehPlot, bokeh_renderer
 
@@ -74,7 +75,7 @@ class TestErrorBarsPlot(TestBokehPlot):
         cds = plot.handles['cds']
         glyph = plot.handles['glyph']
         self.assertEqual(cds.data['color'], np.array(['#000', '#F00', '#0F0']))
-        self.assertEqual(glyph.line_color, {'field': 'color'})
+        self.assertEqual(property_to_dict(glyph.line_color), {'field': 'color'})
 
     def test_errorbars_linear_color_op(self):
         errorbars = ErrorBars([(0, 0, 0.1, 0.2, 0), (0, 1, 0.2, 0.4, 1), (0, 2, 0.6, 1.2, 2)],
@@ -87,7 +88,7 @@ class TestErrorBarsPlot(TestBokehPlot):
         self.assertEqual(cmapper.low, 0)
         self.assertEqual(cmapper.high, 2)
         self.assertEqual(cds.data['color'], np.array([0, 1, 2]))
-        self.assertEqual(glyph.line_color, {'field': 'color', 'transform': cmapper})
+        self.assertEqual(property_to_dict(glyph.line_color), {'field': 'color', 'transform': cmapper})
 
     def test_errorbars_categorical_color_op(self):
         errorbars = ErrorBars([(0, 0, 0.1, 0.2, 'A'), (0, 1, 0.2, 0.4, 'B'), (0, 2, 0.6, 1.2, 'C')],
@@ -99,7 +100,7 @@ class TestErrorBarsPlot(TestBokehPlot):
         self.assertTrue(cmapper, CategoricalColorMapper)
         self.assertEqual(cmapper.factors, ['A', 'B', 'C'])
         self.assertEqual(cds.data['color'], np.array(['A', 'B', 'C']))
-        self.assertEqual(glyph.line_color, {'field': 'color', 'transform': cmapper})
+        self.assertEqual(property_to_dict(glyph.line_color), {'field': 'color', 'transform': cmapper})
 
     def test_errorbars_line_color_op(self):
         errorbars = ErrorBars([(0, 0, 0.1, 0.2, '#000'), (0, 1, 0.2, 0.4, '#F00'), (0, 2, 0.6, 1.2, '#0F0')],
@@ -108,7 +109,7 @@ class TestErrorBarsPlot(TestBokehPlot):
         cds = plot.handles['cds']
         glyph = plot.handles['glyph']
         self.assertEqual(cds.data['line_color'], np.array(['#000', '#F00', '#0F0']))
-        self.assertEqual(glyph.line_color, {'field': 'line_color'})
+        self.assertEqual(property_to_dict(glyph.line_color), {'field': 'line_color'})
 
     def test_errorbars_alpha_op(self):
         errorbars = ErrorBars([(0, 0, 0.1, 0.2, 0), (0, 1, 0.2, 0.4, 0.2), (0, 2, 0.6, 1.2, 0.7)],
@@ -117,7 +118,7 @@ class TestErrorBarsPlot(TestBokehPlot):
         cds = plot.handles['cds']
         glyph = plot.handles['glyph']
         self.assertEqual(cds.data['alpha'], np.array([0, 0.2, 0.7]))
-        self.assertEqual(glyph.line_alpha, {'field': 'alpha'})
+        self.assertEqual(property_to_dict(glyph.line_alpha), {'field': 'alpha'})
 
     def test_errorbars_line_alpha_op(self):
         errorbars = ErrorBars([(0, 0, 0.1, 0.2, 0), (0, 1, 0.2, 0.4, 0.2), (0, 2, 0.6, 1.2, 0.7)],
@@ -126,7 +127,7 @@ class TestErrorBarsPlot(TestBokehPlot):
         cds = plot.handles['cds']
         glyph = plot.handles['glyph']
         self.assertEqual(cds.data['line_alpha'], np.array([0, 0.2, 0.7]))
-        self.assertEqual(glyph.line_alpha, {'field': 'line_alpha'})
+        self.assertEqual(property_to_dict(glyph.line_alpha), {'field': 'line_alpha'})
 
     def test_errorbars_line_width_op(self):
         errorbars = ErrorBars([(0, 0, 0.1, 0.2, 1), (0, 1, 0.2, 0.4, 4), (0, 2, 0.6, 1.2, 8)],
@@ -135,4 +136,4 @@ class TestErrorBarsPlot(TestBokehPlot):
         cds = plot.handles['cds']
         glyph = plot.handles['glyph']
         self.assertEqual(cds.data['line_width'], np.array([1, 4, 8]))
-        self.assertEqual(glyph.line_width, {'field': 'line_width'})
+        self.assertEqual(property_to_dict(glyph.line_width), {'field': 'line_width'})
diff --git a/holoviews/tests/plotting/bokeh/test_graphplot.py b/holoviews/tests/plotting/bokeh/test_graphplot.py
index 1f7161258..ec540936b 100644
--- a/holoviews/tests/plotting/bokeh/test_graphplot.py
+++ b/holoviews/tests/plotting/bokeh/test_graphplot.py
@@ -3,6 +3,7 @@ import numpy as np
 from holoviews.core.data import Dataset
 from holoviews.element import Graph, Nodes, TriMesh, Chord, VLine, circular_layout
 from holoviews.util.transform import dim
+from holoviews.plotting.bokeh.util import property_to_dict, bokeh3
 
 from bokeh.models import (NodesAndLinkedEdges, EdgesAndLinkedNodes, NodesOnly, Patches)
 from bokeh.models.mappers import CategoricalColorMapper, LinearColorMapper
@@ -35,7 +36,11 @@ class TestBokehGraphPlot(TestBokehPlot):
         self.assertEqual(node_source.data['index'], self.source)
         self.assertEqual(edge_source.data['start'], self.source)
         self.assertEqual(edge_source.data['end'], self.target)
-        layout = {str(int(z)): (x, y) for x, y, z in self.graph.nodes.array()}
+        if bokeh3:
+            layout = {z: (x, y) for x, y, z in self.graph.nodes.array()}
+        else:
+            layout = {str(z): (x, y) for x, y, z in self.graph.nodes.array()}
+
         self.assertEqual(layout_source.graph_layout, layout)
 
     def test_plot_graph_annotation_overlay(self):
@@ -59,7 +64,10 @@ class TestBokehGraphPlot(TestBokehPlot):
         edges = graph.edgepaths.split()
         self.assertEqual(edge_source.data['xs'], [path.dimension_values(0) for path in edges])
         self.assertEqual(edge_source.data['ys'], [path.dimension_values(1) for path in edges])
-        layout = {str(int(z)): (x, y) for x, y, z in self.graph.nodes.array()}
+        if bokeh3:
+            layout = {z: (x, y) for x, y, z in self.graph.nodes.array()}
+        else:
+            layout = {str(z): (x, y) for x, y, z in self.graph.nodes.array()}
         self.assertEqual(layout_source.graph_layout, layout)
 
     def test_graph_inspection_policy_nodes(self):
@@ -120,7 +128,7 @@ class TestBokehGraphPlot(TestBokehPlot):
         self.assertIsInstance(cmapper, CategoricalColorMapper)
         self.assertEqual(cmapper.factors, ['Output', 'Input'])
         self.assertEqual(node_source.data['Label'], self.node_info['Label'])
-        self.assertEqual(glyph.fill_color, {'field': 'Label', 'transform': cmapper})
+        self.assertEqual(property_to_dict(glyph.fill_color), {'field': 'Label', 'transform': cmapper})
 
     def test_graph_nodes_numerically_colormapped(self):
         g = self.graph3.opts(color_index='Weight', cmap='viridis')
@@ -132,7 +140,7 @@ class TestBokehGraphPlot(TestBokehPlot):
         self.assertEqual(cmapper.low, self.weights.min())
         self.assertEqual(cmapper.high, self.weights.max())
         self.assertEqual(node_source.data['Weight'], self.node_info2['Weight'])
-        self.assertEqual(glyph.fill_color, {'field': 'Weight', 'transform': cmapper})
+        self.assertEqual(property_to_dict(glyph.fill_color), {'field': 'Weight', 'transform': cmapper})
 
     def test_graph_edges_categorical_colormapped(self):
         g = self.graph3.opts(edge_color_index='start', edge_cmap=['#FFFFFF', '#000000'])
@@ -144,7 +152,7 @@ class TestBokehGraphPlot(TestBokehPlot):
         factors = ['0', '1', '2', '3', '4', '5', '6', '7']
         self.assertEqual(cmapper.factors, factors)
         self.assertEqual(edge_source.data['start_str__'], factors)
-        self.assertEqual(glyph.line_color, {'field': 'start_str__', 'transform': cmapper})
+        self.assertEqual(property_to_dict(glyph.line_color), {'field': 'start_str__', 'transform': cmapper})
 
     def test_graph_edges_numerically_colormapped(self):
         g = self.graph4.opts(edge_color_index='Weight', edge_cmap=['#FFFFFF', '#000000'])
@@ -156,7 +164,7 @@ class TestBokehGraphPlot(TestBokehPlot):
         self.assertEqual(cmapper.low, self.weights.min())
         self.assertEqual(cmapper.high, self.weights.max())
         self.assertEqual(edge_source.data['Weight'], self.node_info2['Weight'])
-        self.assertEqual(glyph.line_color, {'field': 'Weight', 'transform': cmapper})
+        self.assertEqual(property_to_dict(glyph.line_color), {'field': 'Weight', 'transform': cmapper})
 
     ###########################
     #    Styling mapping      #
@@ -170,7 +178,7 @@ class TestBokehGraphPlot(TestBokehPlot):
         plot = bokeh_renderer.get_plot(graph)
         cds = plot.handles['scatter_1_source']
         glyph = plot.handles['scatter_1_glyph']
-        self.assertEqual(glyph.fill_color, {'field': 'node_color'})
+        self.assertEqual(property_to_dict(glyph.fill_color), {'field': 'node_color'})
         self.assertEqual(glyph.line_color, 'black')
         self.assertEqual(cds.data['node_color'], np.array(['red', 'green', 'blue']))
 
@@ -183,7 +191,7 @@ class TestBokehGraphPlot(TestBokehPlot):
         cds = plot.handles['scatter_1_source']
         glyph = plot.handles['scatter_1_glyph']
         cmapper = plot.handles['node_color_color_mapper']
-        self.assertEqual(glyph.fill_color, {'field': 'node_color', 'transform': cmapper})
+        self.assertEqual(property_to_dict(glyph.fill_color), {'field': 'node_color', 'transform': cmapper})
         self.assertEqual(glyph.line_color, 'black')
         self.assertEqual(cds.data['node_color'], np.array([0.5, 1.5, 2.5]))
 
@@ -205,7 +213,7 @@ class TestBokehGraphPlot(TestBokehPlot):
         cds = plot.handles['scatter_1_source']
         glyph = plot.handles['scatter_1_glyph']
         cmapper = plot.handles['node_color_color_mapper']
-        self.assertEqual(glyph.fill_color, {'field': 'node_color', 'transform': cmapper})
+        self.assertEqual(property_to_dict(glyph.fill_color), {'field': 'node_color', 'transform': cmapper})
         self.assertEqual(glyph.line_color, 'black')
         self.assertEqual(cds.data['node_color'], np.array(['A', 'B', 'C']))
 
@@ -217,7 +225,7 @@ class TestBokehGraphPlot(TestBokehPlot):
         plot = bokeh_renderer.get_plot(graph)
         cds = plot.handles['scatter_1_source']
         glyph = plot.handles['scatter_1_glyph']
-        self.assertEqual(glyph.size, {'field': 'node_size'})
+        self.assertEqual(property_to_dict(glyph.size), {'field': 'node_size'})
         self.assertEqual(cds.data['node_size'], np.array([2, 4, 6]))
 
     def test_graph_op_node_alpha(self):
@@ -227,8 +235,8 @@ class TestBokehGraphPlot(TestBokehPlot):
         plot = bokeh_renderer.get_plot(graph)
         cds = plot.handles['scatter_1_source']
         glyph = plot.handles['scatter_1_glyph']
-        self.assertEqual(glyph.fill_alpha, {'field': 'node_alpha'})
-        self.assertEqual(glyph.line_alpha, {'field': 'node_alpha'})
+        self.assertEqual(property_to_dict(glyph.fill_alpha), {'field': 'node_alpha'})
+        self.assertEqual(property_to_dict(glyph.line_alpha), {'field': 'node_alpha'})
         self.assertEqual(cds.data['node_alpha'], np.array([0.2, 0.6, 1]))
 
     def test_graph_op_node_line_width(self):
@@ -238,7 +246,7 @@ class TestBokehGraphPlot(TestBokehPlot):
         plot = bokeh_renderer.get_plot(graph)
         cds = plot.handles['scatter_1_source']
         glyph = plot.handles['scatter_1_glyph']
-        self.assertEqual(glyph.line_width, {'field': 'node_line_width'})
+        self.assertEqual(property_to_dict(glyph.line_width), {'field': 'node_line_width'})
         self.assertEqual(cds.data['node_line_width'], np.array([2, 4, 6]))
 
     def test_graph_op_edge_color(self):
@@ -247,7 +255,7 @@ class TestBokehGraphPlot(TestBokehPlot):
         plot = bokeh_renderer.get_plot(graph)
         cds = plot.handles['multi_line_1_source']
         glyph = plot.handles['multi_line_1_glyph']
-        self.assertEqual(glyph.line_color, {'field': 'edge_color'})
+        self.assertEqual(property_to_dict(glyph.line_color), {'field': 'edge_color'})
         self.assertEqual(cds.data['edge_color'], np.array(['red', 'green', 'blue']))
 
     def test_graph_op_edge_color_linear(self):
@@ -257,7 +265,7 @@ class TestBokehGraphPlot(TestBokehPlot):
         cds = plot.handles['multi_line_1_source']
         glyph = plot.handles['multi_line_1_glyph']
         cmapper = plot.handles['edge_color_color_mapper']
-        self.assertEqual(glyph.line_color, {'field': 'edge_color', 'transform': cmapper})
+        self.assertEqual(property_to_dict(glyph.line_color), {'field': 'edge_color', 'transform': cmapper})
         self.assertEqual(cds.data['edge_color'], np.array([2, 0.5, 3]))
 
     def test_graph_op_edge_color_colorbar(self):
@@ -274,7 +282,7 @@ class TestBokehGraphPlot(TestBokehPlot):
         cds = plot.handles['multi_line_1_source']
         glyph = plot.handles['multi_line_1_glyph']
         cmapper = plot.handles['edge_color_color_mapper']
-        self.assertEqual(glyph.line_color, {'field': 'edge_color', 'transform': cmapper})
+        self.assertEqual(property_to_dict(glyph.line_color), {'field': 'edge_color', 'transform': cmapper})
         self.assertEqual(cds.data['edge_color'], np.array(['C', 'B', 'A']))
 
     def test_graph_op_edge_alpha(self):
@@ -283,7 +291,7 @@ class TestBokehGraphPlot(TestBokehPlot):
         plot = bokeh_renderer.get_plot(graph)
         cds = plot.handles['multi_line_1_source']
         glyph = plot.handles['multi_line_1_glyph']
-        self.assertEqual(glyph.line_alpha, {'field': 'edge_alpha'})
+        self.assertEqual(property_to_dict(glyph.line_alpha), {'field': 'edge_alpha'})
         self.assertEqual(cds.data['edge_alpha'], np.array([0.1, 0.5, 0.3]))
 
     def test_graph_op_edge_line_width(self):
@@ -292,7 +300,7 @@ class TestBokehGraphPlot(TestBokehPlot):
         plot = bokeh_renderer.get_plot(graph)
         cds = plot.handles['multi_line_1_source']
         glyph = plot.handles['multi_line_1_glyph']
-        self.assertEqual(glyph.line_width, {'field': 'edge_line_width'})
+        self.assertEqual(property_to_dict(glyph.line_width), {'field': 'edge_line_width'})
         self.assertEqual(cds.data['edge_line_width'], np.array([2, 10, 6]))
 
 
@@ -314,7 +322,10 @@ class TestBokehTriMeshPlot(TestBokehPlot):
         self.assertEqual(node_source.data['index'], np.arange(4))
         self.assertEqual(edge_source.data['start'], np.arange(2))
         self.assertEqual(edge_source.data['end'], np.arange(1, 3))
-        layout = {str(int(z)): (x, y) for x, y, z in self.trimesh.nodes.array()}
+        if bokeh3:
+            layout = {z: (x, y) for x, y, z in self.trimesh.nodes.array()}
+        else:
+            layout = {str(z): (x, y) for x, y, z in self.trimesh.nodes.array()}
         self.assertEqual(layout_source.graph_layout, layout)
 
     def test_plot_simple_trimesh_filled(self):
@@ -326,7 +337,10 @@ class TestBokehTriMeshPlot(TestBokehPlot):
         self.assertEqual(node_source.data['index'], np.arange(4))
         self.assertEqual(edge_source.data['start'], np.arange(2))
         self.assertEqual(edge_source.data['end'], np.arange(1, 3))
-        layout = {str(int(z)): (x, y) for x, y, z in self.trimesh.nodes.array()}
+        if bokeh3:
+            layout = {z: (x, y) for x, y, z in self.trimesh.nodes.array()}
+        else:
+            layout = {str(z): (x, y) for x, y, z in self.trimesh.nodes.array()}
         self.assertEqual(layout_source.graph_layout, layout)
 
     def test_trimesh_edges_categorical_colormapped(self):
@@ -341,7 +355,7 @@ class TestBokehTriMeshPlot(TestBokehPlot):
         factors = ['0', '1', '2', '3']
         self.assertEqual(cmapper.factors, factors)
         self.assertEqual(edge_source.data['node1_str__'], ['0', '1'])
-        self.assertEqual(glyph.line_color, {'field': 'node1_str__', 'transform': cmapper})
+        self.assertEqual(property_to_dict(glyph.line_color), {'field': 'node1_str__', 'transform': cmapper})
 
     def test_trimesh_nodes_numerically_colormapped(self):
         g = self.trimesh_weighted.opts(
@@ -355,7 +369,7 @@ class TestBokehTriMeshPlot(TestBokehPlot):
         self.assertEqual(cmapper.low, 0)
         self.assertEqual(cmapper.high, 1)
         self.assertEqual(edge_source.data['weight'], np.array([0, 1]))
-        self.assertEqual(glyph.line_color, {'field': 'weight', 'transform': cmapper})
+        self.assertEqual(property_to_dict(glyph.line_color), {'field': 'weight', 'transform': cmapper})
 
     ###########################
     #    Styling mapping      #
@@ -368,7 +382,7 @@ class TestBokehTriMeshPlot(TestBokehPlot):
         plot = bokeh_renderer.get_plot(trimesh)
         cds = plot.handles['scatter_1_source']
         glyph = plot.handles['scatter_1_glyph']
-        self.assertEqual(glyph.fill_color, {'field': 'node_color'})
+        self.assertEqual(property_to_dict(glyph.fill_color), {'field': 'node_color'})
         self.assertEqual(glyph.line_color, 'black')
         self.assertEqual(cds.data['node_color'], np.array(['red', 'green', 'blue', 'black']))
 
@@ -380,7 +394,7 @@ class TestBokehTriMeshPlot(TestBokehPlot):
         cds = plot.handles['scatter_1_source']
         glyph = plot.handles['scatter_1_glyph']
         cmapper = plot.handles['node_color_color_mapper']
-        self.assertEqual(glyph.fill_color, {'field': 'node_color', 'transform': cmapper})
+        self.assertEqual(property_to_dict(glyph.fill_color), {'field': 'node_color', 'transform': cmapper})
         self.assertEqual(glyph.line_color, 'black')
         self.assertEqual(cds.data['node_color'], np.array([2, 1, 3, 4]))
         self.assertEqual(cmapper.low, 1)
@@ -394,7 +408,7 @@ class TestBokehTriMeshPlot(TestBokehPlot):
         cds = plot.handles['scatter_1_source']
         glyph = plot.handles['scatter_1_glyph']
         cmapper = plot.handles['node_color_color_mapper']
-        self.assertEqual(glyph.fill_color, {'field': 'node_color', 'transform': cmapper})
+        self.assertEqual(property_to_dict(glyph.fill_color), {'field': 'node_color', 'transform': cmapper})
         self.assertEqual(glyph.line_color, 'black')
         self.assertEqual(cds.data['node_color'], np.array(['B', 'C', 'A', 'B']))
 
@@ -405,7 +419,7 @@ class TestBokehTriMeshPlot(TestBokehPlot):
         plot = bokeh_renderer.get_plot(trimesh)
         cds = plot.handles['scatter_1_source']
         glyph = plot.handles['scatter_1_glyph']
-        self.assertEqual(glyph.size, {'field': 'node_size'})
+        self.assertEqual(property_to_dict(glyph.size), {'field': 'node_size'})
         self.assertEqual(cds.data['node_size'], np.array([3, 2, 8, 4]))
 
     def test_trimesh_op_node_alpha(self):
@@ -415,8 +429,8 @@ class TestBokehTriMeshPlot(TestBokehPlot):
         plot = bokeh_renderer.get_plot(trimesh)
         cds = plot.handles['scatter_1_source']
         glyph = plot.handles['scatter_1_glyph']
-        self.assertEqual(glyph.fill_alpha, {'field': 'node_alpha'})
-        self.assertEqual(glyph.line_alpha, {'field': 'node_alpha'})
+        self.assertEqual(property_to_dict(glyph.fill_alpha), {'field': 'node_alpha'})
+        self.assertEqual(property_to_dict(glyph.line_alpha), {'field': 'node_alpha'})
         self.assertEqual(cds.data['node_alpha'], np.array([0.2, 0.6, 1, 0.3]))
 
     def test_trimesh_op_node_line_width(self):
@@ -426,7 +440,7 @@ class TestBokehTriMeshPlot(TestBokehPlot):
         plot = bokeh_renderer.get_plot(trimesh)
         cds = plot.handles['scatter_1_source']
         glyph = plot.handles['scatter_1_glyph']
-        self.assertEqual(glyph.line_width, {'field': 'node_line_width'})
+        self.assertEqual(property_to_dict(glyph.line_width), {'field': 'node_line_width'})
         self.assertEqual(cds.data['node_line_width'], np.array([0.2, 0.6, 1, 0.3]))
 
     def test_trimesh_op_edge_color_linear_mean_node(self):
@@ -437,7 +451,7 @@ class TestBokehTriMeshPlot(TestBokehPlot):
         cds = plot.handles['multi_line_1_source']
         glyph = plot.handles['multi_line_1_glyph']
         cmapper = plot.handles['edge_color_color_mapper']
-        self.assertEqual(glyph.line_color, {'field': 'edge_color', 'transform': cmapper})
+        self.assertEqual(property_to_dict(glyph.line_color), {'field': 'edge_color', 'transform': cmapper})
         self.assertEqual(cds.data['edge_color'], np.array([2, 8/3.]))
         self.assertEqual(cmapper.low, 1)
         self.assertEqual(cmapper.high, 4)
@@ -449,7 +463,7 @@ class TestBokehTriMeshPlot(TestBokehPlot):
         plot = bokeh_renderer.get_plot(trimesh)
         cds = plot.handles['multi_line_1_source']
         glyph = plot.handles['multi_line_1_glyph']
-        self.assertEqual(glyph.line_color, {'field': 'edge_color'})
+        self.assertEqual(property_to_dict(glyph.line_color), {'field': 'edge_color'})
         self.assertEqual(cds.data['edge_color'], np.array(['red', 'blue']))
 
     def test_trimesh_op_edge_color_linear(self):
@@ -460,7 +474,7 @@ class TestBokehTriMeshPlot(TestBokehPlot):
         cds = plot.handles['multi_line_1_source']
         glyph = plot.handles['multi_line_1_glyph']
         cmapper = plot.handles['edge_color_color_mapper']
-        self.assertEqual(glyph.line_color, {'field': 'edge_color', 'transform': cmapper})
+        self.assertEqual(property_to_dict(glyph.line_color), {'field': 'edge_color', 'transform': cmapper})
         self.assertEqual(cds.data['edge_color'], np.array([2.4, 3.6]))
         self.assertEqual(cmapper.low, 2.4)
         self.assertEqual(cmapper.high, 3.6)
@@ -473,7 +487,7 @@ class TestBokehTriMeshPlot(TestBokehPlot):
         cds = plot.handles['patches_1_source']
         glyph = plot.handles['patches_1_glyph']
         cmapper = plot.handles['edge_color_color_mapper']
-        self.assertEqual(glyph.fill_color, {'field': 'edge_color', 'transform': cmapper})
+        self.assertEqual(property_to_dict(glyph.fill_color), {'field': 'edge_color', 'transform': cmapper})
         self.assertEqual(glyph.line_color, 'black')
         self.assertEqual(cds.data['edge_color'], np.array([2.4, 3.6]))
         self.assertEqual(cmapper.low, 2.4)
@@ -487,7 +501,7 @@ class TestBokehTriMeshPlot(TestBokehPlot):
         cds = plot.handles['multi_line_1_source']
         glyph = plot.handles['multi_line_1_glyph']
         cmapper = plot.handles['edge_color_color_mapper']
-        self.assertEqual(glyph.line_color, {'field': 'edge_color', 'transform': cmapper})
+        self.assertEqual(property_to_dict(glyph.line_color), {'field': 'edge_color', 'transform': cmapper})
         self.assertEqual(cds.data['edge_color'], np.array(['A', 'B']))
         self.assertEqual(cmapper.factors, ['A', 'B'])
 
@@ -498,7 +512,7 @@ class TestBokehTriMeshPlot(TestBokehPlot):
         plot = bokeh_renderer.get_plot(trimesh)
         cds = plot.handles['multi_line_1_source']
         glyph = plot.handles['multi_line_1_glyph']
-        self.assertEqual(glyph.line_alpha, {'field': 'edge_alpha'})
+        self.assertEqual(property_to_dict(glyph.line_alpha), {'field': 'edge_alpha'})
         self.assertEqual(cds.data['edge_alpha'], np.array([0.7, 0.3]))
 
     def test_trimesh_op_edge_line_width(self):
@@ -508,7 +522,7 @@ class TestBokehTriMeshPlot(TestBokehPlot):
         plot = bokeh_renderer.get_plot(trimesh)
         cds = plot.handles['multi_line_1_source']
         glyph = plot.handles['multi_line_1_glyph']
-        self.assertEqual(glyph.line_width, {'field': 'edge_line_width'})
+        self.assertEqual(property_to_dict(glyph.line_width), {'field': 'edge_line_width'})
         self.assertEqual(cds.data['edge_line_width'], np.array([7, 3]))
 
 
@@ -561,7 +575,7 @@ class TestBokehChordPlot(TestBokehPlot):
         self.assertEqual(cmapper.palette, ['#FFFFFF', '#888888', '#000000'])
         self.assertEqual(source.data['Label'], np.array(['A', 'B', 'C']))
         self.assertEqual(arc_source.data['Label'], np.array(['A', 'B', 'C']))
-        self.assertEqual(glyph.fill_color, {'field': 'Label', 'transform': cmapper})
+        self.assertEqual(property_to_dict(glyph.fill_color), {'field': 'Label', 'transform': cmapper})
 
     def test_chord_nodes_style_map_node_color_colormapped(self):
         g = self.chord.opts(
@@ -578,8 +592,8 @@ class TestBokehChordPlot(TestBokehPlot):
         self.assertEqual(cmapper.palette, ['#FFFFFF', '#888888', '#000000'])
         self.assertEqual(source.data['Label'], np.array(['A', 'B', 'C']))
         self.assertEqual(arc_source.data['Label'], np.array(['A', 'B', 'C']))
-        self.assertEqual(glyph.fill_color, {'field': 'node_color', 'transform': cmapper})
-        self.assertEqual(arc_glyph.line_color, {'field': 'node_color', 'transform': cmapper})
+        self.assertEqual(property_to_dict(glyph.fill_color), {'field': 'node_color', 'transform': cmapper})
+        self.assertEqual(property_to_dict(arc_glyph.line_color), {'field': 'node_color', 'transform': cmapper})
 
     def test_chord_edges_categorically_colormapped(self):
         g = self.chord.opts(
@@ -593,7 +607,7 @@ class TestBokehChordPlot(TestBokehPlot):
         self.assertEqual(cmapper.palette, ['#FFFFFF', '#000000', '#FFFFFF'])
         self.assertEqual(cmapper.factors, ['0', '1', '2'])
         self.assertEqual(edge_source.data['start_str__'], ['0', '0', '1'])
-        self.assertEqual(glyph.line_color, {'field': 'start_str__', 'transform': cmapper})
+        self.assertEqual(property_to_dict(glyph.line_color), {'field': 'start_str__', 'transform': cmapper})
 
     def test_chord_edge_color_style_mapping(self):
         g = self.chord.opts(
@@ -607,4 +621,4 @@ class TestBokehChordPlot(TestBokehPlot):
         self.assertEqual(cmapper.palette, ['#FFFFFF', '#000000', '#FFFFFF'])
         self.assertEqual(cmapper.factors, ['0', '1', '2'])
         self.assertEqual(edge_source.data['edge_color'], np.array(['0', '0', '1']))
-        self.assertEqual(glyph.line_color, {'field': 'edge_color', 'transform': cmapper})
+        self.assertEqual(property_to_dict(glyph.line_color), {'field': 'edge_color', 'transform': cmapper})
diff --git a/holoviews/tests/plotting/bokeh/test_gridplot.py b/holoviews/tests/plotting/bokeh/test_gridplot.py
index 6c20060b5..30e21e911 100644
--- a/holoviews/tests/plotting/bokeh/test_gridplot.py
+++ b/holoviews/tests/plotting/bokeh/test_gridplot.py
@@ -5,11 +5,18 @@ from holoviews.core import (HoloMap, GridSpace, NdOverlay, Dataset,
 from holoviews.element import Curve, Image, Points
 from holoviews.operation import gridmatrix
 from holoviews.streams import Stream
+from holoviews.plotting.bokeh.util import bokeh3
 
 from .test_plot import TestBokehPlot, bokeh_renderer
 
 from bokeh.layouts import Column
-from bokeh.models import Div, ToolbarBox
+from bokeh.models import Div
+
+if bokeh3:
+    from bokeh.models import Toolbar
+else:
+    from bokeh.models import ToolbarBox as Toolbar  # Not completely correct
+
 
 
 class TestGridPlot(TestBokehPlot):
@@ -104,13 +111,17 @@ class TestGridPlot(TestBokehPlot):
         grid = GridSpace({0: Curve([]), 1: Points([])}, 'X').opts(toolbar='left')
         plot = bokeh_renderer.get_plot(grid)
         self.assertIsInstance(plot.state, Column)
-        self.assertIsInstance(plot.state.children[0].children[0], ToolbarBox)
+        if bokeh3:
+            self.assertIsInstance(plot.state.children[0].toolbar, Toolbar)
+        else:
+            self.assertIsInstance(plot.state.children[0].children[0], Toolbar)
+
 
     def test_grid_disable_toolbar(self):
         grid = GridSpace({0: Curve([]), 1: Points([])}, 'X').opts(toolbar=None)
         plot = bokeh_renderer.get_plot(grid)
         self.assertIsInstance(plot.state, Column)
-        self.assertEqual([p for p in plot.state.children if isinstance(p, ToolbarBox)], [])
+        self.assertEqual([p for p in plot.state.children if isinstance(p, Toolbar)], [])
 
     def test_grid_dimensioned_stream_title_update(self):
         stream = Stream.define('Test', test=0)()
diff --git a/holoviews/tests/plotting/bokeh/test_hextilesplot.py b/holoviews/tests/plotting/bokeh/test_hextilesplot.py
index 31ba9e810..71cd52a66 100644
--- a/holoviews/tests/plotting/bokeh/test_hextilesplot.py
+++ b/holoviews/tests/plotting/bokeh/test_hextilesplot.py
@@ -3,6 +3,7 @@ import numpy as np
 from holoviews.core import Dimension
 from holoviews.element import HexTiles
 from holoviews.plotting.bokeh.hex_tiles import hex_binning
+from holoviews.plotting.bokeh.util import property_to_dict
 
 from .test_plot import TestBokehPlot, bokeh_renderer
 
@@ -97,16 +98,16 @@ class TestHexTilesPlot(TestBokehPlot):
         hextiles = HexTiles(np.random.randn(1000, 2)).opts(line_width='Count')
         plot = bokeh_renderer.get_plot(hextiles)
         glyph = plot.handles['glyph']
-        self.assertEqual(glyph.line_width, {'field': 'line_width'})
+        self.assertEqual(property_to_dict(glyph.line_width), {'field': 'line_width'})
 
     def test_hex_tile_alpha_op(self):
         hextiles = HexTiles(np.random.randn(1000, 2)).opts(alpha='Count')
         plot = bokeh_renderer.get_plot(hextiles)
         glyph = plot.handles['glyph']
-        self.assertEqual(glyph.fill_alpha, {'field': 'alpha'})
+        self.assertEqual(property_to_dict(glyph.fill_alpha), {'field': 'alpha'})
 
     def test_hex_tile_scale_op(self):
         hextiles = HexTiles(np.random.randn(1000, 2)).opts(scale='Count')
         plot = bokeh_renderer.get_plot(hextiles)
         glyph = plot.handles['glyph']
-        self.assertEqual(glyph.scale, {'field': 'scale'})
+        self.assertEqual(property_to_dict(glyph.scale), {'field': 'scale'})
diff --git a/holoviews/tests/plotting/bokeh/test_histogramplot.py b/holoviews/tests/plotting/bokeh/test_histogramplot.py
index b61b3603c..ecb9cb093 100644
--- a/holoviews/tests/plotting/bokeh/test_histogramplot.py
+++ b/holoviews/tests/plotting/bokeh/test_histogramplot.py
@@ -5,6 +5,7 @@ import numpy as np
 from holoviews.core.overlay import NdOverlay
 from holoviews.element import Image, Points, Dataset, Histogram
 from holoviews.operation import histogram
+from holoviews.plotting.bokeh.util import property_to_dict
 
 from bokeh.models import DatetimeAxis, CategoricalColorMapper, LinearColorMapper
 
@@ -167,7 +168,7 @@ class TestSideHistogramPlot(LoggingComparisonTestCase, TestBokehPlot):
         cds = plot.handles['cds']
         glyph = plot.handles['glyph']
         self.assertEqual(cds.data['color'], np.array(['#000', '#F00', '#0F0']))
-        self.assertEqual(glyph.fill_color, {'field': 'color'})
+        self.assertEqual(property_to_dict(glyph.fill_color), {'field': 'color'})
         self.assertEqual(glyph.line_color, 'black')
 
     def test_histogram_linear_color_op(self):
@@ -181,7 +182,7 @@ class TestSideHistogramPlot(LoggingComparisonTestCase, TestBokehPlot):
         self.assertEqual(cmapper.low, 0)
         self.assertEqual(cmapper.high, 2)
         self.assertEqual(cds.data['color'], np.array([0, 1, 2]))
-        self.assertEqual(glyph.fill_color, {'field': 'color', 'transform': cmapper})
+        self.assertEqual(property_to_dict(glyph.fill_color), {'field': 'color', 'transform': cmapper})
         self.assertEqual(glyph.line_color, 'black')
 
     def test_histogram_categorical_color_op(self):
@@ -194,7 +195,7 @@ class TestSideHistogramPlot(LoggingComparisonTestCase, TestBokehPlot):
         self.assertTrue(cmapper, CategoricalColorMapper)
         self.assertEqual(cmapper.factors, ['A', 'B', 'C'])
         self.assertEqual(cds.data['color'], np.array(['A', 'B', 'C']))
-        self.assertEqual(glyph.fill_color, {'field': 'color', 'transform': cmapper})
+        self.assertEqual(property_to_dict(glyph.fill_color), {'field': 'color', 'transform': cmapper})
         self.assertEqual(glyph.line_color, 'black')
 
     def test_histogram_line_color_op(self):
@@ -204,8 +205,8 @@ class TestSideHistogramPlot(LoggingComparisonTestCase, TestBokehPlot):
         cds = plot.handles['cds']
         glyph = plot.handles['glyph']
         self.assertEqual(cds.data['line_color'], np.array(['#000', '#F00', '#0F0']))
-        self.assertNotEqual(glyph.fill_color, {'field': 'line_color'})
-        self.assertEqual(glyph.line_color, {'field': 'line_color'})
+        self.assertNotEqual(property_to_dict(glyph.fill_color), {'field': 'line_color'})
+        self.assertEqual(property_to_dict(glyph.line_color), {'field': 'line_color'})
 
     def test_histogram_fill_color_op(self):
         histogram = Histogram([(0, 0, '#000'), (0, 1, '#F00'), (0, 2, '#0F0')],
@@ -214,8 +215,8 @@ class TestSideHistogramPlot(LoggingComparisonTestCase, TestBokehPlot):
         cds = plot.handles['cds']
         glyph = plot.handles['glyph']
         self.assertEqual(cds.data['fill_color'], np.array(['#000', '#F00', '#0F0']))
-        self.assertEqual(glyph.fill_color, {'field': 'fill_color'})
-        self.assertNotEqual(glyph.line_color, {'field': 'fill_color'})
+        self.assertEqual(property_to_dict(glyph.fill_color), {'field': 'fill_color'})
+        self.assertNotEqual(property_to_dict(glyph.line_color), {'field': 'fill_color'})
 
     def test_histogram_alpha_op(self):
         histogram = Histogram([(0, 0, 0), (0, 1, 0.2), (0, 2, 0.7)],
@@ -224,7 +225,7 @@ class TestSideHistogramPlot(LoggingComparisonTestCase, TestBokehPlot):
         cds = plot.handles['cds']
         glyph = plot.handles['glyph']
         self.assertEqual(cds.data['alpha'], np.array([0, 0.2, 0.7]))
-        self.assertEqual(glyph.fill_alpha, {'field': 'alpha'})
+        self.assertEqual(property_to_dict(glyph.fill_alpha), {'field': 'alpha'})
 
     def test_histogram_line_alpha_op(self):
         histogram = Histogram([(0, 0, 0), (0, 1, 0.2), (0, 2, 0.7)],
@@ -233,8 +234,8 @@ class TestSideHistogramPlot(LoggingComparisonTestCase, TestBokehPlot):
         cds = plot.handles['cds']
         glyph = plot.handles['glyph']
         self.assertEqual(cds.data['line_alpha'], np.array([0, 0.2, 0.7]))
-        self.assertEqual(glyph.line_alpha, {'field': 'line_alpha'})
-        self.assertNotEqual(glyph.fill_alpha, {'field': 'line_alpha'})
+        self.assertEqual(property_to_dict(glyph.line_alpha), {'field': 'line_alpha'})
+        self.assertNotEqual(property_to_dict(glyph.fill_alpha), {'field': 'line_alpha'})
 
     def test_histogram_fill_alpha_op(self):
         histogram = Histogram([(0, 0, 0), (0, 1, 0.2), (0, 2, 0.7)],
@@ -243,8 +244,8 @@ class TestSideHistogramPlot(LoggingComparisonTestCase, TestBokehPlot):
         cds = plot.handles['cds']
         glyph = plot.handles['glyph']
         self.assertEqual(cds.data['fill_alpha'], np.array([0, 0.2, 0.7]))
-        self.assertNotEqual(glyph.line_alpha, {'field': 'fill_alpha'})
-        self.assertEqual(glyph.fill_alpha, {'field': 'fill_alpha'})
+        self.assertNotEqual(property_to_dict(glyph.line_alpha), {'field': 'fill_alpha'})
+        self.assertEqual(property_to_dict(glyph.fill_alpha), {'field': 'fill_alpha'})
 
     def test_histogram_line_width_op(self):
         histogram = Histogram([(0, 0, 1), (0, 1, 4), (0, 2, 8)],
@@ -253,7 +254,7 @@ class TestSideHistogramPlot(LoggingComparisonTestCase, TestBokehPlot):
         cds = plot.handles['cds']
         glyph = plot.handles['glyph']
         self.assertEqual(cds.data['line_width'], np.array([1, 4, 8]))
-        self.assertEqual(glyph.line_width, {'field': 'line_width'})
+        self.assertEqual(property_to_dict(glyph.line_width), {'field': 'line_width'})
 
     def test_op_ndoverlay_value(self):
         colors = ['blue', 'red']
diff --git a/holoviews/tests/plotting/bokeh/test_labels.py b/holoviews/tests/plotting/bokeh/test_labels.py
index 7dfdae686..565f4807c 100644
--- a/holoviews/tests/plotting/bokeh/test_labels.py
+++ b/holoviews/tests/plotting/bokeh/test_labels.py
@@ -2,6 +2,7 @@ import numpy as np
 
 from holoviews.core.dimension import Dimension
 from holoviews.element import Labels
+from holoviews.plotting.bokeh.util import property_to_dict
 
 from bokeh.models import LinearColorMapper, CategoricalColorMapper
 
@@ -76,7 +77,7 @@ class TestLabelsPlot(TestBokehPlot):
         self.assertEqual(glyph.x, 'x')
         self.assertEqual(glyph.y, 'y')
         self.assertEqual(glyph.text, 'Label')
-        self.assertEqual(glyph.text_color, {'field': 'text_color', 'transform': cmapper})
+        self.assertEqual(property_to_dict(glyph.text_color), {'field': 'text_color', 'transform': cmapper})
         self.assertEqual(cmapper.low, 0.33333)
         self.assertEqual(cmapper.high, 0.66666)
 
@@ -94,7 +95,7 @@ class TestLabelsPlot(TestBokehPlot):
         self.assertEqual(glyph.x, 'x')
         self.assertEqual(glyph.y, 'y')
         self.assertEqual(glyph.text, 'text')
-        self.assertEqual(glyph.text_color, {'field': 'color', 'transform': cmapper})
+        self.assertEqual(property_to_dict(glyph.text_color), {'field': 'color', 'transform': cmapper})
         self.assertEqual(cmapper.low, 1)
         self.assertEqual(cmapper.high, 2)
 
@@ -109,7 +110,7 @@ class TestLabelsPlot(TestBokehPlot):
         cds = plot.handles['cds']
         glyph = plot.handles['glyph']
         self.assertEqual(cds.data['text_color'], np.array(['#000', '#F00', '#0F0']))
-        self.assertEqual(glyph.text_color, {'field': 'text_color'})
+        self.assertEqual(property_to_dict(glyph.text_color), {'field': 'text_color'})
 
     def test_label_linear_color_op(self):
         labels = Labels([(0, 0, 0), (0, 1, 1), (0, 2, 2)],
@@ -122,7 +123,7 @@ class TestLabelsPlot(TestBokehPlot):
         self.assertEqual(cmapper.low, 0)
         self.assertEqual(cmapper.high, 2)
         self.assertEqual(cds.data['text_color'], np.array([0, 1, 2]))
-        self.assertEqual(glyph.text_color, {'field': 'text_color', 'transform': cmapper})
+        self.assertEqual(property_to_dict(glyph.text_color), {'field': 'text_color', 'transform': cmapper})
 
     def test_label_categorical_color_op(self):
         labels = Labels([(0, 0, 'A'), (0, 1, 'B'), (0, 2, 'C')],
@@ -134,7 +135,7 @@ class TestLabelsPlot(TestBokehPlot):
         self.assertTrue(cmapper, CategoricalColorMapper)
         self.assertEqual(cmapper.factors, ['A', 'B', 'C'])
         self.assertEqual(cds.data['text_color'], np.array(['A', 'B', 'C']))
-        self.assertEqual(glyph.text_color, {'field': 'text_color', 'transform': cmapper})
+        self.assertEqual(property_to_dict(glyph.text_color), {'field': 'text_color', 'transform': cmapper})
 
     def test_label_angle_op(self):
         labels = Labels([(0, 0, 0), (0, 1, 45), (0, 2, 90)],
@@ -143,7 +144,7 @@ class TestLabelsPlot(TestBokehPlot):
         cds = plot.handles['cds']
         glyph = plot.handles['glyph']
         self.assertEqual(cds.data['angle'], np.array([0, 0.785398, 1.570796]))
-        self.assertEqual(glyph.angle, {'field': 'angle'})
+        self.assertEqual(property_to_dict(glyph.angle), {'field': 'angle'})
 
     def test_label_alpha_op(self):
         labels = Labels([(0, 0, 0), (0, 1, 0.2), (0, 2, 0.7)],
@@ -152,7 +153,7 @@ class TestLabelsPlot(TestBokehPlot):
         cds = plot.handles['cds']
         glyph = plot.handles['glyph']
         self.assertEqual(cds.data['text_alpha'], np.array([0, 0.2, 0.7]))
-        self.assertEqual(glyph.text_alpha, {'field': 'text_alpha'})
+        self.assertEqual(property_to_dict(glyph.text_alpha), {'field': 'text_alpha'})
 
     def test_label_font_size_op_strings(self):
         labels = Labels([(0, 0, '10pt'), (0, 1, '4pt'), (0, 2, '8pt')],
@@ -161,7 +162,7 @@ class TestLabelsPlot(TestBokehPlot):
         cds = plot.handles['cds']
         glyph = plot.handles['glyph']
         self.assertEqual(cds.data['text_font_size'], np.array(['10pt', '4pt', '8pt']))
-        self.assertEqual(glyph.text_font_size, {'field': 'text_font_size'})
+        self.assertEqual(property_to_dict(glyph.text_font_size), {'field': 'text_font_size'})
 
     def test_label_font_size_op_ints(self):
         labels = Labels([(0, 0, 10), (0, 1, 4), (0, 2, 8)],
@@ -170,7 +171,7 @@ class TestLabelsPlot(TestBokehPlot):
         cds = plot.handles['cds']
         glyph = plot.handles['glyph']
         self.assertEqual(cds.data['text_font_size'], ['10pt', '4pt', '8pt'])
-        self.assertEqual(glyph.text_font_size, {'field': 'text_font_size'})
+        self.assertEqual(property_to_dict(glyph.text_font_size), {'field': 'text_font_size'})
 
     def test_labels_color_index_color_clash(self):
         labels = Labels([(0, 0, 0), (0, 1, 1), (0, 2, 2)],
diff --git a/holoviews/tests/plotting/bokeh/test_layoutplot.py b/holoviews/tests/plotting/bokeh/test_layoutplot.py
index 1602a67a5..173ad3e0b 100644
--- a/holoviews/tests/plotting/bokeh/test_layoutplot.py
+++ b/holoviews/tests/plotting/bokeh/test_layoutplot.py
@@ -1,6 +1,7 @@
 import datetime as dt
 import re
 
+import pytest
 import numpy as np
 
 from holoviews.core import (HoloMap, GridSpace, Layout, Empty, Dataset,
@@ -9,14 +10,23 @@ from holoviews.element import Curve, Image, Points, Histogram, Scatter
 from holoviews.streams import Stream
 from holoviews.util import render, opts
 from holoviews.util.transform import dim
+from holoviews.plotting.bokeh.util import bokeh3
 
-from bokeh.layouts import Column, Row
-from bokeh.models import Div, ToolbarBox, GlyphRenderer, Tabs, Panel, Spacer, GridBox, Title
-from bokeh.plotting import Figure
+from bokeh.models import Div, GlyphRenderer, Tabs, Spacer, Title, Row, Column
 
 from ...utils import LoggingComparisonTestCase
 from .test_plot import TestBokehPlot, bokeh_renderer
 
+if bokeh3:
+    from bokeh.models.layouts import TabPanel
+    from bokeh.plotting import figure
+    from bokeh.models import GridPlot
+    from bokeh.models import Toolbar
+else:
+    from bokeh.models.layouts import Panel as TabPanel
+    from bokeh.plotting import Figure as figure
+    from bokeh.models import ToolbarBox as Toolbar  # Not completely correct
+    from bokeh.models import GridBox as GridPlot  # Not completely correct
 
 
 class TestLayoutPlot(LoggingComparisonTestCase, TestBokehPlot):
@@ -162,7 +172,40 @@ class TestLayoutPlot(LoggingComparisonTestCase, TestBokehPlot):
                 'font-weight:bold;font-size:12pt">Default: 1</span>')
         self.assertEqual(title.text, text)
 
-    def test_layout_gridspaces(self):
+    @pytest.mark.skipif(not bokeh3, reason="Only work for Bokeh 3")
+    def test_layout_gridspaces_bokeh3(self):
+        layout = (GridSpace({(i, j): Curve(range(i+j)) for i in range(1, 3)
+                             for j in range(2,4)}) +
+                  GridSpace({(i, j): Curve(range(i+j)) for i in range(1, 3)
+                             for j in range(2,4)}) +
+                  Curve(range(10))).cols(2)
+        layout_plot = bokeh_renderer.get_plot(layout)
+        plot = layout_plot.state
+
+        self.assertIsInstance(plot, GridPlot)
+        self.assertEqual(len(plot.children), 3)
+        self.assertIsInstance(plot.toolbar, Toolbar)
+
+        (grid1, *_), (grid2, *_), (fig, *_) = plot.children
+        self.assertIsInstance(grid1, GridPlot)
+        self.assertIsInstance(grid2, GridPlot)
+        self.assertIsInstance(fig, figure)
+
+        self.assertEqual(len(grid1.children), 3)
+        _, (inner_grid1, *_), _ = grid1.children
+        self.assertIsInstance(inner_grid1, GridPlot)
+
+        self.assertEqual(len(grid2.children), 3)
+        _, (inner_grid2, *_), _ = grid2.children
+        self.assertIsInstance(inner_grid2, GridPlot)
+
+        for grid in [inner_grid1, inner_grid2]:
+            self.assertEqual(len(grid.children), 4)
+            for gfig, *_ in grid.children:
+                self.assertIsInstance(gfig, figure)
+
+    @pytest.mark.skipif(bokeh3, reason="Only work for Bokeh 2")
+    def test_layout_gridspaces_bokeh2(self):
         layout = (GridSpace({(i, j): Curve(range(i+j)) for i in range(1, 3)
                              for j in range(2,4)}) +
                   GridSpace({(i, j): Curve(range(i+j)) for i in range(1, 3)
@@ -175,10 +218,10 @@ class TestLayoutPlot(LoggingComparisonTestCase, TestBokehPlot):
         self.assertIsInstance(plot, Column)
         self.assertEqual(len(plot.children), 2)
         toolbar, grid = plot.children
-        self.assertIsInstance(toolbar, ToolbarBox)
-        self.assertIsInstance(grid, GridBox)
+        self.assertIsInstance(toolbar, Toolbar)
+        self.assertIsInstance(grid, GridPlot)
         self.assertEqual(len(grid.children), 3)
-        (col1, _, _), (col2, _, _), (fig, _, _) = grid.children
+        (col1, *_), (col2, *_), _ = grid.children
         self.assertIsInstance(col1, Column)
         self.assertIsInstance(col2, Column)
         grid1 = col1.children[0]
@@ -186,21 +229,19 @@ class TestLayoutPlot(LoggingComparisonTestCase, TestBokehPlot):
 
         # Check the row of GridSpaces
         self.assertEqual(len(grid1.children), 3)
-        _, (col1, _, _), _ = grid1.children
+        _, (col1, *_), _ = grid1.children
         self.assertIsInstance(col1, Column)
         inner_grid1 = col1.children[0]
 
         self.assertEqual(len(grid2.children), 3)
-        _, (col2, _, _), _ = grid2.children
+        _, (col2, *_), _ = grid2.children
         self.assertIsInstance(col2, Column)
         inner_grid2 = col2.children[0]
         for grid in [inner_grid1, inner_grid2]:
             self.assertEqual(len(grid.children), 4)
-            (gfig1, _, _), (gfig2, _, _), (gfig3, _, _), (gfig4, _, _) = grid.children
-            self.assertIsInstance(gfig1, Figure)
-            self.assertIsInstance(gfig2, Figure)
-            self.assertIsInstance(gfig3, Figure)
-            self.assertIsInstance(gfig4, Figure)
+            for gfig, *_ in grid.children:
+                self.assertIsInstance(gfig, figure)
+
 
     def test_layout_instantiate_subplots(self):
         layout = (Curve(range(10)) + Curve(range(10)) + Image(np.random.rand(10,10)) +
@@ -221,12 +262,17 @@ class TestLayoutPlot(LoggingComparisonTestCase, TestBokehPlot):
         plot = bokeh_renderer.get_plot(adjoint)
         adjoint_plot = plot.subplots[(0, 0)]
         self.assertEqual(len(adjoint_plot.subplots), 3)
-        grid = plot.state.children[1]
-        (f1, _, _), (f2, _, _), (s1, _, _) = grid.children
+        if bokeh3:
+            grid = plot.state
+        else:
+            grid = plot.state.children[1]
+        (f1, *_), (f2, *_), (s1, *_) = grid.children
+        self.assertIsInstance(grid, GridPlot)
         self.assertIsInstance(s1, Spacer)
         self.assertEqual(s1.width, 0)
         self.assertEqual(s1.height, 0)
-        self.assertEqual(f1.plot_height, f2.plot_height)
+        self.assertEqual(f1.height, f2.height)
+        self.assertEqual(f1.height, 300)
 
     def test_empty_adjoint_plot_with_renderer(self):
         # https://github.com/holoviz/holoviews/pull/5584
@@ -239,11 +285,15 @@ class TestLayoutPlot(LoggingComparisonTestCase, TestBokehPlot):
     def test_layout_plot_with_adjoints(self):
         layout = (Curve([]) + Curve([]).hist()).cols(1)
         plot = bokeh_renderer.get_plot(layout)
-        toolbar, grid = plot.state.children
-        self.assertIsInstance(toolbar, ToolbarBox)
-        self.assertIsInstance(grid, GridBox)
+        if bokeh3:
+            grid = plot.state
+            toolbar = grid.toolbar
+        else:
+            toolbar, grid = plot.state.children
+        self.assertIsInstance(toolbar, Toolbar)
+        self.assertIsInstance(grid, GridPlot)
         for (fig, _, _) in grid.children:
-            self.assertIsInstance(fig, Figure)
+            self.assertIsInstance(fig, figure)
         self.assertTrue([len([r for r in f.renderers if isinstance(r, GlyphRenderer)])
                          for (f, _, _) in grid.children], [1, 1, 1])
 
@@ -252,8 +302,8 @@ class TestLayoutPlot(LoggingComparisonTestCase, TestBokehPlot):
         plot = bokeh_renderer.get_plot(layout)
         self.assertIsInstance(plot.state, Tabs)
         panel1, panel2 = plot.state.tabs
-        self.assertIsInstance(panel1, Panel)
-        self.assertIsInstance(panel2, Panel)
+        self.assertIsInstance(panel1, TabPanel)
+        self.assertIsInstance(panel2, TabPanel)
         self.assertEqual(panel1.title, 'Curve I')
         self.assertEqual(panel2.title, 'AdjointLayout I')
 
@@ -322,13 +372,17 @@ class TestLayoutPlot(LoggingComparisonTestCase, TestBokehPlot):
     def test_layout_set_toolbar_location(self):
         layout = (Curve([]) + Points([])).opts(toolbar='left')
         plot = bokeh_renderer.get_plot(layout)
-        self.assertIsInstance(plot.state, Row)
-        self.assertIsInstance(plot.state.children[0], ToolbarBox)
+        if bokeh3:
+            self.assertIsInstance(plot.state, GridPlot)
+            self.assertIsInstance(plot.state.toolbar, Toolbar)
+        else:
+            self.assertIsInstance(plot.state, Row)
+            self.assertIsInstance(plot.state.children[0], Toolbar)
 
     def test_layout_disable_toolbar(self):
         layout = (Curve([]) + Points([])).opts(toolbar=None)
         plot = bokeh_renderer.get_plot(layout)
-        self.assertIsInstance(plot.state, GridBox)
+        self.assertIsInstance(plot.state, GridPlot)
         self.assertEqual(len(plot.state.children), 2)
 
     def test_layout_shared_inverted_yaxis(self):
diff --git a/holoviews/tests/plotting/bokeh/test_overlayplot.py b/holoviews/tests/plotting/bokeh/test_overlayplot.py
index c1cfba6e3..432f035c9 100644
--- a/holoviews/tests/plotting/bokeh/test_overlayplot.py
+++ b/holoviews/tests/plotting/bokeh/test_overlayplot.py
@@ -6,6 +6,7 @@ from holoviews.core.options import Cycle
 from holoviews.element import Bars, Box, Curve, ErrorBars, HLine, Points, Scatter, Text, VLine
 from holoviews.streams import Stream, Tap
 from holoviews.util import Dynamic
+from holoviews.plotting.bokeh.util import property_to_dict
 
 from ...utils import LoggingComparisonTestCase
 from .test_plot import TestBokehPlot, bokeh_renderer
@@ -298,7 +299,7 @@ class TestLegends(TestBokehPlot):
         dmap = DynamicMap(cb, kdims=['X']).redim.range(X=(1, 10))
         plot = bokeh_renderer.get_plot(dmap)
         plot.update((3,))
-        legend_labels = [item.label for item in plot.state.legend[0].items]
+        legend_labels = [property_to_dict(item.label) for item in plot.state.legend[0].items]
         self.assertEqual(legend_labels, [{'value': '1'}, {'value': '2'}])
         colors = Cycle().values
         for i, (subplot, color) in enumerate(zip(plot.subplots.values(), colors[3:])):
@@ -310,25 +311,25 @@ class TestLegends(TestBokehPlot):
         hmap = HoloMap({i: Curve([1, 2, 3], label=chr(65+i+2)) * Curve([1, 2, 3], label='B')
                         for i in range(3)})
         plot = bokeh_renderer.get_plot(hmap)
-        legend_labels = [item.label for item in plot.state.legend[0].items]
+        legend_labels = [property_to_dict(item.label) for item in plot.state.legend[0].items]
         self.assertEqual(legend_labels, [{'value': 'C'}, {'value': 'B'}])
         plot.update((1,))
-        legend_labels = [item.label for item in plot.state.legend[0].items]
+        legend_labels = [property_to_dict(item.label) for item in plot.state.legend[0].items]
         self.assertEqual(legend_labels, [{'value': 'B'}, {'value': 'D'}])
         plot.update((2,))
-        legend_labels = [item.label for item in plot.state.legend[0].items]
+        legend_labels = [property_to_dict(item.label) for item in plot.state.legend[0].items]
         self.assertEqual(legend_labels, [{'value': 'B'}, {'value': 'E'}])
 
     def test_holomap_legend_updates_varying_lengths(self):
         hmap = HoloMap({i: Overlay([Curve([1, 2, j], label=chr(65+j)) for j in range(i)]) for i in range(1, 4)})
         plot = bokeh_renderer.get_plot(hmap)
-        legend_labels = [item.label for item in plot.state.legend[0].items]
+        legend_labels = [property_to_dict(item.label) for item in plot.state.legend[0].items]
         self.assertEqual(legend_labels, [{'value': 'A'}])
         plot.update((2,))
-        legend_labels = [item.label for item in plot.state.legend[0].items]
+        legend_labels = [property_to_dict(item.label) for item in plot.state.legend[0].items]
         self.assertEqual(legend_labels, [{'value': 'A'}, {'value': 'B'}])
         plot.update((3,))
-        legend_labels = [item.label for item in plot.state.legend[0].items]
+        legend_labels = [property_to_dict(item.label) for item in plot.state.legend[0].items]
         self.assertEqual(legend_labels, [{'value': 'A'}, {'value': 'B'}, {'value': 'C'}])
 
     def test_dynamicmap_legend_updates(self):
@@ -336,26 +337,26 @@ class TestLegends(TestBokehPlot):
                         for i in range(3)})
         dmap = Dynamic(hmap)
         plot = bokeh_renderer.get_plot(dmap)
-        legend_labels = [item.label for item in plot.state.legend[0].items]
+        legend_labels = [property_to_dict(item.label) for item in plot.state.legend[0].items]
         self.assertEqual(legend_labels, [{'value': 'C'}, {'value': 'B'}])
         plot.update((1,))
-        legend_labels = [item.label for item in plot.state.legend[0].items]
+        legend_labels = [property_to_dict(item.label) for item in plot.state.legend[0].items]
         self.assertEqual(legend_labels, [{'value': 'D'}, {'value': 'B'}])
         plot.update((2,))
-        legend_labels = [item.label for item in plot.state.legend[0].items]
+        legend_labels = [property_to_dict(item.label) for item in plot.state.legend[0].items]
         self.assertEqual(legend_labels, [{'value': 'E'}, {'value': 'B'}])
 
     def test_dynamicmap_legend_updates_add_dynamic_plots(self):
         hmap = HoloMap({i: Overlay([Curve([1, 2, j], label=chr(65+j)) for j in range(i)]) for i in range(1, 4)})
         dmap = Dynamic(hmap)
         plot = bokeh_renderer.get_plot(dmap)
-        legend_labels = [item.label for item in plot.state.legend[0].items]
+        legend_labels = [property_to_dict(item.label) for item in plot.state.legend[0].items]
         self.assertEqual(legend_labels, [{'value': 'A'}])
         plot.update((2,))
-        legend_labels = [item.label for item in plot.state.legend[0].items]
+        legend_labels = [property_to_dict(item.label) for item in plot.state.legend[0].items]
         self.assertEqual(legend_labels, [{'value': 'A'}, {'value': 'B'}])
         plot.update((3,))
-        legend_labels = [item.label for item in plot.state.legend[0].items]
+        legend_labels = [property_to_dict(item.label) for item in plot.state.legend[0].items]
         self.assertEqual(legend_labels, [{'value': 'A'}, {'value': 'B'}, {'value': 'C'}])
 
     def test_dynamicmap_ndoverlay_shrink_number_of_items(self):
diff --git a/holoviews/tests/plotting/bokeh/test_pathplot.py b/holoviews/tests/plotting/bokeh/test_pathplot.py
index c9770f536..8f6b99d60 100644
--- a/holoviews/tests/plotting/bokeh/test_pathplot.py
+++ b/holoviews/tests/plotting/bokeh/test_pathplot.py
@@ -8,6 +8,7 @@ from holoviews.core.options import Cycle
 from holoviews.element import Path, Polygons, Contours
 from holoviews.streams import PolyDraw
 from holoviews.util.transform import dim
+from holoviews.plotting.bokeh.util import property_to_dict
 
 from .test_plot import TestBokehPlot, bokeh_renderer
 
@@ -171,7 +172,7 @@ class TestPathPlot(TestBokehPlot):
         plot = bokeh_renderer.get_plot(path)
         item = plot.state.legend[0].items[0]
         legend = {'field': 'color_str__'}
-        self.assertEqual(item.label, legend)
+        self.assertEqual(property_to_dict(item.label), legend)
         self.assertEqual(item.renderers, [plot.handles['glyph_renderer']])
 
     def test_path_continuously_varying_color_legend_with_labels(self):
@@ -190,7 +191,7 @@ class TestPathPlot(TestBokehPlot):
         item = plot.state.legend[0].items[0]
         legend = {'field': '_color_str___labels'}
         self.assertEqual(cds.data['_color_str___labels'], ['A', 'B', 'C', 'A', 'B', 'C', 'A', 'B'])
-        self.assertEqual(item.label, legend)
+        self.assertEqual(property_to_dict(item.label), legend)
         self.assertEqual(item.renderers, [plot.handles['glyph_renderer']])
 
 
@@ -282,7 +283,7 @@ class TestPolygonPlot(TestBokehPlot):
         cds = plot.handles['source']
         glyph = plot.handles['glyph']
         self.assertEqual(glyph.line_color, 'black')
-        self.assertEqual(glyph.fill_color, {'field': 'fill_color'})
+        self.assertEqual(property_to_dict(glyph.fill_color), {'field': 'fill_color'})
         self.assertEqual(cds.data['color'], np.array(['green', 'red']))
         self.assertEqual(cds.data['fill_color'], np.array(['green', 'red']))
 
@@ -295,7 +296,7 @@ class TestPolygonPlot(TestBokehPlot):
         cds = plot.handles['source']
         glyph = plot.handles['glyph']
         self.assertEqual(glyph.line_color, 'black')
-        self.assertEqual(glyph.fill_color, {'field': 'color'})
+        self.assertEqual(property_to_dict(glyph.fill_color), {'field': 'color'})
         self.assertEqual(cds.data['color'], np.array(['green', 'red']))
 
     def test_polygons_linear_color_op(self):
@@ -308,7 +309,7 @@ class TestPolygonPlot(TestBokehPlot):
         glyph = plot.handles['glyph']
         cmapper = plot.handles['color_color_mapper']
         self.assertEqual(glyph.line_color, 'black')
-        self.assertEqual(glyph.fill_color, {'field': 'color', 'transform': cmapper})
+        self.assertEqual(property_to_dict(glyph.fill_color), {'field': 'color', 'transform': cmapper})
         self.assertEqual(cds.data['color'], np.array([7, 3]))
         self.assertIsInstance(cmapper, LinearColorMapper)
         self.assertEqual(cmapper.low, 3)
@@ -324,7 +325,7 @@ class TestPolygonPlot(TestBokehPlot):
         glyph = plot.handles['glyph']
         cmapper = plot.handles['color_color_mapper']
         self.assertEqual(glyph.line_color, 'black')
-        self.assertEqual(glyph.fill_color, {'field': 'color', 'transform': cmapper})
+        self.assertEqual(property_to_dict(glyph.fill_color), {'field': 'color', 'transform': cmapper})
         self.assertEqual(cds.data['color'], np.array(['b', 'a']))
         self.assertIsInstance(cmapper, CategoricalColorMapper)
         self.assertEqual(cmapper.factors, ['b', 'a'])
@@ -337,8 +338,8 @@ class TestPolygonPlot(TestBokehPlot):
         plot = bokeh_renderer.get_plot(polygons)
         cds = plot.handles['source']
         glyph = plot.handles['glyph']
-        self.assertEqual(glyph.line_alpha, {'field': 'alpha'})
-        self.assertEqual(glyph.fill_alpha, {'field': 'alpha'})
+        self.assertEqual(property_to_dict(glyph.line_alpha), {'field': 'alpha'})
+        self.assertEqual(property_to_dict(glyph.fill_alpha), {'field': 'alpha'})
         self.assertEqual(cds.data['alpha'], np.array([0.7, 0.3]))
 
     def test_polygons_line_width_op(self):
@@ -349,7 +350,7 @@ class TestPolygonPlot(TestBokehPlot):
         plot = bokeh_renderer.get_plot(polygons)
         cds = plot.handles['source']
         glyph = plot.handles['glyph']
-        self.assertEqual(glyph.line_width, {'field': 'line_width'})
+        self.assertEqual(property_to_dict(glyph.line_width), {'field': 'line_width'})
         self.assertEqual(cds.data['line_width'], np.array([7, 3]))
 
     def test_polygons_holes_initialize(self):
@@ -403,7 +404,7 @@ class TestContoursPlot(TestBokehPlot):
         plot = bokeh_renderer.get_plot(contours)
         cds = plot.handles['source']
         glyph = plot.handles['glyph']
-        self.assertEqual(glyph.line_color, {'field': 'color'})
+        self.assertEqual(property_to_dict(glyph.line_color), {'field': 'color'})
         self.assertEqual(cds.data['color'], np.array(['green', 'red']))
 
     def test_contours_linear_color_op(self):
@@ -415,7 +416,7 @@ class TestContoursPlot(TestBokehPlot):
         cds = plot.handles['source']
         glyph = plot.handles['glyph']
         cmapper = plot.handles['color_color_mapper']
-        self.assertEqual(glyph.line_color, {'field': 'color', 'transform': cmapper})
+        self.assertEqual(property_to_dict(glyph.line_color), {'field': 'color', 'transform': cmapper})
         self.assertEqual(cds.data['color'], np.array([7, 3]))
         self.assertIsInstance(cmapper, LinearColorMapper)
         self.assertEqual(cmapper.low, 3)
@@ -450,7 +451,7 @@ class TestContoursPlot(TestBokehPlot):
         glyph = plot.handles['glyph']
         cmapper = plot.handles['color_color_mapper']
         plot.update((0,))
-        self.assertEqual(glyph.line_color, {'field': 'color', 'transform': cmapper})
+        self.assertEqual(property_to_dict(glyph.line_color), {'field': 'color', 'transform': cmapper})
         self.assertEqual(cds.data['color'], np.array([7, 3]))
         self.assertEqual(cmapper.low, 3)
         self.assertEqual(cmapper.high, 7)
@@ -468,7 +469,7 @@ class TestContoursPlot(TestBokehPlot):
         cds = plot.handles['source']
         glyph = plot.handles['glyph']
         cmapper = plot.handles['color_color_mapper']
-        self.assertEqual(glyph.line_color, {'field': 'color', 'transform': cmapper})
+        self.assertEqual(property_to_dict(glyph.line_color), {'field': 'color', 'transform': cmapper})
         self.assertEqual(cds.data['color'], np.array(['b', 'a']))
         self.assertIsInstance(cmapper, CategoricalColorMapper)
         self.assertEqual(cmapper.factors, ['b', 'a'])
@@ -481,7 +482,7 @@ class TestContoursPlot(TestBokehPlot):
         plot = bokeh_renderer.get_plot(contours)
         cds = plot.handles['source']
         glyph = plot.handles['glyph']
-        self.assertEqual(glyph.line_alpha, {'field': 'alpha'})
+        self.assertEqual(property_to_dict(glyph.line_alpha), {'field': 'alpha'})
         self.assertEqual(cds.data['alpha'], np.array([0.7, 0.3]))
 
     def test_contours_line_width_op(self):
@@ -492,5 +493,5 @@ class TestContoursPlot(TestBokehPlot):
         plot = bokeh_renderer.get_plot(contours)
         cds = plot.handles['source']
         glyph = plot.handles['glyph']
-        self.assertEqual(glyph.line_width, {'field': 'line_width'})
+        self.assertEqual(property_to_dict(glyph.line_width), {'field': 'line_width'})
         self.assertEqual(cds.data['line_width'], np.array([7, 3]))
diff --git a/holoviews/tests/plotting/bokeh/test_pointplot.py b/holoviews/tests/plotting/bokeh/test_pointplot.py
index 3e01ad43b..156d75cc0 100644
--- a/holoviews/tests/plotting/bokeh/test_pointplot.py
+++ b/holoviews/tests/plotting/bokeh/test_pointplot.py
@@ -7,6 +7,7 @@ from holoviews.core import NdOverlay
 from holoviews.core.options import Cycle
 from holoviews.element import Points
 from holoviews.streams import Stream
+from holoviews.plotting.bokeh.util import property_to_dict
 
 from .test_plot import TestBokehPlot, bokeh_renderer
 from ..utils import ParamLogStream
@@ -345,8 +346,8 @@ class TestPointPlot(TestBokehPlot):
         cds = plot.handles['cds']
         glyph = plot.handles['glyph']
         self.assertEqual(cds.data['color'], np.array(['#000', '#F00', '#0F0']))
-        self.assertEqual(glyph.fill_color, {'field': 'color'})
-        self.assertEqual(glyph.line_color, {'field': 'color'})
+        self.assertEqual(property_to_dict(glyph.fill_color), {'field': 'color'})
+        self.assertEqual(property_to_dict(glyph.line_color), {'field': 'color'})
 
     def test_point_linear_color_op(self):
         points = Points([(0, 0, 0), (0, 1, 1), (0, 2, 2)],
@@ -359,8 +360,8 @@ class TestPointPlot(TestBokehPlot):
         self.assertEqual(cmapper.low, 0)
         self.assertEqual(cmapper.high, 2)
         self.assertEqual(cds.data['color'], np.array([0, 1, 2]))
-        self.assertEqual(glyph.fill_color, {'field': 'color', 'transform': cmapper})
-        self.assertEqual(glyph.line_color, {'field': 'color', 'transform': cmapper})
+        self.assertEqual(property_to_dict(glyph.fill_color), {'field': 'color', 'transform': cmapper})
+        self.assertEqual(property_to_dict(glyph.line_color), {'field': 'color', 'transform': cmapper})
 
     def test_point_categorical_color_op(self):
         points = Points([(0, 0, 'A'), (0, 1, 'B'), (0, 2, 'C')],
@@ -372,8 +373,8 @@ class TestPointPlot(TestBokehPlot):
         self.assertTrue(cmapper, CategoricalColorMapper)
         self.assertEqual(cmapper.factors, ['A', 'B', 'C'])
         self.assertEqual(cds.data['color'], np.array(['A', 'B', 'C']))
-        self.assertEqual(glyph.fill_color, {'field': 'color', 'transform': cmapper})
-        self.assertEqual(glyph.line_color, {'field': 'color', 'transform': cmapper})
+        self.assertEqual(property_to_dict(glyph.fill_color), {'field': 'color', 'transform': cmapper})
+        self.assertEqual(property_to_dict(glyph.line_color), {'field': 'color', 'transform': cmapper})
 
     def test_point_categorical_color_op_legend_with_labels(self):
         labels = {'A': 'A point', 'B': 'B point', 'C': 'C point'}
@@ -382,7 +383,7 @@ class TestPointPlot(TestBokehPlot):
         plot = bokeh_renderer.get_plot(points)
         cds = plot.handles['cds']
         legend = plot.state.legend[0].items[0]
-        assert legend.label == {'field': '_color_labels'}
+        assert property_to_dict(legend.label) == {'field': '_color_labels'}
         assert cds.data['_color_labels'] == ['A point', 'B point', 'C point']
 
     def test_point_categorical_dtype_color_op(self):
@@ -396,8 +397,8 @@ class TestPointPlot(TestBokehPlot):
         self.assertTrue(cmapper, CategoricalColorMapper)
         self.assertEqual(cmapper.factors, ['apple', 'pear'])
         self.assertEqual(np.asarray(cds.data['color']), np.array(['apple', 'pear', 'apple', 'pear']))
-        self.assertEqual(glyph.fill_color, {'field': 'color', 'transform': cmapper})
-        self.assertEqual(glyph.line_color, {'field': 'color', 'transform': cmapper})
+        self.assertEqual(property_to_dict(glyph.fill_color), {'field': 'color', 'transform': cmapper})
+        self.assertEqual(property_to_dict(glyph.line_color), {'field': 'color', 'transform': cmapper})
 
     def test_point_explicit_cmap_color_op(self):
         points = Points([(0, 0), (0, 1), (0, 2)]).opts(
@@ -410,8 +411,8 @@ class TestPointPlot(TestBokehPlot):
         self.assertEqual(cmapper.factors, ['0', '1', '2'])
         self.assertEqual(cmapper.palette, ['red', 'green', 'blue'])
         self.assertEqual(cds.data['color_str__'], ['0', '1', '2'])
-        self.assertEqual(glyph.fill_color, {'field': 'color_str__', 'transform': cmapper})
-        self.assertEqual(glyph.line_color, {'field': 'color_str__', 'transform': cmapper})
+        self.assertEqual(property_to_dict(glyph.fill_color), {'field': 'color_str__', 'transform': cmapper})
+        self.assertEqual(property_to_dict(glyph.line_color), {'field': 'color_str__', 'transform': cmapper})
 
     def test_point_line_color_op(self):
         points = Points([(0, 0, '#000'), (0, 1, '#F00'), (0, 2, '#0F0')],
@@ -420,8 +421,8 @@ class TestPointPlot(TestBokehPlot):
         cds = plot.handles['cds']
         glyph = plot.handles['glyph']
         self.assertEqual(cds.data['line_color'], np.array(['#000', '#F00', '#0F0']))
-        self.assertNotEqual(glyph.fill_color, {'field': 'line_color'})
-        self.assertEqual(glyph.line_color, {'field': 'line_color'})
+        self.assertNotEqual(property_to_dict(glyph.fill_color), {'field': 'line_color'})
+        self.assertEqual(property_to_dict(glyph.line_color), {'field': 'line_color'})
 
     def test_point_fill_color_op(self):
         points = Points([(0, 0, '#000'), (0, 1, '#F00'), (0, 2, '#0F0')],
@@ -430,8 +431,8 @@ class TestPointPlot(TestBokehPlot):
         cds = plot.handles['cds']
         glyph = plot.handles['glyph']
         self.assertEqual(cds.data['fill_color'], np.array(['#000', '#F00', '#0F0']))
-        self.assertEqual(glyph.fill_color, {'field': 'fill_color'})
-        self.assertNotEqual(glyph.line_color, {'field': 'fill_color'})
+        self.assertEqual(property_to_dict(glyph.fill_color), {'field': 'fill_color'})
+        self.assertNotEqual(property_to_dict(glyph.line_color), {'field': 'fill_color'})
 
     def test_point_angle_op(self):
         points = Points([(0, 0, 0), (0, 1, 45), (0, 2, 90)],
@@ -440,7 +441,7 @@ class TestPointPlot(TestBokehPlot):
         cds = plot.handles['cds']
         glyph = plot.handles['glyph']
         self.assertEqual(cds.data['angle'], np.array([0, 0.785398, 1.570796]))
-        self.assertEqual(glyph.angle, {'field': 'angle'})
+        self.assertEqual(property_to_dict(glyph.angle), {'field': 'angle'})
 
     def test_point_alpha_op(self):
         points = Points([(0, 0, 0), (0, 1, 0.2), (0, 2, 0.7)],
@@ -449,7 +450,7 @@ class TestPointPlot(TestBokehPlot):
         cds = plot.handles['cds']
         glyph = plot.handles['glyph']
         self.assertEqual(cds.data['alpha'], np.array([0, 0.2, 0.7]))
-        self.assertEqual(glyph.fill_alpha, {'field': 'alpha'})
+        self.assertEqual(property_to_dict(glyph.fill_alpha), {'field': 'alpha'})
 
     def test_point_line_alpha_op(self):
         points = Points([(0, 0, 0), (0, 1, 0.2), (0, 2, 0.7)],
@@ -458,8 +459,8 @@ class TestPointPlot(TestBokehPlot):
         cds = plot.handles['cds']
         glyph = plot.handles['glyph']
         self.assertEqual(cds.data['line_alpha'], np.array([0, 0.2, 0.7]))
-        self.assertEqual(glyph.line_alpha, {'field': 'line_alpha'})
-        self.assertNotEqual(glyph.fill_alpha, {'field': 'line_alpha'})
+        self.assertEqual(property_to_dict(glyph.line_alpha), {'field': 'line_alpha'})
+        self.assertNotEqual(property_to_dict(glyph.fill_alpha), {'field': 'line_alpha'})
 
     def test_point_fill_alpha_op(self):
         points = Points([(0, 0, 0), (0, 1, 0.2), (0, 2, 0.7)],
@@ -468,8 +469,8 @@ class TestPointPlot(TestBokehPlot):
         cds = plot.handles['cds']
         glyph = plot.handles['glyph']
         self.assertEqual(cds.data['fill_alpha'], np.array([0, 0.2, 0.7]))
-        self.assertNotEqual(glyph.line_alpha, {'field': 'fill_alpha'})
-        self.assertEqual(glyph.fill_alpha, {'field': 'fill_alpha'})
+        self.assertNotEqual(property_to_dict(glyph.line_alpha), {'field': 'fill_alpha'})
+        self.assertEqual(property_to_dict(glyph.fill_alpha), {'field': 'fill_alpha'})
 
     def test_point_size_op(self):
         points = Points([(0, 0, 1), (0, 1, 4), (0, 2, 8)],
@@ -478,7 +479,7 @@ class TestPointPlot(TestBokehPlot):
         cds = plot.handles['cds']
         glyph = plot.handles['glyph']
         self.assertEqual(cds.data['size'], np.array([1, 4, 8]))
-        self.assertEqual(glyph.size, {'field': 'size'})
+        self.assertEqual(property_to_dict(glyph.size), {'field': 'size'})
 
     def test_point_line_width_op(self):
         points = Points([(0, 0, 1), (0, 1, 4), (0, 2, 8)],
@@ -487,7 +488,7 @@ class TestPointPlot(TestBokehPlot):
         cds = plot.handles['cds']
         glyph = plot.handles['glyph']
         self.assertEqual(cds.data['line_width'], np.array([1, 4, 8]))
-        self.assertEqual(glyph.line_width, {'field': 'line_width'})
+        self.assertEqual(property_to_dict(glyph.line_width), {'field': 'line_width'})
 
     def test_point_marker_op(self):
         points = Points([(0, 0, 'circle'), (0, 1, 'triangle'), (0, 2, 'square')],
@@ -496,7 +497,7 @@ class TestPointPlot(TestBokehPlot):
         cds = plot.handles['cds']
         glyph = plot.handles['glyph']
         self.assertEqual(cds.data['marker'], np.array(['circle', 'triangle', 'square']))
-        self.assertEqual(glyph.marker, {'field': 'marker'})
+        self.assertEqual(property_to_dict(glyph.marker), {'field': 'marker'})
 
     def test_op_ndoverlay_value(self):
         markers = ['circle', 'triangle']
@@ -526,8 +527,8 @@ class TestPointPlot(TestBokehPlot):
         glyph = plot.handles['glyph']
         cmapper = plot.handles['fill_color_color_mapper']
         cmapper2 = plot.handles['color_mapper']
-        self.assertEqual(glyph.fill_color, {'field': 'fill_color', 'transform': cmapper})
-        self.assertEqual(glyph.line_color, {'field': 'color', 'transform': cmapper2})
+        self.assertEqual(property_to_dict(glyph.fill_color), {'field': 'fill_color', 'transform': cmapper})
+        self.assertEqual(property_to_dict(glyph.line_color), {'field': 'color', 'transform': cmapper2})
 
     def test_point_size_index_size_clash(self):
         points = Points([(0, 0, 0), (0, 1, 1), (0, 2, 2)],
diff --git a/holoviews/tests/plotting/bokeh/test_renderer.py b/holoviews/tests/plotting/bokeh/test_renderer.py
index 23bfc7e32..cced66d37 100644
--- a/holoviews/tests/plotting/bokeh/test_renderer.py
+++ b/holoviews/tests/plotting/bokeh/test_renderer.py
@@ -9,6 +9,8 @@ from holoviews import DynamicMap, HoloMap, Image, GridSpace, Table, Curve
 from holoviews.streams import Stream
 from holoviews.plotting import Renderer
 from holoviews.element.comparison import ComparisonTestCase
+from holoviews.plotting.bokeh.util import bokeh3
+
 from pyviz_comms import CommManager
 
 import panel as pn
@@ -80,13 +82,11 @@ class BokehRendererTest(ComparisonTestCase):
         self.assertEqual((w, h), (800, 300))
 
     def test_theme_rendering(self):
-        theme = Theme(
-            json={
-        'attrs' : {
-            'Figure' : {
-                'outline_line_color': '#444444'}
-        }
-            })
+        if bokeh3:
+            attrs = {'figure': {'outline_line_color': '#444444'}}
+        else:
+            attrs = {'Figure': {'outline_line_color': '#444444'}}
+        theme = Theme(json={'attrs' : attrs})
         self.renderer.theme = theme
         plot = self.renderer.get_plot(Curve([]))
         self.renderer.components(plot, 'html')
diff --git a/holoviews/tests/plotting/bokeh/test_spikesplot.py b/holoviews/tests/plotting/bokeh/test_spikesplot.py
index f037541d8..95288a800 100644
--- a/holoviews/tests/plotting/bokeh/test_spikesplot.py
+++ b/holoviews/tests/plotting/bokeh/test_spikesplot.py
@@ -5,6 +5,7 @@ from holoviews.core import NdOverlay
 from holoviews.element import Spikes
 
 from bokeh.models import CategoricalColorMapper, LinearColorMapper
+from holoviews.plotting.bokeh.util import property_to_dict
 
 from ..utils import ParamLogStream
 from .test_plot import TestBokehPlot, bokeh_renderer
@@ -159,7 +160,7 @@ class TestSpikesPlot(TestBokehPlot):
         cds = plot.handles['cds']
         glyph = plot.handles['glyph']
         self.assertEqual(cds.data['color'], np.array(['#000', '#F00', '#0F0']))
-        self.assertEqual(glyph.line_color, {'field': 'color'})
+        self.assertEqual(property_to_dict(glyph.line_color), {'field': 'color'})
 
     def test_spikes_linear_color_op(self):
         spikes = Spikes([(0, 0, 0), (0, 1, 1), (0, 2, 2)],
@@ -172,7 +173,7 @@ class TestSpikesPlot(TestBokehPlot):
         self.assertEqual(cmapper.low, 0)
         self.assertEqual(cmapper.high, 2)
         self.assertEqual(cds.data['color'], np.array([0, 1, 2]))
-        self.assertEqual(glyph.line_color, {'field': 'color', 'transform': cmapper})
+        self.assertEqual(property_to_dict(glyph.line_color), {'field': 'color', 'transform': cmapper})
 
     def test_spikes_categorical_color_op(self):
         spikes = Spikes([(0, 0, 'A'), (0, 1, 'B'), (0, 2, 'C')],
@@ -184,7 +185,7 @@ class TestSpikesPlot(TestBokehPlot):
         self.assertTrue(cmapper, CategoricalColorMapper)
         self.assertEqual(cmapper.factors, ['A', 'B', 'C'])
         self.assertEqual(cds.data['color'], np.array(['A', 'B', 'C']))
-        self.assertEqual(glyph.line_color, {'field': 'color', 'transform': cmapper})
+        self.assertEqual(property_to_dict(glyph.line_color), {'field': 'color', 'transform': cmapper})
 
     def test_spikes_line_color_op(self):
         spikes = Spikes([(0, 0, '#000'), (0, 1, '#F00'), (0, 2, '#0F0')],
@@ -193,7 +194,7 @@ class TestSpikesPlot(TestBokehPlot):
         cds = plot.handles['cds']
         glyph = plot.handles['glyph']
         self.assertEqual(cds.data['line_color'], np.array(['#000', '#F00', '#0F0']))
-        self.assertEqual(glyph.line_color, {'field': 'line_color'})
+        self.assertEqual(property_to_dict(glyph.line_color), {'field': 'line_color'})
 
     def test_spikes_alpha_op(self):
         spikes = Spikes([(0, 0, 0), (0, 1, 0.2), (0, 2, 0.7)],
@@ -202,7 +203,7 @@ class TestSpikesPlot(TestBokehPlot):
         cds = plot.handles['cds']
         glyph = plot.handles['glyph']
         self.assertEqual(cds.data['alpha'], np.array([0, 0.2, 0.7]))
-        self.assertEqual(glyph.line_alpha, {'field': 'alpha'})
+        self.assertEqual(property_to_dict(glyph.line_alpha), {'field': 'alpha'})
 
     def test_spikes_line_alpha_op(self):
         spikes = Spikes([(0, 0, 0), (0, 1, 0.2), (0, 2, 0.7)],
@@ -211,7 +212,7 @@ class TestSpikesPlot(TestBokehPlot):
         cds = plot.handles['cds']
         glyph = plot.handles['glyph']
         self.assertEqual(cds.data['line_alpha'], np.array([0, 0.2, 0.7]))
-        self.assertEqual(glyph.line_alpha, {'field': 'line_alpha'})
+        self.assertEqual(property_to_dict(glyph.line_alpha), {'field': 'line_alpha'})
 
     def test_spikes_line_width_op(self):
         spikes = Spikes([(0, 0, 1), (0, 1, 4), (0, 2, 8)],
@@ -220,7 +221,7 @@ class TestSpikesPlot(TestBokehPlot):
         cds = plot.handles['cds']
         glyph = plot.handles['glyph']
         self.assertEqual(cds.data['line_width'], np.array([1, 4, 8]))
-        self.assertEqual(glyph.line_width, {'field': 'line_width'})
+        self.assertEqual(property_to_dict(glyph.line_width), {'field': 'line_width'})
 
     def test_op_ndoverlay_value(self):
         colors = ['blue', 'red']
diff --git a/holoviews/tests/plotting/bokeh/test_vectorfieldplot.py b/holoviews/tests/plotting/bokeh/test_vectorfieldplot.py
index eb6eafd0d..4d100e760 100644
--- a/holoviews/tests/plotting/bokeh/test_vectorfieldplot.py
+++ b/holoviews/tests/plotting/bokeh/test_vectorfieldplot.py
@@ -1,6 +1,7 @@
 import numpy as np
 
 from holoviews.element import VectorField
+from holoviews.plotting.bokeh.util import property_to_dict
 
 from .test_plot import TestBokehPlot, bokeh_renderer
 from ..utils import ParamLogStream
@@ -25,7 +26,7 @@ class TestVectorFieldPlot(TestBokehPlot):
         glyph = plot.handles['glyph']
         self.assertEqual(cds.data['color'], np.array(['#000', '#F00', '#0F0', '#000',
                                                       '#F00', '#0F0', '#000', '#F00', '#0F0']))
-        self.assertEqual(glyph.line_color, {'field': 'color'})
+        self.assertEqual(property_to_dict(glyph.line_color), {'field': 'color'})
 
     def test_vectorfield_linear_color_op(self):
         vectorfield = VectorField([(0, 0, 0, 1, 0), (0, 1, 0, 1, 1), (0, 2, 0, 1, 2)],
@@ -38,7 +39,7 @@ class TestVectorFieldPlot(TestBokehPlot):
         self.assertEqual(cmapper.low, 0)
         self.assertEqual(cmapper.high, 2)
         self.assertEqual(cds.data['color'], np.array([0, 1, 2, 0, 1, 2, 0, 1, 2]))
-        self.assertEqual(glyph.line_color, {'field': 'color', 'transform': cmapper})
+        self.assertEqual(property_to_dict(glyph.line_color), {'field': 'color', 'transform': cmapper})
 
     def test_vectorfield_categorical_color_op(self):
         vectorfield = VectorField([(0, 0, 0, 1, 'A'), (0, 1, 0, 1, 'B'), (0, 2, 0, 1, 'C')],
@@ -50,7 +51,7 @@ class TestVectorFieldPlot(TestBokehPlot):
         self.assertTrue(cmapper, CategoricalColorMapper)
         self.assertEqual(cmapper.factors, ['A', 'B', 'C'])
         self.assertEqual(cds.data['color'], np.array(['A', 'B', 'C', 'A', 'B', 'C', 'A', 'B', 'C']))
-        self.assertEqual(glyph.line_color, {'field': 'color', 'transform': cmapper})
+        self.assertEqual(property_to_dict(glyph.line_color), {'field': 'color', 'transform': cmapper})
 
     def test_vectorfield_alpha_op(self):
         vectorfield = VectorField([(0, 0, 0, 1, 0), (0, 1, 0, 1, 0.2), (0, 2, 0, 1, 0.7)],
@@ -59,7 +60,7 @@ class TestVectorFieldPlot(TestBokehPlot):
         cds = plot.handles['cds']
         glyph = plot.handles['glyph']
         self.assertEqual(cds.data['alpha'], np.array([0, 0.2, 0.7, 0, 0.2, 0.7, 0, 0.2, 0.7]))
-        self.assertEqual(glyph.line_alpha, {'field': 'alpha'})
+        self.assertEqual(property_to_dict(glyph.line_alpha), {'field': 'alpha'})
 
     def test_vectorfield_line_width_op(self):
         vectorfield = VectorField([(0, 0, 0, 1, 1), (0, 1, 0, 1, 4), (0, 2, 0, 1, 8)],
@@ -68,7 +69,7 @@ class TestVectorFieldPlot(TestBokehPlot):
         cds = plot.handles['cds']
         glyph = plot.handles['glyph']
         self.assertEqual(cds.data['line_width'], np.array([1, 4, 8, 1, 4, 8, 1, 4, 8]))
-        self.assertEqual(glyph.line_width, {'field': 'line_width'})
+        self.assertEqual(property_to_dict(glyph.line_width), {'field': 'line_width'})
 
     def test_vectorfield_color_index_color_clash(self):
         vectorfield = VectorField([(0, 0, 0), (0, 1, 1), (0, 2, 2)],
diff --git a/holoviews/tests/plotting/bokeh/test_violinplot.py b/holoviews/tests/plotting/bokeh/test_violinplot.py
index c84da7b42..036be34f7 100644
--- a/holoviews/tests/plotting/bokeh/test_violinplot.py
+++ b/holoviews/tests/plotting/bokeh/test_violinplot.py
@@ -5,6 +5,7 @@ import numpy as np
 from holoviews.element import Violin
 from holoviews.operation.stats import univariate_kde
 from holoviews.util.transform import dim
+from holoviews.plotting.bokeh.util import property_to_dict
 
 from .test_plot import TestBokehPlot, bokeh_renderer
 
@@ -119,7 +120,7 @@ class TestBokehViolinPlot(TestBokehPlot):
         self.assertTrue(cmapper, LinearColorMapper)
         self.assertEqual(cmapper.low, 0)
         self.assertEqual(cmapper.high, 4)
-        self.assertEqual(glyph.fill_color, {'field': 'violin_color', 'transform': cmapper})
+        self.assertEqual(property_to_dict(glyph.fill_color), {'field': 'violin_color', 'transform': cmapper})
 
     def test_violin_categorical_color_op(self):
         a = np.repeat(np.arange(5), 5)
@@ -132,7 +133,7 @@ class TestBokehViolinPlot(TestBokehPlot):
         self.assertEqual(source.data['violin_color'], b[::5])
         self.assertTrue(cmapper, CategoricalColorMapper)
         self.assertEqual(cmapper.factors, ['A', 'B', 'C', 'D', 'E'])
-        self.assertEqual(glyph.fill_color, {'field': 'violin_color', 'transform': cmapper})
+        self.assertEqual(property_to_dict(glyph.fill_color), {'field': 'violin_color', 'transform': cmapper})
 
     def test_violin_alpha_op(self):
         a = np.repeat(np.arange(5), 5)
@@ -142,7 +143,7 @@ class TestBokehViolinPlot(TestBokehPlot):
         source = plot.handles['patches_1_source']
         glyph = plot.handles['patches_1_glyph']
         self.assertEqual(source.data['violin_alpha'], np.arange(5)/10.)
-        self.assertEqual(glyph.fill_alpha, {'field': 'violin_alpha'})
+        self.assertEqual(property_to_dict(glyph.fill_alpha), {'field': 'violin_alpha'})
 
     def test_violin_line_width_op(self):
         a = np.repeat(np.arange(5), 5)
@@ -152,7 +153,7 @@ class TestBokehViolinPlot(TestBokehPlot):
         source = plot.handles['multi_line_1_source']
         glyph = plot.handles['multi_line_1_glyph']
         self.assertEqual(source.data['outline_line_width'], np.arange(5))
-        self.assertEqual(glyph.line_width, {'field': 'outline_line_width'})
+        self.assertEqual(property_to_dict(glyph.line_width), {'field': 'outline_line_width'})
 
     def test_violin_split_op_multi(self):
         a = np.repeat(np.arange(5), 5)
@@ -164,7 +165,7 @@ class TestBokehViolinPlot(TestBokehPlot):
         cmapper = plot.handles['violin_color_mapper']
         values = ['False', 'True', 'False', 'True', 'False', 'True', 'False', 'True', 'False', 'True']
         self.assertEqual(source.data["dim('b')>2"], values)
-        self.assertEqual(glyph.fill_color, {'field': "dim('b')>2", 'transform': cmapper})
+        self.assertEqual(property_to_dict(glyph.fill_color), {'field': "dim('b')>2", 'transform': cmapper})
 
     def test_violin_split_op_single(self):
         a = np.repeat(np.arange(2), 5)
@@ -174,7 +175,7 @@ class TestBokehViolinPlot(TestBokehPlot):
         glyph = plot.handles['patches_1_glyph']
         cmapper = plot.handles['violin_color_mapper']
         self.assertEqual(source.data["dim('a')"], ['0', '1'])
-        self.assertEqual(glyph.fill_color, {'field': "dim('a')", 'transform': cmapper})
+        self.assertEqual(property_to_dict(glyph.fill_color), {'field': "dim('a')", 'transform': cmapper})
 
     def test_violin_box_linear_color_op(self):
         a = np.repeat(np.arange(5), 5)
@@ -188,7 +189,7 @@ class TestBokehViolinPlot(TestBokehPlot):
         self.assertTrue(cmapper, LinearColorMapper)
         self.assertEqual(cmapper.low, 0)
         self.assertEqual(cmapper.high, 4)
-        self.assertEqual(glyph.fill_color, {'field': 'box_color', 'transform': cmapper})
+        self.assertEqual(property_to_dict(glyph.fill_color), {'field': 'box_color', 'transform': cmapper})
 
     def test_violin_box_categorical_color_op(self):
         a = np.repeat(np.arange(5), 5)
@@ -201,7 +202,7 @@ class TestBokehViolinPlot(TestBokehPlot):
         self.assertEqual(source.data['box_color'], b[::5])
         self.assertTrue(cmapper, CategoricalColorMapper)
         self.assertEqual(cmapper.factors, ['A', 'B', 'C', 'D', 'E'])
-        self.assertEqual(glyph.fill_color, {'field': 'box_color', 'transform': cmapper})
+        self.assertEqual(property_to_dict(glyph.fill_color), {'field': 'box_color', 'transform': cmapper})
 
     def test_violin_box_alpha_op(self):
         a = np.repeat(np.arange(5), 5)
@@ -211,7 +212,7 @@ class TestBokehViolinPlot(TestBokehPlot):
         source = plot.handles['vbar_1_source']
         glyph = plot.handles['vbar_1_glyph']
         self.assertEqual(source.data['box_alpha'], np.arange(5)/10.)
-        self.assertEqual(glyph.fill_alpha, {'field': 'box_alpha'})
+        self.assertEqual(property_to_dict(glyph.fill_alpha), {'field': 'box_alpha'})
 
     def test_violin_box_line_width_op(self):
         a = np.repeat(np.arange(5), 5)
@@ -221,4 +222,4 @@ class TestBokehViolinPlot(TestBokehPlot):
         source = plot.handles['vbar_1_source']
         glyph = plot.handles['vbar_1_glyph']
         self.assertEqual(source.data['box_line_width'], np.arange(5))
-        self.assertEqual(glyph.line_width, {'field': 'box_line_width'})
+        self.assertEqual(property_to_dict(glyph.line_width), {'field': 'box_line_width'})