20200924のCSSに関する記事は8件です。

(´ω`) 箇条書きを樹形図にするアプリを作った(ドッキングパネルを添えて...

わいや。おっさんとはわいのことや。おっさんが言ったことの全てが間違っているとは思わんが確かに受け入れにくい"きもちわるさ"みたいなものを感じ取ったと思う。殺伐とした世の中で、そういう気持ちを忘れないで欲しい。そう思ってこの記事を書いたんだ。って前置きはそのぐらいにして今のとこ毎日書きまくってるな。書きなぐって丸めてぽいするブログみたいな感覚だ。マークダウンも##と```のハイライトしか使ってない。フォロワーが増えることはもうないと悟った。4記事目にして住人として世界の無常さを悟るが如く俺の書く記事ではフォロワーが増えないという事実の目の当たりにし愕然とした。希望などない。だから俺が希望になるよ。QIITAに変わるサービスを作ろうと思った。って前置きを入れて解説してくよ。

何作った

image.png

箇条書き。ってよく書くはずだ。学生でも社会人でも隣のおばちゃん家もそうさ。多分誰もが書く。1マス空けて。2マス開けて...それを樹形図に出来たら更に判りやすくなるんじゃねぇかな。って発想で作った。議事録もメモ程度なら箇条書きにする。するとこうなる...

縦に長くなる箇条書きより判りやすい

image.png

機能について

save tree imageを押せばSVG形式でダウンロードできる。
save indented textを押せばTXT形式でダウンロードできる。
木の高さも深さも調整可能だ。ドラッグでパンできてホイールでズームできる。
image.png
resize:bothしてあるおかげでウィンドウ自体も拡縮できる
ドッキングパネル化するフレームワークのおかげでエディタもビュアーも移動可能だ
image.png

利用しているライブラリ

入力/設定/出力とドッキングパネル化する (golden-layout)
テキストエリアをタブ入力可能にする (TabIndent.js)
ウィンドウのリサイズを検知する (resize-event)
箇条書きをjSON化する (indent2obj)
樹形図を出力する

ソース

sample.html
<!doctype html>
 <html>
  <head>

   <title></title>

   <meta charset='utf-8'>
   <meta content='' name='author'>
   <meta content='' name='application-name'>
   <meta content='' name='description'>
   <meta content='telephone=no,address=no,email=no,date=no,url=no' name='format-detection'>
   <meta content='noimageindex,notranslate,nosnippet,noarchive,nofollow,noindex' name='robots'>
   <meta content='width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no' name='viewport'>

   <link href='js/jquery/golden-layout/theme/base.css' rel='stylesheet'>
   <link href='asset/manifest.json' rel='manifest'>
   <link href='asset/favicon.ico' rel='icon'>

   <style>
     @font-face{
       font-family:'M+2VM+IPAG circle';
       src:url('asset/m+2vm+ipag-circle.ttf');
     }

     html,
     body{
       margin:0;
       width:100%;
       height:100%;
       font-size:12px;
     }
     /*
      * 中央
      */
     main{
       top:50%;
       left:50%;
       position:absolute;
       transform:translate(-50%,-50%);
     }
     /*
      * 背景
      */
     .lm_goldenlayout .lm_content{
       background:white;
     }
     /*
      * 変更
      */
     main .wrap{
       width:80vw;
       height:80vh;
       resize:both;
       overflow:scroll;
     }
     /*
      * 見栄
      */
     main .wrap{
       border-bottom:2px solid rgba(34,36,38,.15);
       box-shadow:rgba(16, 36, 94, 0.4) 0 2px 6px 0;
     }
     /*
      * 設定
      */
     .proper .dg,
     .proper .dg .close-button{
         width:100% !important;
     }
     .proper .dg.main .close-button{
       display:none;
     }
  </style>
 </head>
  <body>
   <main>
     <div id='ui' class='wrap'>
    </div>
  </main>
 </body>
</html>

 <!-- native -->
 <script src='js/native/d3-5.12.0.min.js'></script>
 <script src='js/native/dat-gui/0.7.6.min.js'></script>
 <script src='js/native/tab-indent-0.1.8.min.js'></script>
 <script src='js/native/indent2obj-0.0.3.min.js'></script>

 <!-- jquery -->
 <script src='js/jquery/3.4.1.min.js'></script>
 <script src='js/jquery/golden-layout/1.5.9.min.js'></script>
 <script src='js/jquery/resize-event-1.2.1.min.js'></script>
 <script>

   var app={
     layout:{
       content:
       [
         {
           type:'row',
           content:
           [
             {
               width:29.3,
               type:'column',
               content:
               [
                 {type:'component',componentName:'view',title:'editor'},
                 {type:'component',componentName:'view',title:'proper'}
               ]
             },
             {
               width:70.7,
               type:'stack',
               content:
               [
                 {type:'component',componentName:'view',title:'viewer'}
               ]
             }
           ]
         }
       ]
     },
     plugin:{
       editor:{
         elem:'<textarea></textarea>',
         option:{
           width:'100%',
           height:'100%',
           addClass:'tabIndent',
           css:{
             font:"12px 'M+2VM+IPAG circle",
             outline:'none',
             border:'none'
           }
         }
       },
       viewer:{
         elem:'<div></div>',
         option:{
           id:'svg',
           addClass:'tree',
           css:{
             background:'white'
           }
         }
       }
     },
     config:{
       save:function(){
         localStorage.setItem('tree',$editor.val())
       },
       download:function(){
        var data = d3.select('svg').node().outerHTML
        var blob = new Blob([data])
        var link = document.createElement("a")
            link.href = URL.createObjectURL(blob)
            link.download = new Date().toISOString() + '.svg'
            link.click()
       },
       height:0.3,
       width:11,
     }
   }

   var $ui = $('#ui')

   /*
    * 最低限
    */
   var dat = new dat.GUI({autoPlace:false})
   var golden = new GoldenLayout(app.layout,$ui)
       // GOLDENを初期化()すると呼び出されるDOM挿入先の$コンテナを準備
       golden.registerComponent('view',function(container,state){
         var key = container._config.title,
             ele = container.getElement()
             ele.addClass(key)
             golden[key]=ele
       })

   /*
    * 1.初期化
    */
   function init(){
     golden.init()
   }

   /*
    * DOM生成
    */
   var $editor = $(app.plugin.editor.elem,app.plugin.editor.option),
       $viewer = $(app.plugin.viewer.elem,app.plugin.viewer.option),
       $proper = $(this.dat.domElement)
       $editor.on('keyup',function(){
         update()
       })

   /*
    * 2.GOLDENにDOMを構築
    */
   function construct(){
     // 生成したDOMを$コンテナに挿入
     golden['proper'].append($proper)
     golden['editor'].append($editor)
     golden['viewer'].append($viewer)
   }

   /*
    * 3.ビュアーをセットアップ
    */
   function setup(){
     dat.add(app.config,'height').onChange(update).name('tree height')
     dat.add(app.config,'width').onChange(update).name('tree width')
     dat.add(app.config,'download').name('save tree image')
     dat.add(app.config,'save').name('save indented text')
     $(window).on('resize',function(){
       golden.updateSize()
     })
     $ui.onResize({},function(){
       golden.updateSize()
     })
   }

   /*
    * ツリーはSVG
    */
   var view = d3.select($viewer.get(0))
                .append('svg')
       view.append('g')
       view.attr("xmlns", "http://www.w3.org/2000/svg")
       view.append('style')
           .text(`
             circle
             {
               stroke-width:2px;
               stroke:#05668D;
               fill:white;
               r:6;
             }
             text
             {
               font:12px 'M+2VM+IPAG circle';
             }
             rect
             {
               transform:translateY(-5px);
               stroke:#0cf;
               width:10px;
               heiht:10px;
               fill:white;
             }
             path
             {
               storke-width:2px;
               stroke:#ccc;
               fill:none;
             }
           `)

   function getLinks(arr)
   {
     return arr
       .enter()
       .append('path')
       .attr('d',
         d3.linkHorizontal()
           .x(function(d){ return d.y })
           .y(function(d){ return d.x })
       )
   }
   function getNodes(arr)
   {
     return arr
        .enter()
        .append('g')
        .attr('class',function(d){ 
           return 'node ' + (d.children ? 'node--internal' : 'node--leaf')
        })
        .attr('transform',function(d){
          return "translate(" + d.y + "," + d.x + ")"; 
        })
   }

   function update()
   {
     var w = golden.viewer.width()
     var h = golden.viewer.height()

     view
         .attr('width',w)
         .attr('height',h)

     var g = d3.select('svg > g')  
             d3.select('svg > g').selectAll("*").remove()

     view.call(d3.zoom().on('zoom',function(){
         g.attr('transform',d3.event.transform)
       })
     )

     var vData    = indent2obj($editor.val(),'\t')
     var vRoot    = d3.hierarchy(vData[0]);
     var vNodes   = vRoot.descendants();
     var vLayout  = d3.tree().size([
       app.config.width * vNodes.reverse().length,
       app.config.height * h
     ]);
     var vLinks   = vLayout(vRoot).links();
     var maxDepth = d3.max(vNodes,function(d) {
       return d.depth
     })

    var links = getLinks(g.selectAll('.link').data(vLinks))
    var nodes = getNodes(g.selectAll(".node").data(vNodes))
        nodes.append('circle')
        nodes.append('text')
             .attr('dy','.35em')
             .attr('x',function(d){
               return d.children ? -13 : 13; 
             })
             .attr('text-anchor',function(d){
               return d.children || d._children ? 'end' : 'start'
             })
             .text(function(d){
               return d.data.name
             })
   }

   document.addEventListener('DOMContentLoaded',function(){
     init()
     construct()
     setup()

     if('tree' in localStorage){
       $editor.val(
         localStorage.getItem('tree')
       )
       update()
     }

     tabIndent.config.tab = '\t';
     tabIndent.renderAll()
   })
</script>
 <style>
  @import url('js/jquery/golden-layout/theme/light.css');
  @import url('js/native/dat-gui/theme/light.min.css');
</style/>

 備考

ウィンドウの陰影が恰好よくねぇ?

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

(´ω`) 箇条書き?樹形図にしたら見やすいっておっさんがいっとった

わいや。おっさんとはわいのことや。おっさんが言ったことの全てが間違っているとは思わんが確かに受け入れにくい"きもちわるさ"みたいなものを感じ取ったと思う。殺伐とした世の中で、そういう気持ちを忘れないで欲しい。そう思ってこの記事を書いたんだ。って前置きはそのぐらいにして今のとこ毎日書きまくってるな。書きなぐって丸めてぽいするブログみたいな感覚だ。マークダウンも##と```のハイライトしか使ってない。フォロワーが増えることはもうないと悟った。4記事目にして住人として世界の無常さを悟るが如く俺の書く記事ではフォロワーが増えないという事実の目の当たりにし愕然とした。希望などない。だから俺が希望になるよ。QIITAに変わるサービスを作ろうと思った。って前置きを入れて解説してくよ。

何作った

image.png

箇条書き。ってよく書くはずだ。学生でも社会人でも隣のおばちゃん家もそうさ。多分誰もが書く。1マス空けて。2マス開けて...それを樹形図に出来たら更に判りやすくなるんじゃねぇかな。って発想で作った。議事録もメモ程度なら箇条書きにする。するとこうなる...

縦に長くなる箇条書きより判りやすい

image.png

機能について

save tree imageを押せばSVG形式でダウンロードできる。
save indented textを押せばTXT形式でダウンロードできる。
木の高さも深さも調整可能だ。ドラッグでパンできてホイールでズームできる。
image.png
resize:bothしてあるおかげでウィンドウ自体も拡縮できる
ドッキングパネル化するフレームワークのおかげでエディタもビュアーも移動可能だ
image.png

利用しているライブラリ

入力/設定/出力とドッキングパネル化する (golden-layout)
テキストエリアをタブ入力可能にする (TabIndent.js)
ウィンドウのリサイズを検知する (resize-event)
箇条書きをjSON化する (indent2obj)

ソース

sample.html
<!doctype html>
 <html>
  <head>

   <title></title>

   <meta charset='utf-8'>
   <meta content='' name='author'>
   <meta content='' name='application-name'>
   <meta content='' name='description'>
   <meta content='telephone=no,address=no,email=no,date=no,url=no' name='format-detection'>
   <meta content='noimageindex,notranslate,nosnippet,noarchive,nofollow,noindex' name='robots'>
   <meta content='width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no' name='viewport'>

   <link href='js/jquery/golden-layout/theme/base.css' rel='stylesheet'>
   <link href='asset/manifest.json' rel='manifest'>
   <link href='asset/favicon.ico' rel='icon'>

   <style>
     @font-face{
       font-family:'M+2VM+IPAG circle';
       src:url('asset/m+2vm+ipag-circle.ttf');
     }

     html,
     body{
       margin:0;
       width:100%;
       height:100%;
       font-size:12px;
     }
     /*
      * 中央
      */
     main{
       top:50%;
       left:50%;
       position:absolute;
       transform:translate(-50%,-50%);
     }
     /*
      * 背景
      */
     .lm_goldenlayout .lm_content{
       background:white;
     }
     /*
      * 変更
      */
     main .wrap{
       width:80vw;
       height:80vh;
       resize:both;
       overflow:scroll;
     }
     /*
      * 見栄
      */
     main .wrap{
       border-bottom:2px solid rgba(34,36,38,.15);
       box-shadow:rgba(16, 36, 94, 0.4) 0 2px 6px 0;
     }
     /*
      * 設定
      */
     .proper .dg,
     .proper .dg .close-button{
         width:100% !important;
     }
     .proper .dg.main .close-button{
       display:none;
     }
  </style>
 </head>
  <body>
   <main>
     <div id='ui' class='wrap'>
    </div>
  </main>
 </body>
</html>

 <!-- native -->
 <script src='js/native/d3-5.12.0.min.js'></script>
 <script src='js/native/dat-gui/0.7.6.min.js'></script>
 <script src='js/native/tab-indent-0.1.8.min.js'></script>
 <script src='js/native/indent2obj-0.0.3.min.js'></script>

 <!-- jquery -->
 <script src='js/jquery/3.4.1.min.js'></script>
 <script src='js/jquery/golden-layout/1.5.9.min.js'></script>
 <script src='js/jquery/resize-event-1.2.1.min.js'></script>
 <script>

   var app={
     layout:{
       content:
       [
         {
           type:'row',
           content:
           [
             {
               width:29.3,
               type:'column',
               content:
               [
                 {type:'component',componentName:'view',title:'editor'},
                 {type:'component',componentName:'view',title:'proper'}
               ]
             },
             {
               width:70.7,
               type:'stack',
               content:
               [
                 {type:'component',componentName:'view',title:'viewer'}
               ]
             }
           ]
         }
       ]
     },
     plugin:{
       editor:{
         elem:'<textarea></textarea>',
         option:{
           width:'100%',
           height:'100%',
           addClass:'tabIndent',
           css:{
             font:"12px 'M+2VM+IPAG circle",
             outline:'none',
             border:'none'
           }
         }
       },
       viewer:{
         elem:'<div></div>',
         option:{
           id:'svg',
           addClass:'tree',
           css:{
             background:'white'
           }
         }
       }
     },
     config:{
       save:function(){
         localStorage.setItem('tree',$editor.val())
       },
       download:function(){
        var data = d3.select('svg').node().outerHTML
        var blob = new Blob([data])
        var link = document.createElement("a")
            link.href = URL.createObjectURL(blob)
            link.download = new Date().toISOString() + '.svg'
            link.click()
       },
       height:0.3,
       width:11,
     }
   }

   var $ui = $('#ui')

   /*
    * 最低限
    */
   var dat = new dat.GUI({autoPlace:false})
   var golden = new GoldenLayout(app.layout,$ui)
       // GOLDENを初期化()すると呼び出されるDOM挿入先の$コンテナを準備
       golden.registerComponent('view',function(container,state){
         var key = container._config.title,
             ele = container.getElement()
             ele.addClass(key)
             golden[key]=ele
       })

   /*
    * 1.初期化
    */
   function init(){
     golden.init()
   }

   /*
    * DOM生成
    */
   var $editor = $(app.plugin.editor.elem,app.plugin.editor.option),
       $viewer = $(app.plugin.viewer.elem,app.plugin.viewer.option),
       $proper = $(this.dat.domElement)
       $editor.on('keyup',function(){
         update()
       })

   /*
    * 2.GOLDENにDOMを構築
    */
   function construct(){
     // 生成したDOMを$コンテナに挿入
     golden['proper'].append($proper)
     golden['editor'].append($editor)
     golden['viewer'].append($viewer)
   }

   /*
    * 3.ビュアーをセットアップ
    */
   function setup(){
     dat.add(app.config,'height').onChange(update).name('tree height')
     dat.add(app.config,'width').onChange(update).name('tree width')
     dat.add(app.config,'download').name('save tree image')
     dat.add(app.config,'save').name('save indented text')
     $(window).on('resize',function(){
       golden.updateSize()
     })
     $ui.onResize({},function(){
       golden.updateSize()
     })
   }

   /*
    * ツリーはSVG
    */
   var view = d3.select($viewer.get(0))
                .append('svg')
       view.append('g')
       view.attr("xmlns", "http://www.w3.org/2000/svg")
       view.append('style')
           .text(`
             circle
             {
               stroke-width:2px;
               stroke:#05668D;
               fill:white;
               r:6;
             }
             text
             {
               font:12px 'M+2VM+IPAG circle';
             }
             rect
             {
               transform:translateY(-5px);
               stroke:#0cf;
               width:10px;
               heiht:10px;
               fill:white;
             }
             path
             {
               storke-width:2px;
               stroke:#ccc;
               fill:none;
             }
           `)

   function getLinks(arr)
   {
     return arr
       .enter()
       .append('path')
       .attr('d',
         d3.linkHorizontal()
           .x(function(d){ return d.y })
           .y(function(d){ return d.x })
       )
   }
   function getNodes(arr)
   {
     return arr
        .enter()
        .append('g')
        .attr('class',function(d){ 
           return 'node ' + (d.children ? 'node--internal' : 'node--leaf')
        })
        .attr('transform',function(d){
          return "translate(" + d.y + "," + d.x + ")"; 
        })
   }

   function update()
   {
     var w = golden.viewer.width()
     var h = golden.viewer.height()

     view
         .attr('width',w)
         .attr('height',h)

     var g = d3.select('svg > g')  
             d3.select('svg > g').selectAll("*").remove()

     view.call(d3.zoom().on('zoom',function(){
         g.attr('transform',d3.event.transform)
       })
     )

     var vData    = indent2obj($editor.val(),'\t')
     var vRoot    = d3.hierarchy(vData[0]);
     var vNodes   = vRoot.descendants();
     var vLayout  = d3.tree().size([
       app.config.width * vNodes.reverse().length,
       app.config.height * h
     ]);
     var vLinks   = vLayout(vRoot).links();
     var maxDepth = d3.max(vNodes,function(d) {
       return d.depth
     })

    var links = getLinks(g.selectAll('.link').data(vLinks))
    var nodes = getNodes(g.selectAll(".node").data(vNodes))
        nodes.append('circle')
        nodes.append('text')
             .attr('dy','.35em')
             .attr('x',function(d){
               return d.children ? -13 : 13; 
             })
             .attr('text-anchor',function(d){
               return d.children || d._children ? 'end' : 'start'
             })
             .text(function(d){
               return d.data.name
             })
   }

   document.addEventListener('DOMContentLoaded',function(){
     init()
     construct()
     setup()

     if('tree' in localStorage){
       $editor.val(
         localStorage.getItem('tree')
       )
       update()
     }

     tabIndent.config.tab = '\t';
     tabIndent.renderAll()
   })
</script>
 <style>
  @import url('js/jquery/golden-layout/theme/light.css');
  @import url('js/native/dat-gui/theme/light.min.css');
</style/>

 備考

ウィンドウの陰影が恰好よくねぇ?

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Tyハロトレ26日目

CSS

タイピング練習で月一回提出

P検ーマナビジョン

疑似要素

<p><a href="#">未閲覧は青色</a></p>
<p><a href="#">閲覧済みは灰色</a></p>
<p><a href="#">マウスポインタが重なっている</a></p>
<p><a href="#">クリックされている(押されている)間は赤色</a></p>

疑似要素2.png

優先順位

読み込まれる順番で最新のものが優先される
外部CSS < ヘッド < 本文

!important

index.html
<p style="color: blue;">山田</p>

 !importantは、index.htmlに書いたCSSよりも優先されます。
important2.png

font-family

font-family3.png

text-align

text-align.png

vertical-align

vertical.png

line-height

line-height.png

おうちをイラレで描く

オブジェクト2.png

オブジェクト>パス>平均
平均2.png

連結2.png

全てを表示2.png

平行四辺形2.png

尖り2.png

はさみ、消しゴム、ナイフツール

hasami2.png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

CSSのみでタブとタブコンテンツの表示・非表示を切り替える

HTMLコーダーさんのJSを使わないタブ切り替えが、シンプルな実装で素晴らしかったので忘備録的にメモ。

概要

  • タブは『インプットボックス』+『ラベル』を使用
  • コンテンツエリアは常時非表示にしておく
  • コンテンツエリアは選択されたときのみ表示する

サンプルコード

動かしてないので不具合あるかもですが、こんな感じでした。

<div class="tabs">
  <!-- タブ -->
  <input id="tab_1" type="radio" name="tab" checked="">
  <label class="tab_label" for="tab_1">月別</label>
  <input id="tab_2" type="radio" name="tab">
  <label class="tab_label" for="tab_2">週間別</label>
  <input id="tab_3" type="radio" name="tab">
  <label class="tab_label" for="tab_3">曜日別</label>

  <div class="tab_content" id="content_1">コンテンツ1</div>
  <div class="tab_content" id="content_2">コンテンツ2</div>
  <div class="tab_content" id="content_3">コンテンツ3</div>
</div>
input[name="tab"] {
    display: none;
}

.tabs {
  width: 100%;
}

.tab_label {
  border-top: 1px solid #999999;
  border-bottom: 1px solid #999999;
}

.tabs input:checked + .tab_label {
  border-bottom: 2px solid #55C501;
}

#tab_1:checked ~ #content_1,
#tab_2:checked ~ #content_2,
#tab_3:checked ~ #content_3 {
  display: block;
}

.tab_content {
  display: none;
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

プログラミング初めて3ヶ月

はじめまして!
shikichanと言います。

未経験でWEB系企業に転職する為、6月半ばからプログラミングを始めました!
まずは、progateでHTML&CSSを始めました。初学者にはこれがいいそうです。
54798AC7-69BB-4987-999F-81D39159C1C7_1_201_a.jpeg
これは7月途中のものです。ボチボチ受講しています。

1ヶ月半でだいたいlev.200までいけました。そろそろアウトプット出さないといけないと思い、8月からポートフォリオの作成に取り掛かっています。

これからも引き続きよろしくお願いします。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【初心者でもわかる】CSSだけ。カーソルを載せたら補足説明のバルーンポップアップを出す方法

どうも7noteです。?マークにカーソルを当てると、補足説明が出るやつ作ります。

こういうやつ、作ります。

sample.png

最初はdisplay: none;で非表示にしておき、hover時に表示するような作りにします。

作り方

1) ハテナ(?)を用意。

index.html
<div class="box">
  <div class="ques">?</div>
</div>

簡単に装飾を入れます。

style.css
.ques {
  background: #EEE;    /* 背景色に灰色を指定 */
  width: 1.5em;        /* 横幅を1.5文字分にする */
  line-height: 1.5em;  /* 文字が上下中央にくるように工夫 */
  text-align: center;  /* 文字が左右中央にくるように工夫 */
  border-radius: 50%;  /* 円形に変更 */
}

今こんなかんじ↓
?.png

2) 補足説明のバルーンポップアップを作る。(動きはまだ。)

