Repository: bokeh/bokeh

Issue:
[ENH] Add custom classes to elements
It would be great if you could add custom classes for plots, widgets, layouts and etc.
For example:

```
buttomDiscard = Button(label="Discard", button_type="success", className="customButton")
confirmDialog = widgetbox([buttomDiscard], className="customBox")
```
[ENH] Add custom classes to elements
It would be great if you could add custom classes for plots, widgets, layouts and etc.
For example:

```
buttomDiscard = Button(label="Discard", button_type="success", className="customButton")
confirmDialog = widgetbox([buttomDiscard], className="customBox")
```


Discussion:
Can someone familiar with the code base comment on the complexity of this feature? Can it be achieved during a weekend hack? Would core team accept PR for it?

I don't think it would be that difficult, and it seems like a fairly reasonable escape valve for users to implement more precise styling. I think a PR would be received favorably, tho I'd like to hear thoughts from @birdsarah and @mattpap.

Regarding implementation, it would probably be a single `String` property on `Widget` (or perhaps a different class) and then some small code on the BokehJS side to apply that property value as a class. I guess the question is where exactly the class should be applied. 

My first reaction would be try to implement `_repr_html_()`method in a derived class. Thinking about the feature - I'd probably want to set only css class name from python (like @Deeech suggested, and unlike my[ question on SO ](http://stackoverflow.com/questions/40666632/bokeh-widget-custom-css?noredirect=1#comment68585209_40666632)). So the actual style code will be in .css file that I'd include in /template/index.html
Widgets (for now) are not practically usable inside notebooks. But if/when they will be - providing the actual style code would be more practical (like it is possible with[ IPython widgets](http://stackoverflow.com/questions/37013489/how-to-alight-and-place-ipywidgets))

@Volodymyrk I'm not sure what part `_repr_html` would play in this. That is a feature that is completely specific to Jupyter Notebooks, so I don't think it could be applicable to a general solution (Bokeh must always work the same outside notebooks, too).

Because this feature was urgently needed, I added an example of how to accomplish it by properly extending Bokeh - #5481 
Can someone familiar with the code base comment on the complexity of this feature? Can it be achieved during a weekend hack? Would core team accept PR for it?

I don't think it would be that difficult, and it seems like a fairly reasonable escape valve for users to implement more precise styling. I think a PR would be received favorably, tho I'd like to hear thoughts from @birdsarah and @mattpap.

Regarding implementation, it would probably be a single `String` property on `Widget` (or perhaps a different class) and then some small code on the BokehJS side to apply that property value as a class. I guess the question is where exactly the class should be applied. 

My first reaction would be try to implement `_repr_html_()`method in a derived class. Thinking about the feature - I'd probably want to set only css class name from python (like @Deeech suggested, and unlike my[ question on SO ](http://stackoverflow.com/questions/40666632/bokeh-widget-custom-css?noredirect=1#comment68585209_40666632)). So the actual style code will be in .css file that I'd include in /template/index.html
Widgets (for now) are not practically usable inside notebooks. But if/when they will be - providing the actual style code would be more practical (like it is possible with[ IPython widgets](http://stackoverflow.com/questions/37013489/how-to-alight-and-place-ipywidgets))

@Volodymyrk I'm not sure what part `_repr_html` would play in this. That is a feature that is completely specific to Jupyter Notebooks, so I don't think it could be applicable to a general solution (Bokeh must always work the same outside notebooks, too).

Because this feature was urgently needed, I added an example of how to accomplish it by properly extending Bokeh - #5481 

Resolving patch:
diff --git a/bokeh/models/layouts.py b/bokeh/models/layouts.py
--- a/bokeh/models/layouts.py
+++ b/bokeh/models/layouts.py
@@ -14,7 +14,7 @@
     BOTH_CHILD_AND_ROOT,
 )
 from ..core.enums import SizingMode
-from ..core.properties import abstract, Bool, Enum, Int, Instance, List
+from ..core.properties import abstract, Bool, Enum, Int, Instance, List, Seq, String
 from ..embed import notebook_div
 from ..model import Model
 from ..util.deprecation import deprecated
@@ -66,6 +66,11 @@ class LayoutDOM(Model):
 
     """)
 
+    css_classes = Seq(String, help="""
+    A list of css class names to add to this DOM element. Note: the class names are
+    simply added as-is, no other guarantees are provided.
+    """)
+
     @property
     def html(self):
         from IPython.core.display import HTML
diff --git a/examples/plotting/file/css_classes.py b/examples/plotting/file/css_classes.py
new file mode 100644
--- /dev/null
+++ b/examples/plotting/file/css_classes.py
@@ -0,0 +1,76 @@
+from jinja2 import Template
+
+from bokeh.embed import file_html
+from bokeh.layouts import column
+from bokeh.models import Div, Paragraph
+from bokeh.resources import CDN
+from bokeh.util.browser import view
+
+template = Template("""
+<!DOCTYPE html>
+<html lang="en">
+    <head>
+        <meta charset="utf-8">
+        <title>{{ title if title else "Bokeh Plot" }}</title>
+        {{ bokeh_css }}
+        {{ bokeh_js }}
+        <style>
+        .custom {
+            border-radius: 0.5em;
+            padding: 1em;
+        }
+        .custom-1 {
+            border: 3px solid #2397D8;
+        }
+        .custom-2 {
+            border: 3px solid #14999A;
+            background-color: whitesmoke;
+        }
+        </style>
+    </head>
+    <body>
+        {{ plot_div|indent(8) }}
+        {{ plot_script|indent(8) }}
+    </body>
+</html>
+""")
+
+p = Paragraph(text="The divs below were configured with additional css_classes:")
+
+div1 = Div(text="""
+<p> This Bokeh Div adds the style classes:<p>
+<pre>
+.custom {
+    border-radius: 0.5em;
+    padding: 1em;
+}
+.custom-1 {
+    border: 3px solid #2397D8;
+}
+</pre>
+""")
+div1.css_classes = ["custom", "custom-1"]
+
+div2 = Div(text="""
+<p> This Bokeh Div adds the style classes:<p>
+<pre>
+.custom {
+    border-radius: 0.5em;
+    padding: 1em;
+}
+.custom-2 {
+    border: 3px solid #14999A;
+    background-color: whitesmoke;
+}
+</pre>
+""")
+div2.css_classes = ["custom", "custom-2"]
+
+html = file_html(column(p, div1, div2), template=template, resources=CDN)
+
+output_file = 'css_classes.html'
+
+with open(output_file, 'w') as f:
+    f.write(html)
+
+view(output_file)


Test patch:
diff --git a/bokehjs/test/models/layouts/layout_dom.coffee b/bokehjs/test/models/layouts/layout_dom.coffee
--- a/bokehjs/test/models/layouts/layout_dom.coffee
+++ b/bokehjs/test/models/layouts/layout_dom.coffee
@@ -46,6 +46,12 @@ describe "LayoutDOMView", ->
       layout_view = new LayoutDOMView({ model: @test_layout })
       expect(layout_view.$el.attr('class')).to.be.equal 'bk-layout-scale_height'
 
+    it "should set classes from css_classes", ->
+      @test_layout.sizing_mode = 'fixed'
+      @test_layout.css_classes = ['FOO', 'BAR']
+      layout_view = new LayoutDOMView({ model: @test_layout })
+      expect(layout_view.$el.attr('class')).to.be.equal 'bk-layout-fixed FOO BAR'
+
     it "should set an id matching the model.id", ->
       # This is used by document to find the model and its parents on resize events
       layout_view = new LayoutDOMView({ model: @test_layout })