index.html
<div class="box">
  <div class="ques">?</div>
  <!-- 以下の1行を追加。 -->
  <div class="ex">ここに補足説明文を入れる</div>
</div>
style.css
.box {
  position: relative;  /* 表示位置の基準値とする */
}

.ex {
  position: absolute;  /* boxを基準にする */
  top: 0;        /* 自由に調整 */
  left: 30px;      /* 自由に調整 */
  color: #fff;         /* 文字色を白にする */
  font-size: 14px;
  background: rgba(0,0,0,0.5);  /* 黒バックを半透明にするため、rgbaで指定 */
  padding: 2px 5px;             /* 余白を少々 */
}

3) hover時の動きを入れる。

style.css
.ques:hover {
  cursor: pointer; /* カーソルを指の形にする。 */
}

.ex {
  display: none;  /* 最初は非表示にする。 */
}

.ques:hover + .ex {
  display: block;  /* quesの上にカーソルが乗っている時だけ表示 */
}

+α)ふわっと出したい場合は、以下のように変更。

.ex {
  display: none;
  /*↓に変更*/
  opacity: 0;
  transition : all .3s;
}

.ques:hover + .ex {
  display: block;
  /*↓に変更*/
  opacity: 1;
}

完成!

movie.gif

まとめ

結構簡単に作れます。
表示位置だけtopとleftでpx指定しているので、実際に運用レベルで使うには表示位置の指定方法は少してを加えたほうがよいかも。
ハテナ(?)の位置がかわらなければ今回の方法で十分対応可能。

おそまつ!

~ Qiitaで毎日投稿中!! ~
【初心者向け】HTML・CSSのちょいテク詰め合わせ

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【初心者でもわかる】CSSだけ。カーソルを載せたら補足説明のミニモーダルを出す方法

どうも7noteです。?マークにカーソルを当てると、補足説明が出るやつ作ります。

こういうやつ、作ります。

sample.png

最初はdisplay: none;で非表示にしておき、hover時に表示するような作りにします。

作り方

1) ハテナ(?)を用意。

index.html
<div class="box">
  <div class="ques">?</div>
</div>

簡単に装飾を入れます。

style.css
.ques {
  background: #EEE;    /* 背景色に灰色を指定 */
  width: 1.5em;        /* 横幅を1.5文字分にする */
  line-height: 1.5em;  /* 文字が上下中央にくるように工夫 */
  text-align: center;  /* 文字が左右中央にくるように工夫 */
  border-radius: 50%;  /* 円形に変更 */
}

今こんなかんじ↓
?.png

2) 補足説明のミニモーダルを作る。(動きはまだ。)

index.html
<div class="box">
  <div class="ques">?</div>
  <!-- 以下の1行を追加。 -->
  <div class="ex">ここに補足説明文を入れる</div>
</div>
style.css
.box {
  position: relative;  /* 表示位置の基準値とする */
}

.ex {
  position: absolute;  /* boxを基準にする */
  top: 0;        /* 自由に調整 */
  left: 30px;      /* 自由に調整 */
  color: #fff;         /* 文字色を白にする */
  font-size: 14px;
  background: rgba(0,0,0,0.5);  /* 黒バックを半透明にするため、rgbaで指定 */
  padding: 2px 5px;             /* 余白を少々 */
}

3) hover時の動きを入れる。

style.css
.ques:hover {
  cursor: pointer; /* カーソルを指の形にする。 */
}

.ex {
  display: none;  /* 最初は非表示にする。 */
}

.ques:hover + .ex {
  display: block;  /* quesの上にカーソルが乗っている時だけ表示 */
}

+α)ふわっと出したい場合は、以下のように変更。

.ex {
  display: none;
  /*↓に変更*/
  opacity: 0;
  transition : all .3s;
}

.ques:hover + .ex {
  display: block;
  /*↓に変更*/
  opacity: 1;
}

完成!

movie.gif

まとめ

結構簡単に作れます。
表示位置だけtopとleftでpx指定しているので、実際に運用レベルで使うには表示位置の指定方法は少してを加えたほうがよいかも。
ハテナ(?)の位置がかわらなければ今回の方法で十分対応可能。

おそまつ!

~ Qiitaで毎日投稿中!! ~
【初心者向け】HTML・CSSのちょいテク詰め合わせ

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

amp-img から学ぶ画像の表示のベストプラクティス

AMP は Google が推奨しているウェブコンポーネントフレームワークで、その実装には Web サイトのパフォーマンスを向上させるための知見が詰まっています。
AMP コンポーネントの実装を詳しく見ていくと、 AMP を導入せずにサイトを作る際にも役に立つベストプラクティスを学べるのではないかと思います。
以下では amp-img コンポーネントに注目して web サイト開発における画像表示の実装について掘り下げます。

amp-img

amp-img は AMP 対応のサイトで画像を表示する際に、 HTML の img タグの代わりに使用します。
AMP の built-in 要素であるため、 amp-img 専用の js を追加で読む必要はなく、AMP のランタイムを通して自動的に使用できます。

amp-img の基本の使い方

<amp-img src="/static/sample.jpg" width="1080" height="720" layout="fixed" alt="sample"></amp-img>

参考

画面の解像度に合わせた画像を表示

HTML の img タグと同様に amp-img タグでもsrcset 属性や size属性を使用することができます。
高解像度の画面(Retina ディスプレイなど)で高解像な画像を配信する場合きれいな画像を表示できるメリットがありますが、低解像度の画面でも必要以上に重い画像を時間をかけて読み込まなくてはなりません。
srcset 属性や size 属性を使用することで、画面の解像度に合わせて適切な画像を表示することができます。

<amp-img alt="sample"
  src="/static/sample-640.jpg"
  width="640"
  height="400"
  srcset="/static/sample-640.jpg 640w,
          /static/sample-320.jpg 320w"
  sizes="(min-width: 650px) 50vw, 100vw">
</amp-img>

アートディレクション

ブレークポイントの条件に合わせて画像を変更することをアートディレクションと言います。
amp-img では media 属性によってブレークポイントを指定して画像自体を切り替えることができます。
srcset属性 と size 属性による画像の切り替えは、基本的に解像度のみ異なる同一の画像を切り替えることが想定されていますが、media 属性の場合は画像のアスペクト比が異なっていても問題ありません。

<amp-img alt="sample"
  media="(max-width: 768px)"
  width="226"
  height="340"
  src="/static/sample-medium.jpg"></amp-img>
<amp-img alt="sample"
  media="(mim-width: 769px)"
  width="450"
  height="340"
  src="/static/sample-small.jpg"></amp-img>

通常の HTML タグでアートディレクションを実現する場合、picture タグを使用します。1

<picture>
  <source srcset="/static/sample-medium.jpg" media="(max-width: 768px)">
  <source srcset="/static/sample-small.jpg" media="(mim-width: 769px)">
  <img src="/static/sample-small.jpg" alt="sample">
</picture>

参考

遅延ロード

画像の遅延ロードは、画面に表示されていない画像は読み込まずに、画面のスクロールに応じてあとから画像を読み込むことでウェブページの表示を高速化する手法です。
通常の img タグを使用した場合、ページアクセス時にすべての img タグの画像を読み込みます。
これによりページが最低限の操作を受け付けるようになるまでに時間がかかってしまったり、スクロールしない場合に不要な通信が発生したり、といったことが起きます。

amp-img は画面のスクロール位置に応じて、amp-img タグ内に img タグを生成します。この時に初めて画像の取得のための通信が発生するため効率的に画像をロードすることができます。

<amp-img src="/static/sample.jpg" width="1080" height="720" layout="fixed" alt="sample"></amp-img>

<amp-img src="/static/sample.jpg" width="1080" height="720" layout="fixed" alt="sample" class="i-amphtml-element i-amphtml-layout-fixed i-amphtml-layout-size-defined i-amphtml-layout" i-amphtml-layout="fixed" style="width: 1080px; height: 610px;">
  <img decoding="async" alt="sample" src="/static/sample.jpg" class="i-amphtml-fill-content i-amphtml-replaced-content">
</amp-img>

amp-img を使用せずに遅延ロードを行う場合、スクラッチで実装するほか、lazyloadなどのライブラリの利用や、img タグの loading 属性を使用する方法があります。(ただし、Safari と IE11 は未対応

非同期で画像をデコード

展開されたコードの img タグを見ると、decoding="async"の指定があります。
画像データは通常ファイルサイズを小さくするためにエンコードされているため、ブラウザは画像を表示する際はデータをデコードする必要があります。
img タグに decoding 属性によってブラウザに同期/非同期のどちらでデコードするかのヒントを提供でき、全てのモダンブラウザに対応しています。1
decoding 属性に指定できる値は下記の通りです。

説明
sync 画像を同期的にデコードする
async 画像を非同期でデコードする
auto 優先設定なし(デフォルト値)

amp-img コンポーネントは、img タグにdecoding="async"を付与することで、メインスレッドの処理をブロックせずに画像を非同期的にデコードさせています。

参考

画像のフォールバック

amp-img では、例えば webp の画像を使用する際に、webp 未対応のブラウザ用に jpg の画像を設定したい場合など、画像の読み込みに失敗した際のフォールバック画像を指定することができます。

<amp-img alt="sample" width="1080" height="720" src="/static/sample.webp">
  <amp-img alt="sample" fallback width="1080" height="720" src="/static/samples.jpg"></amp-img>
</amp-img>

amp-img を使用しない場合、picture タグを使用することで実現できます。

<picture>
  <source type="image/webp" srcset="/static/sample.webp" /> // wepb 対応の場合は source タグの画像を提供
  <img src="/static/samples.jpg" width="1080" height="720" alt="sample" />  // wepb 未対応ブラウザで source タグの画像を提供できない場合の代替画像
</picture>

参考
- 画像要素 - HTML: HyperText Markup Language | MDN

レイアウトシフト対策

Google の提唱する UX 指標である Core Web Vitals の一つに Cumulative Layout Shift(CLS) があります。
これは予期せぬレイアウトのズレや崩れを独自に指標化し評価しているもので、画像が読み込まれた際のこのレイアウトの移動によってスコアが下がってしまいます。
amp-img コンポーネントは、画面幅によって画像の幅 / 高さが変化しない場合、画像を表示する明示的なサイズを widthheight に指定することで amp-img のインラインスタイルとして展開され、レイアウト時に画像の表示領域が確保されます。2
画面幅によって画像の表示サイズが変化する場合は、適切なスタイルを付与することによりレイアウトシフトを防いでいます(後述)。

通常の img タグの場合もwidth / height属性への値を指定や、CSS でwidth / heightに具体値を指定することで画像を読み込む前に場所を確保し、レイアウトシフトを防ぐことができます。
2019 年 10 月に WHATWG が img タグの widht / height 属性に基づいてデフォルトのアスペクト比を設定できる仕様が標準化されました。
そのため現在のモダンブラウザでは、画面の幅によって画像の表示サイズが変化するレスポンシブな表示でも widthheight 利用してレイアウトシフトを防ぐことができます。

例えば、下記のようにな img タグを設置した場合、3 : 2 のアスペクト比でブラウザが表示場所を計算して確保してくれます。

<img src="/static/samples.jpg" width="1080" height="720" alt="sample">
img {
  width: 100%;
  height: auto;
}

参考

レスポンシブ対応のスタイル

layout 属性

amp-img コンポーネントは layout 属性を指定することにより、AMP のレイアウトシステムを利用できます。
layout 属性は AMP コンポーネント共通の属性ですが、コンポーネントによってサポートされる値が異なります。
amp-img コンポーネントは container 以外の下記のレイアウトに対応しています。

  • fill
  • fixed
  • fixed-height
  • responsive
  • flex-item
  • intrinsic
  • nodisplay

amp-img タグとその子要素の img タグには、layout 属性の指定に応じた class が指定されます。
レスポンシブ対応をする際に、ウインドウや親のコンテンツのサイズの変化に伴ってどのように画像の表示を変化させるのかは往々にして悩ましいものです。
そんな時に layout 属性による画像を様々な表示パターンとそのスタイルは参考になります。
下記ではそれぞれの layout の値を amp-img に指定した場合のスタイルを詳しく見ていきます。
なお、layout 属性は amp-img 専用の属性ではなく、AMP コンポーネント共通のものであるため、画像以外のコンテンツにも応用できます。
layout 値を指定した際のアニメーション画像は公式ドキュメントからお借りしました。

参考

layout="fill"

親要素のコンテンツボックス全体を埋めるように拡大縮小されます。
widthheight に指定したアスペクト比が親要素のアスペクト比と合わない場合は引き伸ばされて表示されます。
fill.gif

<amp-img src="/static/samples.jpg" width="1080" height="720" layout="fill" alt="sample"></amp-img>

↓ 画像ロード後

<amp-img src="/static/samples.jpg" layout="fill" alt="sample" class="i-amphtml-element i-amphtml-layout-fill i-amphtml-layout-size-defined i-amphtml-layout" i-amphtml-layout="fill" style="--loader-delay-offset:434ms !important;">
  <img decoding="async" alt="sample" src="/static/samples.jpg" class="i-amphtml-fill-content i-amphtml-replaced-content">
</amp-img>
/* amp-img タグのスタイル */
.i-amphtml-layout-size-defined {
  overflow: hidden!important;
}
.i-amphtml-layout-fixed {
  display: block;
  overflow: hidden!important; /* .i-amphtml-layout-size-defined のスタイルによって打ち消される */
  position: absolute;
  top: 0;
  left: 0;
  bottom: 0;
  right: 0;
}
/* img タグのスタイル */
.i-amphtml-layout-size-defined .i-amphtml-fill-content {
  position: absolute;
  top: 0;
  left: 0;
  bottom: 0;
  right: 0;
}
.i-amphtml-replaced-content {
  padding: 0!important;
  border: none!important;
}
.i-amphtml-replaced-content {
  padding: 0!important;
  border: none!important;
}
.i-amphtml-fill-content {
  display: block;
  height: 0;
  max-height: 100%;
  max-width: 100%;
  min-height: 100%;
  min-width: 100%;
  width: 0;
  margin: auto;
}

amp-img タグは .i-amphtml-layout-fixedに指定されているスタイルにより、position: absolute;であるため、直近のposition: static;以外の position の値を持つ親要素の上下左右いっぱいに広がるように表示されます。

img タグは .i-amphtml-layout-size-defined .i-amphtml-fill-content に指定されているスタイルにより、直近のposition: static;以外の position の値を持つ親要素、つまり amp-img の上下左右いっぱいに広がるように表示されます。
img が amp-img のサイズいっぱいになる構造は、どの layout を指定した場合でも共通しています。
layout="fill"の場合は amp-img タグと img タグが共にposition: absolute;であるため、基準となる親要素に高さを明示的に指定する必要があります。

.i-amphtml-replaced-contentに指定されているスタイルは、ユーザーエージェントスタイルを打ち消すためのスタイルです。
また、.i-amphtml-fill-contentに指定されているスタイルは、iOS で iframe をレスポンシブ対応させる際にコンテンツのサイズをコンテナーに収めるための指定のようです。(issue)
layout 属性は他のコンポーネントでも使用されるため、amp-img タグの場合でもこのようなスタイルが適応されます。
これらのスタイルは他の layout 値を指定した場合でも登場しますが、今回の内容には直接関係ないため以下では割愛します。

layout="fixed"

ウインドウのサイズが変化しても画像の大きさは変化せず、widthheight属性に基づいて固定の寸法のまま表示されます。

fixed.gif

<amp-img src="/static/samples.jpg" width="1080" height="720" layout="fixed" alt="sample"></amp-img>

↓ 画像ロード後

<amp-img src="/static/samples.jpg" width="1080" height="720" layout="fixed" alt="sample" class="i-amphtml-element i-amphtml-layout-fixed i-amphtml-layout-size-defined i-amphtml-layout" i-amphtml-layout="fixed" style="width: 1080px; height: 610px; --loader-delay-offset:226ms !important;">
  <img decoding="async" alt="sample" src="/static/samples.jpg" class="i-amphtml-fill-content i-amphtml-replaced-content">
</amp-img>
/* amp-img タグのスタイル */
element.style {
  width: 1080px;
  height: 610px;
  --loader-delay-offset: 188ms !important;
}
.i-amphtml-layout-size-defined {
  overflow: hidden!important;
}
.i-amphtml-layout-fixed {
  display: inline-block;
  position: relative;
}

amp-img タグにはインラインスタイルとして widthheight属性で指定した値が指定されています。
overflow: hidden; が指定されているため、画像の表示サイズよりウインドウが小さくなった場合もスクロールは発生せずに画像は見切れて表示されます。

layout="fixed-height"

高さは固定のまま、幅は使用可能なスペースに合わせて広がります。

fixed-height.gif

<amp-img src="/static/samples.jpg" height="720" layout="fixed-height" alt="sample"></amp-img>

↓ 画像ロード後

<amp-img src="/static/samples.jpg" layout="fixed-height" height="720" alt="sample" class="i-amphtml-element i-amphtml-layout-fixed-height i-amphtml-layout-size-defined i-amphtml-layout" i-amphtml-layout="fixed-height" style="height: 720px; --loader-delay-offset:377ms !important;">
  <img decoding="async" alt="sample" src="/static/samples.jpg" class="i-amphtml-fill-content i-amphtml-replaced-content">
</amp-img>
/* amp-img タグのスタイル */
element.style {
    height: 720px;
}
.i-amphtml-layout-size-defined {
  overflow: hidden!important;
}
.i-amphtml-layout-fixed-height {
  display: block;
  position: relative;
}

layout="responsive"

width / height属性で指定されたアスペクト比を維持したまま、使用可能なスペースに合わせて拡大・縮小します。

responsive.gif

<amp-img src="/static/samples.jpg" width="1080" height="720" layout="responsive" alt="sample"></amp-img>

↓ 画像ロード後

<amp-img src="/static/samples.jpg" layout="fixed-height" height="720" alt="sample" class="i-amphtml-element i-amphtml-layout-fixed-height i-amphtml-layout-size-defined i-amphtml-layout" i-amphtml-layout="fixed-height" style="--loader-delay-offset:377ms !important;">
  <i-amphtml-sizer slot="i-amphtml-svc" style="padding-top: 66.6667%;"></i-amphtml-sizer>
  <img decoding="async" alt="sample" src="/static/samples.jpg" class="i-amphtml-fill-content i-amphtml-replaced-content">
</amp-img>
/* amp-img タグのスタイル */
.i-amphtml-layout-size-defined {
    overflow: hidden!important;
}
/* i-amphtml-sizer タグのスタイル */
element.style {
    padding-top: 66.6667%;
}
.i-amphtml-sizer {
  display: block!important;
}  

layout="responsiveの場合、amp-img の直下に img タグに加えて i-amphtml-sizer タグが挿入されます。
amp-img タグの widthheight に指定した値から、画像のアスペクト比は 1080:720 = 3:2 となります。
このアスペクト比を維持した状態で、表示可能な領域内で画像を拡大・縮小させるために、2 / 3 * 100 ≒ 66.6667%;padding-top として与えています。
i-amphtml-sizer タグによって親要素の amp-img の大きさが確保され、その amp-img の大きさに合わせて img タグが広がることで、画像のアスペクト比を維持したまま画像が拡大・縮小されます。
この方法であれば、IE11 でもレイアウトシフトを防ぐことができます。

layout="flex-item"

フレックスボックスの親要素の直下でフレックスアイテムとして画像を扱いたい場合に使用します。
親要素のスタイルと兄弟要素の数によって画像の大きさが変化します。

flex-item.gif

<amp-img src="/static/samples.jpg" layout="flex-item" alt="sample"></amp-img>

↓ 画像ロード後

<amp-img src="/static/samples.jpg" layout="flex-item" class="i-amphtml-layout-flex-item i-amphtml-layout-size-defined i-amphtml-element i-amphtml-layout" i-amphtml-layout="flex-item" alt="sample">
  <img decoding="async" alt="sample" src="//static/samples.jpg" class="i-amphtml-fill-content i-amphtml-replaced-content">
</amp-img>
/* amp-img タグのスタイル */
.i-amphtml-layout-flex-item {
  display: block;
  position: relative;
  flex: 1 1 auto;
}

flex: 1 1 auto; が指定されているため、フレックスボックスの直下に layout="flex-item"の amp-img を隣接して配置して横に並べた場合、同じ幅で並びます。
また、兄弟要素がlayout="flex-item"の要素のみで高さを持たない場合は、親要素のフレックスボックスに高さを明示的に与える必要があります。

layout="intrinsic"

intrinsic.gif

画像自体の本来のサイズか CSS による制限(max-width など)に達するまで、widthheight属性で指定されたアスペクト比を維持したまま使用可能なスペースに合わせて拡大・縮小します。

<amp-img src="/static/samples.jpg" width="1080" height="720" layout="intrinsic" alt="sample"></amp-img>

↓ 画像ロード後

<amp-img src="/static/samples.jpg" width="1080" height="720" layout="intrinsic" alt="sample" class="i-amphtml-element i-amphtml-layout-intrinsic i-amphtml-layout-size-defined i-amphtml-layout" i-amphtml-layout="intrinsic" style="--loader-delay-offset:241ms !important;">
  <i-amphtml-sizer class="i-amphtml-sizer" slot="i-amphtml-svc">
    <img alt="" role="presentation" aria-hidden="true" class="i-amphtml-intrinsic-sizer" src="data:image/svg+xml;charset=utf-8,<svg height=&quot;720px&quot; width=&quot;1080px&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot; version=&quot;1.1&quot;/>">
  </i-amphtml-sizer>
  <img decoding="async" alt="sample" src="/static/samples.jpg" class="i-amphtml-fill-content i-amphtml-replaced-content">
</amp-img>
/* amp-img タグのスタイル */
.i-amphtml-layout-size-defined {
    overflow: hidden!important;
}
.i-amphtml-layout-intrinsic {
  display: inline-block;
  position: relative;
  max-width: 100%;
}
/* i-amphtml-sizer タグのスタイル */
.i-amphtml-layout-intrinsic .i-amphtml-sizer {
  max-width: 100%;
}
i-amphtml-sizer {
  display: block!important;
}
/* i-amphtml-sizer タグ直下の img タグのスタイル */
.i-amphtml-intrinsic-sizer {
  max-width: 100%;
  display: block!important;
}

layout="responsive"と同様に amp-img の直下に img タグに加えて i-amphtml-sizer タグが挿入されます。
しかし、layout="intrinsic"の場合は i-amphtml-sizer の直下にさらにもう一つ img タグが挿入されます。
layout="responsive"では、空の i-amphtml-sizer タグに指定された padding が画像の表示領域を確保していたのに対し、 layout="intrinsic"では i-amphtml-sizer タグの直下の img タグによって表示される透明な svg 画像によって画像の表示領域を確保します。
svg 画像のサイズは amp-img に指定した widthheight 属性によって決まり、img タグであるため画像自体の大きさより大きくなることはありません。

また、i-amphtml-sizer タグ内の img タグには、role="presentation"aria-hidden="true" を指定することで、見た目を変えるために使用されている重要な意味を持たない要素であることを明示してあり、アクセシビリティにも考慮されていることがわかります。

layout="nodisplay"

要素は非表示になり、スペースを占有しません。

<amp-img layout="nodisplay" src="/static/sample.jpg" width="1080" height="720" layout="nodisplay"></amp-img>

↓ 画像ロード後

<amp-img layout="nodisplay" src="/static/sample.jpg" width="1080" height="720" class="i-amphtml-layout-nodisplay i-amphtml-element" hidden="" i-amphtml-layout="nodisplay"></amp-img>
/* amp-img タグのスタイル */
[hidden] {
    display: none!important;
}

AMP コンポーネント共通の hidden 属性が付与され、display: none;が適用されます。

まとめ

amp-img を使用しない場合でもwidthheightdecoding といった属性を適切に指定するなど、パフォーマンス改善のために画像周りで工夫できることが多くあることがわかりました。
amp-img は遅延ロードやフォールバックといった機能を提供してくれるので、改めて便利だと感じました。
完全に AMP 対応したサイトではないとしても画像を便利に扱うために amp-img を使用するのも一つの選択肢だと思いました。
画像の扱いは web サイトのパフォーマンスを低下させる大きな要因になり得るので、先人の知恵を借りながらベストな実装を追いかけていきたいですね。


  1. IEは非対応: https://caniuse.com/mdn-html_elements_img_decoding 

  2. layout の値により、width / height が必須かどうかは異なります。 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む